Code

Add 'git subtree merge' and 'git subtree pull'.
[git.git] / git-subtree.sh
index 7ae71886f443b4cd12b4af8ee12674b1e18d0755..f2a1c6aae4561fa7d85a4a7faabe4ea8771d74b1 100755 (executable)
@@ -4,17 +4,22 @@
 #
 # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
 #
+if [ $# -eq 0 ]; then
+    set -- -h
+fi
 OPTS_SPEC="\
-git subtree split [--rejoin] [--onto rev] <commit...> -- <path>
-git subtree merge 
-
-git subtree does foo and bar!
+git subtree add --prefix=<prefix> <commit>
+git subtree split [options...] --prefix=<prefix> <commit...>
+git subtree merge --prefix=<prefix> <commit>
+git subtree pull  --prefix=<prefix> <repository> <refspec...>
 --
-h,help   show the help
-q        quiet
-v        verbose
-onto=    existing subtree revision to search for parent
-rejoin   merge the new branch back into HEAD
+h,help        show the help
+q             quiet
+prefix=       the name of the subdir to split out
+ options for 'split'
+onto=         try connecting new tree to an existing one
+rejoin        merge the new branch back into HEAD
+ignore-joins  ignore prior --rejoin commits
 "
 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
 . git-sh-setup
@@ -24,6 +29,7 @@ quiet=
 command=
 onto=
 rejoin=
+ignore_joins=
 
 debug()
 {
@@ -49,8 +55,14 @@ while [ $# -gt 0 ]; do
        shift
        case "$opt" in
                -q) quiet=1 ;;
+               --prefix) prefix="$1"; shift ;;
+               --no-prefix) prefix= ;;
                --onto) onto="$1"; shift ;;
+               --no-onto) onto= ;;
                --rejoin) rejoin=1 ;;
+               --no-rejoin) rejoin= ;;
+               --ignore-joins) ignore_joins=1 ;;
+               --no-ignore-joins) ignore_joins= ;;
                --) break ;;
        esac
 done
@@ -58,24 +70,30 @@ done
 command="$1"
 shift
 case "$command" in
-       split|merge) ;;
+       add|merge|pull) default= ;;
+       split) default="--default HEAD" ;;
        *) die "Unknown command '$command'" ;;
 esac
 
-revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $?
-dirs="$(git rev-parse --sq --no-revs --no-flags "$@")" || exit $?
+if [ -z "$prefix" ]; then
+       die "You must provide the --prefix option."
+fi
+dir="$prefix"
 
-#echo "dirs is {$dirs}"
-eval $(echo set -- $dirs)
-if [ "$#" -ne 1 ]; then
-       die "Must provide exactly one subtree dir (got $#)"
+if [ "$command" != "pull" ]; then
+       revs=$(git rev-parse $default --revs-only "$@") || exit $?
+       dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
+       if [ -n "$dirs" ]; then
+               die "Error: Use --prefix instead of bare filenames."
+       fi
 fi
-dir="$1"
 
 debug "command: {$command}"
 debug "quiet: {$quiet}"
 debug "revs: {$revs}"
 debug "dir: {$dir}"
+debug "opts: {$*}"
+debug
 
 cache_setup()
 {
@@ -149,10 +167,25 @@ copy_commit()
                        GIT_COMMITTER_NAME \
                        GIT_COMMITTER_EMAIL \
                        GIT_COMMITTER_DATE
+               (echo -n '*'; cat ) |  # FIXME
                git commit-tree "$2" $3  # reads the rest of stdin
        ) || die "Can't copy commit $1"
 }
 
+add_msg()
+{
+       dir="$1"
+       latest_old="$2"
+       latest_new="$3"
+       cat <<-EOF
+               Add '$dir/' from commit '$latest_new'
+               
+               git-subtree-dir: $dir
+               git-subtree-mainline: $latest_old
+               git-subtree-split: $latest_new
+       EOF
+}
+
 merge_msg()
 {
        dir="$1"
@@ -167,9 +200,17 @@ merge_msg()
        EOF
 }
 
