Set version to 0.14
[topgit/pro.git] / tg.sh
blob3b65f7d2c9909a8c36688955cd3a271969a8e774
1 #!/bin/sh
2 # TopGit - A different patch queue manager
3 # (C) Petr Baudis <pasky@suse.cz> 2008
4 # (C) Kyle J. McKay <mackyle@gmail.com> 2014,2015
5 # GPLv2
7 TG_VERSION=0.14
9 # Update if you add any code that requires a newer version of git
10 GIT_MINIMUM_VERSION=1.7.7.2
12 ## Auxiliary functions
14 info()
16 echo "${TG_RECURSIVE}tg: $*"
19 die()
21 info "fatal: $*" >&2
22 exit 1
25 wc_l()
27 echo $(wc -l)
30 compare_versions()
32 separator="$1"
33 echo "$3" | tr "${separator}" '\n' | (for l in $(echo "$2"|tr "${separator}" ' '); do
34 read r || return 0
35 [ $l -ge $r ] || return 1
36 [ $l -gt $r ] && return 0
37 done)
40 precheck() {
41 git_ver="$(git version | sed -e 's/^[^0-9][^0-9]*//')"
42 compare_versions . "${git_ver%%[!0-9.]*}" "${GIT_MINIMUM_VERSION}" \
43 || die "git version >= ${GIT_MINIMUM_VERSION} required"
46 case "$1" in version|--version|-V)
47 echo "TopGit version $TG_VERSION"
48 exit 0
49 esac
51 precheck
52 [ "$1" = "precheck" ] && exit 0
54 # cat_file TOPIC:PATH [FROM]
55 # cat the file PATH from branch TOPIC when FROM is empty.
56 # FROM can be -i or -w, than the file will be from the index or worktree,
57 # respectively. The caller should than ensure that HEAD is TOPIC, to make sense.
58 cat_file()
60 path="$1"
61 case "${2-}" in
62 -w)
63 cat "$root_dir/${path#*:}"
65 -i)
66 # ':file' means cat from index
67 git cat-file blob ":${path#*:}"
69 '')
70 git cat-file blob "$path"
73 die "Wrong argument to cat_file: '$2'"
75 esac
78 # get tree for the committed topic
79 get_tree_()
81 echo "$1"
84 # get tree for the base
85 get_tree_b()
87 echo "refs/top-bases/$1"
90 # get tree for the index
91 get_tree_i()
93 git write-tree
96 # get tree for the worktree
97 get_tree_w()
99 i_tree=$(git write-tree)
101 # the file for --index-output needs to sit next to the
102 # current index file
103 : ${GIT_INDEX_FILE:="$git_dir/index"}
104 TMP_INDEX="$(mktemp "${GIT_INDEX_FILE}-tg.XXXXXX")"
105 git read-tree -m $i_tree --index-output="$TMP_INDEX" &&
106 GIT_INDEX_FILE="$TMP_INDEX" &&
107 export GIT_INDEX_FILE &&
108 git diff --name-only -z HEAD |
109 git update-index -z --add --remove --stdin &&
110 git write-tree &&
111 rm -f "$TMP_INDEX"
115 # strip_ref "$(git symbolic-ref HEAD)"
116 # Output will have a leading refs/heads/ or refs/top-bases/ stripped if present
117 strip_ref()
119 case "$1" in
120 refs/heads/*)
121 echo "${1#refs/heads/}"
123 refs/top-bases/*)
124 echo "${1#refs/top-bases/}"
127 echo "$1"
128 esac
131 # pretty_tree NAME [-b | -i | -w]
132 # Output tree ID of a cleaned-up tree without tg's artifacts.
133 # NAME will be ignored for -i and -w, but needs to be present
134 pretty_tree()
136 name=$1
137 source=${2#?}
138 git ls-tree --full-tree "$(get_tree_$source "$name")" |
139 awk -F ' ' '$2 !~ /^.top/' |
140 git mktree
143 # setup_hook NAME
144 setup_hook()
146 tgname="$(basename "$0")"
147 hook_call="\"\$(\"$tgname\" --hooks-path)\"/$1 \"\$@\""
148 if [ -f "$git_dir/hooks/$1" ] && fgrep -q "$hook_call" "$git_dir/hooks/$1"; then
149 # Another job well done!
150 return
152 # Prepare incantation
153 if [ -x "$git_dir/hooks/$1" ]; then
154 hook_call="$hook_call"' || exit $?'
155 else
156 hook_call="exec $hook_call"
158 # Don't call hook if tg is not installed
159 hook_call="if which \"$tgname\" > /dev/null; then $hook_call; fi"
160 # Insert call into the hook
162 echo "#!/bin/sh"
163 echo "$hook_call"
164 [ ! -s "$git_dir/hooks/$1" ] || cat "$git_dir/hooks/$1"
165 } >"$git_dir/hooks/$1+"
166 chmod a+x "$git_dir/hooks/$1+"
167 mv "$git_dir/hooks/$1+" "$git_dir/hooks/$1"
170 # setup_ours (no arguments)
171 setup_ours()
173 if [ ! -s "$git_dir/info/attributes" ] || ! grep -q topmsg "$git_dir/info/attributes"; then
174 [ -d "$git_dir/info" ] || mkdir "$git_dir/info"
176 echo ".topmsg merge=ours"
177 echo ".topdeps merge=ours"
178 } >>"$git_dir/info/attributes"
180 if ! git config merge.ours.driver >/dev/null; then
181 git config merge.ours.name '"always keep ours" merge driver'
182 git config merge.ours.driver 'touch %A'
186 # measure_branch NAME [BASE]
187 measure_branch()
189 _bname="$1"; _base="$2"
190 [ -n "$_base" ] || _base="refs/top-bases/$_bname"
191 # The caller should've verified $name is valid
192 _commits="$(git rev-list "$_bname" ^"$_base" -- | wc_l)"
193 _nmcommits="$(git rev-list --no-merges "$_bname" ^"$_base" -- | wc_l)"
194 if [ $_commits -ne 1 ]; then
195 _suffix="commits"
196 else
197 _suffix="commit"
199 echo "$_commits/$_nmcommits $_suffix"
202 # branch_contains B1 B2
203 # Whether B1 is a superset of B2.
204 branch_contains()
206 [ -z "$(git rev-list --max-count=1 ^"$1" "$2" --)" ]
209 # ref_exists REF
210 # Whether REF is a valid ref name
211 ref_exists()
213 git rev-parse --verify "$@" >/dev/null 2>&1
216 # has_remote BRANCH
217 # Whether BRANCH has a remote equivalent (accepts top-bases/ too)
218 has_remote()
220 [ -n "$base_remote" ] && ref_exists "remotes/$base_remote/$1"
223 branch_annihilated()
225 _branch_name="$1";
227 # use the merge base in case the base is ahead.
228 mb="$(git merge-base "refs/top-bases/$_branch_name" "$_branch_name" 2> /dev/null)";
230 test -z "$mb" || test "$(git rev-parse "$mb^{tree}")" = "$(git rev-parse "$_branch_name^{tree}")";
233 non_annihilated_branches()
235 _pattern="$@"
236 git for-each-ref ${_pattern:-refs/top-bases} |
237 while read rev type ref; do
238 name="${ref#refs/top-bases/}"
239 if branch_annihilated "$name"; then
240 continue
242 echo "$name"
243 done
246 # Make sure our tree is clean
247 ensure_clean_tree()
249 git update-index --ignore-submodules --refresh ||
250 die "the working directory has uncommitted changes (see above) - first commit or reset them"
251 [ -z "$(git diff-index --cached --name-status -r --ignore-submodules HEAD --)" ] ||
252 die "the index has uncommited changes"
255 # is_sha1 REF
256 # Whether REF is a SHA1 (compared to a symbolic name).
257 is_sha1()
259 [ "$(git rev-parse "$1")" = "$1" ]
262 # recurse_deps_int NAME [BRANCHPATH...]
263 # get recursive list of dependencies with leading 0 if branch exists 1 if missing
264 # If no_remotes is non-empty, exclude remotes
265 # If recurse_preorder is non-empty, do a preorder rather than postorder traversal
266 recurse_deps_int()
268 if ! ref_exists "$1"; then
269 [ -z "$2" ] || echo "1 0 $*"
270 continue;
273 # If no_remotes is unset also check our base against remote base.
274 # Checking our head against remote head has to be done in the helper.
275 if test -z "$no_remotes" && has_remote "top-bases/$1"; then
276 echo "0 0 refs/remotes/$base_remote/top-bases/$1 $*"
279 _is_tgish=0
280 if ref_exists "refs/top-bases/$1"; then
281 _is_tgish=1
282 [ -z "$recurse_preorder" -o -z "$2" ] || echo "0 $_is_tgish $*"
284 # if the branch was annihilated, it is considered to have no dependencies
285 if ! branch_annihilated "$1"; then
286 #TODO: handle nonexisting .topdeps?
287 git cat-file blob "$1:.topdeps" 2>/dev/null |
288 while read _dname; do
289 # Shoo shoo, keep our environment alone!
290 (recurse_deps_int "$_dname" "$@")
291 done
295 [ -n "$recurse_preorder" -o -z "$2" ] || echo "0 $_is_tgish $*"
298 # do_eval CMD
299 # helper for recurse_deps so that a return statement executed inside CMD
300 # does not return from recurse_deps. This shouldn't be necessary, but it
301 # seems that it actually is.
302 do_eval()
304 eval "$@"
307 # recurse_deps CMD NAME [BRANCHPATH...]
308 # Recursively eval CMD on all dependencies of NAME.
309 # Dependencies are visited in topological order.
310 # CMD can refer to $_name for queried branch name,
311 # $_dep for dependency name,
312 # $_depchain for space-seperated branch backtrace,
313 # $_dep_missing boolean to check whether $_dep is present
314 # and the $_dep_is_tgish boolean.
315 # It can modify $_ret to affect the return value
316 # of the whole function.
317 # If recurse_deps() hits missing dependencies, it will append
318 # them to space-separated $missing_deps list and skip them
319 # affter calling CMD with _dep_missing set.
320 # remote dependencies are processed if no_remotes is unset.
321 recurse_deps()
323 _cmd="$1"; shift
325 _depsfile="$(get_temp tg-depsfile)"
326 recurse_deps_int "$@" >>"$_depsfile"
328 _ret=0
329 while read _ismissing _istgish _dep _name _deppath; do
330 _depchain="$_name${_deppath:+ $_deppath}"
331 _dep_is_tgish=
332 [ "$_istgish" = "0" ] || _dep_is_tgish=1
333 _dep_missing=
334 if [ "$_ismissing" != "0" ]; then
335 _dep_missing=1
336 case " $missing_deps " in *" $_dep "*) :;; *)
337 missing_deps="${missing_deps:+$missing_deps }$_dep"
338 esac
340 do_eval "$_cmd"
341 done <"$_depsfile"
342 rm -f "$_depsfile"
343 return $_ret
346 # branch_needs_update
347 # This is a helper function for determining whether given branch
348 # is up-to-date wrt. its dependencies. It expects input as if it
349 # is called as a recurse_deps() helper.
350 # In case the branch does need update, it will echo it together
351 # with the branch backtrace on the output (see needs_update()
352 # description for details) and set $_ret to non-zero.
353 branch_needs_update()
355 if [ -n "$_dep_missing" ]; then
356 echo "! $_dep $_depchain"
357 return 0
360 _dep_base_update=
361 if [ -n "$_dep_is_tgish" ]; then
362 branch_annihilated "$_dep" && return 0
364 if has_remote "$_dep"; then
365 branch_contains "$_dep" "refs/remotes/$base_remote/$_dep" || _dep_base_update=%
367 # This can possibly override the remote check result;
368 # we want to sync with our base first
369 branch_contains "$_dep" "refs/top-bases/$_dep" || _dep_base_update=:
372 if [ -n "$_dep_base_update" ]; then
373 # _dep needs to be synced with its base/remote
374 echo "$_dep_base_update $_dep $_depchain"
375 _ret=1
376 elif [ -n "$_name" ] && ! branch_contains "refs/top-bases/$_name" "$_dep"; then
377 # Some new commits in _dep
378 echo "$_dep $_depchain"
379 _ret=1
383 # needs_update NAME
384 # This function is recursive; it outputs reverse path from NAME
385 # to the branch (e.g. B_DIRTY B1 B2 NAME), one path per line,
386 # inner paths first. Innermost name can be ':' if the head is
387 # not in sync with the base, '%' if the head is not in sync
388 # with the remote (in this order of priority) or '!' if depednecy
389 # is missing.
390 # It will also return non-zero status if NAME needs update.
391 # If needs_update() hits missing dependencies, it will append
392 # them to space-separated $missing_deps list and skip them.
393 needs_update()
395 recurse_deps branch_needs_update "$@"
398 # branch_empty NAME [-i | -w]
399 branch_empty()
401 [ "$(pretty_tree "$1" -b)" = "$(pretty_tree "$1" ${2-})" ]
404 # list_deps [-i | -w]
405 # -i/-w apply only to HEAD
406 list_deps()
408 head_from=${1-}
409 head="$(git symbolic-ref -q HEAD)" ||
410 head="..detached.."
412 git for-each-ref refs/top-bases |
413 while read rev type ref; do
414 name="${ref#refs/top-bases/}"
415 if branch_annihilated "$name"; then
416 continue;
419 from=$head_from
420 [ "refs/heads/$name" = "$head" ] ||
421 from=
422 cat_file "$name:.topdeps" $from | while read dep; do
423 dep_is_tgish=true
424 ref_exists "refs/top-bases/$dep" ||
425 dep_is_tgish=false
426 if ! "$dep_is_tgish" || ! branch_annihilated $dep; then
427 echo "$name $dep"
429 done
430 done
433 # switch_to_base NAME [SEED]
434 switch_to_base()
436 _base="refs/top-bases/$1"; _seed="$2"
437 # We have to do all the hard work ourselves :/
438 # This is like git checkout -b "$_base" "$_seed"
439 # (or just git checkout "$_base"),
440 # but does not create a detached HEAD.
441 git read-tree -u -m HEAD "${_seed:-$_base}"
442 [ -z "$_seed" ] || git update-ref "$_base" "$_seed"
443 git symbolic-ref HEAD "$_base"
446 # Show the help messages.
447 do_help()
449 _www=
450 if [ "$1" = "-w" ]; then
451 _www=1
452 shift
454 if [ -z "$1" ] ; then
455 # This is currently invoked in all kinds of circumstances,
456 # including when the user made a usage error. Should we end up
457 # providing more than a short help message, then we should
458 # differentiate.
459 # Petr's comment: http://marc.info/?l=git&m=122718711327376&w=2
461 ## Build available commands list for help output
463 cmds=
464 sep=
465 for cmd in "@cmddir@"/tg-*; do
466 ! [ -r "$cmd" ] && continue
467 # strip directory part and "tg-" prefix
468 cmd="$(basename "$cmd")"
469 cmd="${cmd#tg-}"
470 cmds="$cmds$sep$cmd"
471 sep="|"
472 done
474 echo "TopGit version $TG_VERSION - A different patch queue manager"
475 echo "Usage: tg ( help [-w] [<command>] | [-C <dir>] [-r <remote>] ($cmds) ...)"
476 echo "Use \"tg help tg\" for overview of TopGit"
477 elif [ -r "@cmddir@"/tg-$1 -o -r "@sharedir@/tg-$1.txt" ] ; then
478 if [ -n "$_www" ]; then
479 nohtml=
480 if ! [ -r "@sharedir@/topgit.html" ]; then
481 echo "`basename $0`: missing html help file:" \
482 "@sharedir@/topgit.html" 1>&2
483 nohtml=1
485 if ! [ -r "@sharedir@/tg-$1.html" ]; then
486 echo "`basename $0`: missing html help file:" \
487 "@sharedir@/tg-$1.html" 1>&2
488 nohtml=1
490 if [ -n "$nohtml" ]; then
491 echo "`basename $0`: use" \
492 "\"`basename $0` help $1\" instead" 1>&2
493 exit 1
495 git web--browse -c help.browser "@sharedir@/tg-$1.html"
496 exit
498 setup_pager
500 if [ -r "@cmddir@"/tg-$1 ] ; then
501 "@cmddir@"/tg-$1 -h 2>&1 || :
502 echo
504 if [ -r "@sharedir@/tg-$1.txt" ] ; then
505 cat "@sharedir@/tg-$1.txt"
507 } | "$TG_PAGER"
508 else
509 echo "`basename $0`: no help for $1" 1>&2
510 do_help
511 exit 1
515 ## Pager stuff
517 # isatty FD
518 isatty()
520 test -t $1
523 # setup_pager
524 # Set TG_PAGER to a valid executable
525 # After calling, code to be paged should be surrounded with {...} | "$TG_PAGER"
526 setup_pager()
528 isatty 1 || { TG_PAGER=cat; return 0; }
530 if [ -z "$TG_PAGER_IN_USE" ]; then
531 # TG_PAGER = GIT_PAGER | PAGER | less
532 # NOTE: GIT_PAGER='' is significant
533 if [ -n "${GIT_PAGER+set}" ]; then
534 TG_PAGER="$GIT_PAGER"
535 elif [ -n "${PAGER+set}" ]; then
536 TG_PAGER="$PAGER"
537 else
538 TG_PAGER="less"
540 : ${TG_PAGER:=cat}
541 else
542 TG_PAGER=cat
545 # Set pager default environment variables
546 # see pager.c:setup_pager
547 if [ -z "${LESS+set}" ]; then
548 export LESS="-FRSX"
550 if [ -z "${LV+set}" ]; then
551 export LV="-c"
554 # this is needed so e.g. `git diff` will still colorize it's output if
555 # requested in ~/.gitconfig with color.diff=auto
556 export GIT_PAGER_IN_USE=1
558 # this is needed so we don't get nested pagers
559 export TG_PAGER_IN_USE=1
562 # get_temp NAME [-d]
563 # creates a new temporary file (or directory with -d) in the global
564 # temporary directory $tg_tmp_dir with pattern prefix NAME
565 get_temp()
567 mktemp ${2-} "$tg_tmp_dir/$1.XXXXXX"
570 ## Initial setup
571 initial_setup()
573 # suppress the merge log editor feature since git 1.7.10
575 export GIT_MERGE_AUTOEDIT=no
576 git_dir="$(git rev-parse --git-dir)"
577 root_dir="$(git rev-parse --show-cdup)"; root_dir="${root_dir:-.}"
579 # Make sure root_dir doesn't end with a trailing slash.
581 root_dir="${root_dir%/}"
582 [ -n "$base_remote" ] || base_remote="$(git config topgit.remote 2>/dev/null)" || :
584 # create global temporary directories, inside GIT_DIR
586 tg_tmp_dir="$(mktemp -d "$git_dir/tg-tmp.XXXXXX")"
587 trap "rm -rf \"$tg_tmp_dir\"" EXIT
590 ## Startup
592 [ -d "@cmddir@" ] ||
593 die "No command directory: '@cmddir@'"
595 if [ -n "$tg__include" ]; then
597 # We were sourced from another script for our utility functions;
598 # this is set by hooks. Skip the rest of the file. A simple return doesn't
599 # work as expected in every shell. See http://bugs.debian.org/516188
601 # ensure setup happens
603 initial_setup
605 else
607 set -e
609 tg="$0"
610 tgdir="$(dirname "$tg")"
611 tgname="$(basename "$tg")"
613 # If tg contains a '/' but does not start with one then replace it with an absolute path
615 case "$tg" in /*) :;; */*)
616 tgdir="$(cd "$(dirname "$0")" && pwd -P)"
617 tg="$tgdir/$tgname"
618 esac
620 cmd=
621 while :; do case "$1" in
623 help|--help|-h)
624 cmd=help
625 shift
626 break;;
628 --hooks-path)
629 cmd=hooks-path
630 shift
631 break;;
634 shift
635 if [ -z "$1" ]; then
636 echo "Option -r requires an argument." >&2
637 do_help
638 exit 1
640 base_remote="$1"
641 tg="$tgdir/$tgname -r $base_remote"
642 shift;;
645 shift
646 if [ -z "$1" ]; then
647 echo "Option -C requires an argument." >&2
648 do_help
649 exit 1
651 cd "$1"
652 shift;;
655 shift
656 break;;
659 echo "Invalid option $1 (subcommand options must appear AFTER the subcommand)." >&2
660 do_help
661 exit 1;;
664 break;;
666 esac; done
668 [ -n "$cmd" ] || { cmd="$1"; shift; }
670 ## Dispatch
672 [ -n "$cmd" ] || { do_help; exit 1; }
674 case "$cmd" in
676 help)
677 do_help "$@"
678 exit 0;;
680 hooks-path)
681 # Internal command
682 echo "@hooksdir@";;
685 [ -r "@cmddir@"/tg-$cmd ] || {
686 echo "Unknown subcommand: $cmd" >&2
687 do_help
688 exit 1
691 initial_setup
693 # make sure merging the .top* files will always behave sanely
695 setup_ours
696 setup_hook "pre-commit"
698 . "@cmddir@"/tg-$cmd;;
699 esac
703 # vim:noet