chiark / gitweb /
TopGit - A different patch queue manager
authorPetr Baudis <pasky@suse.cz>
Sat, 2 Aug 2008 19:17:07 +0000 (21:17 +0200)
committerPetr Baudis <pasky@suse.cz>
Sat, 2 Aug 2008 19:17:07 +0000 (21:17 +0200)
Initial commit for TopGit. It is probably still awfully buggy,
but should be actually feature-complete for v0.1 now. The most
basic variants of create, delete, info, patch, summary and update
commands plus a trivial pre-commit hook is available, and we can
deal with recursive updates too.

.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
hooks/pre-commit.sh [new file with mode: 0644]
tg-create.sh [new file with mode: 0644]
tg-delete.sh [new file with mode: 0644]
tg-info.sh [new file with mode: 0644]
tg-patch.sh [new file with mode: 0644]
tg-summary.sh [new file with mode: 0644]
tg-update.sh [new file with mode: 0644]
tg.sh [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..53ca141
--- /dev/null
@@ -0,0 +1,8 @@
+hooks/pre-commit
+tg-create
+tg-delete
+tg-info
+tg-patch
+tg-summary
+tg-update
+tg
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..3913d66
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+# Set PREFIX to wherever you want to install TopGit
+PREFIX = $(HOME)
+bindir = $(PREFIX)/bin
+cmddir = $(PREFIX)/libexec/topgit
+hooksdir = $(cmddir)/hooks
+
+
+commands_in = tg-create.sh tg-delete.sh tg-info.sh tg-patch.sh tg-summary.sh tg-update.sh
+hooks_in = hooks/pre-commit.sh
+
+commands_out = $(patsubst %.sh,%,$(commands_in))
+hooks_out = $(patsubst %.sh,%,$(hooks_in))
+
+all::  tg $(commands_out) $(hooks_out)
+
+tg $(commands_out) $(hooks_out): % : %.sh
+       @echo "[SED] $@"
+       @sed -e 's#@cmddir@#$(cmddir)#g;' \
+               -e 's#@hooksdir@#$(hooksdir)#g' \
+               -e 's#@bindir@#$(bindir)#g' \
+               $@.sh >$@+ && \
+       chmod +x $@+ && \
+       mv $@+ $@
+
+
+install:: all
+       install tg "$(bindir)"
+       install -d -m 755 "$(cmddir)"
+       install $(commands_out) "$(cmddir)"
+       install -d -m 755 "$(hooksdir)"
+       install $(hooks_out) "$(hooksdir)"
+
+clean::
+       rm -f tg $(commands_out) $(hooks_out)
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..848e3d2
--- /dev/null
+++ b/README
@@ -0,0 +1,321 @@
+TopGit - A different patch queue manager
+
+
+DESCRIPTION
+-----------
+
+TopGit aims to make handling of large amount of interdependent topic
+branches easier. In fact, it is designed especially for the case
+when you maintain a queue of third-party patches on top of another
+(perhaps Git-controlled) project and want to easily organize, maintain
+and submit them - TopGit achieves that by keeping a separate topic
+branch for each patch and providing few tools to maintain the branches.
+
+
+RATIONALE
+---------
+
+Why not use something like StGIT or Guilt or rebase -i for that?
+The advantage of these tools is their simplicity; they work with patch
+_series_ and defer to the reflog facility for version control of patches
+(reordering of patches is not version-controlled at all). But there are
+several disadvantages - for one, these tools (especially StGIT) do not
+actually fit well with plain Git at all - it is basically impossible
+to take advantage of index efectively when using StGIT. But more
+importantly, these tools horribly fail in the face of distributed
+environment.
+
+TopGit has been designed around three main tenents:
+
+       (i) TopGit is as thin layer on top of Git as possible.
+You still maintain your index and commit using Git, TopGit will
+only automate few indispensable tasks.
+
+       (ii) TopGit is anxious about _keeping_ your history. It will
+never rewrite your history and all metadata are also tracked by Git,
+smoothly and non-obnoxiously. It is useful if there is a _single_
+point when the history is cleaned up, and that is at the point of
+inclusion in the upstream project; locally, you can see how your
+patch has evolved and easily return to older versions.
+
+       (iii) TopGit is specifically designed to work in distributed
+environment. You can have several instances of TopGit-aware repositories
+and smoothly keep them all up-to-date and transfer your changes between
+them.
+
+As mentioned above, the main intended use-case for TopGit is tracking
+third-party patches, where each patch is effectively a single topic
+branch.  In order to flexibly accomodate even complex scenarios when
+you track many patches where many are independent but some depend
+on others, TopGit ignores the ancient Quilt heritage of patch series
+and instead allows the patches to freely form graphs (DAGs just like
+Git history itself, only "one lever higher"). For now, you have
+to manually specify which patches does the current one depend
+on, but TopGit might help you with that in the future in a darcs-like
+fashion.
+
+A glossary plug: The union (i.e. merge) of patch dependencies is
+called a _base_ of the patch (topic branch).
+
+Of course, TopGit is perhaps not the right tool for you:
+
+       (i) TopGit is not complicated, but StGIT et al. are somewhat
+simpler, conceptually.  If you just want to make a linear purely-local
+patch queue, deferring to StGIT instead might make more sense.
+
+       (ii) While keeping your history anxiously, in some extreme
+cases the TopGit-generated history graph will perhaps be a little
+too complex. ;-)
+
+
+SYNOPSIS
+--------
+
+       ## Create and evolve a topic branch
+       $ tg create t/gitweb/pathinfo-action
+       tg: Automatically marking dependency on master
+       tg: Creating t/gitweb/pathinfo-action base from master...
+       $ ..hack..
+       $ git commit
+       $ ..fix a mistake..
+       $ git commit
+
+       ## Create another topic branch on top of the former one
+       $ tg create t/gitweb/nifty-links
+       tg: Automatically marking dependency on t/gitweb/pathinfo-action
+       tg: Creating t/gitweb/nifty-links base from t/gitweb/pathinfo-action...
+       $ ..hack..
+       $ git commit
+
+       ## Create another topic branch on top of specified one and submit
+       ## the resulting patch upstream
+       $ tg create -d master t/revlist/author-fixed
+       tg: Creating t/revlist/author-fixed base from master...
+       $ ..hack..
+       $ git commit
+       $ tg patch -m
+       tg: Sent t/revlist/author-fixed
+       From: pasky@suse.cz
+       To: git@vger.kernel.org
+       Cc: gitster@pobox.com
+       Subject: [PATCH] Fix broken revlist --author when --fixed-string
+
+       ## Create another topic branch depending on two others non-trivially
+       $ tg create -d t/revlist/author-fixed,t/gitweb/nifty-links t/whatever
+       tg: Creating t/whatever base from t/revlist/author-fixed...
+       tg: Merging t/whatever base with t/gitweb/nifty-links...
+       Merge failed!
+       tg: Please commit merge resolution and call: tg create
+       tg: It is also safe to abort this operation using `git reset --hard`
+       tg: but please remember you are on the base branch now;
+       tg: you will want to switch to a different branch.
+       $ ..resolve..
+       $ git commit
+       tg: Resuming t/whatever setup...
+       $ tg create t/whatever
+       $ ..hack..
+       $ git commit
+
+       ## Update a single topic branch and propagate the changes to
+       ## a different one
+       $ git checkout t/gitweb/nifty-links
+       $ ..hack..
+       $ git commit
+       $ git checkout t/whatever
+       $ tg info
+       Topic Branch: t/whatever (1 commit)
+       Subject: [PATCH] Whatever patch
+       Base: 3f47ebc1
+       Depends: t/revlist/author-fixed t/gitweb/nifty-links
+       Needs update from:
+               t/gitweb/nifty-links (1 commit)
+       $ tg update
+       tg: Updating base with t/gitweb/nifty-links changes...
+       Merge failed!
+       tg: Please commit merge resolution and call `tg update` again.
+       tg: It is also safe to abort this operation using `git reset --hard`,
+       tg: but please remember you are on the base branch now;
+       tg: you will want to switch to a different branch.
+       $ ..resolve..
+       $ git commit
+       $ tg update
+       tg: Updating t/whatever against new base...
+       Merge failed!
+       tg: Please resolve the merge and commit. No need to do anything else.
+       tg: You can abort this operation using `git reset --hard` now
+       tg: and retry this merge later using `tg update`.
+       $ ..resolve..
+       $ git commit
+
+       ## Update a single topic branch and propagate the changes
+       ## further through the dependency chain
+       $ git checkout t/gitweb/pathinfo-action
+       $ ..hack..
+       $ git commit
+       $ git checkout t/whatever
+       $ tg info
+       Topic Branch: t/whatever (1/2 commits)
+       Subject: [PATCH] Whatever patch
+       Base: 0ab2c9b3
+       Depends: t/revlist/author-fixed t/gitweb/nifty-links
+       Needs update from:
+               t/gitweb/pathinfo-action (<= t/gitweb/nifty-links) (1 commit)
+       $ tg update
+       tg: Recursing to t/gitweb/nifty-links...
+       [t/gitweb/nifty-links] tg: Updating base with t/gitweb/pathinfo-action changes...
+       Merge failed!
+       [t/gitweb/nifty-links] tg: Please commit merge resolution and call `tg update` again.
+       [t/gitweb/nifty-links] tg: It is also safe to abort this operation using `git reset --hard`,
+       [t/gitweb/nifty-links] tg: but please remember you are on the base branch now;
+       [t/gitweb/nifty-links] tg: you will want to switch to a different branch.
+       [t/gitweb/nifty-links] tg: You are in a subshell. If you abort the merge,
+       [t/gitweb/nifty-links] tg: use `exit` to abort the recursive update altogether.
+       [t/gitweb/nifty-links] $ ..resolve..
+       [t/gitweb/nifty-links] $ git commit
+       [t/gitweb/nifty-links] $ tg update
+       [t/gitweb/nifty-links] tg: Updating t/gitweb/nifty-links against new base...
+       Merge failed!
+       [t/gitweb/nifty-links] tg: Please resolve the merge and commit.
+       [t/gitweb/nifty-links] tg: You can abort this operation using `git reset --hard`.
+       [t/gitweb/nifty-links] tg: You are in a subshell. After you either commit or abort
+       [t/gitweb/nifty-links] tg: your merge, use `exit` to proceed with the recursive update.
+       [t/gitweb/nifty-links] $ ..resolve..
+       [t/gitweb/nifty-links] $ git commit
+       [t/gitweb/nifty-links] $ exit
+       tg: Updating base with t/gitweb/nifty-links changes...
+       tg: Updating t/whatever against new base...
+
+
+USAGE
+-----
+
+The 'tg' tool of TopGit has several subcommands:
+
+tg help
+~~~~~~~
+       Our sophisticated integrated help facility. Doesn't do
+       a whole lot for now.
+
+tg create
+~~~~~~~~~
+       Create a new TopGit-controlled topic branch of a given name
+       (required argument) and switch to it. If no dependencies
+       are specified using the '-d' paremeter, the current branch
+       is assumed to be the only dependency.
+
+       After `tg create`, you should insert the patch description
+       to the '.topmsg' file.
+
+       The main task of `tg create` is to set up the topic branch
+       base from the dependencies. This may fail due to merge conflicts.
+       In that case, after you commit the conflicts resolution,
+       you should call `tg create` again (without any arguments);
+       it will detect that you are on a topic branch base ref and
+       resume the topic branch creation operation.
+
+       '-d':
+               Manually specified dependencies. A comma- or
+               space-separated list of branch names.
+
+tg delete
+~~~~~~~~~
+       Remove a TopGit-controlled topic branch of given name
+       (required argument). Normally, this command will remove
+       only empty branch (base == head); use '-f' to remove
+       non-empty branch.
+
+       Currently, this command will _NOT_ remove the branch from
+       the dependency list in other branches. You need to take
+       care of this _manually_. This is even more complicated
+       in combination with '-f', in that case you need to manually
+       unmerge the removed branch's changes from the branches
+       depending on it.
+
+       TODO: '-a' to delete all empty branches, depfix, revert
+
+tg info
+~~~~~~~
+       Show a summary information about the current or specified
+       topic branch.
+
+tg patch
+~~~~~~~~
+       Generate a patch from the current or specified topic branch.
+       This means that the diff between the topic branch base and
+       head (latest commit) is shown, appended to the description
+       found in the .topmsg file.
+
+       The patch is by default simply dumped to stdout. In the future,
+       tg patch will be able to automatically send the patches by mail
+       or save them to files.
+
+       TODO: tg patch -i to base at index instead of branch,
+               -w for working tree
+
+tg summary
+~~~~~~~~~~
+       Show overview of all TopGit-tracked topic branches and their
+       up-to-date status ('D' marks that it is out-of-date wrt. its
+       dependencies, 'B' marks that it is out-of-date wrt. its base).
+
+tg update
+~~~~~~~~~
+       Update the current topic branch wrt. changes in the branches
+       it depends on. This is made 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.
+
+       In case your dependencies are not up-to-date, tg update
+       will first recurse into them and update these.
+
+       TODO: tg update -a for updating all topic branches
+
+TODO: Some infrastructure for sharing topic branches between
+       repositories easily
+
+
+IMPLEMENTATION
+--------------
+
+TopGit stores all the topic branches in the regular refs/heads/
+namespace, (we recommend to mark them with the 't/' prefix).
+Except that, TopGit also maintains a set of auxiliary refs in
+refs/top-*. Currently, only refs/top-bases/ is used, containing
+the current _base_ of the given topic branch - this is basically
+a merge of all the branches the topic branch depends on; it is
+updated during `tg update` and then merged to the topic branch,
+and it is the base of a patch generated from the topic branch by
+`tg patch`.
+
+All the metadata is tracked within the source tree and history
+of the topic branch itself, in .top* files; these files are kept
+isolated within the topic branches during TopGit-controlled merges
+and are of course omitted during `tg patch`. The state of these
+files in base commits is undefined; look at them only in the topic
+branches themselves.  Currently, two files are defined:
+
+       .topmsg: Contains the description of the topic branch
+in a mail-like format, plus the author information,
+whatever Cc headers you choose or the post-three-dashes message.
+When mailing out your patch, basically only few extra headers
+mail headers are inserted and the patch itself is appended.
+Thus, as your patches evolve, you can record nuances like whether
+the paricular patch should have To-list/Cc-maintainer or vice
+versa and similar nuances, if your project is into that.
+
+       .topdeps: Contains the one-per-line list of branches
+your patch depends on, pre-seeded with `tg create`. (Continuously
+updated) merge of these branches will be the "base" of your topic
+branch.
+
+TopGit also automagically installs a bunch of custom commit-related
+hooks that will verify if you are committing the .top* files in sane
+state. It will add the hooks to separate files within the hooks/
+subdirectory and merely insert calls of them to the appropriate hooks
+and make them executable (but make sure the original hooks code
+is not called if the hook was not executable beforehand).
+
+Another automagically installed piece is .git/info/attributes specifier
+for an 'ours' merge strategy for the files .topmsg and .topdeps, and
+the (intuitive) 'ours' merge strategy definition in .git/config.
diff --git a/hooks/pre-commit.sh b/hooks/pre-commit.sh
new file mode 100644 (file)
index 0000000..da63185
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+
+## Set up all the tg machinery
+
+set -e
+tg__include=1
+tg_util() {
+       . "@bindir@"/tg
+}
+tg_util
+
+
+## Generally have fun
+
+# Don't do anything on non-topgit branch
+git rev-parse --verify "$(git symbolic-ref HEAD | sed 's/heads/top-bases/')" >/dev/null 2>&1 ||
+       exit 0
+
+[ -s "$root_dir/.topdeps" ] ||
+       die ".topdeps is missing"
+[ -s "$root_dir/.topmsg" ] ||
+       die ".topmsg is missing"
+
+# TODO: Verify .topdeps for valid branch names and against cycles
diff --git a/tg-create.sh b/tg-create.sh
new file mode 100644 (file)
index 0000000..b5c7789
--- /dev/null
@@ -0,0 +1,118 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+deps= # List of dependent branches
+restarted= # Set to 1 if we are picking up in the middle of base setup
+merge= # List of branches to be merged; subset of $deps
+name=
+
+
+## Parse options
+
+while [ -n "$1" ]; do
+       arg="$1"; shift
+       case "$arg" in
+       -d)
+               deps="$(echo "$1" | sed 's/,/ /g')"; shift;;
+       -*)
+               echo "Usage: tg create [-d DEPS...] NAME" >&2
+               exit 1;;
+       *)
+               [ -z "$name" ] || die "name already specified ($name)"
+               name="$arg";;
+       esac
+done
+
+
+## Auto-guess dependencies
+
+if [ -z "$deps" ]; then
+       head="$(git symbolic-ref HEAD)"
+       bname="${heads#refs/top-bases/}"
+       if [ "$bname" != "$head" -a -s "$git_dir/top-deps" -a -s "$git_dir/top-merge" ]; then
+               # We are on a base branch now; resume merge!
+               deps="$(cat "$git_dir/top-deps")"
+               merge="$(cat "$git_dir/top-merge") "
+               name="$base"
+               restarted=1
+               info "Resuming $name setup..."
+       else
+               # The common case
+               [ -z "$name" ] && die "no branch name given"
+               deps="${head#refs/heads/}"
+               [ "$deps" != "$head" ] || die "refusing to auto-depend on non-head ref ($head)"
+               info "Automatically marking dependency on $deps"
+       fi
+fi
+
+[ -n "$merge" -o -n "$restarted" ] || merge="$deps "
+
+for d in $deps; do
+       git rev-parse --verify "$d" >/dev/null 2>&1 ||
+               die "unknown branch dependency '$d'"
+done
+! git rev-parse --verify "$name" >/dev/null 2>&1 ||
+       die "branch '$name' already exists"
+
+# Clean up any stale stuff
+rm -f "$git_dir/top-deps" "$git_dir/top-merge"
+
+
+## Create base
+
+if [ -n "$merge" ]; then
+       # Unshift the first item from the to-merge list
+       branch="${merge%% *}"
+       merge="${merge#* }"
+       info "Creating $name base from $branch..."
+       switch_to_base "$name" "$branch"
+fi
+
+
+## Merge other dependencies into the base
+
+while [ -n "$merge" ]; do
+       # Unshift the first item from the to-merge list
+       branch="${merge%% *}"
+       merge="${merge#* }"
+       info "Merging $name base with $branch..."
+
+       if ! git merge "$branch"; then
+               info "Please commit merge resolution and call: tg create"
+               info "It is also safe to abort this operation using \`git reset --hard\`"
+               info "but please remember you are on the base branch now;"
+               info "you will want to switch to a different branch."
+               echo "$deps" >"$git_dir/top-deps"
+               echo "$merge" >"$git_dir/top-merge"
+               exit 2
+       fi
+done
+
+
+## Set up the topic branch
+
+git checkout -b "$name"
+
+echo "$deps" | sed 's/ /\n/g' >"$root_dir/.topdeps"
+git add "$root_dir/.topdeps"
+
+author="$(git var GIT_AUTHOR_IDENT)"
+author_addr="${author%> *}>"
+{
+       echo "From: $author_addr"
+       echo "Subject: [PATCH] $1"
+       echo
+       cat <<EOT
+<patch description>
+
+Signed-off-by: $author_addr
+EOT
+} >"$root_dir/.topmsg"
+git add "$root_dir/.topmsg"
+
+
+
+info "Topic branch $name successfully set up. Please fill .topmsg now."
+info "You MUST do an initial commit. To abort: git rm -f .top* && git checkout ${deps%% *} && tg delete $name"
diff --git a/tg-delete.sh b/tg-delete.sh
new file mode 100644 (file)
index 0000000..287c4fa
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+force= # Whether to delete non-empty branch
+name=
+
+
+## Parse options
+
+while [ -n "$1" ]; do
+       arg="$1"; shift
+       case "$arg" in
+       -f)
+               force=1;;
+       -*)
+               echo "Usage: tg delete [-f] NAME" >&2
+               exit 1;;
+       *)
+               [ -z "$name" ] || die "name already specified ($name)"
+               name="$arg";;
+       esac
+done
+
+
+## Sanity checks
+
+[ -n "$name" ] || die "no branch name specified"
+branchrev="$(git rev-parse --verify "$name" 2>/dev/null)" ||
+       die "invalid branch name: $name"
+baserev="$(git rev-parse --verify "refs/top-bases/$name" 2>/dev/null)" ||
+       die "not a TopGit topic branch: $name"
+[ "$(git symbolic-ref HEAD)" != "refs/heads/$name" ] ||
+       die "cannot delete your current branch"
+
+nonempty=
+[ -z "$(git diff-tree "refs/top-bases/$name" "$name" | fgrep -v "      .top")" ] || nonempty=1
+
+[ -z "$nonempty" ] || [ -n "$force" ] || die "branch is non-empty: $name"
+
+
+## Wipe out
+
+git update-ref -d "refs/top-bases/$name" "$baserev"
+git update-ref -d "refs/heads/$name" "$branchrev"
diff --git a/tg-info.sh b/tg-info.sh
new file mode 100644 (file)
index 0000000..ce99809
--- /dev/null
@@ -0,0 +1,61 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+name=
+
+
+## Parse options
+
+while [ -n "$1" ]; do
+       arg="$1"; shift
+       case "$arg" in
+       -*)
+               echo "Usage: tg info [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/##')"
+base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" ||
+       die "not a TopGit-controlled branch"
+
+measure="$(measure_branch "$name" "$base_rev")"
+
+echo "Topic Branch: $name ($measure)"
+if [ "$(git rev-parse --short "$name")" = "$base_rev" ]; then
+       echo "No commits."
+       exit 0
+fi
+
+git cat-file blob "$name:.topmsg" | grep ^Subject:
+
+echo "Base: $base_rev"
+branch_contains "$name" "$base_rev" ||
+       echo "Base is newer than head! Please run \`tg update\`."
+
+deps="$(git cat-file blob "$name:.topdeps")"
+echo "Depends: $deps"
+
+depcheck="$(mktemp)"
+needs_update "$name" >"$depcheck"
+if [ -s "$depcheck" ]; then
+       echo "Needs update from:"
+       cat "$depcheck" |
+               sed 's/ [^ ]* *$//' | # last is $name
+               sed 's/^: //' | # don't distinguish base updates
+               while read dep chain; do
+                       echo -n "$dep "
+                       [ -n "$chain" ] && echo -n "(<= $(echo "$chain" | sed 's/ / <= /')) "
+                       dep_parent="${chain%% *}"
+                       echo -n "($(measure_branch "$dep" "${dep2:-$name}"))"
+                       echo
+               done | sed 's/^/\t/'
+else
+       echo "Up-to-date."
+fi
+rm "$depcheck"
diff --git a/tg-patch.sh b/tg-patch.sh
new file mode 100644 (file)
index 0000000..45da304
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+name=
+
+
+## Parse options
+
+while [ -n "$1" ]; do
+       arg="$1"; shift
+       case "$arg" in
+       -*)
+               echo "Usage: tg patch [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/##')"
+base_rev="$(git rev-parse --short --verify "refs/top-bases/$name" 2>/dev/null)" ||
+       die "not a TopGit-controlled branch"
+
+git cat-file blob "$name:.topmsg"
+echo
+[ -n "$(git grep '^[-]--' "$name" -- ".topmsg")" ] || echo '---'
+
+# Evil obnoxious hack to work around the lack of git diff --exclude
+git_is_stupid="$(mktemp)"
+git diff-tree --name-only "$base_rev" "$name" |
+       fgrep -vx ".topdeps" |
+       fgrep -vx ".topmsg" >"$git_is_stupid" || : # fgrep likes to fail randomly?
+if [ -s "$git_is_stupid" ]; then
+       cat "$git_is_stupid" | xargs git diff --patch-with-stat "$base_rev" "$name" --
+else
+       echo "No changes."
+fi
+rm "$git_is_stupid"
+
+echo '-- '
+echo "tg: ($base_rev..) $name (depends on $(git cat-file blob "$name:.topdeps"))"
+branch_contains "$name" "$base_rev" ||
+       echo "tg: The patch is out-of-date wrt. the base! Run \`tg update\`."
diff --git a/tg-summary.sh b/tg-summary.sh
new file mode 100644 (file)
index 0000000..c608a18
--- /dev/null
@@ -0,0 +1,38 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+
+## Parse options
+
+if [ -n "$1" ]; then
+       echo "Usage: tg summary" >&2
+       exit 1
+fi
+
+
+## List branches
+
+git for-each-ref | cut -f 2 |
+       while read ref; do
+               name="${ref#refs/heads/}"
+               [ "$name" != "$ref" ] ||
+                       continue # eew, not a branch
+               git rev-parse --verify "refs/top-bases/$name" >/dev/null 2>&1 ||
+                       continue # not a TopGit branch
+
+               deps_update=' '
+               [ -z "$(needs_update "$name")" ] || deps_update='D'
+               base_update=' '
+               branch_contains "$name" "refs/top-bases/$name" || base_update='B'
+
+               if [ "$(git rev-parse "$name")" != "$(git rev-parse "refs/top-bases/$name")" ]; then
+                       subject="$(git cat-file blob "$name:.topmsg" | sed -n 's/^Subject: //p')"
+               else
+                       # No commits yet
+                       subject="(No commits)"
+               fi
+
+               printf '%s%s\t%-31s\t%s\n' "$deps_update" "$base_update" "$name" "$subject"
+       done
diff --git a/tg-update.sh b/tg-update.sh
new file mode 100644 (file)
index 0000000..03b8f3f
--- /dev/null
@@ -0,0 +1,108 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+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\)/##')"
+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)"
+needs_update "$name" >"$depcheck"
+if [ -s "$depcheck" ]; then
+       # We need to switch to the base branch
+       # ...but only if we aren't there yet (from failed previous merge)
+       HEAD="$(git symbolic-ref HEAD)"
+       if [ "$HEAD" = "${HEAD#refs/top-bases/}" ]; then
+               switch_to_base "$name"
+       fi
+
+       cat "$depcheck" |
+               sed 's/ [^ ]* *$//' | # last is $name
+               sed 's/.* \([^ ]*\)$/+\1/' | # only immediate dependencies
+               sed 's/^\([^+]\)/-\1/' | # now each line is +branch or -branch (+ == recurse)
+               uniq -s 1 | # fold branch lines; + always comes before - and thus wins within uniq
+               while read depline; do
+                       action="${depline:0:1}"
+                       dep="${depline:1}"
+
+                       # We do not distinguish between dependencies out-of-date
+                       # and base out-of-date cases for $dep here, but thanks
+                       # to needs_update returning : for the latter, we do
+                       # correctly recurse here in both cases.
+
+                       if [ x"$action" = x+ ]; then
+                               info "Recursing to $dep..."
+                               git checkout -q "$dep"
+                               (
+                               export TG_RECURSIVE="[$dep] $TG_RECURSIVE"
+                               export PS1="[$dep] $PS1"
+                               while ! tg update; do
+                                       # The merge got stuck! Let the user fix it up.
+                                       info "You are in a subshell. If you abort the merge,"
+                                       info "use \`exit 1\` to abort the recursive update altogether."
+                                       if ! sh -i; then
+                                               info "Ok, you aborated the merge. Now, you just need to"
+                                               info "switch back to some sane branch using \`git checkout\`."
+                                               exit 3
+                                       fi
+                               done
+                               )
+                               switch_to_base "$name"
+                       fi
+
+                       info "Updating base with $dep changes..."
+                       if ! git merge "$dep"; then
+                               if [ -z "$TG_RECURSIVE" ]; then
+                                       resume='`tg update` again'
+                               else # subshell
+                                       resume='exit'
+                               fi
+                               info "Please commit merge resolution and call $resume."
+                               info "It is also safe to abort this operation using \`git reset --hard\`,"
+                               info "but please remember that you are on the base branch now;"
+                               info "you will want to switch to some normal branch afterwards."
+                               rm "$depcheck"
+                               exit 2
+                       fi
+               done
+
+       # Home, sweet home...
+       git checkout -q "$name"
+else
+       info "The base is up-to-date."
+fi
+rm "$depcheck"
+
+
+## Second, update our head with the base
+
+if branch_contains "$name" "refs/top-bases/$name"; then
+       info "The $name head is up-to-date wrt. the base."
+       exit 0
+fi
+info "Updating $name against new base..."
+if ! git merge "refs/top-bases/$name"; then
+       if [ -z "$TG_RECURSIVE" ]; then
+               info "Please commit merge resolution. No need to do anything else"
+               info "You can abort this operation using \`git reset --hard\` now"
+               info "and retry this merge later using \`tg update\`."
+       else # subshell
+               info "Please commit merge resolution and call exit."
+               info "You can abort this operation using \`git reset --hard\`."
+       fi
+fi
diff --git a/tg.sh b/tg.sh
new file mode 100644 (file)
index 0000000..ae3dcda
--- /dev/null
+++ b/tg.sh
@@ -0,0 +1,162 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+
+## Auxiliary functions
+
+info()
+{
+       echo "${TG_RECURSIVE}tg: $*"
+}
+
+die()
+{
+       info "fatal: $*"
+       exit 1
+}
+
+# setup_hook NAME
+setup_hook()
+{
+       hook_call="\"\$(tg --hooks-path)\"/$1 \"\$@\""
+       if fgrep -q "$hook_call" "$git_dir/hooks/$1"; then
+               # Another job well done!
+               return
+       fi
+       # Prepare incanation
+       if [ -x "$git_dir/hooks/$1" ]; then
+               hook_call="$hook_call"' || exit $?'
+       else
+               hook_call="exec $hook_call"
+       fi
+       # Insert call into the hook
+       {
+               echo "#!/bin/sh"
+               echo "$hook_call"
+               cat "$git_dir/hooks/$1"
+       } >"$git_dir/hooks/$1+"
+       chmod a+x "$git_dir/hooks/$1+"
+       mv "$git_dir/hooks/$1+" "$git_dir/hooks/$1"
+}
+
+# setup_ours (no arguments)
+setup_ours()
+{
+       if [ ! -s "$git_dir/info/gitattributes" ] || ! grep -q topmsg "$git_dir/info/gitattributes"; then
+               {
+                       echo -e ".topmsg\tmerge=ours"
+                       echo -e ".topdeps\tmerge=ours"
+               } >>"$git_dir/info/gitattributes"
+       fi
+       if ! git config merge.ours.driver >/dev/null; then
+               git config merge.ours.name '"always keep ours" merge driver'
+               git config merge.ours.driver 'touch %A'
+       fi
+}
+
+# measure_branch NAME [BASE]
+measure_branch()
+{
+       _name="$1"; _base="$2"
+       [ -n "$_base" ] || _base="refs/top-bases/$_name"
+       # The caller should've verified $name is valid
+       _commits="$(git rev-list "$_name" ^"$_base" | wc -l)"
+       _nmcommits="$(git rev-list --no-merges "$_name" ^"$_base" | wc -l)"
+       if [ $_commits -gt 1 ]; then
+               _suffix="commits"
+       else
+               _suffix="commit"
+       fi
+       echo "$_commits/$_nmcommits $_suffix"
+}
+
+# branch_contains B1 B2
+# Whether B1 is a superset of B2.
+branch_contains()
+{
+       [ "$(git rev-list ^"$1" "$2" | wc -l)" -eq 0 ]
+}
+
+# needs_update NAME [BRANCHPATH...]
+# This function is recursive; it outputs reverse path from NAME
+# to the branch (e.g. B_DIRTY B1 B2 NAME), one path per line,
+# inner paths first. Innermost name can be ':' if the head is
+# not in sync with the base.
+needs_update()
+{
+       {
+       git cat-file blob "$1:.topdeps" 2>/dev/null |
+               while read _dep; do
+                       _dep_is_tgish=1
+                       git rev-parse --verify "refs/top-bases/$_dep" >/dev/null 2>&1 ||
+                               _dep_is_tgish=
+
+                       # Shoo shoo, keep our environment alone!
+                       [ -z "$_dep_is_tgish" ] || (needs_update "$_dep" "$@")
+
+                       _dep_base_uptodate=1
+                       if [ -n "$_dep_is_tgish" ]; then
+                               branch_contains "$_dep" "refs/top-bases/$_dep" || _dep_base_uptodate=
+                       fi
+
+                       if [ -z "$_dep_base_uptodate" ]; then
+                               # _dep needs to be synced with its base
+                               echo ": $_dep $*"
+                       elif ! branch_contains "refs/top-bases/$1" "$_dep"; then
+                               # Some new commits in _dep
+                               echo "$_dep $*"
+                       fi
+               done
+       } || : # $1 is not tracked by TopGit anymore
+}
+
+# switch_to_base NAME [SEED]
+switch_to_base()
+{
+       _base="refs/top-bases/$1"; _seed="$2"
+       # We have to do all the hard work ourselves :/
+       # This is like git checkout -b "$_base" "$_seed"
+       # (or just git checkout "$_base"),
+       # but does not create a detached HEAD.
+       git read-tree -u -m HEAD "${_seed:-$_base}"
+       [ -z "$_seed" ] || git update-ref "$_base" "$_seed"
+       git symbolic-ref HEAD "$_base"
+}
+
+
+## Initial setup
+
+set -e
+git_dir="$(git rev-parse --git-dir)"
+root_dir="$(git rev-parse --show-cdup)"; root_dir="${root_dir:-.}"
+# make sure merging the .top* files will always behave sanely
+setup_ours
+setup_hook "pre-commit"
+
+
+## Dispatch
+
+# We were sourced from another script for our utility functions;
+# this is set by hooks.
+[ -z "$tg__include" ] || return 0
+
+cmd="$1"
+[ -n "$cmd" ] || die "He took a duck in the face at two hundred and fifty knots"
+shift
+
+case "$cmd" in
+help)
+       echo "TopGit - A different patch queue manager"
+       echo "Usage: tg (create|delete|info|patch|summary|update|help) ..."
+       exit 1;;
+create|delete|info|patch|summary|update)
+       . "@cmddir@"/tg-$cmd;;
+--hooks-path)
+       # Internal command
+       echo "@hooksdir@";;
+*)
+       echo "Unknown subcommand: $cmd" >&2
+       exit 1;;
+esac