aeafadac95d4bb25e38d90ee58d0d9ba4f85d59f
   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
 128# if a commit doesn't have a parent, this might not work.  But we only want
 129# to remove the parent from the rev-list, and since it doesn't exist, it won't
 130# be there anyway, so do nothing in that case.
 131try_remove_previous()
 132{
 133        if git rev-parse "$1^" >/dev/null 2>&1; then
 134                echo "^$1^"
 135        fi
 136}
 137
 138find_existing_splits()
 139{
 140        debug "Looking for prior splits..."
 141        dir="$1"
 142        revs="$2"
 143        git log --grep="^git-subtree-dir: $dir\$" \
 144                --pretty=format:'%s%n%n%b%nEND' "$revs" |
 145        while read a b junk; do
 146                case "$a" in
 147                        git-subtree-mainline:) main="$b" ;;
 148                        git-subtree-split:) sub="$b" ;;
 149                        *)
 150                                if [ -n "$main" -a -n "$sub" ]; then
 151                                        debug "  Prior: $main -> $sub"
 152                                        cache_set $main $sub
 153                                        try_remove_previous "$main"
 154                                        try_remove_previous "$sub"
 155                                        main=
 156                                        sub=
 157                                fi
 158                                ;;
 159                esac
 160        done
 161}
 162
 163copy_commit()
 164{
 165        # We're doing to set some environment vars here, so
 166        # do it in a subshell to get rid of them safely later
 167        git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
 168        (
 169                read GIT_AUTHOR_NAME
 170                read GIT_AUTHOR_EMAIL
 171                read GIT_AUTHOR_DATE
 172                read GIT_COMMITTER_NAME
 173                read GIT_COMMITTER_EMAIL
 174                read GIT_COMMITTER_DATE
 175                export  GIT_AUTHOR_NAME \
 176                        GIT_AUTHOR_EMAIL \
 177                        GIT_AUTHOR_DATE \
 178                        GIT_COMMITTER_NAME \
 179                        GIT_COMMITTER_EMAIL \
 180                        GIT_COMMITTER_DATE
 181                (echo -n '*'; cat ) |  # FIXME
 182                git commit-tree "$2" $3  # reads the rest of stdin
 183        ) || die "Can't copy commit $1"
 184}
 185
 186add_msg()
 187{
 188        dir="$1"
 189        latest_old="$2"
 190        latest_new="$3"
 191        cat <<-EOF
 192                Add '$dir/' from commit '$latest_new'
 193                
 194                git-subtree-dir: $dir
 195                git-subtree-mainline: $latest_old
 196                git-subtree-split: $latest_new
 197        EOF
 198}
 199
 200merge_msg()
 201{
 202        dir="$1"
 203        latest_old="$2"
 204        latest_new="$3"
 205        cat <<-EOF
 206                Split '$dir/' into commit '$latest_new'
 207                
 208                git-subtree-dir: $dir
 209                git-subtree-mainline: $latest_old
 210                git-subtree-split: $latest_new
 211        EOF
 212}
 213
 214toptree_for_commit()
 215{
 216        commit="$1"
 217        git log -1 --pretty=format:'%T' "$commit" -- || exit $?
 218}
 219
 220subtree_for_commit()
 221{
 222        commit="$1"
 223        dir="$2"
 224        git ls-tree "$commit" -- "$dir" |
 225        while read mode type tree name; do
 226                assert [ "$name" = "$dir" ]
 227                echo $tree
 228                break
 229        done
 230}
 231
 232tree_changed()
 233{
 234        tree=$1
 235        shift
 236        if [ $# -ne 1 ]; then
 237                return 0   # weird parents, consider it changed
 238        else
 239                ptree=$(toptree_for_commit $1)
 240                if [ "$ptree" != "$tree" ]; then
 241                        return 0   # changed
 242                else
 243                        return 1   # not changed
 244                fi
 245        fi
 246}
 247
 248copy_or_skip()
 249{
 250        rev="$1"
 251        tree="$2"
 252        newparents="$3"
 253        assert [ -n "$tree" ]
 254
 255        identical=
 256        p=
 257        for parent in $newparents; do
 258                ptree=$(toptree_for_commit $parent) || exit $?
 259                if [ "$ptree" = "$tree" ]; then
 260                        # an identical parent could be used in place of this rev.
 261                        identical="$parent"
 262                fi
 263                if [ -n "$ptree" ]; then
 264                        parentmatch="$parentmatch$parent"
 265                        p="$p -p $parent"
 266                fi
 267        done
 268        
 269        if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
 270                echo $identical
 271        else
 272                copy_commit $rev $tree "$p" || exit $?
 273        fi
 274}
 275
 276ensure_clean()
 277{
 278        if ! git diff-index HEAD --exit-code --quiet; then
 279                die "Working tree has modifications.  Cannot add."
 280        fi
 281        if ! git diff-index --cached HEAD --exit-code --quiet; then
 282                die "Index has modifications.  Cannot add."
 283        fi
 284}
 285
 286cmd_add()
 287{
 288        if [ -e "$dir" ]; then
 289                die "'$dir' already exists.  Cannot add."
 290        fi
 291        ensure_clean
 292        
 293        set -- $revs
 294        if [ $# -ne 1 ]; then
 295                die "You must provide exactly one revision.  Got: '$revs'"
 296        fi
 297        rev="$1"
 298        
 299        debug "Adding $dir as '$rev'..."
 300        git read-tree --prefix="$dir" $rev || exit $?
 301        git checkout "$dir" || exit $?
 302        tree=$(git write-tree) || exit $?
 303        
 304        headrev=$(git rev-parse HEAD) || exit $?
 305        if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
 306                headp="-p $headrev"
 307        else
 308                headp=
 309        fi
 310        commit=$(add_msg "$dir" "$headrev" "$rev" |
 311                 git commit-tree $tree $headp -p "$rev") || exit $?
 312        git reset "$commit" || exit $?
 313}
 314
 315cmd_split()
 316{
 317        debug "Splitting $dir..."
 318        cache_setup || exit $?
 319        
 320        if [ -n "$onto" ]; then
 321                debug "Reading history for --onto=$onto..."
 322                git rev-list $onto |
 323                while read rev; do
 324                        # the 'onto' history is already just the subdir, so
 325                        # any parent we find there can be used verbatim
 326                        debug "  cache: $rev"
 327                        cache_set $rev $rev
 328                done
 329        fi
 330        
 331        if [ -n "$ignore_joins" ]; then
 332                unrevs=
 333        else
 334                unrevs="$(find_existing_splits "$dir" "$revs")"
 335        fi
 336        
 337        # We can't restrict rev-list to only "$dir" here, because that leaves out
 338        # critical information about commit parents.
 339        debug "git rev-list --reverse --parents $revs $unrevs"
 340        git rev-list --reverse --parents $revs $unrevs |
 341        while read rev parents; do
 342                debug
 343                debug "Processing commit: $rev"
 344                exists=$(cache_get $rev)
 345                if [ -n "$exists" ]; then
 346                        debug "  prior: $exists"
 347                        continue
 348                fi
 349                debug "  parents: $parents"
 350                newparents=$(cache_get $parents)
 351                debug "  newparents: $newparents"
 352                
 353                tree=$(subtree_for_commit $rev "$dir")
 354                debug "  tree is: $tree"
 355                [ -z $tree ] && continue
 356
 357                newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
 358                debug "  newrev is: $newrev"
 359                cache_set $rev $newrev
 360                cache_set latest_new $newrev
 361                cache_set latest_old $rev
 362        done || exit $?
 363        latest_new=$(cache_get latest_new)
 364        if [ -z "$latest_new" ]; then
 365                die "No new revisions were found"
 366        fi
 367        
 368        if [ -n "$rejoin" ]; then
 369                debug "Merging split branch into HEAD..."
 370                latest_old=$(cache_get latest_old)
 371                git merge -s ours \
 372                        -m "$(merge_msg $dir $latest_old $latest_new)" \
 373                        $latest_new >&2
 374        fi
 375        echo $latest_new
 376        exit 0
 377}
 378
 379cmd_merge()
 380{
 381        ensure_clean
 382        
 383        set -- $revs
 384        if [ $# -ne 1 ]; then
 385                die "You must provide exactly one revision.  Got: '$revs'"
 386        fi
 387        rev="$1"
 388        
 389        git merge -s subtree $rev
 390}
 391
 392cmd_pull()
 393{
 394        ensure_clean
 395        set -x
 396        git pull -s subtree "$@"
 397}
 398
 399"cmd_$command" "$@"