chiark / gitweb /
tg-export: New command for cleaning up history
authorPetr Baudis <pasky@suse.cz>
Sun, 10 Aug 2008 20:30:15 +0000 (22:30 +0200)
committerPetr Baudis <pasky@suse.cz>
Sun, 10 Aug 2008 20:30:15 +0000 (22:30 +0200)
Makefile
README
tg-export.sh [new file with mode: 0644]
tg.sh

index cf842d686a1ff7db9f15a330ac5e565a29940824..6eade1e1679a04f9bae79211cb91ee7fe3c69099 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ sharedir = $(PREFIX)/share/topgit
 hooksdir = $(cmddir)/hooks
 
 
-commands_in = tg-create.sh tg-delete.sh tg-info.sh tg-patch.sh tg-summary.sh tg-update.sh
+commands_in = tg-create.sh tg-delete.sh tg-export.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))
diff --git a/README b/README
index a4839f45803b62ec2dcf06eda571c4b51fa89bf7..275043b2c2bd05060dd011a369e9739cce1cad07 100644 (file)
--- a/README
+++ b/README
@@ -261,6 +261,50 @@ tg summary
        TODO: Speed up by an order of magnitude
        TODO: Graph view
 
+tg export
+~~~~~~~~~
+       Create a new branch containing tidied-up history
+       of the current topic branch and its dependencies,
+       suitable for pull by upstream - each topic branch
+       corresponds to a single commit in the cleaned up history
+       (corresponding basically exactly to `tg patch` output
+       for the topic branch).
+
+       You can use this collapsed structure either for providing
+       a pull source for upstream, or further linearization e.g.
+       for creation of a quilt series using git log:
+
+               git log --pretty=email -p --topo-order origin..exported
+
+       To better understand the function of `tg export`,
+       consider this dependency structure of topic branches:
+
+       origin/master - t/foo/blue - t/foo/red - master
+                    `- t/bar/good <,----------'
+                    `- t/baz      ------------'
+
+       (Where each of the branches may have hefty history.) Then
+
+       master$ tg export for-linus
+
+       will create this commit structure on branch for-linus:
+
+       origin/master - t/foo/blue -. merge - t/foo/red -.. merge - master
+                    `- t/bar/good <,-------------------'/
+                    `- t/baz      ---------------------'
+
+       The command works on the current topic branch
+       and requires one mandatory argument: the name of the branch
+       where the exported result shall be stored.
+       The branch will be silently overwritten if it exists already!
+       Use git reflog to recover in case of mistake.
+
+       Usage: tg export BRANCH
+
+       TODO: Make stripping of non-essential headers configurable
+       TODO: Make stripping of [PATCH] and other prefixes configurable
+       TODO: --quilt and --mbox options for other modes of operation
+
 tg update
 ~~~~~~~~~
        Update the current topic branch wrt. changes in the branches
