From 308892d0c071135031994606170c8be6653e7197 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Sat, 2 Aug 2008 21:17:07 +0200 Subject: [PATCH 1/1] TopGit - A different patch queue manager 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 | 8 ++ Makefile | 34 +++++ README | 321 ++++++++++++++++++++++++++++++++++++++++++++ hooks/pre-commit.sh | 28 ++++ tg-create.sh | 118 ++++++++++++++++ tg-delete.sh | 46 +++++++ tg-info.sh | 61 +++++++++ tg-patch.sh | 46 +++++++ tg-summary.sh | 38 ++++++ tg-update.sh | 108 +++++++++++++++ tg.sh | 162 ++++++++++++++++++++++ 11 files changed, 970 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README create mode 100644 hooks/pre-commit.sh create mode 100644 tg-create.sh create mode 100644 tg-delete.sh create mode 100644 tg-info.sh create mode 100644 tg-patch.sh create mode 100644 tg-summary.sh create mode 100644 tg-update.sh create mode 100644 tg.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53ca141 --- /dev/null +++ b/.gitignore @@ -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 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 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 index 0000000..da63185 --- /dev/null +++ b/hooks/pre-commit.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 index 0000000..b5c7789 --- /dev/null +++ b/tg-create.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 < + +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 index 0000000..287c4fa --- /dev/null +++ b/tg-delete.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 index 0000000..ce99809 --- /dev/null +++ b/tg-info.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 index 0000000..45da304 --- /dev/null +++ b/tg-patch.sh @@ -0,0 +1,46 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 index 0000000..c608a18 --- /dev/null +++ b/tg-summary.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 index 0000000..03b8f3f --- /dev/null +++ b/tg-update.sh @@ -0,0 +1,108 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 index 0000000..ae3dcda --- /dev/null +++ b/tg.sh @@ -0,0 +1,162 @@ +#!/bin/sh +# TopGit - A different patch queue manager +# (c) Petr Baudis 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 -- 2.30.2