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