diff --git a/tg-export.sh b/tg-export.sh
new file mode 100644 (file)
index 0000000..9f19300
--- /dev/null
@@ -0,0 +1,147 @@
+#!/bin/sh
+# TopGit - A different patch queue manager
+# (c) Petr Baudis <pasky@suse.cz>  2008
+# GPLv2
+
+name=
+output=
+
+
+## Parse options
+
+while [ -n "$1" ]; do
+       arg="$1"; shift
+       case "$arg" in
+       -*)
+               echo "Usage: tg export NEWBRANCH" >&2
+               exit 1;;
+       *)
+               [ -z "$output" ] || die "new branch already specified ($output)"
+               output="$arg";;
+       esac
+done
+
+
+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 on a TopGit-controlled branch"
+
+[ -n "$output" ] ||
+       die "no target branch specified"
+
+! git rev-parse --verify "$output" >/dev/null 2>&1 ||
+       die "target branch '$output' already exists; first run: git branch -D $output"
+
+
+playground="$(mktemp -d)"
+trap 'rm -rf "$playground"' EXIT
+
+# Trusty Cogito code:
+load_author()
+{
+       if [ -z "$GIT_AUTHOR_NAME" ] && echo "$1" | grep -q '^[^< ]'; then
+               export GIT_AUTHOR_NAME="$(echo "$1" | sed 's/ *<.*//')"
+       fi
+       if [ -z "$GIT_AUTHOR_EMAIL" ] && echo "$1" | grep -q '<.*>'; then
+               export GIT_AUTHOR_EMAIL="$(echo "$1" | sed 's/.*<\(.*\)>.*/\1/')"
+       fi
+}
+
+# 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 ".top*"
+        git write-tree)
+}
+
+# collapsed_commit NAME
+# Produce a collapsed commit of branch NAME.
+collapsed_commit()
+{
+       name="$1"
+
+       rm -f "$playground/^pre" "$playground/^post"
+       >"$playground/^body"
+
+       # Get commit message and authorship information
+       git cat-file blob "$name:.topmsg" >"$playground/^msg"
+       while read line; do
+               if [ -z "$line" ]; then
+                       # end of header
+                       cat >"$playground/^body"
+                       break
+               fi
+               case "$line" in
+               From:*) load_author "${line#From: }";;
+               Subject:*) echo "${line#Subject: }" >>"$playground/^pre";;
+               *) echo "$line" >>"$playground/^post";;
+               esac
+       done <"$playground/^msg"
+
+       # Determine parent
+       parent="$(cut -f 1 "$playground/$name^parents")"
+       if [ "$(cat "$playground/$name^parents" | wc -l)" -gt 1 ]; then
+               # Produce a merge commit first
+               parent="$({
+                       echo "TopGit-driven merge of branches:"
+                       echo
+                       cut -f 2 "$playground/$name^parents"
+               } | git commit-tree "$(pretty_tree "refs/top-bases/$name")" \
+                       $(for p in $parent; do echo -p $p; done))"
+       fi
+
+       {
+               if [ -s "$playground/^pre" ]; then
+                       cat "$playground/^pre"
+                       echo
+               fi
+               cat "$playground/^body"
+               [ ! -s "$playground/^post" ] || cat "$playground/^post"
+       } | git commit-tree "$(pretty_tree "$name")" -p "$parent"
+
+       echo "$name" >>"$playground/^ticker"
+}
+
+# collapse_one
+# This will collapse a single branch, using information about
+# previously collapsed branches stored in $playground.
+collapse_one()
+{
+       branch_needs_update >/dev/null
+       [ "$_ret" -eq 0 ] ||
+               die "cancelling $_ret export of $_dep (-> $_name): branch not up-to-date"
+
+       if [ -s "$playground/$_dep" ]; then
+               # We've already seen this dep
+               commit="$(cat "$playground/$_dep")"
+
+       elif [ -z "$_dep_is_tgish" ]; then
+               # This dep is not for rewrite
+               commit="$(git rev-parse --verify "$_dep")"
+
+       else
+               # First time hitting this dep; the common case
+               commit="$(collapsed_commit "$_dep")"
+
+               mkdir -p "$playground/$(dirname "$_dep")"
+               echo "$commit" >"$playground/$_dep"
+               echo "Collapsed $_dep"
+       fi
+
+       # Propagate our work through the dependency chain
+       mkdir -p "$playground/$(dirname "$_name")"
+       echo "$commit   $_dep" >>"$playground/$_name^parents"
+}
+
+# Collapse all the branches - this way, collapse_one will be
+# called in topological order.
+recurse_deps collapse_one "$name"
+(_ret=0; _dep="$name"; _name=""; _dep_is_tgish=1; collapse_one)
+
+git update-ref "refs/heads/$output" "$(cat "$playground/$name")"
+
+depcount="$(cat "$playground/^ticker" | wc -l)"
+echo "Exported topic branch $name (total $depcount topics) to branch $output"
diff --git a/tg.sh b/tg.sh
index 4fef7796c53e59bc5829ca3a727c65a58e4eb3ab..a844b5ed2828a27d8018955913474bb5274040ed 100644 (file)
--- a/tg.sh
+++ b/tg.sh
@@ -139,7 +139,7 @@ branch_needs_update()
                # _dep needs to be synced with its base
                echo ": $_dep $_depchain"
                _ret=1
-       elif ! branch_contains "refs/top-bases/$_name" "$_dep"; then
+       elif [ -n "$_name" ] && ! branch_contains "refs/top-bases/$_name" "$_dep"; then
                # Some new commits in _dep
                echo "$_dep $_depchain"
                _ret=1