tg: silently ignore a missing .topdeps and treat it as empty
[topgit/pro.git] / tg.sh
bloba1b055349fd85c3519ff5a0c2ab5107fbe2408a5
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.13
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 hook_call="\"\$($tg --hooks-path)\"/$1 \"\$@\""
147 if [ -f "$git_dir/hooks/$1" ] &&
148 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 \"$tg\" > /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 recurse_deps_int()
266 if ! ref_exists "$1"; then
267 [ -z "$2" ] || echo "1 0 $*"
268 continue;
271 # If no_remotes is unset also check our base against remote base.
272 # Checking our head against remote head has to be done in the helper.
273 if test -z "$no_remotes" && has_remote "top-bases/$1"; then
274 echo "0 0 refs/remotes/$base_remote/top-bases/$1 $*"
277 _is_tgish=0
278 if ref_exists "refs/top-bases/$1"; then
279 _is_tgish=1
281 # if the branch was annihilated, it is considered to have no dependencies
282 if ! branch_annihilated "$1"; then
283 #TODO: handle nonexisting .topdeps?
284 git cat-file blob "$1:.topdeps" 2>/dev/null |
285 while read _dname; do
286 # Shoo shoo, keep our environment alone!
287 (recurse_deps_int "$_dname" "$@")
288 done
292 [ -z "$2" ] || echo "0 $_is_tgish $*"
295 # do_eval CMD
296 # helper for recurse_deps so that a return statement executed inside CMD
297 # does not return from recurse_deps. This shouldn't be necessary, but it
298 # seems that it actually is.
299 do_eval()
301 eval "$@"
304 # recurse_deps CMD NAME [BRANCHPATH...]
305 # Recursively eval CMD on all dependencies of NAME.
306 # Dependencies are visited in topological order.
307 # CMD can refer to $_name for queried branch name,
308 # $_dep for dependency name,
309 # $_depchain for space-seperated branch backtrace,
310 # $_dep_missing boolean to check whether $_dep is present
311 # and the $_dep_is_tgish boolean.
312 # It can modify $_ret to affect the return value
313 # of the whole function.
314 # If recurse_deps() hits missing dependencies, it will append
315 # them to space-separated $missing_deps list and skip them
316 # affter calling CMD with _dep_missing set.
317 # remote dependencies are processed if no_remotes is unset.
318 recurse_deps()
320 _cmd="$1"; shift
322 _depsfile="$(get_temp tg-depsfile)"
323 recurse_deps_int "$@" >>"$_depsfile"
325 _ret=0
326 while read _ismissing _istgish _dep _name _deppath; do
327 _depchain="$_name${_deppath:+ $_deppath}"
328 _dep_is_tgish=
329 [ "$_istgish" = "0" ] || _dep_is_tgish=1
330 _dep_missing=
331 if [ "$_ismissing" != "0" ]; then
332 _dep_missing=1
333 case " $missing_deps " in *" $_dep "*) :;; *)
334 missing_deps="${missing_deps:+$missing_deps }$_dep"
335 esac
337 do_eval "$_cmd"
338 done <"$_depsfile"
339 rm -f "$_depsfile"
340 return $_ret
343 # branch_needs_update
344 # This is a helper function for determining whether given branch
345 # is up-to-date wrt. its dependencies. It expects input as if it
346 # is called as a recurse_deps() helper.
347 # In case the branch does need update, it will echo it together
348 # with the branch backtrace on the output (see needs_update()
349 # description for details) and set $_ret to non-zero.
350 branch_needs_update()
352 if [ -n "$_dep_missing" ]; then
353 echo "! $_dep $_depchain"
354 return 0
357 _dep_base_update=
358 if [ -n "$_dep_is_tgish" ]; then
359 branch_annihilated "$_dep" && return 0
361 if has_remote "$_dep"; then
362 branch_contains "$_dep" "refs/remotes/$base_remote/$_dep" || _dep_base_update=%
364 # This can possibly override the remote check result;
365 # we want to sync with our base first
366 branch_contains "$_dep" "refs/top-bases/$_dep" || _dep_base_update=:
369 if [ -n "$_dep_base_update" ]; then
370 # _dep needs to be synced with its base/remote
371 echo "$_dep_base_update $_dep $_depchain"
372 _ret=1
373 elif [ -n "$_name" ] && ! branch_contains "refs/top-bases/$_name" "$_dep"; then
374 # Some new commits in _dep
375 echo "$_dep $_depchain"
376 _ret=1
380 # needs_update NAME
381 # This function is recursive; it outputs reverse path from NAME
382 # to the branch (e.g. B_DIRTY B1 B2 NAME), one path per line,
383 # inner paths first. Innermost name can be ':' if the head is
384 # not in sync with the base, '%' if the head is not in sync
385 # with the remote (in this order of priority) or '!' if depednecy
386 # is missing.
387 # It will also return non-zero status if NAME needs update.
388 # If needs_update() hits missing dependencies, it will append
389 # them to space-separated $missing_deps list and skip them.
390 needs_update()
392 recurse_deps branch_needs_update "$@"
395 # branch_empty NAME [-i | -w]
396 branch_empty()
398 [ "$(pretty_tree "$1" -b)" = "$(pretty_tree "$1" ${2-})" ]
401 # list_deps [-i | -w]
402 # -i/-w apply only to HEAD
403 list_deps()
405 head_from=${1-}
406 head="$(git symbolic-ref -q HEAD)" ||
407 head="..detached.."
409 git for-each-ref refs/top-bases |
410 while read rev type ref; do
411 name="${ref#refs/top-bases/}"
412 if branch_annihilated "$name"; then
413 continue;
416 from=$head_from
417 [ "refs/heads/$name" = "$head" ] ||
418 from=
419 cat_file "$name:.topdeps" $from | while read dep; do
420 dep_is_tgish=true
421 ref_exists "refs/top-bases/$dep" ||
422 dep_is_tgish=false
423 if ! "$dep_is_tgish" || ! branch_annihilated $dep; then
424 echo "$name $dep"
426 done
427 done
430 # switch_to_base NAME [SEED]
431 switch_to_base()
433 _base="refs/top-bases/$1"; _seed="$2"
434 # We have to do all the hard work ourselves :/
435 # This is like git checkout -b "$_base" "$_seed"
436 # (or just git checkout "$_base"),
437 # but does not create a detached HEAD.
438 git read-tree -u -m HEAD "${_seed:-$_base}"
439 [ -z "$_seed" ] || git update-ref "$_base" "$_seed"
440 git symbolic-ref HEAD "$_base"
443 # Show the help messages.
444 do_help()
446 _www=
447 if [ "$1" = "-w" ]; then
448 _www=1
449 shift
451 if [ -z "$1" ] ; then
452 # This is currently invoked in all kinds of circumstances,
453 # including when the user made a usage error. Should we end up
454 # providing more than a short help message, then we should
455 # differentiate.
456 # Petr's comment: http://marc.info/?l=git&m=122718711327376&w=2
458 ## Build available commands list for help output
460 cmds=
461 sep=
462 for cmd in "@cmddir@"/tg-*; do
463 ! [ -r "$cmd" ] && continue
464 # strip directory part and "tg-" prefix
465 cmd="$(basename "$cmd")"
466 cmd="${cmd#tg-}"
467 cmds="$cmds$sep$cmd"
468 sep="|"
469 done
471 echo "TopGit version $TG_VERSION - A different patch queue manager"
472 echo "Usage: tg ( help [-w] [<command>] | [-r <remote>] ($cmds) ...)"
473 echo "Use \"tg help tg\" for overview of TopGit"
474 elif [ -r "@cmddir@"/tg-$1 -o -r "@sharedir@/tg-$1.txt" ] ; then
475 if [ -n "$_www" ]; then
476 nohtml=
477 if ! [ -r "@sharedir@/topgit.html" ]; then
478 echo "`basename $0`: missing html help file:" \
479 "@sharedir@/topgit.html" 1>&2
480 nohtml=1
482 if ! [ -r "@sharedir@/tg-$1.html" ]; then
483 echo "`basename $0`: missing html help file:" \
484 "@sharedir@/tg-$1.html" 1>&2
485 nohtml=1
487 if [ -n "$nohtml" ]; then
488 echo "`basename $0`: use" \
489 "\"`basename $0` help $1\" instead" 1>&2
490 exit 1
492 git web--browse -c help.browser "@sharedir@/tg-$1.html"
493 exit
495 setup_pager
497 if [ -r "@cmddir@"/tg-$1 ] ; then
498 "@cmddir@"/tg-$1 -h 2>&1 || :
499 echo
501 if [ -r "@sharedir@/tg-$1.txt" ] ; then
502 cat "@sharedir@/tg-$1.txt"
504 } | "$TG_PAGER"
505 else
506 echo "`basename $0`: no help for $1" 1>&2
507 do_help
508 exit 1
512 ## Pager stuff
514 # isatty FD
515 isatty()
517 test -t $1
520 # setup_pager
521 # Set TG_PAGER to a valid executable
522 # After calling, code to be paged should be surrounded with {...} | "$TG_PAGER"
523 setup_pager()
525 isatty 1 || { TG_PAGER=cat; return 0; }
527 if [ -z "$TG_PAGER_IN_USE" ]; then
528 # TG_PAGER = GIT_PAGER | PAGER | less
529 # NOTE: GIT_PAGER='' is significant
530 if [ -n "${GIT_PAGER+set}" ]; then
531 TG_PAGER="$GIT_PAGER"
532 elif [ -n "${PAGER+set}" ]; then
533 TG_PAGER="$PAGER"
534 else
535 TG_PAGER="less"
537 : ${TG_PAGER:=cat}
538 else
539 TG_PAGER=cat
542 # Set pager default environment variables
543 # see pager.c:setup_pager
544 if [ -z "${LESS+set}" ]; then
545 export LESS="-FRSX"
547 if [ -z "${LV+set}" ]; then
548 export LV="-c"
551 # this is needed so e.g. `git diff` will still colorize it's output if
552 # requested in ~/.gitconfig with color.diff=auto
553 export GIT_PAGER_IN_USE=1
555 # this is needed so we don't get nested pagers
556 export TG_PAGER_IN_USE=1
559 # get_temp NAME [-d]
560 # creates a new temporary file (or directory with -d) in the global
561 # temporary directory $tg_tmp_dir with pattern prefix NAME
562 get_temp()
564 mktemp ${2-} "$tg_tmp_dir/$1.XXXXXX"
567 ## Startup
569 [ -d "@cmddir@" ] ||
570 die "No command directory: '@cmddir@'"
572 ## Initial setup
574 cmd="$1"
575 [ -z "$tg__include" ] || cmd="include" # ensure setup happens
576 case "$cmd" in
577 help|--help|-h)
580 if [ -n "$cmd" ]; then
581 set -e
582 # suppress the merge log editor feature since git 1.7.10
583 export GIT_MERGE_AUTOEDIT=no
584 git_dir="$(git rev-parse --git-dir)"
585 root_dir="$(git rev-parse --show-cdup)"; root_dir="${root_dir:-.}"
586 # Make sure root_dir doesn't end with a trailing slash.
587 root_dir="${root_dir%/}"
588 base_remote="$(git config topgit.remote 2>/dev/null)" || :
589 tg="tg"
590 # make sure merging the .top* files will always behave sanely
591 setup_ours
592 setup_hook "pre-commit"
593 # create global temporary directories, inside GIT_DIR
594 tg_tmp_dir="$(mktemp -d "$git_dir/tg-tmp.XXXXXX")"
595 trap "rm -rf \"$tg_tmp_dir\"" EXIT
597 esac
599 ## Dispatch
601 # We were sourced from another script for our utility functions;
602 # this is set by hooks. Skip the rest of the file. A simple return doesn't
603 # work as expected in every shell. See http://bugs.debian.org/516188
604 if [ -z "$tg__include" ]; then
606 if [ "$1" = "-r" ]; then
607 shift
608 if [ -z "$1" ]; then
609 echo "Option -r requires an argument." >&2
610 do_help
611 exit 1
613 base_remote="$1"; shift
614 tg="$tg -r $base_remote"
615 cmd="$1"
618 [ -n "$cmd" ] || { do_help; exit 1; }
619 shift
621 case "$cmd" in
622 help|--help|-h)
623 do_help "$@"
624 exit 0;;
625 --hooks-path)
626 # Internal command
627 echo "@hooksdir@";;
629 [ -r "@cmddir@"/tg-$cmd ] || {
630 echo "Unknown subcommand: $cmd" >&2
631 do_help
632 exit 1
634 . "@cmddir@"/tg-$cmd;;
635 esac
639 # vim:noet