sort assertion to make it more generic
[gitweb.git] / git-subtree.sh
index 2dc99e82cd0aa3c23587c77dbe4eac76d355385b..962d5ff50939afac605d467926b10a25c88dc930 100755 (executable)
@@ -8,28 +8,46 @@ if [ $# -eq 0 ]; then
     set -- -h
 fi
 OPTS_SPEC="\
-git subtree add <--prefix=prefix <commit>
-git subtree split [options...] <--prefix=prefix> <commit...>
-git subtree merge 
+git subtree add   --prefix=<prefix> <commit>
+git subtree merge --prefix=<prefix> <commit>
+git subtree pull  --prefix=<prefix> <repository> <refspec...>
+git subtree split --prefix=<prefix> <commit...>
 --
 h,help        show the help
 q             quiet
+d             show debug messages
 prefix=       the name of the subdir to split out
+ options for 'split'
+annotate=     add a prefix to commit message of new commits
+b,branch=     create a new branch from the split subtree
+ignore-joins  ignore prior --rejoin commits
 onto=         try connecting new tree to an existing one
 rejoin        merge the new branch back into HEAD
-ignore-joins  ignore prior --rejoin commits
+ options for 'add', 'merge', and 'pull'
+squash        merge subtree changes as a single commit
 "
 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
 . git-sh-setup
 require_work_tree
 
 quiet=
+branch=
+debug=
 command=
 onto=
 rejoin=
 ignore_joins=
+annotate=
+squash=
 
 debug()
+{
+       if [ -n "$debug" ]; then
+               echo "$@" >&2
+       fi
+}
+
+say()
 {
        if [ -z "$quiet" ]; then
                echo "$@" >&2
@@ -53,6 +71,10 @@ while [ $# -gt 0 ]; do
        shift
        case "$opt" in
                -q) quiet=1 ;;
+               -d) debug=1 ;;
+               --annotate) annotate="$1"; shift ;;
+               --no-annotate) annotate= ;;
+               -b) branch="$1"; shift ;;
                --prefix) prefix="$1"; shift ;;
                --no-prefix) prefix= ;;
                --onto) onto="$1"; shift ;;
@@ -61,34 +83,39 @@ while [ $# -gt 0 ]; do
                --no-rejoin) rejoin= ;;
                --ignore-joins) ignore_joins=1 ;;
                --no-ignore-joins) ignore_joins= ;;
+               --squash) squash=1 ;;
+               --no-squash) squash= ;;
                --) break ;;
+               *) die "Unexpected option: $opt" ;;
        esac
 done
 
 command="$1"
 shift
 case "$command" in
-       add|merge) default= ;;
+       add|merge|pull) default= ;;
        split) default="--default HEAD" ;;
        *) die "Unknown command '$command'" ;;
 esac
 
-revs=$(git rev-parse $default --revs-only "$@") || exit $?
-
 if [ -z "$prefix" ]; then
        die "You must provide the --prefix option."
 fi
 dir="$prefix"
 
-dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
-if [ -n "$dirs" ]; then
-       die "Error: Use --prefix instead of bare filenames."
+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
 
 debug "command: {$command}"
 debug "quiet: {$quiet}"
 debug "revs: {$revs}"
 debug "dir: {$dir}"
+debug "opts: {$*}"
 debug
 
 cache_setup()
@@ -121,25 +148,87 @@ cache_set()
        echo "$newrev" >"$cachedir/$oldrev"
 }
 
+rev_exists()
+{
+       if git rev-parse "$1" >/dev/null 2>&1; then
+               return 0
+       else
+               return 1
+       fi
+}
+
+# if a commit doesn't have a parent, this might not work.  But we only want
+# to remove the parent from the rev-list, and since it doesn't exist, it won't
+# be there anyway, so do nothing in that case.
+try_remove_previous()
+{
+       if rev_exists "$1^"; then
+               echo "^$1^"
+       fi
+}
+
+find_latest_squash()
+{
+       debug "Looking for latest squash ($dir)..."
+       dir="$1"
+       sq=
+       main=
+       sub=
+       git log --grep="^git-subtree-dir: $dir\$" \
+               --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
+       while read a b junk; do
+               debug "$a $b $junk"
+               debug "{{$sq/$main/$sub}}"
+               case "$a" in
+                       START) sq="$b" ;;
+                       git-subtree-mainline:) main="$b" ;;
+                       git-subtree-split:) sub="$b" ;;
+                       END)
+                               if [ -n "$sub" ]; then
+                                       if [ -n "$main" ]; then
+                                               # a rejoin commit?
+                                               # Pretend its sub was a squash.
+                                               sq="$sub"
+                                       fi
+                                       debug "Squash found: $sq $sub"
+                                       echo "$sq" "$sub"
+                                       break
+                               fi
+                               sq=
+                               main=
+                               sub=
+                               ;;
+               esac
+       done
+}
+
 find_existing_splits()
 {
        debug "Looking for prior splits..."
        dir="$1"
        revs="$2"
+       main=
+       sub=
        git log --grep="^git-subtree-dir: $dir\$" \
-               --pretty=format:'%s%n%n%b%nEND' "$revs" |
+               --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
        while read a b junk; do
                case "$a" in
+                       START) main="$b"; sq="$b" ;;
                        git-subtree-mainline:) main="$b" ;;
                        git-subtree-split:) sub="$b" ;;
