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