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