git-subtree.shon commit Fix a few typos/grammar-o's in the preceding commit. (9a40fcc)
   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 merge --prefix=<prefix> <commit>
  13git subtree pull  --prefix=<prefix> <repository> <refspec...>
  14git subtree push  --prefix=<prefix> <repository> <refspec...>
  15git subtree split --prefix=<prefix> <commit...>
  16--
  17h,help        show the help
  18q             quiet
  19d             show debug messages
  20P,prefix=     the name of the subdir to split out
  21m,message=    use the given message as the commit message for the merge commit
  22 options for 'split'
  23annotate=     add a prefix to commit message of new commits
  24b,branch=     create a new branch from the split subtree
  25ignore-joins  ignore prior --rejoin commits
  26onto=         try connecting new tree to an existing one
  27rejoin        merge the new branch back into HEAD
  28 options for 'add', 'merge', 'pull' and 'push'
  29squash        merge subtree changes as a single commit
  30"
  31eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
  32
  33PATH=$PATH:$(git --exec-path)
  34. git-sh-setup
  35
  36require_work_tree
  37
  38quiet=
  39branch=
  40debug=
  41command=
  42onto=
  43rejoin=
  44ignore_joins=
  45annotate=
  46squash=
  47message=
  48
  49debug()
  50{
  51        if [ -n "$debug" ]; then
  52                echo "$@" >&2
  53        fi
  54}
  55
  56say()
  57{
  58        if [ -z "$quiet" ]; then
  59                echo "$@" >&2
  60        fi
  61}
  62
  63assert()
  64{
  65        if "$@"; then
  66                :
  67        else
  68                die "assertion failed: " "$@"
  69        fi
  70}
  71
  72
  73#echo "Options: $*"
  74
  75while [ $# -gt 0 ]; do
  76        opt="$1"
  77        shift
  78        case "$opt" in
  79                -q) quiet=1 ;;
  80                -d) debug=1 ;;
  81                --annotate) annotate="$1"; shift ;;
  82                --no-annotate) annotate= ;;
  83                -b) branch="$1"; shift ;;
  84                -P) prefix="$1"; shift ;;
  85                -m) message="$1"; shift ;;
  86                --no-prefix) prefix= ;;
  87                --onto) onto="$1"; shift ;;
  88                --no-onto) onto= ;;
  89                --rejoin) rejoin=1 ;;
  90                --no-rejoin) rejoin= ;;
  91                --ignore-joins) ignore_joins=1 ;;
  92                --no-ignore-joins) ignore_joins= ;;
  93                --squash) squash=1 ;;
  94                --no-squash) squash= ;;
  95                --) break ;;
  96                *) die "Unexpected option: $opt" ;;
  97        esac
  98done
  99
 100command="$1"
 101shift
 102case "$command" in
 103        add|merge|pull) default= ;;
 104        split|push) default="--default HEAD" ;;
 105        *) die "Unknown command '$command'" ;;
 106esac
 107
 108if [ -z "$prefix" ]; then
 109        die "You must provide the --prefix option."
 110fi
 111
 112case "$command" in
 113        add) [ -e "$prefix" ] && 
 114                die "prefix '$prefix' already exists." ;;
 115        *)   [ -e "$prefix" ] || 
 116                die "'$prefix' does not exist; use 'git subtree add'" ;;
 117esac
 118
 119dir="$(dirname "$prefix/.")"
 120
 121if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
 122        revs=$(git rev-parse $default --revs-only "$@") || exit $?
 123        dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
 124        if [ -n "$dirs" ]; then
 125                die "Error: Use --prefix instead of bare filenames."
 126        fi
 127fi
 128
 129debug "command: {$command}"
 130debug "quiet: {$quiet}"
 131debug "revs: {$revs}"
 132debug "dir: {$dir}"
 133debug "opts: {$*}"
 134debug
 135
 136cache_setup()
 137{
 138        cachedir="$GIT_DIR/subtree-cache/$$"
 139        rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
 140        mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
 141        debug "Using cachedir: $cachedir" >&2
 142}
 143
 144cache_get()
 145{
 146        for oldrev in $*; do
 147                if [ -r "$cachedir/$oldrev" ]; then
 148                        read newrev <"$cachedir/$oldrev"
 149                        echo $newrev
 150                fi
 151        done
 152}
 153
 154cache_set()
 155{
 156        oldrev="$1"
 157        newrev="$2"
 158        if [ "$oldrev" != "latest_old" \
 159             -a "$oldrev" != "latest_new" \
 160             -a -e "$cachedir/$oldrev" ]; then
 161                die "cache for $oldrev already exists!"
 162        fi
 163        echo "$newrev" >"$cachedir/$oldrev"
 164}
 165
 166rev_exists()
 167{
 168        if git rev-parse "$1" >/dev/null 2>&1; then
 169                return 0
 170        else
 171                return 1
 172        fi
 173}
 174
 175rev_is_descendant_of_branch()
 176{
 177        newrev="$1"
 178        branch="$2"
 179        branch_hash=$(git rev-parse $branch)
 180        match=$(git rev-list -1 $branch_hash ^$newrev)
 181
 182        if [ -z "$match" ]; then
 183                return 0
 184        else
 185                return 1
 186        fi
 187}
 188
 189# if a commit doesn't have a parent, this might not work.  But we only want
 190# to remove the parent from the rev-list, and since it doesn't exist, it won't
 191# be there anyway, so do nothing in that case.
 192try_remove_previous()
 193{
 194        if rev_exists "$1^"; then
 195                echo "^$1^"
 196        fi
 197}
 198
 199find_latest_squash()
 200{
 201        debug "Looking for latest squash ($dir)..."
 202        dir="$1"
 203        sq=
 204        main=
 205        sub=
 206        git log --grep="^git-subtree-dir: $dir/*\$" \
 207                --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
 208        while read a b junk; do
 209                debug "$a $b $junk"
 210                debug "{{$sq/$main/$sub}}"
 211                case "$a" in
 212                        START) sq="$b" ;;
 213                        git-subtree-mainline:) main="$b" ;;
 214                        git-subtree-split:) sub="$b" ;;
 215                        END)
 216                                if [ -n "$sub" ]; then
 217                                        if [ -n "$main" ]; then
 218                                                # a rejoin commit?
 219                                                # Pretend its sub was a squash.
 220                                                sq="$sub"
 221                                        fi
 222                                        debug "Squash found: $sq $sub"
 223                                        echo "$sq" "$sub"
 224                                        break
 225                                fi
 226                                sq=
 227                                main=
 228                                sub=
 229                                ;;
 230                esac
 231        done
 232}
 233
 234find_existing_splits()
 235{
 236        debug "Looking for prior splits..."
 237        dir="$1"
 238        revs="$2"
 239        main=
 240        sub=
 241        git log --grep="^git-subtree-dir: $dir/*\$" \
 242                --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
 243        while read a b junk; do
 244                case "$a" in
 245                        START) sq="$b" ;;
 246                        git-subtree-mainline:) main="$b" ;;
 247                        git-subtree-split:) sub="$b" ;;
 248                        END)
 249                                debug "  Main is: '$main'"
 250                                if [ -z "$main" -a -n "$sub" ]; then
 251                                        # squash commits refer to a subtree
 252                                        debug "  Squash: $sq from $sub"
 253                                        cache_set "$sq" "$sub"
 254                                fi
 255                                if [ -n "$main" -a -n "$sub" ]; then
 256                                        debug "  Prior: $main -> $sub"
 257                                        cache_set $main $sub
 258                                        cache_set $sub $sub
 259                                        try_remove_previous "$main"
 260                                        try_remove_previous "$sub"
 261                                fi
 262                                main=
 263                                sub=
 264                                ;;
 265                esac
 266        done
 267}
 268
 269copy_commit()
 270{
 271        # We're going to set some environment vars here, so
 272        # do it in a subshell to get rid of them safely later
 273        debug copy_commit "{$1}" "{$2}" "{$3}"
 274        git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
 275        (
 276                read GIT_AUTHOR_NAME
 277                read GIT_AUTHOR_EMAIL
 278                read GIT_AUTHOR_DATE
 279                read GIT_COMMITTER_NAME
 280                read GIT_COMMITTER_EMAIL
 281                read GIT_COMMITTER_DATE
 282                export  GIT_AUTHOR_NAME \
 283                        GIT_AUTHOR_EMAIL \
 284                        GIT_AUTHOR_DATE \
 285                        GIT_COMMITTER_NAME \
 286                        GIT_COMMITTER_EMAIL \
 287                        GIT_COMMITTER_DATE
 288                (echo -n "$annotate"; cat ) |
 289                git commit-tree "$2" $3  # reads the rest of stdin
 290        ) || die "Can't copy commit $1"
 291}
 292
 293add_msg()
 294{
 295        dir="$1"
 296        latest_old="$2"
 297        latest_new="$3"
 298        if [ -n "$message" ]; then
 299                commit_message="$message"
 300        else
 301                commit_message="Add '$dir/' from commit '$latest_new'"
 302        fi
 303        cat <<-EOF
 304                $commit_message
 305                
 306                git-subtree-dir: $dir
 307                git-subtree-mainline: $latest_old
 308                git-subtree-split: $latest_new
 309        EOF
 310}
 311
 312add_squashed_msg()
 313{
 314        if [ -n "$message" ]; then
 315                echo "$message"
 316        else
 317                echo "Merge commit '$1' as '$2'"
 318        fi
 319}
 320
 321rejoin_msg()
 322{
 323        dir="$1"
 324        latest_old="$2"
 325        latest_new="$3"
 326        if [ -n "$message" ]; then
 327                commit_message="$message"
 328        else
 329                commit_message="Split '$dir/' into commit '$latest_new'"
 330        fi
 331        cat <<-EOF
 332                $commit_message
 333                
 334                git-subtree-dir: $dir
 335                git-subtree-mainline: $latest_old
 336                git-subtree-split: $latest_new
 337        EOF
 338}
 339
 340squash_msg()
 341{
 342        dir="$1"
 343        oldsub="$2"
 344        newsub="$3"
 345        newsub_short=$(git rev-parse --short "$newsub")
 346        
 347        if [ -n "$oldsub" ]; then
 348                oldsub_short=$(git rev-parse --short "$oldsub")
 349                echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
 350                echo
 351                git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
 352                git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
 353        else
 354                echo "Squashed '$dir/' content from commit $newsub_short"
 355        fi
 356        
 357        echo
 358        echo "git-subtree-dir: $dir"
 359        echo "git-subtree-split: $newsub"
 360}
 361
 362toptree_for_commit()
 363{
 364        commit="$1"
 365        git log -1 --pretty=format:'%T' "$commit" -- || exit $?
 366}
 367
 368subtree_for_commit()
 369{
 370        commit="$1"
 371        dir="$2"
 372        git ls-tree "$commit" -- "$dir" |
 373        while read mode type tree name; do
 374                assert [ "$name" = "$dir" ]
 375                assert [ "$type" = "tree" ]
 376                echo $tree
 377                break
 378        done
 379}
 380
 381tree_changed()
 382{
 383        tree=$1
 384        shift
 385        if [ $# -ne 1 ]; then
 386                return 0   # weird parents, consider it changed
 387        else
 388                ptree=$(toptree_for_commit $1)
 389                if [ "$ptree" != "$tree" ]; then
 390                        return 0   # changed
 391                else
 392                        return 1   # not changed
 393                fi
 394        fi
 395}
 396
 397new_squash_commit()
 398{
 399        old="$1"
 400        oldsub="$2"
 401        newsub="$3"
 402        tree=$(toptree_for_commit $newsub) || exit $?
 403        if [ -n "$old" ]; then
 404                squash_msg "$dir" "$oldsub" "$newsub" | 
 405                        git commit-tree "$tree" -p "$old" || exit $?
 406        else
 407                squash_msg "$dir" "" "$newsub" |
 408                        git commit-tree "$tree" || exit $?
 409        fi
 410}
 411
 412copy_or_skip()
 413{
 414        rev="$1"
 415        tree="$2"
 416        newparents="$3"
 417        assert [ -n "$tree" ]
 418
 419        identical=
 420        nonidentical=
 421        p=
 422        gotparents=
 423        for parent in $newparents; do
 424                ptree=$(toptree_for_commit $parent) || exit $?
 425                [ -z "$ptree" ] && continue
 426                if [ "$ptree" = "$tree" ]; then
 427                        # an identical parent could be used in place of this rev.
 428                        identical="$parent"
 429                else
 430                        nonidentical="$parent"
 431                fi
 432                
 433                # sometimes both old parents map to the same newparent;
 434                # eliminate duplicates
 435                is_new=1
 436                for gp in $gotparents; do
 437                        if [ "$gp" = "$parent" ]; then
 438                                is_new=
 439                                break
 440                        fi
 441                done
 442                if [ -n "$is_new" ]; then
 443                        gotparents="$gotparents $parent"
 444                        p="$p -p $parent"
 445                fi
 446        done
 447        
 448        if [ -n "$identical" ]; then
 449                echo $identical
 450        else
 451                copy_commit $rev $tree "$p" || exit $?
 452        fi
 453}
 454
 455ensure_clean()
 456{
 457        if ! git diff-index HEAD --exit-code --quiet 2>&1; then
 458                die "Working tree has modifications.  Cannot add."
 459        fi
 460        if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
 461                die "Index has modifications.  Cannot add."
 462        fi
 463}
 464
 465cmd_add()
 466{
 467        if [ -e "$dir" ]; then
 468                die "'$dir' already exists.  Cannot add."
 469        fi
 470
 471        ensure_clean
 472        
 473        if [ $# -eq 1 ]; then
 474                "cmd_add_commit" "$@"
 475        elif [ $# -eq 2 ]; then
 476                "cmd_add_repository" "$@"
 477        else
 478            say "error: parameters were '$@'"
 479            die "Provide either a refspec or a repository and refspec."
 480        fi
 481}
 482
 483cmd_add_repository()
 484{
 485        echo "git fetch" "$@"
 486        repository=$1
 487        refspec=$2
 488        git fetch "$@" || exit $?
 489        revs=FETCH_HEAD
 490        set -- $revs
 491        cmd_add_commit "$@"
 492}
 493
 494cmd_add_commit()
 495{
 496        revs=$(git rev-parse $default --revs-only "$@") || exit $?
 497        set -- $revs
 498        rev="$1"
 499        
 500        debug "Adding $dir as '$rev'..."
 501        git read-tree --prefix="$dir" $rev || exit $?
 502        git checkout -- "$dir" || exit $?
 503        tree=$(git write-tree) || exit $?
 504        
 505        headrev=$(git rev-parse HEAD) || exit $?
 506        if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
 507                headp="-p $headrev"
 508        else
 509                headp=
 510        fi
 511        
 512        if [ -n "$squash" ]; then
 513                rev=$(new_squash_commit "" "" "$rev") || exit $?
 514                commit=$(add_squashed_msg "$rev" "$dir" |
 515                         git commit-tree $tree $headp -p "$rev") || exit $?
 516        else
 517                commit=$(add_msg "$dir" "$headrev" "$rev" |
 518                         git commit-tree $tree $headp -p "$rev") || exit $?
 519        fi
 520        git reset "$commit" || exit $?
 521        
 522        say "Added dir '$dir'"
 523}
 524
 525cmd_split()
 526{
 527        debug "Splitting $dir..."
 528        cache_setup || exit $?
 529        
 530        if [ -n "$onto" ]; then
 531                debug "Reading history for --onto=$onto..."
 532                git rev-list $onto |
 533                while read rev; do
 534                        # the 'onto' history is already just the subdir, so
 535                        # any parent we find there can be used verbatim
 536                        debug "  cache: $rev"
 537                        cache_set $rev $rev
 538                done
 539        fi
 540        
 541        if [ -n "$ignore_joins" ]; then
 542                unrevs=
 543        else
 544                unrevs="$(find_existing_splits "$dir" "$revs")"
 545        fi
 546        
 547        # We can't restrict rev-list to only $dir here, because some of our
 548        # parents have the $dir contents the root, and those won't match.
 549        # (and rev-list --follow doesn't seem to solve this)
 550        grl='git rev-list --reverse --parents $revs $unrevs'
 551        revmax=$(eval "$grl" | wc -l)
 552        revcount=0
 553        createcount=0
 554        eval "$grl" |
 555        while read rev parents; do
 556                revcount=$(($revcount + 1))
 557                say -n "$revcount/$revmax ($createcount)
"
 558                debug "Processing commit: $rev"
 559                exists=$(cache_get $rev)
 560                if [ -n "$exists" ]; then
 561                        debug "  prior: $exists"
 562                        continue
 563                fi
 564                createcount=$(($createcount + 1))
 565                debug "  parents: $parents"
 566                newparents=$(cache_get $parents)
 567                debug "  newparents: $newparents"
 568                
 569                tree=$(subtree_for_commit $rev "$dir")
 570                debug "  tree is: $tree"
 571                
 572                # ugly.  is there no better way to tell if this is a subtree
 573                # vs. a mainline commit?  Does it matter?
 574                if [ -z $tree ]; then
 575                        if [ -n "$newparents" ]; then
 576                                cache_set $rev $rev
 577                        fi
 578                        continue
 579                fi
 580
 581                newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
 582                debug "  newrev is: $newrev"
 583                cache_set $rev $newrev
 584                cache_set latest_new $newrev
 585                cache_set latest_old $rev
 586        done || exit $?
 587        latest_new=$(cache_get latest_new)
 588        if [ -z "$latest_new" ]; then
 589                die "No new revisions were found"
 590        fi
 591        
 592        if [ -n "$rejoin" ]; then
 593                debug "Merging split branch into HEAD..."
 594                latest_old=$(cache_get latest_old)
 595                git merge -s ours \
 596                        -m "$(rejoin_msg $dir $latest_old $latest_new)" \
 597                        $latest_new >&2 || exit $?
 598        fi
 599        if [ -n "$branch" ]; then
 600                if rev_exists "refs/heads/$branch"; then
 601                        if ! rev_is_descendant_of_branch $latest_new $branch; then
 602                                die "Branch '$branch' is not an ancestor of commit '$latest_new'."
 603                        fi
 604                        action='Updated'
 605                else
 606                        action='Created'
 607                fi
 608                git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
 609                say "$action branch '$branch'"
 610        fi
 611        echo $latest_new
 612        exit 0
 613}
 614
 615cmd_merge()
 616{
 617        revs=$(git rev-parse $default --revs-only "$@") || exit $?
 618        ensure_clean
 619        
 620        set -- $revs
 621        if [ $# -ne 1 ]; then
 622                die "You must provide exactly one revision.  Got: '$revs'"
 623        fi
 624        rev="$1"
 625        
 626        if [ -n "$squash" ]; then
 627                first_split="$(find_latest_squash "$dir")"
 628                if [ -z "$first_split" ]; then
 629                        die "Can't squash-merge: '$dir' was never added."
 630                fi
 631                set $first_split
 632                old=$1
 633                sub=$2
 634                if [ "$sub" = "$rev" ]; then
 635                        say "Subtree is already at commit $rev."
 636                        exit 0
 637                fi
 638                new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
 639                debug "New squash commit: $new"
 640                rev="$new"
 641        fi
 642
 643        version=$(git version)
 644        if [ "$version" \< "git version 1.7" ]; then
 645                if [ -n "$message" ]; then
 646                        git merge -s subtree --message="$message" $rev
 647                else
 648                        git merge -s subtree $rev
 649                fi
 650        else
 651                if [ -n "$message" ]; then
 652                        git merge -Xsubtree="$prefix" --message="$message" $rev
 653                else
 654                        git merge -Xsubtree="$prefix" $rev
 655                fi
 656        fi
 657}
 658
 659cmd_pull()
 660{
 661        ensure_clean
 662        git fetch "$@" || exit $?
 663        revs=FETCH_HEAD
 664        set -- $revs
 665        cmd_merge "$@"
 666}
 667
 668cmd_push()
 669{
 670        if [ $# -ne 2 ]; then
 671            die "You must provide <repository> <refspec>"
 672        fi
 673        if [ -e "$dir" ]; then
 674            repository=$1
 675            refspec=$2
 676            echo "git push using: " $repository $refspec
 677            git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
 678        else
 679            die "'$dir' must already exist. Try 'git subtree add'."
 680        fi
 681}
 682
 683"cmd_$command" "$@"