-                       *)
+                       END)
+                               if [ -z "$main" -a -n "$sub" ]; then
+                                       # squash commits refer to a subtree
+                                       cache_set "$sq" "$sub"
+                               fi
                                if [ -n "$main" -a -n "$sub" ]; then
                                        debug "  Prior: $main -> $sub"
                                        cache_set $main $sub
-                                       echo "^$main^ ^$sub^"
-                                       main=
-                                       sub=
+                                       try_remove_previous "$main"
+                                       try_remove_previous "$sub"
                                fi
+                               main=
+                               sub=
                                ;;
                esac
        done
@@ -147,8 +236,9 @@ find_existing_splits()
 
 copy_commit()
 {
-       # We're doing to set some environment vars here, so
+       # We're going to set some environment vars here, so
        # do it in a subshell to get rid of them safely later
+       debug copy_commit "{$1}" "{$2}" "{$3}"
        git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
        (
                read GIT_AUTHOR_NAME
@@ -163,7 +253,7 @@ copy_commit()
                        GIT_COMMITTER_NAME \
                        GIT_COMMITTER_EMAIL \
                        GIT_COMMITTER_DATE
-               (echo -n '*'; cat ) |  # FIXME
+               (echo -n "$annotate"; cat ) |
                git commit-tree "$2" $3  # reads the rest of stdin
        ) || die "Can't copy commit $1"
 }
@@ -182,7 +272,7 @@ add_msg()
        EOF
 }
 
-merge_msg()
+rejoin_msg()
 {
        dir="$1"
        latest_old="$2"
@@ -196,6 +286,28 @@ merge_msg()
        EOF
 }
 
+squash_msg()
+{
+       dir="$1"
+       oldsub="$2"
+       newsub="$3"
+       newsub_short=$(git rev-parse --short "$newsub")
+       
+       if [ -n "$oldsub" ]; then
+               oldsub_short=$(git rev-parse --short "$oldsub")
+               echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
+               echo
+               git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
+               git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
+       else
+               echo "Squashed '$dir/' content from commit $newsub_short"
+       fi
+       
+       echo
+       echo "git-subtree-dir: $dir"
+       echo "git-subtree-split: $newsub"
+}
+
 toptree_for_commit()
 {
        commit="$1"
@@ -230,6 +342,21 @@ tree_changed()
        fi
 }
 
+new_squash_commit()
+{
+       old="$1"
+       oldsub="$2"
+       newsub="$3"
+       tree=$(toptree_for_commit $newsub) || exit $?
+       if [ -n "$old" ]; then
+               squash_msg "$dir" "$oldsub" "$newsub" | 
+                       git commit-tree "$tree" -p "$old" || exit $?
+       else
+               squash_msg "$dir" "" "$newsub" |
+                       git commit-tree "$tree" || exit $?
+       fi
+}
+
 copy_or_skip()
 {
        rev="$1"
@@ -238,37 +365,58 @@ copy_or_skip()
        assert [ -n "$tree" ]
 
        identical=
+       nonidentical=
        p=
+       gotparents=
        for parent in $newparents; do
                ptree=$(toptree_for_commit $parent) || exit $?
+               [ -z "$ptree" ] && continue
                if [ "$ptree" = "$tree" ]; then
                        # an identical parent could be used in place of this rev.
                        identical="$parent"
+               else
+                       nonidentical="$parent"
                fi
-               if [ -n "$ptree" ]; then
-                       parentmatch="$parentmatch$parent"
+               
+               # sometimes both old parents map to the same newparent;
+               # eliminate duplicates
+               is_new=1
+               for gp in $gotparents; do
+                       if [ "$gp" = "$parent" ]; then
+                               is_new=
+                               break
+                       fi
+               done
+               if [ -n "$is_new" ]; then
+                       gotparents="$gotparents $parent"
                        p="$p -p $parent"
                fi
        done
        
-       if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
+       if [ -n "$identical" ]; then
                echo $identical
        else
                copy_commit $rev $tree "$p" || exit $?
        fi
 }
 
