Test commit
[cogito/jonas.git] / cg-log
blob76e512859bc641640f19445c0837236125bd8dff
1 #!/usr/bin/env bash
3 # Show the list of changes
4 # Copyright (c) Petr Baudis, 2005.
5 # Copyright (c) David Woodhouse, 2005.
7 # Display log of changes on a given branch, within a given range of commits,
8 # and/or concerning given set of files. The log can be further filtered to
9 # e.g. only changes touching a given string or done by a given user. Several
10 # output formats are available.
12 # The output will automatically be displayed in a pager unless it is piped to
13 # a program.
15 # OPTIONS
16 # -------
17 # Arguments not interpreted as options will be interpreted as filenames;
18 # cg-log then displays only changes in those files.
20 # -c:: Colorize
21 # Colorize the output. You can customize the colors using the
22 # $CG_COLORS environment variable (see below).
24 # -d:: Show diffs against previous commits
25 # Accompany each commit with a diff against previous commit.
26 # Turns off '-f'.
28 # --diffcore ARGS:: Diffcore arguments to pass the Git diff command
29 # Pass the given diffcore arguments the called Git diff command.
30 # See e.g. git-diff-tree(1) documentation for the list of possible
31 # arguments; '-R', '-B', and '-C' might be of particular interest
32 # ('-M' is sometimes passed automatically, but not always). This
33 # is mostly only relevant in conjunction with the '-d' option.
35 # -f:: List affected files
36 # List affected files. (No effect when passed along '-s'.)
37 # Turns off '-d'.
39 # -r FROM_ID[..TO_ID]:: Limit to a set of revisions
40 # Limit the log information to a set of revisions using either
41 # '-r FROM_ID[..TO_ID]' or '-r FROM_ID -r TO_ID'. In both cases the
42 # option expects IDs which resolve to commits and will include the
43 # specified IDs. If 'TO_ID' is omitted all commits from 'FROM_ID'
44 # to the initial commit is shown. If no revisions is specified,
45 # the log information starting from 'HEAD' will be shown.
47 # -D DATE:: Limit to revisions newer than given DATE
48 # Limit the log information to revisions newer than given DATE,
49 # and on second time further restrain it to revisions older than
50 # given date. Therefore, '-D "2 days ago" -D "yesterday"' will
51 # show all the commits from the day before yesterday.
53 # -m:: End the log at the merge base of the revision set
54 # End the log listing at the merge base of the -r arguments
55 # to HEAD and 'origin' or the current branch's default remote
56 # branch, see `cg-fetch` for details).
58 # -M, --merges:: Show merge commits
59 # Display merge commits in the log.
61 # -R, --no-renames:: Do not follow renames
62 # This flag is currently no-op. `cg-log` will not follow file history
63 # across renames.
65 # -s:: Short output format of the log entries
66 # Show the log entries one per line. The entry summary contains
67 # information about the commit date, the author, the first line
68 # of the commit log and the commit ID. Long author names and commit
69 # IDs are trimmed and marked with an ending tilde (~).
71 # --summary:: Group commits by author
72 # Generate the changes summary, listing the commit titles grouped
73 # by their author. This is also known as a "shortlog", suitable
74 # e.g. for contribution summaries of announcements.
76 # -S, --pickaxe STRING:: Limit to changes touching STRING ("pick-axe")
77 # List only commits with changes concerning STRING (also known as
78 # pick-axe). In other words, only commits where the parent contains
79 # STRING and the child does not contain it at the same place in
80 # a file or vice versa are shown. The STRING may contain any
81 # special characters or even newlines (but you might need to quote
82 # it properly when calling `cg-log` from a shell). It is matched
83 # verbatim.
85 # -u USERNAME:: Limit to commit where author/committer matches USERNAME
86 # List only commits where author or committer contains 'USERNAME'.
87 # The search for 'USERNAME' is case-insensitive.
89 # -v:: Verbose header listing
90 # By default, only the 'commit' and 'author' headers are shown. This
91 # makes `cg-log` show even the other commit headers - 'tree', 'parent',
92 # and 'committer'.
94 # ENVIRONMENT VARIABLES
95 # ---------------------
96 # PAGER::
97 # The pager to display log information in, defaults to `less`.
99 # PAGER_FLAGS::
100 # Flags to pass to the pager.
102 # CG_COLORS::
103 # Colon-separated list of 'name=color' pairs, where name is
104 # one of logcommit, logheader, logauthor, logcommitter,
105 # logfilemod, logfileadd, logfiledel, logfileren, logsignoff,
106 # logsumauthor, logsumtrim, logsumcommit, logsumdate, default,
107 # and value is an ECMA-48 SGR sequence (see e.g. console_codes(4)).
108 # You can also customize the diff colors; see `cg-diff` documentation
109 # for the appropriate color names.
111 # CG_COLORS_AUTO::
112 # Even if -c was passed or specified in ~/.cgrc, if this option
113 # is set, use colors only when the output is a terminal and it
114 # supports colors.
116 # CG_LESS::
117 # This is what the $LESS environment variable value will be set
118 # to before invoking $PAGER. It defaults to $LESS concatenated
119 # with the `R` and `S` flags to allow displaying of colorized output
120 # and to avoid long lines from wrapping when using `-s`.
122 # CONFIGURATION VARIABLES
123 # -----------------------
124 # The following GIT configuration file variables are recognized:
126 # log.usecolor::
127 # If enabled, colorify the output like with -c if the output
128 # is a terminal.
130 # EXAMPLE USAGE
131 # -------------
132 # To show a log of changes between two releases tagged as 'releasetag-0.9'
133 # and 'releasetag-0.10' do:
135 # $ cg-log -r releasetag-0.9..releasetag-0.10
137 # Similarily, to see which commits are in branch A but not in branch B,
139 # $ cg-log -r B..A
141 # (meaning "all the commits which newly appear along the way from B to A").
143 # If you see a dubious "if (current->uid = 0)" test in a file and wonder
144 # about its genesis, you can run
146 # $ cg-log -d -S "if (current->uid = 0)" filename
148 # to show the commits adding, removing or modifying that string, together
149 # with the relevant patches (you can obviously refrain from limiting
150 # the pick-axe to a particular file, but it will make it significantly
151 # slower).
153 # NOTES
154 # -----
155 # The ':' is equivalent to '..' in revisions range specification (to make
156 # things more comfortable to SVN users). See cogito(7) for more details
157 # about revision specification.
159 # Testsuite: TODO
161 USAGE="cg-log [-D DATE] [-r FROM_ID[..TO_ID]] [-d] [-s | --summary] [OTHER_OPTIONS] [FILE]..."
162 _git_wc_unneeded=1
164 . "${COGITO_LIB}"cg-Xlib || exit 1
165 # Try to fix the annoying "Broken pipe" output. May not help, but apparently
166 # at least somewhere it does. Bash is broken.
167 trap exit SIGPIPE
171 setup_colors()
173 local C="logcommit=32:logheader=32"
174 C="$C:logauthor=36:logcommitter=35:logsignoff=33"
175 C="$C:logfilemod=34:logfileadd=36:logfiledel=31:logfileren=33"
176 C="$C:logsumauthor=36:logsumtrim=35"
177 C="$C:logsumcommit=34:logsumdate=32"
178 C="$C:default=0"
179 [ "$show_diffs" ] && C="$C:logcommit=32;1"
180 # Remove bolds from diffcolors - they tended to overshadow cg-log's
181 # highlighting, making it less obvious where one commit ends and
182 # another appears.
183 colorify_setup "$C:${colorify_diffcolors//1;/}"
184 collogfile_M0="$collogfilemod"
185 collogfile_A0="$collogfileadd"
186 collogfile_D0="$collogfiledel"
187 collogfile_R0="$collogfileren"
188 collogfile_R1="$collogfileadd"
191 print_oneline()
193 commit="${commit%:*}"
194 author="${author% <*}"
195 [ "${#author}" -gt 15 ] && author="${author:0:14}$collogsumtrim~"
196 sumcommit="${commit:0:12}$collogsumtrim~"
198 # We want wordsplitting in the $date here, to get
199 # TZ as separate argument.
200 date=(${committer#*> })
201 showdate ${date[*]} '+%F %H:%M'; date="$_showdate"
203 read -r title
205 printf "$collogsumcommit%s $collogsumauthor%-15s $collogsumdate%s $coldefault%s\n" \
206 "$sumcommit" "$author" "$date" "${title:3}"
209 list_commit_files()
211 if [ ${#files[@]} -eq 0 ]; then
212 echo " * no changes:"
213 echo
214 return
216 tree1="$1"
217 tree2="$2"
218 line=
219 sep=" * "
220 for (( i=0; i < ${#files[@]}; i++ )); do
221 echo -n "$sep"
222 sep="$collogfilemod, "
223 line="$line$sep${files[$i]}"
224 if [ ${#line} -le 74 ]; then
225 echo -n "${filecols[$i]}${files[$i]}"
226 else
227 line=" ${files[$i]}"
228 echo "$coldefault"
229 echo -n " ${filecols[$i]}${files[$i]}"
231 done
232 echo "$coldefault:"
233 echo
236 print_commit_contents()
238 [ "$list_files" ] && list_commit_files
239 echo "$msg"
242 reset_commit_info()
244 commit=
245 tree=
246 parents=()
247 author=
248 committer=
249 msg=
250 files=()
251 filecols=()
253 # The $state variable mostly describes what should happen on the next
254 # empty line.
255 state=printhdr
258 process_commit_line()
260 if [ "$key" = "%" ] || [ "$key" = "%$collogsignoff" ]; then
261 # The fast common case
262 [ "$state" = silent ] || msg="$msg ${rest#?}
264 return
266 case "$key" in
267 "commit"|"diff-tree")
268 oldcommit="$commit"
269 reset_commit_info
270 commit="${rest:0:40}"
271 # If we've just seen a commit, we are seeing multiple
272 # instances of the same commit caused by git-diff-tree --stdin
273 # hitting a merge. We show only the diff against the first
274 # parent since heuristically, this is the interesting one.
275 # In some cases, this might not be true, but this is hopefully
276 # a good general strategy (always except in the change-here-
277 # -merge-there-fastforward-here case and doing anything else
278 # results in unusably huge file lists etc).
279 [ "$commit" = "$oldcommit" ] && state=silent
281 "tree")
282 tree="$rest"
284 "parent")
285 parents[${#parents[@]}]="$rest"
287 "committer")
288 committer="$rest"
290 "author")
291 author="$rest"
294 orest="$rest";
295 rest="${orest#* }"
296 crest="${orest%% *}"
297 crest="${crest%[0-9][0-9][0-9]}" # rename similarity
299 while [ x"$rest" != x"$orest" ]; do
300 local l="collogfile_${crest:(-1):1}$i"
301 filecols[${#files[@]}]="${!l}"
302 files[${#files[@]}]="${rest%% *}"
303 # Multiple tab-separated filenames are present in case
304 # of rename entries.
305 orest="$rest"
306 rest="${rest#* }"
307 i=$((i+1))
308 done
311 if [ "$state" = silent ]; then
312 return
314 if [ "$state" = waitdiff ]; then
315 # We cannot hook this to ^: since the diff may be empty
316 [ "$show_diffs" ] && msg="$msg
318 state=showcommit
319 return
321 if [ "$state" = showcommit ]; then
322 print_commit_contents
323 [ "$show_diffs" ] && echo
324 state=randomjunk
325 return
327 if [ "$state" != printhdr ]; then
328 die "internal error - state '$state'"
331 if [ "$user" ]; then
332 if ! [[ "$author" == *"$user"* || "$committer" == *"$user"* ]]; then
333 state=silent
334 return
337 if [ "$oneline" ]; then
338 print_oneline
339 state=silent
340 return
343 merge=
344 [ ! "$verbose" -a ${#parents[@]} -gt 1 ] && merge=" (merge)"
345 echo "${collogcommit}Commit: ${commit%:*}$merge $coldefault"
347 if [ "$verbose" ]; then
348 echo "${collogheader}Tree: $tree $coldefault"
350 for parent in "${parents[@]}"; do
351 echo "${collogheader}Parent: $parent $coldefault"
352 done
355 # We want wordsplitting in the $date here, to get
356 # TZ as separate argument.
357 date=(${author#*> })
358 showdate ${date[*]}; pdate="$_showdate"
359 [ "$pdate" ] && author="${author%> *}> $pdate"
360 echo "${collogauthor}Author: $author $coldefault"
362 if [ "$verbose" ]; then
363 date=(${committer#*> })
364 showdate ${date[*]}; pdate="$_showdate"
365 [ "$pdate" ] && committer="${committer%> *}> $pdate"
366 echo "${collogcommitter}Committer: $committer $coldefault"
369 echo
370 if [ "$difffilter" ]; then
371 state=waitdiff
372 else
373 state=showcommit
376 esac
379 print_commit_log()
381 [ "$show_diffs" ] || colorify_diffsed=
382 reset_commit_info
383 sed -e '
384 s/^:/: /
385 s/^ \(.*\)/% @\1/
386 /^% *@[Ss]igned-[Oo]ff-[Bb]y:.*/ s/^% @\(.*\)/% @'$collogsignoff'\1'$coldefault'/
387 /^% *@[Aa][Cc][Kk]ed-[Bb]y:.*/ s/^% @\(.*\)/% @'$collogsignoff'\1'$coldefault'/
388 ' -e "$colorify_diffsed" | { while IFS=$'\n' read -r line; do
389 trap exit SIGPIPE
390 if [ "$state" = "showcommit" -a "$show_diffs" -a -n "$line" ]; then
391 [ x"${line#% @}" = x"$line" ] || line=" ${line#% @}" # undo sed damage
392 [ "$state" = silent ] || msg="$msg$line
394 continue
396 key="${line%%[ ]*}"
397 rest="${line#*[ ]}"
398 process_commit_line
399 done; [ "$state" = "showcommit" ] && print_commit_contents # the last commit
404 colors=
405 collogheader=
406 collogauthor=
407 collogcommitter=
408 collogfiles=
409 collogsignoff=
410 collogsumauthor=
411 collogsumcommit=
412 collogsumdate=
413 collogsumtrim=
414 coldefault=
415 difffilter=
416 followrenames=
417 neverfollowrenames=
418 verbose=
420 list_files=
421 show_diffs=
422 diffcore=
423 id1=
424 id2=
425 oneline=
426 shortlog=
427 user=
428 mergebase=
429 date_from=
430 date_to=
431 no_merges=--no-merges
432 always=--always
433 diffmerges=
434 diffpatches=
435 pickaxe=()
437 while optparse; do
438 if optparse -c; then
439 colors=1
440 elif optparse -d; then
441 show_diffs=1
442 difffilter=showdiffs
443 diffpatches=-p
444 list_files=
445 elif optparse -f; then
446 list_files=1
447 difffilter=listfiles
448 show_diffs=
449 diffpatches=
450 elif optparse -u=; then
451 user="$OPTARG"
452 elif optparse -r=; then
453 if echo "$OPTARG" | fgrep -q '..'; then
454 id1="${OPTARG%..*}"
455 id2="${OPTARG#*..}"
456 # id2 was specified as empty commit, that is HEAD;
457 # but leaving it empty now would give the code below
458 # wrong idea.
459 [ "$id2" ] || id2="HEAD"
460 elif echo "$OPTARG" | grep -q ':'; then
461 id1="${OPTARG%:*}"
462 id2="${OPTARG#*:}"
463 [ "$id2" ] || id2="HEAD"
464 elif [ -z "$id1" ]; then
465 id1="$OPTARG"
466 elif [ -z "$id2" ]; then
467 id2="$OPTARG"
468 else
469 die "too many revisions"
471 elif optparse -D=; then
472 if [ -z "$date_from" ]; then
473 date_from="--max-age=$(date -d "$OPTARG" +%s)" || exit 1
474 else
475 date_to="--min-age=$(date -d "$OPTARG" +%s)" || exit 1
477 elif optparse -d=; then
478 die "the -d option was renamed to -D"
479 elif optparse -m; then
480 mergebase=1
481 elif optparse -M || optparse --merges; then
482 no_merges=
483 diffmerges=-m
484 elif optparse -R || optparse --no-renames; then
485 neverfollowrenames=1
486 elif optparse -s; then
487 oneline=1
488 elif optparse -S= || optparse --pickaxe=; then
489 always=
490 pickaxe=(-S"$OPTARG")
491 # The trouble with this is that less behaves really strange.
492 # It withholds the output until it reads all the input, for
493 # example. So this didn't work out very well so far. :-(
494 # pickaxe_less=$'+/\013\022'"${OPTARG}"
495 # pickaxe_less="${pickaxe_less%$'\n'*}" # stupid less!
496 difffilter=pickaxe
497 elif optparse --diffcore=; then
498 diffcore="$OPTARG"
499 elif optparse --summary; then
500 shortlog=1
501 elif optparse -v; then
502 verbose=1
503 else
504 optfail
506 done
509 # [ "$pickaxe_less" -a "$show_diffs" ] && _local_CG_LESS="$pickaxe_less"
510 colorify_detect "$colors" log && setup_colors
511 if [ "$show_diffs" -a "${ARGS[*]}" ]; then
512 #warn "-d is buggy and cannot follow renames yet; implying --no-renames"
513 neverfollowrenames=1
517 # Word splitting is ok here and we want to auto-drop empty dates.
518 revls="$no_merges $date_from $date_to"
521 if [ "$mergebase" ]; then
522 [ "$id1" ] || id1="HEAD"
523 [ "$id2" ] || { id2="$(choose_origin refs/heads "what to log against?")" || exit 1; }
525 id1="$(cg-object-id -c "$id1")" || exit 1
526 id2="$(cg-object-id -c "$id2")" || exit 1
527 conservative_merge_base "$id1" "$id2" || exit 1
528 [ "$_cg_base_conservative" ] &&
529 warn -b "multiple merge bases, picking the most conservative one"
530 id1="$_cg_baselist"
532 else
533 id1="$(cg-object-id -c "$id1")" || exit 1
536 if [ "$id2" ]; then
537 id2="$(cg-object-id -c "$id2")" || exit 1
538 revls="$revls ^$id1"
539 revlsstart="$id2"
540 else
541 revlsstart="$id1"
545 if [ "$shortlog" ]; then
546 fmt="--pretty=short"
547 else
548 fmt="--pretty=raw"
551 sep=
552 if [ "${ARGS[*]}" ]; then
553 [ "$neverfollowrenames" ] || followrenames=1
554 sep=--
557 # Translate arguments to relpath:
558 if [ "$_git_relpath" ]; then
559 for (( i=0; i<${#ARGS[@]}; i++ )); do
560 ARGS[$i]="$_git_relpath${ARGS[$i]}"
561 done
564 [ "$followrenames" ] && difffilter=followrenames
567 # A curious pipeline:
568 rev_extract()
570 # XXX: Following renames is broken and turns out to be massive
571 # performance hog.
572 # if [ "$followrenames" ]; then
573 # [ "${ARGS[*]}" ] || die "internal error: no files to follow renames on"
574 # # We ignore $fmt but that's no biggie, shortlog
575 # # will actually work anyway.
576 # "${COGITO_LIB}"cg-Xfollowrenames $revls -- \
577 # --root --pickaxe-all $diffmerges $diffpatches \
578 # $always $diffcore "${pickaxe[@]}" -- \
579 # $revlsstart -- "${ARGS[@]}"
580 # el
581 if [ "$difffilter" ]; then
582 git-rev-list $revls $revlsstart $sep "${ARGS[@]}" | \
583 git-diff-tree -r --stdin --root --pickaxe-all \
584 $diffmerges $diffpatches $always $diffcore $fmt \
585 "${pickaxe[@]}"
586 else
587 git-rev-list $revls $revlsstart $fmt $sep "${ARGS[@]}"
591 rev_show()
593 if [ "$shortlog" ]; then
594 git-shortlog | pager
595 else
596 # LESS="S" will prevent less to wrap too long titles
597 # to multiple lines; you can scroll horizontally.
598 print_commit_log | _local_CG_LESS="S $_local_CG_LESS" pager
602 rev_extract | rev_show
604 exit 0