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