e6d8ce8817d39a813c44bf46d6810ff110690128
   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 search for parent
  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                (echo -n '*'; cat ) |  # FIXME
 153                git commit-tree "$2" $3  # reads the rest of stdin
 154        ) || die "Can't copy commit $1"
 155}
 156
 157merge_msg()
 158{
 159        dir="$1"
 160        latest_old="$2"
 161        latest_new="$3"
 162        cat <<-EOF
 163                Split '$dir/' into commit '$latest_new'
 164                
 165                git-subtree-dir: $dir
 166                git-subtree-mainline: $latest_old
 167                git-subtree-split: $latest_new
 168        EOF
 169}
 170
 171toptree_for_commit()
 172{
 173        commit="$1"
 174        git log -1 --pretty=format:'%T' "$commit" -- || exit $?
 175}
 176
 177subtree_for_commit()
 178{
 179        commit="$1"
 180        dir="$2"
 181        git ls-tree "$commit" -- "$dir" |
 182        while read mode type tree name; do
 183                assert [ "$name" = "$dir" ]
 184                echo $tree
 185                break
 186        done
 187}
 188
 189tree_changed()
 190{
 191        tree=$1
 192        shift
 193        if [ $# -ne 1 ]; then
 194                return 0   # weird parents, consider it changed
 195        else
 196                ptree=$(toptree_for_commit $1)
 197                if [ "$ptree" != "$tree" ]; then
 198                        return 0   # changed
 199                else
 200                        return 1   # not changed
 201                fi
 202        fi
 203}
 204
 205cmd_split()
 206{
 207        debug "Splitting $dir..."
 208        cache_setup || exit $?
 209        
 210        if [ -n "$onto" ]; then
 211                debug "Reading history for --onto=$onto..."
 212                git rev-list $onto |
 213                while read rev; do
 214                        # the 'onto' history is already just the subdir, so
 215                        # any parent we find there can be used verbatim
 216                        debug "  cache: $rev"
 217                        cache_set $rev $rev
 218                done
 219        fi
 220        
 221        unrevs="$(find_existing_splits "$dir" "$revs")"
 222        
 223        debug "git rev-list --reverse $revs $unrevs"
 224        git rev-list --reverse --parents $revs $unrevsx |
 225        while read rev parents; do
 226                debug
 227                debug "Processing commit: $rev"
 228                exists=$(cache_get $rev)
 229                if [ -n "$exists" ]; then
 230                        debug "  prior: $exists"
 231                        continue
 232                fi
 233                debug "  parents: $parents"
 234                newparents=$(cache_get $parents)
 235                debug "  newparents: $newparents"
 236                
 237                tree=$(subtree_for_commit $rev "$dir")
 238                debug "  tree is: $tree"
 239                [ -z $tree ] && continue
 240
 241                p=""
 242                for parent in $newparents; do
 243                        p="$p -p $parent"
 244                done
 245                        
 246                if tree_changed $tree $newparents; then
 247                        newrev=$(copy_commit $rev $tree "$p") || exit $?
 248                else
 249                        newrev="$newparents"
 250                fi
 251                debug "  newrev is: $newrev"
 252                cache_set $rev $newrev
 253                cache_set latest_new $newrev
 254                cache_set latest_old $rev
 255        done || exit $?
 256        latest_new=$(cache_get latest_new)
 257        if [ -z "$latest_new" ]; then
 258                die "No new revisions were found"
 259        fi
 260        
 261        if [ -n "$rejoin" ]; then
 262                debug "Merging split branch into HEAD..."
 263                latest_old=$(cache_get latest_old)
 264                git merge -s ours \
 265                        -m "$(merge_msg $dir $latest_old $latest_new)" \
 266                        $latest_new >&2
 267        fi
 268        echo $latest_new
 269        exit 0
 270}
 271
 272cmd_merge()
 273{
 274        die "merge command not implemented yet"
 275}
 276
 277"cmd_$command"