chiark / gitweb /
Merge commit 'refs/top-bases/fixes/ensure-worktree' into fixes/ensure-worktree fixes/ensure-worktree
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 28 Dec 2011 01:51:43 +0000 (01:51 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 28 Dec 2011 01:51:43 +0000 (01:51 +0000)
Conflicts:
tg.sh

19 files changed:
.gitignore
README
contrib/tg-completion.bash
hooks/pre-commit.sh
tg-base.sh [new file with mode: 0644]
tg-depend.sh
tg-export.sh
tg-files.sh [new file with mode: 0644]
tg-info.sh
tg-log.sh [new file with mode: 0644]
tg-mail.sh
tg-next.sh [new file with mode: 0644]
tg-patch.sh
tg-prev.sh [new file with mode: 0644]
tg-push.sh
tg-remote.sh
tg-summary.sh
tg-update.sh
tg.sh

index 2f6d991..6cfab6e 100644 (file)
@@ -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 (file)
--- 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
index 0ee233c..e34b66f 100755 (executable)
@@ -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 ;;
index 9d677e9..9519560 100644 (file)
@@ -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 (file)
index 0000000..d28cac1
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# (c) Per Cederqvist <ceder@lysator.liu.se>  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
index e1a6f17..474ccda 100644 (file)
@@ -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
index a16c290..486ec94 100644 (file)
@@ -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 (file)
index 0000000..507efcb
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  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
+
index 7d6a34c..10e257e 100644 (file)
@@ -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 (file)
index 0000000..8a8d527
--- /dev/null
+++ b/tg-log.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# (c) Bert Wesarg <Bert.Wesarg@googlemail.com>  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"
index 8167ade..17ce02c 100644 (file)
@@ -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 (file)
index 0000000..93dd5b5
--- /dev/null
@@ -0,0 +1,45 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# (c) Bert Wesarg <Bert.Wesarg@googlemail.com>  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
index 7bafdfe..9def6e5 100644 (file)
@@ -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 (file)
index 0000000..1f1e0c1
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# (c) Bert Wesarg <Bert.Wesarg@googlemail.com>  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
index 199d738..a928fba 100644 (file)
@@ -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()
 {
index 86dcd9a..61774d7 100644 (file)
@@ -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'"
 
 
index 50ee883..1c99e22 100644 (file)
@@ -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 <<EOT
@@ -47,68 +57,98 @@ graph [
 EOT
 fi
 
+if [ -n "$sort" ]; then
+       tsort_input="$(get_temp tg-summary-sort)"
+       exec 4>$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
index 73280c6..5162c52 100644 (file)
@@ -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 85f0047..87d3424 100644 (file)
--- a/tg.sh
+++ b/tg.sh
@@ -14,7 +14,7 @@ info()
 
 die()
 {
-       info "fatal: $*"
+       info "fatal: $*" >&2
        exit 1
 }
 
@@ -32,27 +32,79 @@ ensure_git_repo_or_die()
        esac
 }
 
-# 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()
 {
@@ -166,7 +218,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
@@ -199,7 +251,6 @@ recurse_deps()
                eval "$_cmd"
        done <"$_depsfile"
        missing_deps="${missing_deps# }"
-       rm "$_depsfile"
        return $_ret
 }
 
@@ -247,10 +298,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]
@@ -329,19 +412,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
@@ -363,6 +455,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