git-subtree.shon commit Use information about prior splits to make sure merges work right. (8b4a77f)
   1#!/bin/bash
   2#
   3# git-subtree.sh: split/join git repositories in subdirectories of this one
   4#
   5# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
   6#
   7OPTS_SPEC="\
   8git subtree split [--rejoin] [--onto rev] <commit...> -- <path>
   9git subtree merge 
  10
  11git subtree does foo and bar!
  12--
  13h,help   show the help
  14q        quiet
  15v        verbose
  16onto=    existing subtree revision to connect, if any
  17rejoin   merge the new branch back into HEAD
  18"
  19eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
  20. git-sh-setup
  21require_work_tree
  22
  23quiet=
  24command=
  25onto=
  26rejoin=
  27
  28debug()
  29{
  30        if [ -z "$quiet" ]; then
  31                echo "$@" >&2
  32        fi
  33}
  34
  35assert()
  36{
  37        if "$@"; then
  38                :
  39        else
  40                die "assertion failed: " "$@"
  41        fi
  42}
  43
  44
  45#echo "Options: $*"
  46
  47while [ $# -gt 0 ]; do
  48        opt="$1"
  49        shift
  50        case "$opt" in
  51                -q) quiet=1 ;;
  52                --onto) onto="$1"; shift ;;
  53                --rejoin) rejoin=1 ;;
  54                --) break ;;
  55        esac
  56done
  57
  58command="$1"
  59shift
  60case "$command" in
  61        split|merge) ;;
  62        *) die "Unknown command '$command'" ;;
  63esac
  64
  65revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $?
  66dirs="$(git rev-parse --sq --no-revs --no-flags "$@")" || exit $?
  67
  68#echo "dirs is {$dirs}"
  69eval $(echo set -- $dirs)
  70if [ "$#" -ne 1 ]; then
  71        die "Must provide exactly one subtree dir (got $#)"
  72fi
  73dir="$1"
  74
  75debug "command: {$command}"
  76debug "quiet: {$quiet}"
  77debug "revs: {$revs}"
  78debug "dir: {$dir}"
  79
  80cache_setup()
  81{
  82        cachedir="$GIT_DIR/subtree-cache/$$"
  83        rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
  84        mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
  85        debug "Using cachedir: $cachedir" >&2
  86}
  87
  88cache_get()
  89{
  90        for oldrev in $*; do
  91                if [ -r "$cachedir/$oldrev" ]; then
  92                        read newrev <"$cachedir/$oldrev"
  93                        echo $newrev
  94                fi
  95        done
  96}
  97
  98cache_set()
  99{
 100        oldrev="$1"
 101        newrev="$2"
 102        if [ "$oldrev" != "latest_old" \
 103             -a "$oldrev" != "latest_new" \
 104             -a -e "$cachedir/$oldrev" ]; then
 105                die "cache for $oldrev already exists!"
 106        fi
 107        echo "$newrev" >"$cachedir/$oldrev"
 108}
 109
 110find_existing_splits()
 111{
 112        debug "Looking for prior splits..."
 113        dir="$1"
 114        revs="$2"
 115        git log --grep="^git-subtree-dir: $dir\$" \
 116                --pretty=format:'%s%n%n%b%nEND' "$revs" |
 117        while read a b junk; do
 118                case "$a" in
 119                        git-subtree-mainline:) main="$b" ;;
 120                        git-subtree-split:) sub="$b" ;;
 121                        *)
 122                                if [ -n "$main" -a -n "$sub" ]; then
 123                                        debug "  Prior: $main -> $sub"
 124                                        cache_set $main $sub
 125                                        echo "^$main^ ^$sub^"
 126                                        main=
 127                                        sub=
 128                                fi
 129                                ;;
 130                esac
 131        done
 132}
 133
 134copy_commit()
 135{
 136        # We're doing to set some environment vars here, so
 137        # do it in a subshell to get rid of them safely later
 138        git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
 139        (
 140                read GIT_AUTHOR_NAME
 141                read GIT_AUTHOR_EMAIL
 142                read GIT_AUTHOR_DATE
 143                read GIT_COMMITTER_NAME
 144                read GIT_COMMITTER_EMAIL
 145                read GIT_COMMITTER_DATE
 146                export  GIT_AUTHOR_NAME \
 147                        GIT_AUTHOR_EMAIL \
 148                        GIT_AUTHOR_DATE \
 149                        GIT_COMMITTER_NAME \
 150                        GIT_COMMITTER_EMAIL \
 151                        GIT_COMMITTER_DATE
 152                git commit-tree "$2" $3  # reads the rest of stdin
 153        ) || die "Can't copy commit $1"
 154}
 155
 156merge_msg()
 157{
 158        dir="$1"
 159        latest_old="$2"
 160        latest_new="$3"
 161        cat <<-EOF
 162                Split '$dir/' into commit '$latest_new'
 163                
 164                git-subtree-dir: $dir
 165                git-subtree-mainline: $latest_old
 166                git-subtree-split: $latest_new
 167        EOF
 168}
 169
 170cmd_split()
 171{
 172        debug "Splitting $dir..."
 173        cache_setup || exit $?
 174        
 175        unrevs="$(find_existing_splits "$dir" "$revs")"
 176        
 177        git rev-list --reverse --parents $revs $unrevs -- "$dir" |
 178        while read rev parents; do
 179                exists=$(cache_get $rev)
 180                newparents=$(cache_get $parents)
 181                debug
 182                debug "Processing commit: $rev / $newparents"
 183                
 184                if [ -n "$exists" ]; then
 185                        debug "  prior: $exists"
 186                        continue
 187                fi
 188                
 189                git ls-tree $rev -- "$dir" |
 190                while read mode type tree name; do
 191                        assert [ "$name" = "$dir" ]
 192                        debug "  tree is: $tree"
 193                        p=""
 194                        for parent in $newparents; do
 195                                p="$p -p $parent"
 196                        done
 197                        
 198                        newrev=$(copy_commit $rev $tree "$p") || exit $?
 199                        debug "  newrev is: $newrev"
 200                        cache_set $rev $newrev
 201                        cache_set latest_new $newrev
 202                        cache_set latest_old $rev
 203                done || exit $?
 204        done || exit $?
 205        latest_new=$(cache_get latest_new)
 206        if [ -z "$latest_new" ]; then
 207                die "No new revisions were found"
 208        fi
 209        
 210        if [ -n "$rejoin" ]; then
 211                debug "Merging split branch into HEAD..."
 212                latest_old=$(cache_get latest_old)
 213                git merge -s ours \
 214                        -m "$(merge_msg $dir $latest_old $latest_new)" \
 215                        $latest_new
 216        fi
 217        echo $latest_new
 218        exit 0
 219}
 220
 221cmd_merge()
 222{
 223        die "merge command not implemented yet"
 224}
 225
 226"cmd_$command"