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