-cmd_add()
+ensure_clean()
 {
-       if [ -e "$dir" ]; then
-               die "'$dir' already exists.  Cannot add."
-       fi
        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'"
@@ -286,13 +434,26 @@ cmd_add()
        else
                headp=
        fi
-       commit=$(add_msg "$dir" "$headrev" "$rev" |
-                git commit-tree $tree $headp -p "$rev") || exit $?
+       
+       if [ -n "$squash" ]; then
+               rev=$(new_squash_commit "" "" "$rev") || exit $?
+               commit=$(echo "Merge commit '$rev' as '$dir'" |
+                        git commit-tree $tree $headp -p "$rev") || exit $?
+       else
+               commit=$(add_msg "$dir" "$headrev" "$rev" |
+                        git commit-tree $tree $headp -p "$rev") || exit $?
+       fi
        git reset "$commit" || exit $?
+       
+       say "Added dir '$dir'"
 }
 
 cmd_split()
 {
+       if [ -n "$branch" ] && rev_exists "refs/heads/$branch"; then
+               die "Branch '$branch' already exists."
+       fi
+
        debug "Splitting $dir..."
        cache_setup || exit $?
        
@@ -313,24 +474,33 @@ cmd_split()
                unrevs="$(find_existing_splits "$dir" "$revs")"
        fi
        
-       # 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 |
+       # We can't restrict rev-list to only $dir here, because some of our
+       # parents have the $dir contents the root, and those won't match.
+       # (and rev-list --follow doesn't seem to solve this)
+       grl='git rev-list --reverse --parents $revs $unrevs'
+       revmax=$(eval "$grl" | wc -l)
+       revcount=0
+       createcount=0
+       eval "$grl" |
        while read rev parents; do
-               debug
+               revcount=$(($revcount + 1))
+               say -n "$revcount/$revmax ($createcount)\r"
                debug "Processing commit: $rev"
                exists=$(cache_get $rev)
                if [ -n "$exists" ]; then
                        debug "  prior: $exists"
                        continue
                fi
+               createcount=$(($createcount + 1))
                debug "  parents: $parents"
                newparents=$(cache_get $parents)
                debug "  newparents: $newparents"
                
                tree=$(subtree_for_commit $rev "$dir")
                debug "  tree is: $tree"
+               
+               # ugly.  is there no better way to tell if this is a subtree
+               # vs. a mainline commit?  Does it matter?
                [ -z $tree ] && continue
 
                newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
@@ -348,8 +518,13 @@ cmd_split()
                debug "Merging split branch into HEAD..."
                latest_old=$(cache_get latest_old)
                git merge -s ours \
-                       -m "$(merge_msg $dir $latest_old $latest_new)" \
-                       $latest_new >&2
+                       -m "$(rejoin_msg $dir $latest_old $latest_new)" \
+                       $latest_new >&2 || exit $?
+       fi
+       if [ -n "$branch" ]; then
+               git update-ref -m 'subtree split' "refs/heads/$branch" \
+                       $latest_new "" || exit $?
+               say "Created branch '$branch'"
        fi
        echo $latest_new
        exit 0
@@ -357,7 +532,39 @@ 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"
+       
+       if [ -n "$squash" ]; then
+               first_split="$(find_latest_squash "$dir")"
+               if [ -z "$first_split" ]; then
+                       die "Can't squash-merge: '$dir' was never added."
+               fi
+               set $first_split
+               old=$1
+               sub=$2
+               if [ "$sub" = "$rev" ]; then
+                       say "Subtree is already at commit $rev."
+                       exit 0
+               fi
+               new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
+               debug "New squash commit: $new"
+               rev="$new"
+       fi
+       
+       git merge -s subtree $rev
+}
+
+cmd_pull()
+{
+       ensure_clean
+       set -x
+       git pull -s subtree "$@"
 }
 
-"cmd_$command"
+"cmd_$command" "$@"