Guilt v0.22
[guilt.git] / guilt
blob492bce76b37092d29dc6d6c6eda7f574f69c6847
1 #!/bin/bash
3 # Copyright (c) Josef "Jeff" Sipek, 2006, 2007
6 GUILT_VERSION="0.22"
7 GUILT_NAME="Missing Pieces"
9 # we change directories ourselves
10 SUBDIRECTORY_OK=1
12 . git-sh-setup
14 function guilt_commands
16 local command
17 for command in $0-*
19 if [ -f "$command" -a -x "$command" ]
20 then
21 echo ${command##$0-}
23 done
26 if [ `basename $0` = "guilt" ]; then
27 # being run as standalone
29 # by default, we shouldn't fail
30 cmd=
32 if [ $# -ne 0 ]; then
33 # take first arg, and try to execute it
35 arg="$1"
36 dir=`dirname $0`
38 if [ -x "$dir/guilt-$arg" ]; then
39 cmd=$arg
40 else
41 # might be a short handed
42 for command in $(guilt_commands); do
43 case $command in
44 $arg*)
45 if [ -x "$dir/guilt-$command" ]; then
46 cmd=$command
49 esac
50 done
52 if [ $cmd ]; then
53 shift
54 exec "$dir/guilt-$cmd" "$@"
56 # this is not reached because of the exec
57 die "Exec failed! Something is terribly wrong!"
58 else
59 echo "Command $arg not found" >&2
60 echo "" >&2
64 # no args passed or invalid command entered, just output help summary
66 echo "Guilt v$GUILT_VERSION"
67 echo ""
68 echo "Pick a command:"
69 for x in `dirname $0`/guilt-*; do
70 [ -x $x ] && echo -e ${x##$0-}
71 done | sort | column | column -t | sed -e $'s/^/\t/'
73 echo ""
74 echo "Example:"
75 echo -e "\tguilt-push"
76 echo "or"
77 echo -e "\tguilt push"
79 # now, let's exit
80 exit 1
83 ########
86 # Library goodies
89 # usage: valid_patchname <patchname>
90 function valid_patchname
92 [ `echo "$1" | grep -e '^/' | wc -l` -gt 0 ] && return 1
93 [ `echo "$1" | grep -e '^\./' | wc -l` -gt 0 ] && return 1
94 [ `echo "$1" | grep -e '^\.\./' | wc -l` -gt 0 ] && return 1
95 [ `echo "$1" | grep -e '/\./' | wc -l` -gt 0 ] && return 1
96 [ `echo "$1" | grep -e '/\.\./' | wc -l` -gt 0 ] && return 1
97 [ `echo "$1" | grep -e '/\.$' | wc -l` -gt 0 ] && return 1
98 [ `echo "$1" | grep -e '/\.\.$' | wc -l` -gt 0 ] && return 1
99 [ `echo "$1" | grep -e '/$' | wc -l` -gt 0 ] && return 1
100 return 0
103 function get_branch
105 git-symbolic-ref HEAD | sed -e 's,^refs/heads/,,'
108 function verify_branch
110 local b=$branch
112 [ ! -d "$GIT_DIR/patches" ] &&
113 echo "Patches directory doesn't exist, try guilt-init" >&2 &&
114 return 1
115 [ ! -d "$GIT_DIR/patches/$b" ] &&
116 echo "Branch $b is not initialized, try guilt-init" >&2 &&
117 return 1
118 [ ! -f "$GIT_DIR/patches/$b/series" ] &&
119 echo "Branch $b does not have a series file" >&2 &&
120 return 1
121 [ ! -f "$GIT_DIR/patches/$b/status" ] &&
122 echo "Branch $b does not have a status file" >&2 &&
123 return 1
124 [ -f "$GIT_DIR/patches/$b/applied" ] &&
125 echo "Warning: Branch $b has 'applied' file - guilt is not compatible with stgit" >&2 &&
126 return 1
128 return 0
131 function get_top
133 tail -1 "$GUILT_DIR/$branch/status" | cut -d: -f 2-
136 function get_prev
138 local n=`wc -l < "$GUILT_DIR/$branch/status"`
139 local n=`expr $n - 1`
141 local idx=0
142 cat "$GUILT_DIR/$branch/status" | while read p; do
143 idx=`expr $idx + 1`
144 [ $idx -lt $n ] && continue
145 [ $idx -gt $n ] && break
147 echo "$p"
148 done
151 function get_series
153 # ignore all lines matching:
154 # - empty lines
155 # - whitespace only
156 # - optional whitespace followed by '#' followed by more
157 # optional whitespace
158 grep -ve '^[[:space:]]*\(#.*\)*$' < "$series"
161 # usage: do_make_header <hash>
162 function do_make_header
164 # which revision do we want to work with?
165 local rev="$1"
167 # we should try to work with commit objects only
168 if [ `git-cat-file -t "$rev"` != "commit" ]; then
169 echo "Hash $rev is not a commit object" >&2
170 echo "Aborting..." >&2
171 exit 2
174 # get the author line from the commit object
175 local author=`git-cat-file -p "$rev" | grep -e '^author ' | head -1`
177 # strip the timestamp & '^author ' string
178 author=`echo "$author" | sed -e 's/^author //' -e 's/ [0-9]* [+-]*[0-9][0-9]*$//'`
180 git-cat-file -p "$rev" | awk "
181 BEGIN{ok=0}
182 (ok==1){print \$0; print \"\nFrom: $author\"; ok=2; next}
183 (ok==2){print \$0}
184 /^\$/ && (ok==0){ok=1}
188 # usage: do_get_header patchfile
189 function do_get_header
191 # The complexity arises from the fact that we want to ignore the
192 # From line and the empty line after it if it exists
194 # 2nd line skips the From line
195 # 3rd line skips the empty line right after a From line
196 do_get_full_header "$1" | awk '
197 BEGIN{skip=0}
198 /^From:/{skip=1; next}
199 /^[ \t\f\n\r\v]*$/ && (skip==1){skip=0; next}
200 {print $0}
201 END{}
205 # usage: do_get_full_header patchfile
206 function do_get_full_header
208 # 2nd line checks for the begining of a patch
209 # 3rd line outputs the line if it didn't get pruned by the above rules
210 cat "$1" | awk '
211 BEGIN{ok=1}
212 /^(diff|---)/{ok=0}
213 (ok==1){print $0}
214 END{}
218 # usage: assert_head_check
219 function assert_head_check
221 local eh=`tail -1 < "$applied" | cut -d: -f 1`
223 if ! head_check "$eh"; then
224 die "aborting..."
227 return 0
230 # usage: head_check <expected hash>
231 function head_check
233 # make sure we're not doing funky things to commits that don't
234 # belong to us
235 local ch=`cat "$GIT_DIR/refs/heads/$branch"`
237 # if the expected hash is empty, just return
238 [ -z "$1" ] && return 0
240 if [ "$ch" != "$1" ]; then
241 echo "Expected HEAD commit $1" >&2
242 echo " got $ch" >&2
243 return 1
245 return 0
248 # usage: series_insert_patch <patchname>
249 function series_insert_patch
251 local top=`get_top`
253 if [ ! -z "$top" ]; then
254 sed -i -e "s,^$top\$,$top\n$1," "$series"
255 else
256 echo "$1" > "$series.tmp"
257 cat "$series" >> "$series.tmp"
258 mv "$series.tmp" "$series"
262 # usage: pop_patch
263 function pop_patch
265 pop_many_patches HEAD^ 1
268 # usage: pop_many_patches <commitish> <number of patches>
269 function pop_many_patches
271 assert_head_check
273 cd "$TOP_DIR"
275 git-reset --hard "$1" > /dev/null
276 head -n "-$2" < "$applied" > "$applied.tmp"
277 mv "$applied.tmp" "$applied"
279 cd - 2>&1 >/dev/null
281 # update references to top, bottom, and base
282 update_stack_tags
285 # usage: update_stack_tags
286 function update_stack_tags
288 # bail if autotagging is not enabled
289 if [ $autotag -eq 0 ]; then
290 return 0
293 if [ `wc -l < $applied` -gt 0 ]; then
294 # there are patches applied, therefore we must get the top,
295 # bottom and base hashes, and update the tags
297 local top_hash=`git-rev-parse HEAD`
298 local bottom_hash=`head -1 < $applied | cut -d: -f1`
299 local base_hash=`git-rev-parse $bottom_hash^`
301 echo $top_hash > "$GIT_DIR/refs/tags/${branch}_top"
302 echo $bottom_hash > "$GIT_DIR/refs/tags/${branch}_bottom"
303 echo $base_hash > "$GIT_DIR/refs/tags/${branch}_base"
304 else
305 # there are no patches applied, therefore we must remove the
306 # tags to old top, bottom, and base
308 rm -f "$GIT_DIR/refs/tags/${branch}_top"
309 rm -f "$GIT_DIR/refs/tags/${branch}_bottom"
310 rm -f "$GIT_DIR/refs/tags/${branch}_base"
314 # usage: push_patch patchname [bail_action]
315 function push_patch
317 local p="$GUILT_DIR/$branch/$1"
318 local pname="$1"
319 local bail_action="$2"
321 local bail=0
322 local reject="--reject"
324 assert_head_check
326 cd "$TOP_DIR"
328 # apply the patch if and only if there is something to apply
329 if [ `git-apply --numstat "$p" | wc -l` -gt 0 ]; then
330 if [ "$bail_action" = abort ]; then
331 reject=""
333 git-apply -C$guilt_push_diff_context \
334 $reject "$p" > /dev/null 2> /tmp/guilt.log.$$
335 bail=$?
337 if [ $bail -ne 0 ]; then
338 cat /tmp/guilt.log.$$ >&2
339 if [ "$bail_action" = abort ]; then
340 return $bail
344 # FIXME: Path munging is being done, we need to convince
345 # git-apply to just give us list of files with \0 as a
346 # delimiter, and pass -z to git-update-index
347 git-apply --numstat "$p" | cut -f 3- | git-update-index --add --remove --stdin
350 # grab a commit message out of the patch
351 do_get_header "$p" > /tmp/guilt.msg.$$
353 # make a default commit message if patch doesn't contain one
354 [ ! -s /tmp/guilt.msg.$$ ] && echo "patch $pname" > /tmp/guilt.msg.$$
356 # extract a From line from the patch header, and set
357 # GIT_AUTHOR_{NAME,EMAIL}
358 local author_str=`cat "$p" | grep -e '^From: ' | sed -e 's/^From: //'`
359 if [ ! -z "$author_str" ]; then
360 local backup_author_name="$GIT_AUTHOR_NAME"
361 local backup_author_email="$GIT_AUTHOR_EMAIL"
362 GIT_AUTHOR_NAME=`echo $author_str | sed -e 's/ *<.*$//'`
363 GIT_AUTHOR_EMAIL=`echo $author_str | sed -e 's/[^<]*//'`
365 if [ -z "$GIT_AUTHOR_NAME" ]; then
366 GIT_AUTHOR_NAME=" "
369 export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL
371 local backup_author_date="$GIT_AUTHOR_DATE"
372 local backup_committer_date="$GIT_COMMITTER_DATE"
373 export GIT_AUTHOR_DATE=`stat -c %y "$p"`
374 export GIT_COMMITTER_DATE=$GIT_AUTHOR_DATE
376 # commit
377 local treeish=`git-write-tree`
378 local commitish=`git-commit-tree $treeish -p HEAD < /tmp/guilt.msg.$$`
379 echo $commitish > $GIT_DIR/`git-symbolic-ref HEAD`
381 # mark patch as applied
382 echo "$commitish:$pname" >> $applied
384 cd - 2>&1 >/dev/null
386 # update references to top, bottom, and base of the stack
387 update_stack_tags
389 # restore original GIT_AUTHOR_{NAME,EMAIL}
390 if [ ! -z "$author_str" ]; then
391 if [ ! -z "$backup_author_name" ]; then
392 export GIT_AUTHOR_NAME="$backup_author_name"
393 else
394 unset GIT_AUTHOR_NAME
397 if [ ! -z "$backup_author_name" ]; then
398 export GIT_AUTHOR_EMAIL="$backup_author_email"
399 else
400 unset GIT_AUTHOR_EMAIL
403 if [ ! -z "$backup_author_date" ]; then
404 export GIT_AUTHOR_DATE="$backup_author_date"
405 else
406 unset GIT_AUTHOR_DATE
408 if [ ! -z "$backup_committer_date" ]; then
409 export GIT_COMMITTER_DATE="$backup_committer_date"
410 else
411 unset GIT_COMMITTER_DATE
414 rm -f /tmp/guilt.msg.$$ /tmp/guilt.log.$$
416 return $bail
419 # usage: must_commit_first
420 function must_commit_first
422 [ `git-diff-files | wc -l` -eq 0 ]
423 return $?
426 # usage: refresh_patch patchname
427 function refresh_patch
429 local p="$GUILT_DIR/$branch/$1"
431 assert_head_check
433 cd "$TOP_DIR"
435 git-diff-files --name-only | (while read n; do git-update-index "$n" ; done)
437 # get the patch header
438 do_get_full_header "$p" > /tmp/guilt.diff.$$
440 # get the new patch
441 git-diff HEAD^ >> /tmp/guilt.diff.$$
443 # move the new patch in
444 mv "$p" "$p~"
445 mv /tmp/guilt.diff.$$ $p
447 cd - 2>&1 >/dev/null
449 # drop the currently applied patch, pop_patch does it's own cd
450 # $TOP_DIR
451 pop_patch
453 # push_patch does it's own cd $TOP_DIR
454 push_patch "$1"
457 # usage: munge_hash_range <hash range>
459 # this means:
460 # <hash> - one commit
461 # <hash>.. - hash until head (excludes hash, includes head)
462 # ..<hash> - until hash (includes hash)
463 # <hash1>..<hash2> - from hash to hash (inclusive)
465 # The output of this function is suitable to be passed to git-rev-list
466 function munge_hash_range
468 local l=`echo "$1" | sed -e 's/\.\./ /'`
470 local h1=`echo "$l" | cut -s -d' ' -f 1`
471 local h2=`echo "$l" | cut -s -d' ' -f 2`
473 if [ -z "$h1" -a -z "$h2" ]; then
474 # e.g., "v0.19"
475 echo "$l^..$l"
476 elif [ -z "$h1" ]; then
477 # e.g., "..v0.10"
478 echo "$h2"
479 elif [ -z "$h2" ]; then
480 # e.g., "v0.19.."
481 echo "$h1..HEAD"
482 else
483 # e.g., "v0.19-rc1..v0.19"
484 echo "$h1..$h2"
489 # Some constants
492 # used for: git-apply -C <val>
493 guilt_push_diff_context=1
496 # Parse any part of .git/config that belongs to us
499 # autotag?
500 autotag=`git-config guilt.autotag`
501 [ -z "$autotag" ] && autotag=1
504 # The following gets run every time this file is source'd
507 TOP_DIR=`git-rev-parse --show-cdup`
508 if [ -z "$TOP_DIR" ]; then
509 TOP_DIR="./"
512 GUILT_DIR="$GIT_DIR/patches"
514 branch=`get_branch`
516 # most of the time we want to verify that the repo's branch has been
517 # initialized, but every once in a blue moon (e.g., we want to run guilt-init),
518 # we must avoid the checks
519 if [ -z "$DO_NOT_CHECK_BRANCH_EXISTENCE" ]; then
520 verify_branch || exit 1
523 # very useful files
524 series="$GUILT_DIR/$branch/series"
525 applied="$GUILT_DIR/$branch/status"
527 # determine an editor to use for anything interactive (fall back to vi)
528 editor="vi"
529 [ ! -z "$EDITOR" ] && editor="$EDITOR"
531 # determine a pager to use for anything interactive (fall back to more)
532 pager="more"
533 [ -z "$pager" ] && pager="$PAGER"