-tree_for_commit()
+toptree_for_commit()
+{
+       commit="$1"
+       git log -1 --pretty=format:'%T' "$commit" -- || exit $?
+}
+
+subtree_for_commit()
 {
-       git ls-tree "$1" -- "$dir" |
+       commit="$1"
+       dir="$2"
+       git ls-tree "$commit" -- "$dir" |
        while read mode type tree name; do
                assert [ "$name" = "$dir" ]
                echo $tree
@@ -184,7 +225,7 @@ tree_changed()
        if [ $# -ne 1 ]; then
                return 0   # weird parents, consider it changed
        else
-               ptree=$(tree_for_commit $1)
+               ptree=$(toptree_for_commit $1)
                if [ "$ptree" != "$tree" ]; then
                        return 0   # changed
                else
@@ -193,13 +234,80 @@ tree_changed()
        fi
 }
 
+copy_or_skip()
+{
+       rev="$1"
+       tree="$2"
+       newparents="$3"
+       assert [ -n "$tree" ]
+
+       identical=
+       p=
+       for parent in $newparents; do
+               ptree=$(toptree_for_commit $parent) || exit $?
+               if [ "$ptree" = "$tree" ]; then
+                       # an identical parent could be used in place of this rev.
+                       identical="$parent"
+               fi
+               if [ -n "$ptree" ]; then
+                       parentmatch="$parentmatch$parent"
+                       p="$p -p $parent"
+               fi
+       done
+       
+       if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
+               echo $identical
+       else
+               copy_commit $rev $tree "$p" || exit $?
+       fi
+}
+
+ensure_clean()
+{
+       if ! git diff-index HEAD --exit-code --quiet; then
+               die "Working tree has modifications.  Cannot add."
+       fi
+       if ! git diff-index --cached HEAD --exit-code --quiet; then
+               die "Index has modifications.  Cannot add."
+       fi
+}
+
+cmd_add()
+{
+       if [ -e "$dir" ]; then
+               die "'$dir' already exists.  Cannot add."
+       fi
+       ensure_clean
+       
+       set -- $revs
+       if [ $# -ne 1 ]; then
+               die "You must provide exactly one revision.  Got: '$revs'"
+       fi
+       rev="$1"
+       
+       debug "Adding $dir as '$rev'..."
+       git read-tree --prefix="$dir" $rev || exit $?
+       git checkout "$dir" || exit $?
+       tree=$(git write-tree) || exit $?
+       
+       headrev=$(git rev-parse HEAD) || exit $?
+       if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
+               headp="-p $headrev"
+       else
+               headp=
+       fi
+       commit=$(add_msg "$dir" "$headrev" "$rev" |
+                git commit-tree $tree $headp -p "$rev") || exit $?
+       git reset "$commit" || exit $?
+}
+
 cmd_split()
 {
        debug "Splitting $dir..."
        cache_setup || exit $?
        
        if [ -n "$onto" ]; then
-               echo "Reading history for --onto=$onto..."
+               debug "Reading history for --onto=$onto..."
                git rev-list $onto |
                while read rev; do
                        # the 'onto' history is already just the subdir, so
@@ -209,10 +317,16 @@ cmd_split()
                done
        fi
        
-       unrevs="$(find_existing_splits "$dir" "$revs")"
+       if [ -n "$ignore_joins" ]; then
+               unrevs=
+       else
+               unrevs="$(find_existing_splits "$dir" "$revs")"
+       fi
        
-       debug "git rev-list --reverse $revs $unrevs"
-       git rev-list --reverse --parents $revs $unrevsx |
+       # We can't restrict rev-list to only "$dir" here, because that leaves out
+       # critical information about commit parents.
+       debug "git rev-list --reverse --parents $revs $unrevs"
+       git rev-list --reverse --parents $revs $unrevs |
        while read rev parents; do
                debug
                debug "Processing commit: $rev"
@@ -225,20 +339,11 @@ cmd_split()
                newparents=$(cache_get $parents)
                debug "  newparents: $newparents"
                
-               tree=$(tree_for_commit $rev)
+               tree=$(subtree_for_commit $rev "$dir")
                debug "  tree is: $tree"
                [ -z $tree ] && continue
 
-               p=""
-               for parent in $newparents; do
-                       p="$p -p $parent"
-               done
-                       
-               if tree_changed $tree $parents; then
-                       newrev=$(copy_commit $rev $tree "$p") || exit $?
-               else
-                       newrev="$newparents"
-               fi
+               newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
                debug "  newrev is: $newrev"
                cache_set $rev $newrev
                cache_set latest_new $newrev
@@ -254,7 +359,7 @@ cmd_split()
                latest_old=$(cache_get latest_old)
                git merge -s ours \
                        -m "$(merge_msg $dir $latest_old $latest_new)" \
-                       $latest_new
+                       $latest_new >&2
        fi
        echo $latest_new
        exit 0
@@ -262,7 +367,22 @@ cmd_split()
 
 cmd_merge()
 {
-       die "merge command not implemented yet"
+       ensure_clean
+       
+       set -- $revs
+       if [ $# -ne 1 ]; then
+               die "You must provide exactly one revision.  Got: '$revs'"
+       fi
+       rev="$1"
+       
+       git merge -s subtree $rev
+}
+
+cmd_pull()
+{
+       ensure_clean
+       set -x
+       git pull -s subtree "$@"
 }
 
-"cmd_$command"
+"cmd_$command" "$@"