X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=topgit.git;a=blobdiff_plain;f=tg-export.sh;h=d68a6acd6d7286fabb1e59fde256dea29283d0f1;hp=9f1930027e6bc6325d170b6e83fe8db2e2bed11f;hb=120b1ea119b87b3ea292f0c2e2416540e790fcd1;hpb=074eb3f1b432d95880fcc2c5b388bebac75046e9 diff --git a/tg-export.sh b/tg-export.sh index 9f19300..d68a6ac 100644 --- a/tg-export.sh +++ b/tg-export.sh @@ -4,7 +4,11 @@ # GPLv2 name= +branches= output= +driver=collapse +flatten=false +numbered=false ## Parse options @@ -12,75 +16,90 @@ output= while [ -n "$1" ]; do arg="$1"; shift case "$arg" in + -b) + branches="$1"; shift;; + --flatten) + flatten=true;; + --numbered) + flatten=true; + numbered=true;; + --quilt) + driver=quilt;; + --collapse) + driver=collapse;; + --linearize) + driver=linearize;; -*) - echo "Usage: tg export NEWBRANCH" >&2 + echo "Usage: tg [...] export ([--collapse] NEWBRANCH | [-b BRANCH1,BRANCH2...] --quilt DIRECTORY | --linearize NEWBRANCH)" >&2 exit 1;; *) - [ -z "$output" ] || die "new branch already specified ($output)" + [ -z "$output" ] || die "output 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" +[ -z "$branches" -o "$driver" = "quilt" ] || + die "-b works only with the quilt driver" -! git rev-parse --verify "$output" >/dev/null 2>&1 || - die "target branch '$output' already exists; first run: git branch -D $output" +[ "$driver" = "quilt" ] || ! "$numbered" || + die "--numbered works only with the quilt driver"; +[ "$driver" = "quilt" ] || ! "$flatten" || + die "--flatten works only with the quilt driver" -playground="$(mktemp -d)" -trap 'rm -rf "$playground"' EXIT +if [ -z "$branches" ]; then + # this check is only needed when no branches have been passed + 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" + branches="$name" +else + name="${branches##*,}" # the last of the comma-separated items +fi +# $name holds the current branch +# $branches holds the comma-separated list of branches +# $name is equal to the last of the list of branches -# 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() +playground="$(get_temp tg-export -d)" + + +## Collapse driver + +create_tg_commit() { - (export GIT_INDEX_FILE="$playground/^index" - git read-tree "$1" - git update-index --force-remove ".top*" - git write-tree) + name="$1" + tree="$2" + parent="$3" + + # Get commit message and authorship information + git cat-file blob "$name:.topmsg" | git mailinfo "$playground/^msg" /dev/null > "$playground/^info" + + GIT_AUTHOR_NAME="$(sed -n '/^Author/ s/Author: //p' "$playground/^info")" + GIT_AUTHOR_EMAIL="$(sed -n '/^Email/ s/Email: //p' "$playground/^info")" + GIT_AUTHOR_DATE="$(sed -n '/^Date/ s/Date: //p' "$playground/^info")" + SUBJECT="$(sed -n '/^Subject/ s/Subject: //p' "$playground/^info")" + + test -n "$GIT_AUTHOR_NAME" && export GIT_AUTHOR_NAME + test -n "$GIT_AUTHOR_EMAIL" && export GIT_AUTHOR_EMAIL + test -n "$GIT_AUTHOR_DATE" && export GIT_AUTHOR_DATE + + (printf '%s\n\n' "$SUBJECT"; cat "$playground/^msg") | + git stripspace | + git commit-tree "$tree" -p "$parent" } # collapsed_commit NAME # Produce a collapsed commit of branch NAME. collapsed_commit() { - name="$1" + local name; 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 @@ -89,31 +108,24 @@ 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 - { - 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" + if branch_empty "$name"; then + echo "$parent"; + else + create_tg_commit "$name" "$(pretty_tree $name)" "$parent" + fi; echo "$name" >>"$playground/^ticker" } -# collapse_one +# collapse # This will collapse a single branch, using information about # previously collapsed branches stored in $playground. -collapse_one() +collapse() { - 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")" @@ -124,11 +136,10 @@ collapse_one() else # First time hitting this dep; the common case + echo "Collapsing $_dep" commit="$(collapsed_commit "$_dep")" - mkdir -p "$playground/$(dirname "$_dep")" echo "$commit" >"$playground/$_dep" - echo "Collapsed $_dep" fi # Propagate our work through the dependency chain @@ -136,12 +147,160 @@ collapse_one() 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")" +## Quilt driver + +quilt() +{ + if [ -z "$_dep_is_tgish" ]; then + # This dep is not for rewrite + return + fi + + if "$flatten"; then + bn="$(echo "$_dep.diff" | sed -e 's#_#__#g' -e 's#/#_#g')"; + dn=""; + else + bn="$(basename "$_dep.diff")"; + dn="$(dirname "$_dep.diff")/"; + if [ "x$dn" = "x./" ]; then + dn=""; + fi; + fi; + + if [ -e "$playground/$_dep" ]; then + # We've already seen this dep + return + fi + + mkdir -p "$playground/$(dirname "$_dep")"; + touch "$playground/$_dep"; + + if branch_empty "$_dep"; then + echo "Skip empty patch $_dep"; + else + if "$numbered"; then + number="$(echo $(($(cat "$playground/^number" 2>/dev/null) + 1)))"; + bn="$(printf "%04u-$bn" $number)"; + echo "$number" >"$playground/^number"; + fi; + + echo "Exporting $_dep" + mkdir -p "$output/$dn"; + $tg patch "$_dep" >"$output/$dn$bn" + echo "$dn$bn -p1" >>"$output/series" + fi +} + +linearize() +{ + if test ! -f "$playground/^BASE"; then + head="$(git rev-parse --verify "$_dep")" + echo "$head" > "$playground/^BASE" + git checkout -q "$head" + return; + fi; + + head=$(git rev-parse --verify HEAD) + + if [ -z "$_dep_is_tgish" ]; then + # merge in $_dep unless already included + rev="$(git rev-parse --verify "$_dep")"; + common="$(git merge-base --all HEAD "$_dep")"; + if test "$rev" = "$common"; then + # already included, just skip + :; + else + retmerge=0; + + git merge -s recursive "$_dep" || retmerge="$?"; + if test "x$retmerge" != "x0"; then + echo fix up the merge, commit and then exit; + #todo error handling + sh -i /dev/null + [ "$_ret" -eq 0 ] || + die "cancelling export of $_dep (-> $_name): branch not up-to-date" + + $driver +} + +# Call driver on all the branches - this will happen +# in topological order. +echo "$branches" | tr , '\n' | while read name; do + recurse_deps driver "$name" + (_ret=0; _dep="$name"; _name=""; _dep_is_tgish=1; driver) +done + +if [ "$driver" = "collapse" ]; then + 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" + +elif [ "$driver" = "quilt" ]; then + depcount="$(cat "$output/series" | wc -l)" + echo "Exported topic branch $name (total $depcount topics) to directory $output" + +elif [ "$driver" = "linearize" ]; then + git checkout -q -b $output + + echo $name + if test $(git rev-parse "$(pretty_tree $name)^{tree}") != $(git rev-parse "HEAD^{tree}"); then + echo "Warning: Exported result doesn't match"; + echo "tg-head=$(git rev-parse "$name"), exported=$(git rev-parse "HEAD")"; + #git diff $head HEAD; + fi; + +fi -depcount="$(cat "$playground/^ticker" | wc -l)" -echo "Exported topic branch $name (total $depcount topics) to branch $output" +# vim:noet