Merge remote branch 'origin/master'
[gitweb.git] / git-subtree.sh
index f7fe11117869460aad9f572cf0302b5f06ee0563..a15d91ffb1b3d3318409619449745dbcfb393abf 100755 (executable)
@@ -11,23 +11,30 @@ OPTS_SPEC="\
 git subtree add   --prefix=<prefix> <commit>
 git subtree merge --prefix=<prefix> <commit>
 git subtree pull  --prefix=<prefix> <repository> <refspec...>
+git subtree push  --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
+P,prefix=     the name of the subdir to split out
+m,message=    use the given message as the commit message for the merge commit
  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
- options for 'merge' and 'pull'
+ options for 'add', 'merge', 'pull' and 'push'
 squash        merge subtree changes as a single commit
 "
 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
+
+OPATH=$PATH
+PATH=$(git --exec-path):$PATH
 . git-sh-setup
+PATH=$OPATH  # apparently needed for some versions of msysgit
+
 require_work_tree
 
 quiet=
@@ -39,6 +46,7 @@ rejoin=
 ignore_joins=
 annotate=
 squash=
+message=
 
 debug()
 {
@@ -75,7 +83,8 @@ while [ $# -gt 0 ]; do
                --annotate) annotate="$1"; shift ;;
                --no-annotate) annotate= ;;
                -b) branch="$1"; shift ;;
-               --prefix) prefix="$1"; shift ;;
+               -P) prefix="$1"; shift ;;
+               -m) message="$1"; shift ;;
                --no-prefix) prefix= ;;
                --onto) onto="$1"; shift ;;
                --no-onto) onto= ;;
@@ -94,16 +103,24 @@ command="$1"
 shift
 case "$command" in
        add|merge|pull) default= ;;
-       split) default="--default HEAD" ;;
+       split|push) default="--default HEAD" ;;
        *) die "Unknown command '$command'" ;;
 esac
 
 if [ -z "$prefix" ]; then
        die "You must provide the --prefix option."
 fi
-dir="$prefix"
 
-if [ "$command" != "pull" ]; then
+case "$command" in
+       add) [ -e "$prefix" ] && 
+               die "prefix '$prefix' already exists." ;;
+       *)   [ -e "$prefix" ] || 
+               die "'$prefix' does not exist; use 'git subtree add'" ;;
+esac
+
+dir="$(dirname "$prefix/.")"
+
+if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
        revs=$(git rev-parse $default --revs-only "$@") || exit $?
        dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
        if [ -n "$dirs" ]; then
@@ -157,6 +174,20 @@ rev_exists()
        fi
 }
 
+rev_is_descendant_of_branch()
+{
+       newrev="$1"
+       branch="$2"
+       branch_hash=$(git rev-parse $branch)
+       match=$(git rev-list -1 $branch_hash ^$newrev)
+
+       if [ -z "$match" ]; 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.
@@ -169,11 +200,16 @@ try_remove_previous()
 
 find_latest_squash()
 {
-       debug "Looking for latest squash..."
+       debug "Looking for latest squash ($dir)..."
        dir="$1"
-       git log --grep="^git-subtree-dir: $dir\$" \
+       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" ;;
@@ -202,17 +238,26 @@ find_existing_splits()
        debug "Looking for prior splits..."
        dir="$1"
        revs="$2"
-       git log --grep="^git-subtree-dir: $dir\$" \
-               --pretty=format:'%s%n%n%b%nEND%n' $revs |
+       main=
+       sub=
+       git log --grep="^git-subtree-dir: $dir/*\$" \
+               --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
        while read a b junk; do
                case "$a" in
-                       START) main="$b" ;;
+                       START) sq="$b" ;;
                        git-subtree-mainline:) main="$b" ;;
                        git-subtree-split:) sub="$b" ;;
                        END)
+                               debug "  Main is: '$main'"
+                               if [ -z "$main" -a -n "$sub" ]; then
+                                       # squash commits refer to a subtree
+                                       debug "  Squash: $sq from $sub"
+                                       cache_set "$sq" "$sub"
+                               fi
                                if [ -n "$main" -a -n "$sub" ]; then
                                        debug "  Prior: $main -> $sub"
                                        cache_set $main $sub
+                                       cache_set $sub $sub
                                        try_remove_previous "$main"
                                        try_remove_previous "$sub"
                                fi
@@ -252,8 +297,13 @@ add_msg()
        dir="$1"
        latest_old="$2"
        latest_new="$3"
+       if [ -n "$message" ]; then
+               commit_message="$message"
+       else
+               commit_message="Add '$dir/' from commit '$latest_new'"
+       fi
        cat <<-EOF
-               Add '$dir/' from commit '$latest_new'
+               $commit_message
                
                git-subtree-dir: $dir
                git-subtree-mainline: $latest_old
@@ -261,13 +311,27 @@ add_msg()
        EOF
 }
 
