From: Ian Jackson Date: Wed, 28 Dec 2011 01:56:26 +0000 (+0000) Subject: Merge commit 'refs/top-bases/fixes/tg--r-require-arg' into fixes/tg--r-require-arg X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=topgit.git;a=commitdiff_plain;h=7fc96160f2d30e75194eb3d8958a4f910a101735;hp=f1b31d39c87bbd77f8c4a243e4e639e7d7f453dd Merge commit 'refs/top-bases/fixes/tg--r-require-arg' into fixes/tg--r-require-arg --- diff --git a/.gitignore b/.gitignore index 2f6d991..6cfab6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,48 @@ +# Emacs auto-save files +*~ + +# vim swap files +.*.swp + +# quilt working directory +/.pc + +# .deb build stamp +/build-stamp + /hooks/pre-commit +/tg +/tg-base +/tg-base.txt /tg-create /tg-create.txt /tg-delete /tg-delete.txt /tg-depend /tg-depend.txt +/tg-export +/tg-export.txt +/tg-files +/tg-files.txt +/tg-import +/tg-import.txt /tg-info /tg-info.txt /tg-mail /tg-mail.txt +/tg-next +/tg-next.txt +/tg-log +/tg-log.txt /tg-patch /tg-patch.txt +/tg-prev +/tg-prev.txt +/tg-push +/tg-push.txt +/tg-remote +/tg-remote.txt /tg-summary /tg-summary.txt /tg-update /tg-update.txt -/tg-export -/tg-export.txt -/tg-import -/tg-import.txt -/tg-remote -/tg-remote.txt -/tg-push -/tg-push.txt -/tg -.*.swp - -# quilt working directory -/.pc - -# .deb build stamp -/build-stamp diff --git a/README b/README index c418ff4..ed8d358 100644 --- a/README +++ b/README @@ -272,6 +272,14 @@ tg depend TODO: Subcommand for removing dependencies, obviously +tg files +~~~~~~~~ + List files changed by the current or specified topic branch. + + Options: + -i list files based on index instead of branch + -w list files based on working tree instead of branch + tg info ~~~~~~~ Show a summary information about the current or specified @@ -315,6 +323,10 @@ tg mail to let `git send-email` ask for confirmation before sending any mail. + Options: + -i base patch generation on index instead of branch + -w base patch generation on working tree instead of branch + TODO: 'tg mail patchfile' to mail an already exported patch TODO: mailing patch series TODO: specifying additional options and addresses on command @@ -327,7 +339,7 @@ tg remote and 'git push' to operate on them. (Do NOT use 'git push --all' for your pushes - plain 'git push' will do the right thing.) - It takes a mandatory remote name argument, and optional + It takes a optional remote name argument, and optional '--populate' switch - use that for your origin-style remote, it will seed the local topic branch system based on the remote topic branches. '--populate' will also make 'tg remote' @@ -351,6 +363,21 @@ tg summary pass '--graphviz' to get a dot-suitable output to draw a dependency graph between the topic branches. + You can also use the --sort option to sort the branches using + a topological sort. This is especially useful if each + TopGit-tracked topic branch depends on a single parent branch, + since it will then print the branches in the dependency + order. In more complex scenarios, a text graph view would be + much more useful, but that is not yet implemented. + + The --deps option outputs dependency informations between + branches in a machine-readable format. Feed this to "tsort" + to get the output from --sort. + + Options: + -i Use TopGit meta data from the index instead of branch + -w Use TopGit meta data from the working tree instead of branch + TODO: Speed up by an order of magnitude TODO: Text graph view @@ -472,13 +499,15 @@ tg import tg update ~~~~~~~~~ - Update the current topic branch wrt. changes in the branches - it depends on and remote branches. + Update the current or specified topic branch wrt. changes in the + branches it depends on and remote branches. This is performed in two phases - first, changes within the dependencies are merged to the base, then the base is merged into the topic branch. The output will guide you in case of conflicts. + After the update the current branch is the specified one. + In case your dependencies are not up-to-date, tg update will first recurse into them and update these. @@ -497,8 +526,37 @@ tg push repository. By default the remote gets all dependencies (both tgish and non-tgish) and bases pushed to. -TODO: tg rename +tg base +~~~~~~~ + Prints the base commit of the current topic branch. Silently + exits with exit code 1 if you are not working on a TopGit + branch. + +tg log +~~~~~~ + Prints the git log of the named topgit branch. + Note: if you have merged changes from a different repository, this + command might not list all interesting commits. + +tg prev +~~~~~~~ + Outputs the direct dependencies for the current or named patch. + + Options: + -i show dependencies based on index instead of branch + -w show dependencies based on working tree instead of branch + +tg next +~~~~~~~ + Outputs all patches which directly depend on the current or + named patch. + + Options: + -i show dependencies based on index instead of branch + -w show dependencies based on working tree instead of branch + +TODO: tg rename IMPLEMENTATION -------------- @@ -596,3 +654,13 @@ All commands by default refer to the remote that 'tg remote --populate' was called on the last time ('topgit.remote' configuration variable). You can manually run any command with a different base remote by passing '-r REMOTE' _before_ the subcommand name. + + +POINTERS +-------- + +The following references are useful to understand the development of topgit and +its subcommands. + +tg depend: + http://lists-archives.org/git/688698-add-list-and-rm-sub-commands-to-tg-depend.html diff --git a/contrib/tg-completion.bash b/contrib/tg-completion.bash index 0ee233c..e34b66f 100755 --- a/contrib/tg-completion.bash +++ b/contrib/tg-completion.bash @@ -344,11 +344,29 @@ _tg_info () esac } +_tg_log () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "$cur" in + *) + __tgcomp "$(__tg_topics)" + esac +} + _tg_mail () { local cur="${COMP_WORDS[COMP_CWORD]}" case "$cur" in + -*) + __tgcomp " + -i + -w + -s + -r + " + ;; *) __tgcomp "$(__tg_topics)" esac @@ -416,14 +434,55 @@ _tg_summary () *) __tgcomp " --graphviz + --sort + --deps -t + -i + -w " esac } _tg_update () { - COMPREPLY=() + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "$cur" in + *) + __tgcomp "$(__tg_topics)" + esac +} + +_tg_next () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "$cur" in + -*) + __tgcomp " + -i + -w + " + ;; + *) + __tgcomp "$(__tg_heads)" + esac +} + +_tg_prev () +{ + local cur="${COMP_WORDS[COMP_CWORD]}" + + case "$cur" in + -*) + __tgcomp " + -i + -w + " + ;; + *) + __tgcomp "$(__tg_topics)" + esac } ### }}} @@ -467,11 +526,15 @@ _tg () delete) _tg_delete ;; depend) _tg_depend ;; export) _tg_export ;; + files) _tg_patch ;; help) _tg_help ;; import) _tg_import ;; info) _tg_info ;; + log) _tg_log ;; mail) _tg_mail ;; + next) _tg_next ;; patch) _tg_patch ;; + prev) _tg_prev ;; push) _tg_push ;; remote) _tg_remote ;; summary) _tg_summary ;; diff --git a/hooks/pre-commit.sh b/hooks/pre-commit.sh index 9d677e9..9519560 100644 --- a/hooks/pre-commit.sh +++ b/hooks/pre-commit.sh @@ -20,7 +20,8 @@ tg_util if head_=$(git symbolic-ref -q HEAD); then case "$head_" in refs/heads/*) - git rev-parse -q --verify "refs/top-bases${head_#refs/heads}" >/dev/null || exit 0;; + head_="${head_#refs/heads/}" + git rev-parse -q --verify "refs/top-bases/$head_" >/dev/null || exit 0;; *) exit 0;; esac @@ -29,10 +30,77 @@ else exit 0; fi -# TODO: check the index, not the working copy -[ -s "$root_dir/.topdeps" ] || - die ".topdeps is missing" -[ -s "$root_dir/.topmsg" ] || - die ".topmsg is missing" +check_topfile() +{ + local tree file ls_line type size + tree=$1 + file=$2 -# TODO: Verify .topdeps for valid branch names and against cycles + ls_line="$(git ls-tree --long "$tree" "$file")" || + die "Can't ls tree for $file" + + [ -n "$ls_line" ] || + die "$file is missing" + + # check for type and size + set -- $ls_line + type=$2 + size=$4 + + # check file is of type blob (file) + [ "x$type" = "xblob" ] || + die "$file is not a file" + + # check for positive size + [ "$size" -gt 0 ] || + die "$file has empty size" +} + +tree=$(git write-tree) || + die "Can't write tree" + +check_topfile "$tree" ".topdeps" +check_topfile "$tree" ".topmsg" + +check_cycle_name() +{ + [ "$head_" != "$_dep" ] || + die "TopGit dependencies form a cycle: perpetrator is $_name" +} + +# we only need to check newly added deps and for these if a path exists to the +# current HEAD +git diff --cached "$root_dir/.topdeps" | + awk ' +BEGIN { in_hunk = 0; } +/^@@ / { in_hunk = 1; } +/^\+/ { if (in_hunk == 1) printf("%s\n", substr($0, 2)); } +/^[^@ +-]/ { in_hunk = 0; } +' | + while read newly_added; do + ref_exists "$newly_added" || + die "Invalid branch as dependent: $newly_added" + + # check for self as dep + [ "$head_" != "$newly_added" ] || + die "Can't have myself as dep" + + # deps can be non-tgish but we can't run recurse_deps() on them + ref_exists "refs/top-bases/$newly_added" || + continue + + # recurse_deps uses dfs but takes the .topdeps from the tree, + # therefore no endless loop in the cycle-check + no_remotes=1 recurse_deps check_cycle_name "$newly_added" + done + +# check for repetitions of deps +depdir="$(get_temp tg-depdir -d)" || + die "Can't check for multiple occurrences of deps" +cat_file "$head_:.topdeps" -i | + while read dep; do + [ ! -d "$depdir/$dep" ] || + die "Multiple occurrences of the same dep: $dep" + mkdir -p "$depdir/$dep" || + die "Can't check for multiple occurrences of deps" + done diff --git a/tg-base.sh b/tg-base.sh new file mode 100644 index 0000000..d28cac1 --- /dev/null +++ b/tg-base.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 2008 +# (c) Per Cederqvist 2010 +# GPLv2 + +name="$(git symbolic-ref HEAD | sed 's#^refs/\(heads\|top-bases\)/##')" +base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || exit 1 +echo $base_rev diff --git a/tg-depend.sh b/tg-depend.sh index e1a6f17..474ccda 100644 --- a/tg-depend.sh +++ b/tg-depend.sh @@ -6,13 +6,18 @@ name= +usage() +{ + echo "Usage: tg [...] depend add NAME" >&2 + exit 1 +} + ## Parse options subcmd="$1"; shift || : case "$subcmd" in -h|"") - echo "Usage: tg [...] depend add NAME" >&2 - exit 1;; + usage;; add) ;; *) @@ -23,8 +28,7 @@ while [ -n "$1" ]; do arg="$1"; shift case "$arg" in -*) - echo "Usage: tg [...] depend add NAME" >&2 - exit 1;; + usage;; *) [ -z "$name" ] || die "name already specified ($name)" name="$arg";; @@ -38,11 +42,30 @@ done branchrev="$(git rev-parse --verify "$name" 2>/dev/null)" || die "invalid branch name: $name" +# Check that we are on a TopGit branch. +current_name="$(git symbolic-ref HEAD | sed 's#^refs/\(heads\|top-bases\)/##')" +current_base_rev="$(git rev-parse --short --verify "refs/top-bases/$current_name" 2>/dev/null)" || + die "not a TopGit-controlled branch" + ## Record new dependency +depend_add() +{ + [ "$name" = "$current_name" ] && + die "$name cannot depend on itself." + + { $tg summary --deps; echo "$current_name" "$name"; } | + tsort >/dev/null || + die "tg: that dependency would introduce a dependency loop" + + grep -F -x -e "$name" "$root_dir/.topdeps" >/dev/null && + die "tg: $current_name already depends on $name" + + echo "$name" >>"$root_dir/.topdeps" + git add -f "$root_dir/.topdeps" + git commit -m"New TopGit dependency: $name" + $tg update +} -echo "$name" >>"$root_dir/.topdeps" -git add -f "$root_dir/.topdeps" -git commit -m"New TopGit dependency: $name" -$tg update +depend_$subcmd # vim:noet diff --git a/tg-export.sh b/tg-export.sh index a16c290..486ec94 100644 --- a/tg-export.sh +++ b/tg-export.sh @@ -57,22 +57,11 @@ if [ -z "$branches" ]; then fi -playground="$(mktemp -d -t tg-export.XXXXXX)" -trap 'rm -rf "$playground"' EXIT +playground="$(get_temp tg-export -d)" ## Collapse driver -# pretty_tree NAME -# Output tree ID of a cleaned-up tree without tg's artifacts. -pretty_tree() -{ - (export GIT_INDEX_FILE="$playground/^index" - git read-tree "$1" - git update-index --force-remove ".topmsg" ".topdeps" - git write-tree) -} - create_tg_commit() { name="$1" @@ -113,7 +102,7 @@ collapsed_commit() echo "TopGit-driven merge of branches:" echo cut -f 2 "$playground/$name^parents" - } | git commit-tree "$(pretty_tree "refs/top-bases/$name")" \ + } | git commit-tree "$(pretty_tree "$name" -b)" \ $(for p in $parent; do echo -p $p; done))" fi @@ -228,7 +217,7 @@ linearize() else retmerge=0; - git merge-recursive "$(pretty_tree "refs/top-bases/$_dep")" -- HEAD "$(pretty_tree "refs/heads/$_dep")" || retmerge="$?"; + git merge-recursive "$(pretty_tree "$_dep" -b)" -- HEAD "$(pretty_tree "refs/heads/$_dep")" || retmerge="$?"; if test "x$retmerge" != "x0"; then git rerere; diff --git a/tg-files.sh b/tg-files.sh new file mode 100644 index 0000000..507efcb --- /dev/null +++ b/tg-files.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 2008 +# GPLv2 + +name= +head_from= + + +## Parse options + +while [ -n "$1" ]; do + arg="$1"; shift + case "$arg" in + -i|-w) + [ -z "$head_from" ] || die "-i and -w are mutually exclusive" + head_from="$arg";; + -*) + echo "Usage: tg [...] files [-i | -w] [NAME]" >&2 + exit 1;; + *) + [ -z "$name" ] || die "name already specified ($name)" + name="$arg";; + esac +done + + +head="$(git symbolic-ref HEAD)" +head="${head#refs/heads/}" + +[ -n "$name" ] || + name="$head" +base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || + die "not a TopGit-controlled branch" + +if [ -n "$head_from" ] && [ "$name" != "$head" ]; then + die "$head_from makes only sense for the current branch" +fi + +b_tree=$(pretty_tree "$name" -b) +t_tree=$(pretty_tree "$name" $head_from) + +git diff-tree --name-only -r $b_tree $t_tree + +# vim:noet + diff --git a/tg-info.sh b/tg-info.sh index 7d6a34c..10e257e 100644 --- a/tg-info.sh +++ b/tg-info.sh @@ -51,7 +51,7 @@ fi git cat-file blob "$name:.topdeps" | sed '1{ s/^/Depends: /; n; }; s/^/ /;' -depcheck="$(mktemp -t tg-depcheck.XXXXXX)" +depcheck="$(get_temp tg-depcheck)" missing_deps= needs_update "$name" >"$depcheck" || : if [ -n "$missing_deps" ]; then @@ -72,6 +72,5 @@ if [ -s "$depcheck" ]; then else echo "Up-to-date." fi -rm "$depcheck" # vim:noet diff --git a/tg-log.sh b/tg-log.sh new file mode 100644 index 0000000..8a8d527 --- /dev/null +++ b/tg-log.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 2008 +# (c) Bert Wesarg 2009 +# GPLv2 + +name= + + +## Parse options + +while [ -n "$1" ]; do + arg="$1"; shift + case "$arg" in + --) + break;; + -*) + echo "Usage: tg [...] log [NAME] [-- GIT LOG OPTIONS...]" >&2 + exit 1;; + *) + [ -z "$name" ] || die "name already specified ($name)" + name="$arg";; + esac +done + +[ -n "$name" ] || name="$(git symbolic-ref HEAD | sed 's#^refs/heads/##')" +base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || + die "not a TopGit-controlled branch" + +git log --first-parent --no-merges "$@" "refs/top-bases/$name".."$name" diff --git a/tg-mail.sh b/tg-mail.sh index 8167ade..17ce02c 100644 --- a/tg-mail.sh +++ b/tg-mail.sh @@ -3,6 +3,7 @@ # GPLv2 name= +head_from= send_email_args= in_reply_to= @@ -12,12 +13,15 @@ in_reply_to= while [ -n "$1" ]; do arg="$1"; shift case "$arg" in + -i|-w) + [ -z "$head_from" ] || die "-i and -w are mutually exclusive" + head_from="$arg";; -s) send_email_args="$1"; shift;; -r) in_reply_to="$1"; shift;; -*) - echo "Usage: tg [...] mail [-s SEND_EMAIL_ARGS] [-r REFERENCE_MSGID] [NAME]" >&2 + echo "Usage: tg [...] mail [-s SEND_EMAIL_ARGS] [-r REFERENCE_MSGID] [-i | -w] [NAME]" >&2 exit 1;; *) [ -z "$name" ] || die "name already specified ($name)" @@ -25,7 +29,8 @@ while [ -n "$1" ]; do esac done -[ -n "$name" ] || name="$(git symbolic-ref HEAD | sed 's#^refs/heads/##')" +head="$(git symbolic-ref HEAD | sed 's#^refs/heads/##')" +[ -n "$name" ] || name="$head" base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || die "not a TopGit-controlled branch" @@ -34,9 +39,10 @@ if [ -n "$in_reply_to" ]; then fi -patchfile="$(mktemp -t tg-mail.XXXXXX)" +patchfile="$(get_temp tg-mail)" -$tg patch "$name" >"$patchfile" +# let tg patch sort out whether $head_from makes sense for $name +$tg patch "$name" $head_from >"$patchfile" header="$(sed -e '/^$/,$d' -e "s,','\\\\'',g" "$patchfile")" @@ -54,6 +60,4 @@ people= # NOTE: git-send-email handles cc itself eval git send-email $send_email_args "$people" "$patchfile" -rm "$patchfile" - # vim:noet diff --git a/tg-next.sh b/tg-next.sh new file mode 100644 index 0000000..93dd5b5 --- /dev/null +++ b/tg-next.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 2008 +# (c) Bert Wesarg 2009 +# GPLv2 + +name= +head_from= + + +## Parse options + +while [ -n "$1" ]; do + arg="$1"; shift + case "$arg" in + -i|-w) + [ -z "$head_from" ] || die "-i and -w are mutually exclusive" + head_from="$arg";; + -*) + echo "Usage: tg next [-i | -w] [NAME]" >&2 + exit 1;; + *) + [ -z "$name" ] || die "name already specified ($name)" + name="$arg";; + esac +done + +head="$(git rev-parse --abbrev-ref=loose HEAD)" +[ -n "$name" ] || + name="$head" + +git for-each-ref --format='%(refname)' refs/top-bases | + while read ref; do + parent="${ref#refs/top-bases/}" + + from=$head_from + # select .topdeps source for HEAD branch + [ "x$parent" = "x$head" ] || + from= + + cat_file "$parent:.topdeps" $from | fgrep -qx "$name" || + continue + + echo "$parent" + done diff --git a/tg-patch.sh b/tg-patch.sh index 7bafdfe..9def6e5 100644 --- a/tg-patch.sh +++ b/tg-patch.sh @@ -5,9 +5,7 @@ name= -topic= -diff_opts= -diff_committed_only=yes # will be unset for index/worktree +head_from= ## Parse options @@ -15,13 +13,9 @@ diff_committed_only=yes # will be unset for index/worktree while [ -n "$1" ]; do arg="$1"; shift case "$arg" in - -i) - topic='(i)' - diff_opts="$diff_opts --cached"; - diff_committed_only=;; - -w) - topic='(w)' - diff_committed_only=;; + -i|-w) + [ -z "$head_from" ] || die "-i and -w are mutually exclusive" + head_from="$arg";; -*) echo "Usage: tg [...] patch [-i | -w] [NAME]" >&2 exit 1;; @@ -31,40 +25,56 @@ while [ -n "$1" ]; do esac done +head="$(git symbolic-ref HEAD)" +head="${head#refs/heads/}" -[ -n "$name" -a -z "$diff_committed_only" ] && - die "-i/-w are mutually exclusive with NAME" - -[ -n "$name" ] || name="$(git symbolic-ref HEAD | sed 's#^refs/\(heads\|top-bases\)/##')" +[ -n "$name" ] || + name="$head" base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || die "not a TopGit-controlled branch" -# if not index/worktree, topic is current branch -[ -z "$topic" ] && topic="$name" +if [ -n "$head_from" ] && [ "$name" != "$head" ]; then + die "$head_from makes only sense for the current branch" +fi setup_pager -cat_file "$topic:.topmsg" -echo -[ -n "$(git grep $diff_opts '^[-]--' ${diff_committed_only:+"$name"} -- ".topmsg")" ] || echo '---' - -# Evil obnoxious hack to work around the lack of git diff --exclude -git_is_stupid="$(mktemp -t tg-patch-changes.XXXXXX)" -git diff --name-only $diff_opts "$base_rev" ${diff_committed_only:+"$name"} -- | - fgrep -vx ".topdeps" | - fgrep -vx ".topmsg" >"$git_is_stupid" || : # fgrep likes to fail randomly? -if [ -s "$git_is_stupid" ]; then - cd "$root_dir" - cat "$git_is_stupid" | xargs git diff -a --patch-with-stat $diff_opts "$base_rev" ${diff_committed_only:+"$name"} -- -else + +# put out the commit message +# and put an empty line out, if the last one in the message was not an empty line +# and put out "---" if the commit message does not have one yet +cat_file "$name:.topmsg" $head_from | + awk ' +/^---/ { + has_3dash=1; +} + { + need_empty = 1; + if ($0 == "") + need_empty = 0; + print; +} +END { + if (need_empty) + print ""; + if (!has_3dash) + print "---"; +} +' + +b_tree=$(pretty_tree "$name" -b) +t_tree=$(pretty_tree "$name" $head_from) + +if [ $b_tree = $t_tree ]; then echo "No changes." +else + git diff-tree -p --stat $b_tree $t_tree fi -rm "$git_is_stupid" echo '-- ' -echo "tg: ($base_rev..) $name (depends on: $(cat_file "$topic:.topdeps" | paste -s -d' '))" +echo "tg: ($base_rev..) $name (depends on: $(cat_file "$name:.topdeps" $head_from | paste -s -d' '))" branch_contains "$name" "$base_rev" || echo "tg: The patch is out-of-date wrt. the base! Run \`$tg update\`." diff --git a/tg-prev.sh b/tg-prev.sh new file mode 100644 index 0000000..1f1e0c1 --- /dev/null +++ b/tg-prev.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 2008 +# (c) Bert Wesarg 2009 +# GPLv2 + +name= +head_from= + + +## Parse options + +while [ -n "$1" ]; do + arg="$1"; shift + case "$arg" in + -i|-w) + [ -z "$head_from" ] || die "-i and -w are mutually exclusive" + head_from="$arg";; + -*) + echo "Usage: tg next [-i | -w] [NAME]" >&2 + exit 1;; + *) + [ -z "$name" ] || die "name already specified ($name)" + name="$arg";; + esac +done + +head="$(git rev-parse --abbrev-ref=loose HEAD)" +[ -n "$name" ] || + name="$head" +base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || + die "not a TopGit-controlled branch" + +# select .topdeps source for HEAD branch +[ "x$name" = "x$head" ] || + head_from= + +cat_file "$name:.topdeps" $head_from diff --git a/tg-push.sh b/tg-push.sh index 199d738..a928fba 100644 --- a/tg-push.sh +++ b/tg-push.sh @@ -45,8 +45,7 @@ for name in $branches; do ref_exists "$name" || die "detached HEAD? Can't push $name" done -_listfile="$(mktemp -t tg-push-listfile.XXXXXX)" -trap "rm -f \"$_listfile\"" 0 +_listfile="$(get_temp tg-push-listfile)" push_branch() { diff --git a/tg-remote.sh b/tg-remote.sh index 86dcd9a..61774d7 100644 --- a/tg-remote.sh +++ b/tg-remote.sh @@ -15,13 +15,16 @@ while [ -n "$1" ]; do --populate) populate=1;; -*) - echo "Usage: tg [...] remote [--populate] REMOTE" >&2 + echo "Usage: tg [...] remote [--populate] [REMOTE]" >&2 exit 1;; *) name="$arg";; esac done +[ -n "$name" ] || + name="$base_remote" + git config "remote.$name.url" >/dev/null || die "unknown remote '$name'" diff --git a/tg-summary.sh b/tg-summary.sh index 50ee883..1c99e22 100644 --- a/tg-summary.sh +++ b/tg-summary.sh @@ -5,27 +5,37 @@ terse= graphviz= - +sort= +deps= +head_from= ## Parse options while [ -n "$1" ]; do arg="$1"; shift case "$arg" in + -i|-w) + [ -z "$head_from" ] || die "-i and -w are mutually exclusive" + head_from="$arg";; -t) terse=1;; --graphviz) graphviz=1;; + --sort) + sort=1;; + --deps) + deps=1;; *) - echo "Usage: tg [...] summary [-t | --graphviz]" >&2 + echo "Usage: tg [...] summary [-t | --sort | --deps | --graphviz] [-i | -w]" >&2 exit 1;; esac done curname="$(git symbolic-ref HEAD | sed 's#^refs/\(heads\|top-bases\)/##')" -! [ -n "$terse" -a -n "$graphviz" ] || - die "-t and --graphviz options are mutual exclusive" +[ "$terse$graphviz$sort$deps" = "" ] || + [ "$terse$graphviz$sort$deps" = "1" ] || + die "mutually exclusive options given" if [ -n "$graphviz" ]; then cat <$tsort_input + exec 5<$tsort_input +fi ## List branches +process_branch() +{ + missing_deps= + + current=' ' + [ "$name" != "$curname" ] || current='>' + from=$head_from + [ "$name" = "$curname" ] || + from= + nonempty=' ' + ! branch_empty "$name" $from || nonempty='0' + remote=' ' + [ -z "$base_remote" ] || remote='l' + ! has_remote "$name" || remote='r' + rem_update=' ' + [ "$remote" != 'r' ] || ! ref_exists "refs/remotes/$base_remote/top-bases/$name" || { + branch_contains "refs/top-bases/$name" "refs/remotes/$base_remote/top-bases/$name" && + branch_contains "$name" "refs/remotes/$base_remote/$name" + } || rem_update='R' + [ "$rem_update" = 'R' ] || branch_contains "refs/remotes/$base_remote/$name" "$name" 2>/dev/null || + rem_update='L' + deps_update=' ' + needs_update "$name" >/dev/null || deps_update='D' + deps_missing=' ' + [ -z "$missing_deps" ] || deps_missing='!' + base_update=' ' + branch_contains "$name" "refs/top-bases/$name" || base_update='B' + + if [ "$(git rev-parse "$name")" != "$rev" ]; then + subject="$(cat_file "$name:.topmsg" $from | sed -n 's/^Subject: //p')" + else + # No commits yet + subject="(No commits)" + fi + + printf '%s\t%-31s\t%s\n' "$current$nonempty$remote$rem_update$deps_update$deps_missing$base_update" \ + "$name" "$subject" +} + +if [ -n "$deps" ]; then + list_deps $head_from + exit 0 +fi + git for-each-ref refs/top-bases | while read rev type ref; do name="${ref#refs/top-bases/}" if branch_annihilated "$name"; then continue; - fi; + fi if [ -n "$terse" ]; then echo "$name" - continue - fi - if [ -n "$graphviz" ]; then - git cat-file blob "$name:.topdeps" | while read dep; do + elif [ -n "$graphviz$sort" ]; then + from=$head_from + [ "$name" = "$curname" ] || + from= + cat_file "$name:.topdeps" $from | while read dep; do dep_is_tgish=true ref_exists "refs/top-bases/$dep" || dep_is_tgish=false if ! "$dep_is_tgish" || ! branch_annihilated $dep; then - echo "\"$name\" -> \"$dep\";" + if [ -n "$graphviz" ]; then + echo "\"$name\" -> \"$dep\";" + if [ "$name" = "$curname" ] || [ "$dep" = "$curname" ]; then + echo "\"$curname\" [style=filled,fillcolor=yellow];" + fi + else + echo "$name $dep" >&4 + fi fi done - continue - fi - - missing_deps= - - current=' ' - [ "$name" != "$curname" ] || current='>' - nonempty=' ' - ! branch_empty "$name" || nonempty='0' - remote=' ' - [ -z "$base_remote" ] || remote='l' - ! has_remote "$name" || remote='r' - rem_update=' ' - [ "$remote" != 'r' ] || ! ref_exists "refs/remotes/$base_remote/top-bases/$name" || { - branch_contains "refs/top-bases/$name" "refs/remotes/$base_remote/top-bases/$name" && - branch_contains "$name" "refs/remotes/$base_remote/$name" - } || rem_update='R' - [ "$rem_update" = 'R' ] || branch_contains "refs/remotes/$base_remote/$name" "$name" 2>/dev/null || - rem_update='L' - deps_update=' ' - needs_update "$name" >/dev/null || deps_update='D' - deps_missing=' ' - [ -z "$missing_deps" ] || deps_missing='!' - base_update=' ' - branch_contains "$name" "refs/top-bases/$name" || base_update='B' - - if [ "$(git rev-parse "$name")" != "$rev" ]; then - subject="$(git cat-file blob "$name:.topmsg" | sed -n 's/^Subject: //p')" else - # No commits yet - subject="(No commits)" + process_branch fi - - printf '%s\t%-31s\t%s\n' "$current$nonempty$remote$rem_update$deps_update$deps_missing$base_update" \ - "$name" "$subject" done if [ -n "$graphviz" ]; then echo '}' fi +if [ -n "$sort" ]; then + tsort <&5 +fi + + # vim:noet diff --git a/tg-update.sh b/tg-update.sh index 73280c6..5162c52 100644 --- a/tg-update.sh +++ b/tg-update.sh @@ -8,20 +8,26 @@ name= ## Parse options -if [ -n "$1" ]; then - echo "Usage: tg [...] update" >&2 - exit 1 -fi - - -name="$(git symbolic-ref HEAD | sed 's#^refs/\(heads\|top-bases\)/##')" +while [ -n "$1" ]; do + arg="$1"; shift + case "$arg" in + -*) + echo "Usage: tg [...] update [NAME]" >&2 + exit 1;; + *) + [ -z "$name" ] || die "name already specified ($name)" + name="$arg";; + esac +done + +[ -n "$name" ] || name="$(git symbolic-ref HEAD | sed 's#^refs/\(heads\|top-bases\)/##')" base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" || die "not a TopGit-controlled branch" ## First, take care of our base -depcheck="$(mktemp -t tg-depcheck.XXXXXX)" +depcheck="$(get_temp tg-depcheck)" missing_deps= needs_update "$name" >"$depcheck" || : [ -z "$missing_deps" ] || die "some dependencies are missing: $missing_deps" @@ -90,7 +96,6 @@ if [ -s "$depcheck" ]; then else info "The base is up-to-date." fi -rm "$depcheck" # Home, sweet home... # (We want to always switch back, in case we were on the base from failed diff --git a/tg.sh b/tg.sh index 9d08d63..9082d88 100644 --- a/tg.sh +++ b/tg.sh @@ -14,31 +14,83 @@ info() die() { - info "fatal: $*" + info "fatal: $*" >&2 exit 1 } -# cat_file "topic:file" -# Like `git cat-file blob $1`, but topics '(i)' and '(w)' means index and worktree +# cat_file TOPIC:PATH [FROM] +# cat the file PATH from branch TOPIC when FROM is empty. +# FROM can be -i or -w, than the file will be from the index or worktree, +# respectively. The caller should than ensure that HEAD is TOPIC, to make sense. cat_file() { - arg="$1" - case "$arg" in - '(w):'*) - arg=$(echo "$arg" | tail --bytes=+5) - cat "$arg" - return + path="$1" + case "${2-}" in + -w) + cat "$root_dir/${path#*:}" ;; - '(i):'*) + -i) # ':file' means cat from index - arg=$(echo "$arg" | tail --bytes=+5) - git cat-file blob ":$arg" + git cat-file blob ":${path#*:}" + ;; + '') + git cat-file blob "$path" ;; *) - git cat-file blob "$arg" + die "Wrong argument to cat_file: '$2'" + ;; esac } +# get tree for the committed topic +get_tree_() +{ + echo "$1" +} + +# get tree for the base +get_tree_b() +{ + echo "refs/top-bases/$1" +} + +# get tree for the index +get_tree_i() +{ + git write-tree +} + +# get tree for the worktree +get_tree_w() +{ + i_tree=$(git write-tree) + ( + # the file for --index-output needs to sit next to the + # current index file + : ${GIT_INDEX_FILE:="$git_dir/index"} + TMP_INDEX="$(mktemp "${GIT_INDEX_FILE}-tg.XXXXXX")" + git read-tree -m $i_tree --index-output="$TMP_INDEX" && + GIT_INDEX_FILE="$TMP_INDEX" && + export GIT_INDEX_FILE && + git diff --name-only -z HEAD | + git update-index -z --add --remove --stdin && + git write-tree && + rm -f "$TMP_INDEX" + ) +} + +# pretty_tree NAME [-b | -i | -w] +# Output tree ID of a cleaned-up tree without tg's artifacts. +# NAME will be ignored for -i and -w, but needs to be present +pretty_tree() +{ + name=$1 + source=${2#?} + git ls-tree --full-tree "$(get_tree_$source "$name")" | + awk -F ' ' '$2 !~ /^.top/' | + git mktree +} + # setup_hook NAME setup_hook() { @@ -152,7 +204,7 @@ recurse_deps() _name="$1"; # no shift _depchain="$*" - _depsfile="$(mktemp -t tg-depsfile.XXXXXX)" + _depsfile="$(get_temp tg-depsfile)" # If no_remotes is unset check also our base against remote base. # Checking our head against remote head has to be done in the helper. if test -z "$no_remotes" && has_remote "top-bases/$_name"; then @@ -185,7 +237,6 @@ recurse_deps() eval "$_cmd" done <"$_depsfile" missing_deps="${missing_deps# }" - rm "$_depsfile" return $_ret } @@ -233,10 +284,42 @@ needs_update() recurse_deps branch_needs_update "$@" } -# branch_empty NAME +# branch_empty NAME [-i | -w] branch_empty() { - [ -z "$(git diff-tree "refs/top-bases/$1" "$1" -- | fgrep -v " .top")" ] + [ "$(pretty_tree "$1" -b)" = "$(pretty_tree "$1" ${2-})" ] +} + +# list_deps [-i | -w] +# -i/-w apply only to HEAD +list_deps() +{ + local head + local head_from + local from + head_from=${1-} + head="$(git symbolic-ref -q HEAD)" || + head="..detached.." + + git for-each-ref refs/top-bases | + while read rev type ref; do + name="${ref#refs/top-bases/}" + if branch_annihilated "$name"; then + continue; + fi + + from=$head_from + [ "refs/heads/$name" = "$head" ] || + from= + cat_file "$name:.topdeps" $from | while read dep; do + dep_is_tgish=true + ref_exists "refs/top-bases/$dep" || + dep_is_tgish=false + if ! "$dep_is_tgish" || ! branch_annihilated $dep; then + echo "$name $dep" + fi + done + done } # switch_to_base NAME [SEED] @@ -315,19 +398,28 @@ setup_pager() # now spawn pager export LESS="${LESS:-FRSX}" # as in pager.c:pager_preexec() - _pager_fifo_dir="$(mktemp -t -d tg-pager-fifo.XXXXXX)" - _pager_fifo="$_pager_fifo_dir/0" - mkfifo -m 600 "$_pager_fifo" + # setup_pager should be called only once per command + pager_fifo="$tg_tmp_dir/pager" + mkfifo -m 600 "$pager_fifo" - "$TG_PAGER" < "$_pager_fifo" & - exec > "$_pager_fifo" # dup2(pager_fifo.in, 1) + "$TG_PAGER" < "$pager_fifo" & + exec > "$pager_fifo" # dup2(pager_fifo.in, 1) # this is needed so e.g. `git diff` will still colorize it's output if # requested in ~/.gitconfig with color.diff=auto export GIT_PAGER_IN_USE=1 # atexit(close(1); wait pager) - trap "exec >&-; rm \"$_pager_fifo\"; rmdir \"$_pager_fifo_dir\"; wait" EXIT + # deliberately overwrites the global EXIT trap + trap "exec >&-; rm -rf \"$tg_tmp_dir\"; wait" EXIT +} + +# get_temp NAME [-d] +# creates a new temporary file (or directory with -d) in the global +# temporary directory $tg_tmp_dir with pattern prefix NAME +get_temp() +{ + mktemp ${2-} "$tg_tmp_dir/$1.XXXXXX" } ## Startup @@ -347,6 +439,9 @@ tg="tg" # make sure merging the .top* files will always behave sanely setup_ours setup_hook "pre-commit" +# create global temporary directories, inside GIT_DIR +tg_tmp_dir="$(mktemp -d "$git_dir/tg-tmp.XXXXXX")" +trap "rm -rf \"$tg_tmp_dir\"" EXIT ## Dispatch