f2a1c6aae4561fa7d85a4a7faabe4ea8771d74b1
   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#
   7if [ $# -eq 0 ]; then
   8    set -- -h
   9fi
  10OPTS_SPEC="\
  11git subtree add --prefix=<prefix> <commit>
  12git subtree split [options...] --prefix=<prefix> <commit...>
  13git subtree merge --prefix=<prefix> <commit>
  14git subtree pull  --prefix=<prefix> <repository> <refspec...>
  15--
  16h,help        show the help
  17q             quiet
  18prefix=       the name of the subdir to split out
  19 options for 'split'
  20onto=         try connecting new tree to an existing one
  21rejoin        merge the new branch back into HEAD
  22ignore-joins  ignore prior --rejoin commits
  23"
  24eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
  25. git-sh-setup
  26require_work_tree
  27
  28quiet=
  29command=
  30onto=
  31rejoin=
  32ignore_joins=
  33
  34debug()
  35{
  36        if [ -z "$quiet" ]; then
  37                echo "$@" >&2
  38        fi
  39}
  40
  41assert()
  42{
  43        if "$@"; then
  44                :
  45        else
  46                die "assertion failed: " "$@"
  47        fi
  48}
  49
  50
  51#echo "Options: $*"
  52
  53while [ $# -gt 0 ]; do
  54        opt="$1"
  55        shift
  56        case "$opt" in
  57                -q) quiet=1 ;;
  58                --prefix) prefix="$1"; shift ;;
  59                --no-prefix) prefix= ;;
  60                --onto) onto="$1"; shift ;;
  61                --no-onto) onto= ;;
  62                --rejoin) rejoin=1 ;;
  63                --no-rejoin) rejoin= ;;
  64                --ignore-joins) ignore_joins=1 ;;
  65                --no-ignore-joins) ignore_joins= ;;
  66                --) break ;;
  67        esac
  68done
  69
  70command="$1"
  71shift
  72case "$command" in
  73        add|merge|pull) default= ;;
  74        split) default="--default HEAD" ;;
  75        *) die "Unknown command '$command'" ;;
  76esac
  77
  78if [ -z "$prefix" ]; then
  79        die "You must provide the --prefix option."
  80fi
  81dir="$prefix"
  82
  83if [ "$command" != "pull" ]; then
  84        revs=$(git rev-parse $default --revs-only "$@") || exit $?
  85        dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
  86        if [ -n "$dirs" ]; then
  87                die "Error: Use --prefix instead of bare filenames."
  88        fi
  89fi
  90
  91debug "command: {$command}"
  92debug "quiet: {$quiet}"
  93debug "revs: {$revs}"
  94debug "dir: {$dir}"
  95debug "opts: {$*}"
  96debug
  97
  98cache_setup()
  99{
 100        cachedir="$GIT_DIR/subtree-cache/$$"
 101        rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
 102        mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
 103        debug "Using cachedir: $cachedir" >&2
 104}
 105
 106cache_get()
 107{
 108        for oldrev in $*; do
 109                if [ -r "$cachedir/$oldrev" ]; then
 110                        read newrev <"$cachedir/$oldrev"
 111                        echo $newrev
 112                fi
 113        done
 114}
 115
 116cache_set()
 117{
 118        oldrev="$1"
 119        newrev="$2"
 120        if [ "$oldrev" != "latest_old" \
 121             -a "$oldrev" != "latest_new" \
 122             -a -e "$cachedir/$oldrev" ]; then
 123                die "cache for $oldrev already exists!"
 124        fi
 125        echo "$newrev" >"$cachedir/$oldrev"
 126}
 127
 128find_existing_splits()
 129{
 130        debug "Looking for prior splits..."
 131        dir="$1"
 132        revs="$2"
 133        git log --grep="^git-subtree-dir: $dir\$" \
 134                --pretty=format:'%s%n%n%b%nEND' "$revs" |
 135        while read a b junk; do
 136                case "$a" in
 137                        git-subtree-mainline:) main="$b" ;;
 138                        git-subtree-split:) sub="$b" ;;
 139                        *)
 140                                if [ -n "$main" -a -n "$sub" ]; then
 141                                        debug "  Prior: $main -> $sub"
 142                                        cache_set $main $sub
 143                                        echo "^$main^ ^$sub^"
 144                                        main=
 145                                        sub=
 146                                fi
 147                                ;;
 148                esac
 149        done
 150}
 151
 152copy_commit()
 153{
 154        # We're doing to set some environment vars here, so
 155        # do it in a subshell to get rid of them safely later
 156        git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
 157        (
 158                read GIT_AUTHOR_NAME
 159                read GIT_AUTHOR_EMAIL
 160                read GIT_AUTHOR_DATE
 161                read GIT_COMMITTER_NAME
 162                read GIT_COMMITTER_EMAIL
 163                read GIT_COMMITTER_DATE
 164                export  GIT_AUTHOR_NAME \
 165                        GIT_AUTHOR_EMAIL \
 166                        GIT_AUTHOR_DATE \
 167                        GIT_COMMITTER_NAME \
 168                        GIT_COMMITTER_EMAIL \
 169                        GIT_COMMITTER_DATE
 170                (echo -n '*'; cat ) |  # FIXME
 171                git commit-tree "$2" $3  # reads the rest of stdin
 172        ) || die "Can't copy commit $1"
 173}
 174
 175add_msg()
 176{
 177        dir="$1"
 178        latest_old="$2"
 179        latest_new="$3"
 180        cat <<-EOF
 181                Add '$dir/' from commit '$latest_new'
 182                
 183                git-subtree-dir: $dir
 184                git-subtree-mainline: $latest_old
 185                git-subtree-split: $latest_new
 186        EOF
 187}
 188
 189merge_msg()
 190{
 191        dir="$1"
 192        latest_old="$2"
 193        latest_new="$3"
 194        cat <<-EOF
 195                Split '$dir/' into commit '$latest_new'
 196                
 197                git-subtree-dir: $dir
 198                git-subtree-mainline: $latest_old
 199                git-subtree-split: $latest_new
 200        EOF
 201}
 202
 203toptree_for_commit()
 204{
 205        commit="$1"
 206        git log -1 --pretty=format:'%T' "$commit" -- || exit $?
 207}
 208
 209subtree_for_commit()
 210{
 211        commit="$1"
 212        dir="$2"
 213        git ls-tree "$commit" -- "$dir" |
 214        while read mode type tree name; do
 215                assert [ "$name" = "$dir" ]
 216                echo $tree
 217                break
 218        done
 219}
 220
 221tree_changed()
 222{
 223        tree=$1
 224        shift
 225        if [ $# -ne 1 ]; then
 226                return 0   # weird parents, consider it changed
 227        else
 228                ptree=$(toptree_for_commit $1)
 229                if [ "$ptree" != "$tree" ]; then
 230                        return 0   # changed
 231                else
 232                        return 1   # not changed
 233                fi
 234        fi
 235}
 236
 237copy_or_skip()
 238{
 239        rev="$1"
 240        tree="$2"
 241        newparents="$3"
 242        assert [ -n "$tree" ]
 243
 244        identical=
 245        p=
 246        for parent in $newparents; do
 247                ptree=$(toptree_for_commit $parent) || exit $?
 248                if [ "$ptree" = "$tree" ]; then
 249                        # an identical parent could be used in place of this rev.
 250                        identical="$parent"
 251                fi
 252                if [ -n "$ptree" ]; then
 253                        parentmatch="$parentmatch$parent"
 254                        p="$p -p $parent"
 255                fi
 256        done
 257        
 258        if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
 259                echo $identical
 260        else
 261                copy_commit $rev $tree "$p" || exit $?
 262        fi
 263}
 264
 265ensure_clean()
 266{
 267        if ! git diff-index HEAD --exit-code --quiet; then
 268                die "Working tree has modifications.  Cannot add."
 269        fi
 270        if ! git diff-index --cached HEAD --exit-code --quiet; then
 271                die "Index has modifications.  Cannot add."
 272        fi
 273}
 274
 275cmd_add()
 276{
 277        if [ -e "$dir" ]; then
 278                die "'$dir' already exists.  Cannot add."
 279        fi
 280        ensure_clean
 281        
 282        set -- $revs
 283        if [ $# -ne 1 ]; then
 284                die "You must provide exactly one revision.  Got: '$revs'"
 285        fi
 286        rev="$1"
 287        
 288        debug "Adding $dir as '$rev'..."
 289        git read-tree --prefix="$dir" $rev || exit $?
 290        git checkout "$dir" || exit $?
 291        tree=$(git write-tree) || exit $?
 292        
 293        headrev=$(git rev-parse HEAD) || exit $?
 294        if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
 295                headp="-p $headrev"
 296        else
 297                headp=
 298        fi
 299        commit=$(add_msg "$dir" "$headrev" "$rev" |
 300                 git commit-tree $tree $headp -p "$rev") || exit $?
 301        git reset "$commit" || exit $?
 302}
 303
 304cmd_split()
 305{
 306        debug "Splitting $dir..."
 307        cache_setup || exit $?
 308        
 309        if [ -n "$onto" ]; then
 310                debug "Reading history for --onto=$onto..."
 311                git rev-list $onto |
 312                while read rev; do
 313                        # the 'onto' history is already just the subdir, so
 314                        # any parent we find there can be used verbatim
 315                        debug "  cache: $rev"
 316                        cache_set $rev $rev
 317                done
 318        fi
 319        
 320        if [ -n "$ignore_joins" ]; then
 321                unrevs=
 322        else
 323                unrevs="$(find_existing_splits "$dir" "$revs")"
 324        fi
 325        
 326        # We can't restrict rev-list to only "$dir" here, because that leaves out
 327        # critical information about commit parents.
 328        debug "git rev-list --reverse --parents $revs $unrevs"
 329        git rev-list --reverse --parents $revs $unrevs |
 330        while read rev parents; do
 331                debug
 332                debug "Processing commit: $rev"
 333                exists=$(cache_get $rev)
 334                if [ -n "$exists" ]; then
 335                        debug "  prior: $exists"
 336                        continue
 337                fi
 338                debug "  parents: $parents"
 339                newparents=$(cache_get $parents)
 340                debug "  newparents: $newparents"
 341                
 342                tree=$(subtree_for_commit $rev "$dir")
 343                debug "  tree is: $tree"
 344                [ -z $tree ] && continue
 345
 346                newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
 347                debug "  newrev is: $newrev"
 348                cache_set $rev $newrev
 349                cache_set latest_new $newrev
 350                cache_set latest_old $rev
 351        done || exit $?
 352        latest_new=$(cache_get latest_new)
 353        if [ -z "$latest_new" ]; then
 354                die "No new revisions were found"
 355        fi
 356        
 357        if [ -n "$rejoin" ]; then
 358                debug "Merging split branch into HEAD..."
 359                latest_old=$(cache_get latest_old)
 360                git merge -s ours \
 361                        -m "$(merge_msg $dir $latest_old $latest_new)" \
 362                        $latest_new >&2
 363        fi
 364        echo $latest_new
 365        exit 0
 366}
 367
 368cmd_merge()
 369{
 370        ensure_clean
 371        
 372        set -- $revs
 373        if [ $# -ne 1 ]; then
 374                die "You must provide exactly one revision.  Got: '$revs'"
 375        fi
 376        rev="$1"
 377        
 378        git merge -s subtree $rev
 379}
 380
 381cmd_pull()
 382{
 383        ensure_clean
 384        set -x
 385        git pull -s subtree "$@"
 386}
 387
 388"cmd_$command" "$@"