+add_squashed_msg()
+{
+       if [ -n "$message" ]; then
+               echo "$message"
+       else
+               echo "Merge commit '$1' as '$2'"
+       fi
+}
+
 rejoin_msg()
 {
        dir="$1"
        latest_old="$2"
        latest_new="$3"
+       if [ -n "$message" ]; then
+               commit_message="$message"
+       else
+               commit_message="Split '$dir/' into commit '$latest_new'"
+       fi
        cat <<-EOF
-               Split '$dir/' into commit '$latest_new'
+               $commit_message
                
                git-subtree-dir: $dir
                git-subtree-mainline: $latest_old
@@ -280,21 +344,21 @@ squash_msg()
        dir="$1"
        oldsub="$2"
        newsub="$3"
-       oldsub_short=$(git rev-parse --short "$oldsub")
        newsub_short=$(git rev-parse --short "$newsub")
-       cat <<-EOF
-               Squashed '$dir/' changes from $oldsub_short..$newsub_short
-       
-       EOF
        
-       git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
-       git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
+       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
        
-       cat <<-EOF
-               
-               git-subtree-dir: $dir
-               git-subtree-split: $newsub
-       EOF
+       echo
+       echo "git-subtree-dir: $dir"
+       echo "git-subtree-split: $newsub"
 }
 
 toptree_for_commit()
@@ -310,6 +374,7 @@ subtree_for_commit()
        git ls-tree "$commit" -- "$dir" |
        while read mode type tree name; do
                assert [ "$name" = "$dir" ]
+               assert [ "$type" = "tree" ]
                echo $tree
                break
        done
@@ -337,8 +402,13 @@ new_squash_commit()
        oldsub="$2"
        newsub="$3"
        tree=$(toptree_for_commit $newsub) || exit $?
-       squash_msg "$dir" "$oldsub" "$newsub" | 
-               git commit-tree "$tree" -p "$old" || 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()
@@ -386,10 +456,10 @@ copy_or_skip()
 
 ensure_clean()
 {
-       if ! git diff-index HEAD --exit-code --quiet; then
+       if ! git diff-index HEAD --exit-code --quiet 2>&1; then
                die "Working tree has modifications.  Cannot add."
        fi
-       if ! git diff-index --cached HEAD --exit-code --quiet; then
+       if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
                die "Index has modifications.  Cannot add."
        fi
 }
@@ -399,17 +469,39 @@ 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'"
+       if [ $# -eq 1 ]; then
+               "cmd_add_commit" "$@"
+       elif [ $# -eq 2 ]; then
+               "cmd_add_repository" "$@"
+       else
+           say "error: parameters were '$@'"
+           die "Provide either a refspec or a repository and refspec."
        fi
+}
+
+cmd_add_repository()
+{
+       echo "git fetch" "$@"
+       repository=$1
+       refspec=$2
+       git fetch "$@" || exit $?
+       revs=FETCH_HEAD
+       set -- $revs
+       cmd_add_commit "$@"
+}
+
+cmd_add_commit()
+{
+       revs=$(git rev-parse $default --revs-only "$@") || exit $?
+       set -- $revs
        rev="$1"
        
        debug "Adding $dir as '$rev'..."
        git read-tree --prefix="$dir" $rev || exit $?
-       git checkout "$dir" || exit $?
+       git checkout -- "$dir" || exit $?
        tree=$(git write-tree) || exit $?
        
        headrev=$(git rev-parse HEAD) || exit $?
@@ -418,17 +510,22 @@ 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=$(add_squashed_msg "$rev" "$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 $?
        
@@ -476,7 +573,12 @@ cmd_split()
                
                # ugly.  is there no better way to tell if this is a subtree
                # vs. a mainline commit?  Does it matter?
-               [ -z $tree ] && continue
+               if [ -z $tree ]; then
+                       if [ -n "$newparents" ]; then
+                               cache_set $rev $rev
+                       fi
+                       continue
+               fi
 
                newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
                debug "  newrev is: $newrev"
@@ -497,9 +599,16 @@ cmd_split()
                        $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'"
+               if rev_exists "refs/heads/$branch"; then
+                       if ! rev_is_descendant_of_branch $latest_new $branch; then
+                               die "Branch '$branch' is not an ancestor of commit '$latest_new'."
+                       fi
+                       action='Updated'
+               else
+                       action='Created'
+               fi
+               git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
+               say "$action branch '$branch'"
        fi
        echo $latest_new
        exit 0
@@ -507,6 +616,7 @@ cmd_split()
 
 cmd_merge()
 {
+       revs=$(git rev-parse $default --revs-only "$@") || exit $?
        ensure_clean
        
        set -- $revs
@@ -531,15 +641,45 @@ cmd_merge()
                debug "New squash commit: $new"
                rev="$new"
        fi
-       
-       git merge -s subtree $rev
+
+       version=$(git version)
+       if [ "$version" \< "git version 1.7" ]; then
+               if [ -n "$message" ]; then
+                       git merge -s subtree --message="$message" $rev
+               else
+                       git merge -s subtree $rev
+               fi
+       else
+               if [ -n "$message" ]; then
+                       git merge -Xsubtree="$prefix" --message="$message" $rev
+               else
+                       git merge -Xsubtree="$prefix" $rev
+               fi
+       fi
 }
 
 cmd_pull()
 {
        ensure_clean
-       set -x
-       git pull -s subtree "$@"
+       git fetch "$@" || exit $?
+       revs=FETCH_HEAD
+       set -- $revs
+       cmd_merge "$@"
+}
+
+cmd_push()
+{
+       if [ $# -ne 2 ]; then
+           die "You must provide <repository> <refspec>"
+       fi
+       if [ -e "$dir" ]; then
+           repository=$1
+           refspec=$2
+           echo "git push using: " $repository $refspec
+           git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
+       else
+           die "'$dir' must already exist. Try 'git subtree add'."
+       fi
 }
 
 "cmd_$command" "$@"