regression: test guilt-files -v
[guilt.git] / guilt
blob5bf92be18011e03cacdccd8346d6ea1080212800
1 #!/bin/sh
3 # Copyright (c) Josef "Jeff" Sipek, 2006-2008
6 GUILT_VERSION="0.29"
7 GUILT_NAME="Bleecker Street"
9 # If the first argument is one of the below, display the man page instead of
10 # the rather silly and mostly useless usage string
11 case $1 in
12 -h|--h|--he|--hel|--help)
13 shift
14 exec "guilt-help" "`basename $0`"
15 exit
17 -V|--ver|--versi|--versio|--version)
18 echo "Guilt version $GUILT_VERSION"
19 exit
21 esac
23 # we change directories ourselves
24 SUBDIRECTORY_OK=1
26 . git-sh-setup
29 # Git version check
31 gitver=`git --version | cut -d' ' -f3`
32 case "$gitver" in
33 1.5.*) ;; # git config
34 *) die "Unsupported version of git ($gitver)" ;;
35 esac
38 # Shell library
41 # echo -n is a bashism, use printf instead
42 _disp()
44 printf "%b" "$*"
47 # echo -e is a bashism, use printf instead
48 disp()
50 printf "%b\n" "$*"
53 noerr()
55 "$@" 2>/dev/null
58 silent()
60 "$@" >/dev/null 2>/dev/null
63 ########
65 guilt_commands()
67 find "`dirname $0`" -maxdepth 1 -name "guilt-*" -type f -perm +111 | sed -e "s/.*\\/`basename $0`-//"
70 if [ "`basename $0`" = "guilt" ]; then
71 # being run as standalone
73 # by default, we shouldn't fail
74 cmd=
76 if [ $# -ne 0 ]; then
77 # take first arg, and try to execute it
79 arg="$1"
80 dir=`dirname $0`
82 if [ -x "$dir/guilt-$arg" ]; then
83 cmd=$arg
84 else
85 # might be a short handed
86 for command in $(guilt_commands); do
87 case $command in
88 $arg*)
89 if [ -x "$dir/guilt-$command" ]; then
90 cmd=$command
93 esac
94 done
96 if [ -n "$cmd" ]; then
97 shift
98 exec "$dir/guilt-$cmd" "$@"
100 # this is not reached because of the exec
101 die "Exec failed! Something is terribly wrong!"
102 else
103 disp "Command $arg not found" >&2
104 disp "" >&2
108 # no args passed or invalid command entered, just output help summary
110 disp "Guilt v$GUILT_VERSION"
111 disp ""
112 disp "Pick a command:"
113 guilt_commands | sort | column | column -t | sed -e 's/^/ /'
115 disp ""
116 disp "Example:"
117 disp "\tguilt-push"
118 disp "or"
119 disp "\tguilt push"
121 # now, let's exit
122 exit 1
125 ########
128 # Library goodies
131 # usage: valid_patchname <patchname>
132 valid_patchname()
134 case "$1" in
135 /*|./*|../*|*/./*|*/../*|*/.|*/..|*/|*\ *|*\ *)
136 return 1;;
138 return 0;;
139 esac
142 get_branch()
144 git symbolic-ref HEAD | sed -e 's,^refs/heads/,,'
147 verify_branch()
149 [ ! -d "$GIT_DIR/patches" ] &&
150 die "Patches directory doesn't exist, try guilt-init"
151 [ ! -d "$GIT_DIR/patches/$branch" ] &&
152 die "Branch $branch is not initialized, try guilt-init"
153 [ ! -f "$GIT_DIR/patches/$branch/series" ] &&
154 die "Branch $branch does not have a series file"
155 [ ! -f "$GIT_DIR/patches/$branch/status" ] &&
156 die "Branch $branch does not have a status file"
157 [ -f "$GIT_DIR/patches/$branch/applied" ] &&
158 die "Warning: Branch $branch has 'applied' file - guilt is not compatible with stgit"
161 get_top()
163 tail -1 "$GUILT_DIR/$branch/status"
166 get_prev()
168 if [ `wc -l < "$GUILT_DIR/$branch/status"` -gt 1 ]; then
169 tail -n 2 "$GUILT_DIR/$branch/status" | head -n 1
173 get_series()
175 # ignore all lines matching:
176 # - empty lines
177 # - whitespace only
178 # - optional whitespace followed by '#' followed by more
179 # optional whitespace
180 grep -ve '^[[:space:]]*\(#.*\)*$' "$series"
183 # usage: do_make_header <hash>
184 do_make_header()
186 # we should try to work with commit objects only
187 if [ `git cat-file -t "$1"` != "commit" ]; then
188 disp "Hash $1 is not a commit object" >&2
189 disp "Aborting..." >&2
190 exit 2
193 git cat-file -p "$1" | awk '
194 BEGIN{headers=1; firstline=1}
195 /^author / && headers {
196 sub(/^author +/, "");
197 sub(/ [0-9]* [+-]*[0-9][0-9]*$/, "");
198 author=$0
200 !headers {
201 print
202 if (firstline) {
203 firstline = 0;
204 print "\nFrom: " author;
207 /^$/ && headers { headers = 0 }
211 # usage: do_get_patch patchfile
212 do_get_patch()
214 cat "$1" | awk '
215 BEGIN{}
216 /^(diff |---)/,/END{}/
220 # usage: do_get_header patchfile
221 do_get_header()
223 # The complexity arises from the fact that we want to ignore the
224 # From line and the empty line after it if it exists
226 # 2nd line skips the From line
227 # 3rd line skips the empty line right after a From line
228 # 4th line terminates execution when we encounter the diff
229 cat "$1" | awk '
230 BEGIN{skip=0}
231 /^Subject:/ && (NR==1){print substr($0, 10); next}
232 /^From:/{skip=1; next}
233 /^[ \t\f\n\r\v]*$/ && (skip==1){skip=0; next}
234 /^(diff |---)/{exit}
235 {print $0}
236 END{}
240 # usage: do_get_full_header patchfile
241 do_get_full_header()
243 # 2nd line checks for the begining of a patch
244 # 3rd line outputs the line if it didn't get pruned by the above rules
245 cat "$1" | awk '
246 BEGIN{}
247 /^(diff |---)/{exit}
248 {print $0}
249 END{}
253 # usage: assert_head_check
254 assert_head_check()
256 if ! head_check refs/patches/$branch/`get_top`; then
257 die "aborting..."
261 # usage: head_check <expected hash>
262 head_check()
264 # make sure we're not doing funky things to commits that don't
265 # belong to us
267 case "$1" in
269 # the expected hash is empty
270 return 0 ;;
271 refs/patches/$branch/)
272 # the expected hash is an invalid rev
273 return 0 ;;
274 esac
276 if [ "`git rev-parse refs/heads/$branch`" != "`git rev-parse $1`" ]; then
277 disp "Expected HEAD commit $1" >&2
278 disp " got `git rev-parse refs/heads/$branch`" >&2
279 return 1
281 return 0
284 # usage: series_insert_patch <patchname>
285 series_insert_patch()
287 awk -v top="`get_top`" -v new="$1" \
288 'BEGIN{if (top == "") print new;}
290 print $0;
291 if (top != "" && top == $0) print new;
292 }' "$series" > "$series.tmp"
293 mv "$series.tmp" "$series"
296 # usage: series_remove_patch <patchname>
297 series_remove_patch()
299 grep -v "^$1\$" < "$series" > "$series.tmp"
300 mv "$series.tmp" "$series"
303 # usage: series_rename_patch <oldname> <newname>
304 series_rename_patch()
306 awk -v old="$1" -v new="$2" \
307 '{ if ($0 == old) print new; else print $0 }' \
308 "$series" > "$series.tmp"
310 mv "$series.tmp" "$series"
313 # usage: series_rename_patch <oldpatchname> <newpatchname>
314 ref_rename_patch()
316 git update-ref "refs/patches/$branch/$2" `git rev-parse "refs/patches/$branch/$1"`
317 remove_ref "refs/patches/$branch/$1"
320 # Beware! This is one of the few (only?) places where we modify the applied
321 # file directly
323 # usage: applied_rename_patch <oldname> <newname>
324 applied_rename_patch()
326 awk -v old="$1" -v new="$2" \
327 'BEGIN{FS=":"}
328 { if ($0 == old)
329 print new;
330 else
331 print;
332 }' "$applied" > "$applied.tmp"
334 mv "$applied.tmp" "$applied"
337 # usage: remove_patch_refs
338 # reads patch names from stdin
339 remove_patch_refs()
341 while read pname; do
342 remove_ref "refs/patches/$branch/$pname"
343 done
346 # usage: pop_many_patches <commitish> <number of patches>
347 pop_many_patches()
349 assert_head_check
352 cd "$TOP_DIR"
354 # remove the patches refs
355 tail -$2 < "$applied" | remove_patch_refs
357 git reset --hard "$1" > /dev/null
358 head -n "-$2" < "$applied" > "$applied.tmp"
359 mv "$applied.tmp" "$applied"
362 # update references to top, bottom, and base
363 update_stack_tags
366 # usage: pop_all_patches
367 pop_all_patches()
369 pop_many_patches \
370 `git rev-parse refs/patches/$branch/$(head -1 "$applied")^` \
371 `wc -l < "$applied"`
374 # usage: remove_ref <refname>
375 remove_ref()
378 # does the ref exist?
379 r=`git show-ref --verify -s "$1" 2> /dev/null`
380 [ $? -ne 0 ] && exit 0
382 # remove it
383 git update-ref -d "$1" "$r"
387 # usage: update_stack_tags [force]
389 # if [force] is non-empty, then do not check for autotagging being enabled,
390 # just assume it is
391 update_stack_tags()
393 # bail if autotagging is not enabled
394 if [ -z "$1" -a $autotag -eq 0 ]; then
395 return 0
398 if [ -s "$applied" ]; then
399 # there are patches applied, therefore we must get the top,
400 # bottom and base hashes, and update the tags
402 git update-ref "refs/tags/${branch}_top" `git rev-parse HEAD`
403 git update-ref "refs/tags/${branch}_bottom" `git rev-parse refs/patches/$branch/$(head -1 < $applied)`
404 git update-ref "refs/tags/${branch}_base" `git rev-parse refs/patches/$branch/$(head -1 < $applied)^`
405 else
406 # there are no patches applied, therefore we must remove the
407 # tags to old top, bottom, and base
409 remove_ref "refs/tags/${branch}_top"
410 remove_ref "refs/tags/${branch}_bottom"
411 remove_ref "refs/tags/${branch}_base"
415 # usage: push_patch patchname [bail_action]
416 push_patch()
418 __push_patch_bail=0
421 TMP_LOG=`get_tmp_file log`
422 TMP_MSG=`get_tmp_file msg`
424 p="$GUILT_DIR/$branch/$1"
425 pname="$1"
426 bail_action="$2"
427 reject="--reject"
429 assert_head_check
430 cd "$TOP_DIR"
432 # apply the patch if and only if there is something to apply
433 if [ `git apply --numstat "$p" | wc -l` -gt 0 ]; then
434 if [ "$bail_action" = abort ]; then
435 reject=""
437 git apply -C$guilt_push_diff_context --index \
438 $reject "$p" > /dev/null 2> "$TMP_LOG"
439 __push_patch_bail=$?
441 if [ $__push_patch_bail -ne 0 ]; then
442 cat "$TMP_LOG" >&2
443 if [ "$bail_action" = "abort" ]; then
444 rm -f "$TMP_LOG" "$TMP_MSG"
445 return $__push_patch_bail
450 # grab a commit message out of the patch
451 do_get_header "$p" > "$TMP_MSG"
453 # make a default commit message if patch doesn't contain one
454 [ ! -s "$TMP_MSG" ] && echo "patch $pname" > "$TMP_MSG"
456 # extract a From line from the patch header, and set
457 # GIT_AUTHOR_{NAME,EMAIL}
458 author_str=`sed -n -e '/^From:/ { s/^From: //; p; q; }; /^(diff |---)/ q' "$p"`
459 if [ ! -z "$author_str" ]; then
460 GIT_AUTHOR_NAME=`echo $author_str | sed -e 's/ *<.*$//'`
461 export GIT_AUTHOR_NAME="${GIT_AUTHOR_NAME:-" "}"
462 export GIT_AUTHOR_EMAIL="`echo $author_str | sed -e 's/[^<]*//'`"
465 # must strip nano-second part otherwise git gets very
466 # confused, and makes up strange timestamps from the past
467 # (chances are it decides to interpret it as a unix
468 # timestamp).
469 export GIT_AUTHOR_DATE="`stat -c %y "$p" | sed -e '
470 s/^\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\) \([0-9]\{2\}\):\([0-9]\{2\}\):\([0-9]\{2\}\)\.[0-9]* \(.*\)$/\1-\2-\3 \4:\5:\6 \7/'`"
471 export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
473 # commit
474 treeish=`git write-tree`
475 commitish=`git commit-tree $treeish -p HEAD < "$TMP_MSG"`
476 git update-ref HEAD $commitish
478 # mark patch as applied
479 git update-ref "refs/patches/$branch/$pname" $commitish ""
481 echo "$pname" >> $applied
483 rm -f "$TMP_MSG" "$TMP_LOG"
486 # sub-shell funky-ness
487 __push_patch_bail=$?
489 # update references to top, bottom, and base of the stack
490 update_stack_tags
492 return $__push_patch_bail
495 # usage: must_commit_first
496 must_commit_first()
498 [ `git diff-files | wc -l` -eq 0 ]
499 return $?
502 # usage: fold_patch patchname
503 fold_patch()
505 set -- "$1" "`get_top`"
507 assert_head_check
509 push_patch "$1"
511 __refresh_patch "$2" HEAD^^ 2 "" ""
513 series_remove_patch "$1"
516 # usage: refresh_patch patchname gengitdiff incldiffstat
517 refresh_patch()
519 __refresh_patch "$1" HEAD^ 1 "$2" "$3"
522 # usage: __refresh_patch patchname commitish number_of_commits gengitdiff
523 # incldiffstat
524 __refresh_patch()
526 assert_head_check
529 TMP_DIFF=`get_tmp_file diff`
531 cd "$TOP_DIR"
532 p="$GUILT_DIR/$branch/$1"
534 git diff-files --name-only | (while read n; do git update-index "$n" ; done)
536 # get the patch header
537 do_get_full_header "$p" > "$TMP_DIFF"
539 [ ! -z "$4" ] && diffopts="-C -M --find-copies-harder"
541 if [ ! -z "$5" ]; then
543 echo "---"
544 git diff --stat $diffopts "$2"
545 echo ""
546 ) >> "$TMP_DIFF"
549 # get the new patch
550 git diff $diffopts "$2" >> "$TMP_DIFF"
552 # move the new patch in
553 mv "$p" "$p~"
554 mv "$TMP_DIFF" $p
557 # drop the currently applied patch, pop_many_patches does it's own
558 # cd $TOP_DIR
559 pop_many_patches "$2" "$3"
561 # push_patch does it's own cd $TOP_DIR
562 push_patch "$1"
565 # usage: munge_hash_range <hash range>
567 # this means:
568 # <hash> - one commit
569 # <hash>.. - hash until head (excludes hash, includes head)
570 # ..<hash> - until hash (includes hash)
571 # <hash1>..<hash2> - from hash to hash (inclusive)
573 # The output of this function is suitable to be passed to "git rev-list"
574 munge_hash_range()
576 case "$1" in
577 *..*..*|*\ *)
578 # double .. or space is illegal
579 return 1;;
580 ..*)
581 # e.g., "..v0.10"
582 echo ${1#..};;
583 *..)
584 # e.g., "v0.19.."
585 echo ${1%..}..HEAD;;
586 *..*)
587 # e.g., "v0.19-rc1..v0.19"
588 echo ${1%%..*}..${1#*..};;
590 # e.g., "v0.19"
591 echo $1^..$1;;
592 *) # empty
593 return 1;;
594 esac
595 return 0
598 # usage: get_tmp_file <prefix>
600 # Get a unique filename and create the file in a non-racy way
601 get_tmp_file()
603 while true; do
604 mktemp "/tmp/guilt.$1.XXXXXXXXXXXXXXX" && break
605 done
608 # usage: guilt_hook <hook name> <args....>
609 guilt_hook()
611 __hookname="$1"
612 [ ! -x "$GIT_DIR/hooks/guilt/$__hookname" ] && return 0
614 shift
616 "$GIT_DIR/hooks/guilt/$__hookname" "$@"
617 return $?
621 # Some constants
624 # used for: git apply -C <val>
625 guilt_push_diff_context=1
627 # default autotag value
628 AUTOTAG_DEFAULT=1
631 # Parse any part of .git/config that belongs to us
634 # autotag?
635 autotag=`git config guilt.autotag`
636 [ -z "$autotag" ] && autotag=$AUTOTAG_DEFAULT
639 # The following gets run every time this file is source'd
642 TOP_DIR=`git rev-parse --show-cdup`
643 if [ -z "$TOP_DIR" ]; then
644 TOP_DIR="./"
647 GUILT_DIR="$GIT_DIR/patches"
649 branch=`get_branch`
651 # most of the time we want to verify that the repo's branch has been
652 # initialized, but every once in a blue moon (e.g., we want to run guilt-init),
653 # we must avoid the checks
654 if [ -z "$DO_NOT_CHECK_BRANCH_EXISTENCE" ]; then
655 verify_branch
657 # do not check the status file format (guilt-repair needs this,
658 # otherwise nothing can do what's necessary to bring the repo into a
659 # useable state)
660 if [ -z "$DO_NOT_CHECK_STATUS_FILE_FORMAT" ]; then
661 [ -s "$GIT_DIR/patches/$branch/status" ] &&
662 grep "^[0-9a-f]\{40\}:" "$GIT_DIR/patches/$branch/status" > /dev/null &&
663 die "Status file appears to use old format, try guilt-repair --status"
667 # very useful files
668 series="$GUILT_DIR/$branch/series"
669 applied="$GUILT_DIR/$branch/status"
671 # determine an editor to use for anything interactive (fall back to vi)
672 editor="vi"
673 [ ! -z "$EDITOR" ] && editor="$EDITOR"
675 # determine a pager to use for anything interactive (fall back to more)
676 pager="more"
677 [ ! -z "$PAGER" ] && pager="$PAGER"