3 # git-subtree.sh: split/join git repositories in subdirectories of this one 
   5 # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com> 
  11 git subtree add   --prefix=<prefix> <commit> 
  12 git subtree merge --prefix=<prefix> <commit> 
  13 git subtree pull  --prefix=<prefix> <repository> <refspec...> 
  14 git subtree push  --prefix=<prefix> <repository> <refspec...> 
  15 git subtree split --prefix=<prefix> <commit...> 
  20 P,prefix=     the name of the subdir to split out 
  21 m,message=    use the given message as the commit message for the merge commit 
  23 annotate=     add a prefix to commit message of new commits 
  24 b,branch=     create a new branch from the split subtree 
  25 ignore-joins  ignore prior --rejoin commits 
  26 onto=         try connecting new tree to an existing one 
  27 rejoin        merge the new branch back into HEAD 
  28  options for 'add', 'merge', 'pull' and 'push' 
  29 squash        merge subtree changes as a single commit 
  31 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) 
  33 PATH
=$PATH:$(git --exec-path) 
  51         if [ -n "$debug" ]; then 
  58         if [ -z "$quiet" ]; then 
  68                 die 
"assertion failed: " "$@" 
  75 while [ $# -gt 0 ]; do 
  81                 --annotate) annotate
="$1"; shift ;; 
  82                 --no-annotate) annotate
= ;; 
  83                 -b) branch
="$1"; shift ;; 
  84                 -P) prefix
="$1"; shift ;; 
  85                 -m) message
="$1"; shift ;; 
  86                 --no-prefix) prefix
= ;; 
  87                 --onto) onto
="$1"; shift ;; 
  90                 --no-rejoin) rejoin
= ;; 
  91                 --ignore-joins) ignore_joins
=1 ;; 
  92                 --no-ignore-joins) ignore_joins
= ;; 
  94                 --no-squash) squash
= ;; 
  96                 *) die 
"Unexpected option: $opt" ;; 
 103         add
|merge
|pull
) default
= ;; 
 104         split|push
) default
="--default HEAD" ;; 
 105         *) die 
"Unknown command '$command'" ;; 
 108 if [ -z "$prefix" ]; then 
 109         die 
"You must provide the --prefix option." 
 113         add
) [ -e "$prefix" ] &&  
 114                 die 
"prefix '$prefix' already exists." ;; 
 115         *)   [ -e "$prefix" ] ||  
 116                 die 
"'$prefix' does not exist; use 'git subtree add'" ;; 
 119 dir
="$(dirname "$prefix/.")" 
 121 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then 
 122         revs
=$(git rev-parse $default --revs-only "$@") || exit $?
 
 123         dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
 
 124         if [ -n "$dirs" ]; then 
 125                 die 
"Error: Use --prefix instead of bare filenames." 
 129 debug 
"command: {$command}" 
 130 debug 
"quiet: {$quiet}" 
 131 debug 
"revs: {$revs}" 
 138         cachedir
="$GIT_DIR/subtree-cache/$$" 
 139         rm -rf "$cachedir" || die 
"Can't delete old cachedir: $cachedir" 
 140         mkdir -p "$cachedir" || die 
"Can't create new cachedir: $cachedir" 
 141         debug 
"Using cachedir: $cachedir" >&2 
 147                 if [ -r "$cachedir/$oldrev" ]; then 
 148                         read newrev 
<"$cachedir/$oldrev" 
 158         if [ "$oldrev" != "latest_old" \
 
 159              -a "$oldrev" != "latest_new" \
 
 160              -a -e "$cachedir/$oldrev" ]; then 
 161                 die 
"cache for $oldrev already exists!" 
 163         echo "$newrev" >"$cachedir/$oldrev" 
 168         if git 
rev-parse "$1" >/dev
/null 
2>&1; then 
 175 rev_is_descendant_of_branch
() 
 179         branch_hash
=$(git rev-parse $branch) 
 180         match
=$(git rev-list -1 $branch_hash ^$newrev) 
 182         if [ -z "$match" ]; then 
 189 # if a commit doesn't have a parent, this might not work.  But we only want 
 190 # to remove the parent from the rev-list, and since it doesn't exist, it won't 
 191 # be there anyway, so do nothing in that case. 
 192 try_remove_previous
() 
 194         if rev_exists 
"$1^"; then 
 201         debug 
"Looking for latest squash ($dir)..." 
 206         git log 
--grep="^git-subtree-dir: $dir/*\$" \
 
 207                 --pretty=format
:'START %H%n%s%n%n%b%nEND%n' HEAD 
| 
 208         while read a b junk
; do 
 210                 debug 
"{{$sq/$main/$sub}}" 
 213                         git
-subtree-mainline:) main
="$b" ;; 
 214                         git
-subtree-split:) sub
="$b" ;; 
 216                                 if [ -n "$sub" ]; then 
 217                                         if [ -n "$main" ]; then 
 219                                                 # Pretend its sub was a squash. 
 222                                         debug 
"Squash found: $sq $sub" 
 234 find_existing_splits
() 
 236         debug 
"Looking for prior splits..." 
 241         git log 
--grep="^git-subtree-dir: $dir/*\$" \
 
 242                 --pretty=format
:'START %H%n%s%n%n%b%nEND%n' $revs | 
 243         while read a b junk
; do 
 246                         git
-subtree-mainline:) main
="$b" ;; 
 247                         git
-subtree-split:) sub
="$b" ;; 
 249                                 debug 
"  Main is: '$main'" 
 250                                 if [ -z "$main" -a -n "$sub" ]; then 
 251                                         # squash commits refer to a subtree 
 252                                         debug 
"  Squash: $sq from $sub" 
 253                                         cache_set 
"$sq" "$sub" 
 255                                 if [ -n "$main" -a -n "$sub" ]; then 
 256                                         debug 
"  Prior: $main -> $sub" 
 259                                         try_remove_previous 
"$main" 
 260                                         try_remove_previous 
"$sub" 
 271         # We're going to set some environment vars here, so 
 272         # do it in a subshell to get rid of them safely later 
 273         debug copy_commit 
"{$1}" "{$2}" "{$3}" 
 274         git log 
-1 --pretty=format
:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | 
 277                 read GIT_AUTHOR_EMAIL
 
 279                 read GIT_COMMITTER_NAME
 
 280                 read GIT_COMMITTER_EMAIL
 
 281                 read GIT_COMMITTER_DATE
 
 282                 export  GIT_AUTHOR_NAME \
 
 286                         GIT_COMMITTER_EMAIL \
 
 288                 (echo -n "$annotate"; cat ) | 
 289                 git commit
-tree "$2" $3  # reads the rest of stdin 
 290         ) || die 
"Can't copy commit $1" 
 298         if [ -n "$message" ]; then 
 299                 commit_message
="$message" 
 301                 commit_message
="Add '$dir/' from commit '$latest_new'" 
 306                 git-subtree-dir: $dir 
 307                 git-subtree-mainline: $latest_old 
 308                 git-subtree-split: $latest_new 
 314         if [ -n "$message" ]; then 
 317                 echo "Merge commit '$1' as '$2'" 
 326         if [ -n "$message" ]; then 
 327                 commit_message
="$message" 
 329                 commit_message
="Split '$dir/' into commit '$latest_new'" 
 334                 git-subtree-dir: $dir 
 335                 git-subtree-mainline: $latest_old 
 336                 git-subtree-split: $latest_new 
 345         newsub_short
=$(git rev-parse --short "$newsub") 
 347         if [ -n "$oldsub" ]; then 
 348                 oldsub_short
=$(git rev-parse --short "$oldsub") 
 349                 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short" 
 351                 git log 
--pretty=tformat
:'%h %s' "$oldsub..$newsub" 
 352                 git log 
--pretty=tformat
:'REVERT: %h %s' "$newsub..$oldsub" 
 354                 echo "Squashed '$dir/' content from commit $newsub_short" 
 358         echo "git-subtree-dir: $dir" 
 359         echo "git-subtree-split: $newsub" 
 365         git log 
-1 --pretty=format
:'%T' "$commit" -- || exit $?
 
 372         git 
ls-tree "$commit" -- "$dir" | 
 373         while read mode 
type tree name
; do 
 374                 assert 
[ "$name" = "$dir" ] 
 375                 assert 
[ "$type" = "tree" ] 
 385         if [ $# -ne 1 ]; then 
 386                 return 0   # weird parents, consider it changed 
 388                 ptree
=$(toptree_for_commit $1) 
 389                 if [ "$ptree" != "$tree" ]; then 
 392                         return 1   # not changed 
 402         tree
=$(toptree_for_commit $newsub) || exit $?
 
 403         if [ -n "$old" ]; then 
 404                 squash_msg 
"$dir" "$oldsub" "$newsub" |  
 405                         git commit
-tree "$tree" -p "$old" || exit $?
 
 407                 squash_msg 
"$dir" "" "$newsub" | 
 408                         git commit
-tree "$tree" || exit $?
 
 417         assert 
[ -n "$tree" ] 
 423         for parent 
in $newparents; do 
 424                 ptree
=$(toptree_for_commit $parent) || exit $?
 
 425                 [ -z "$ptree" ] && continue 
 426                 if [ "$ptree" = "$tree" ]; then 
 427                         # an identical parent could be used in place of this rev. 
 430                         nonidentical
="$parent" 
 433                 # sometimes both old parents map to the same newparent; 
 434                 # eliminate duplicates 
 436                 for gp 
in $gotparents; do 
 437                         if [ "$gp" = "$parent" ]; then 
 442                 if [ -n "$is_new" ]; then 
 443                         gotparents
="$gotparents $parent" 
 448         if [ -n "$identical" ]; then 
 451                 copy_commit 
$rev $tree "$p" || exit $?
 
 457         if ! git 
diff-index HEAD 
--exit-code --quiet 2>&1; then 
 458                 die 
"Working tree has modifications.  Cannot add." 
 460         if ! git 
diff-index --cached HEAD 
--exit-code --quiet 2>&1; then 
 461                 die 
"Index has modifications.  Cannot add." 
 467         if [ -e "$dir" ]; then 
 468                 die 
"'$dir' already exists.  Cannot add." 
 473         if [ $# -eq 1 ]; then 
 474                 "cmd_add_commit" "$@" 
 475         elif [ $# -eq 2 ]; then 
 476                 "cmd_add_repository" "$@" 
 478             say 
"error: parameters were '$@'" 
 479             die 
"Provide either a refspec or a repository and refspec." 
 485         echo "git fetch" "$@" 
 488         git fetch 
"$@" || exit $?
 
 496         revs
=$(git rev-parse $default --revs-only "$@") || exit $?
 
 500         debug 
"Adding $dir as '$rev'..." 
 501         git 
read-tree --prefix="$dir" $rev || exit $?
 
 502         git checkout 
-- "$dir" || exit $?
 
 503         tree
=$(git write-tree) || exit $?
 
 505         headrev
=$(git rev-parse HEAD) || exit $?
 
 506         if [ -n "$headrev" -a "$headrev" != "$rev" ]; then 
 512         if [ -n "$squash" ]; then 
 513                 rev=$(new_squash_commit "" "" "$rev") || exit $?
 
 514                 commit
=$
(add_squashed_msg 
"$rev" "$dir" | 
 515                          git commit
-tree $tree $headp -p "$rev") || exit $?
 
 517                 commit
=$
(add_msg 
"$dir" "$headrev" "$rev" | 
 518                          git commit
-tree $tree $headp -p "$rev") || exit $?
 
 520         git 
reset "$commit" || exit $?
 
 522         say 
"Added dir '$dir'" 
 527         debug 
"Splitting $dir..." 
 528         cache_setup 
|| exit $?
 
 530         if [ -n "$onto" ]; then 
 531                 debug 
"Reading history for --onto=$onto..." 
 534                         # the 'onto' history is already just the subdir, so 
 535                         # any parent we find there can be used verbatim 
 541         if [ -n "$ignore_joins" ]; then 
 544                 unrevs
="$(find_existing_splits "$dir" "$revs")" 
 547         # We can't restrict rev-list to only $dir here, because some of our 
 548         # parents have the $dir contents the root, and those won't match. 
 549         # (and rev-list --follow doesn't seem to solve this) 
 550         grl
='git rev-list --reverse --parents $revs $unrevs' 
 551         revmax
=$(eval "$grl" | wc -l) 
 555         while read rev parents
; do 
 556                 revcount
=$(($revcount + 1)) 
 557                 say 
-n "$revcount/$revmax ($createcount)
" 
 558                 debug 
"Processing commit: $rev" 
 559                 exists
=$(cache_get $rev) 
 560                 if [ -n "$exists" ]; then 
 561                         debug 
"  prior: $exists" 
 564                 createcount
=$(($createcount + 1)) 
 565                 debug 
"  parents: $parents" 
 566                 newparents
=$(cache_get $parents) 
 567                 debug 
"  newparents: $newparents" 
 569                 tree
=$(subtree_for_commit $rev "$dir") 
 570                 debug 
"  tree is: $tree" 
 572                 # ugly.  is there no better way to tell if this is a subtree 
 573                 # vs. a mainline commit?  Does it matter? 
 574                 if [ -z $tree ]; then 
 575                         if [ -n "$newparents" ]; then 
 581                 newrev
=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
 
 582                 debug 
"  newrev is: $newrev" 
 583                 cache_set 
$rev $newrev 
 584                 cache_set latest_new 
$newrev 
 585                 cache_set latest_old 
$rev 
 587         latest_new
=$(cache_get latest_new) 
 588         if [ -z "$latest_new" ]; then 
 589                 die 
"No new revisions were found" 
 592         if [ -n "$rejoin" ]; then 
 593                 debug 
"Merging split branch into HEAD..." 
 594                 latest_old
=$(cache_get latest_old) 
 596                         -m "$(rejoin_msg $dir $latest_old $latest_new)" \
 
 597                         $latest_new >&2 || exit $?
 
 599         if [ -n "$branch" ]; then 
 600                 if rev_exists 
"refs/heads/$branch"; then 
 601                         if ! rev_is_descendant_of_branch 
$latest_new $branch; then 
 602                                 die 
"Branch '$branch' is not an ancestor of commit '$latest_new'." 
 608                 git update
-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
 
 609                 say 
"$action branch '$branch'" 
 617         revs
=$(git rev-parse $default --revs-only "$@") || exit $?
 
 621         if [ $# -ne 1 ]; then 
 622                 die 
"You must provide exactly one revision.  Got: '$revs'" 
 626         if [ -n "$squash" ]; then 
 627                 first_split
="$(find_latest_squash "$dir")" 
 628                 if [ -z "$first_split" ]; then 
 629                         die 
"Can't squash-merge: '$dir' was never added." 
 634                 if [ "$sub" = "$rev" ]; then 
 635                         say 
"Subtree is already at commit $rev." 
 638                 new
=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
 
 639                 debug 
"New squash commit: $new" 
 643         version
=$(git version) 
 644         if [ "$version" \
< "git version 1.7" ]; then 
 645                 if [ -n "$message" ]; then 
 646                         git merge 
-s subtree 
--message="$message" $rev 
 648                         git merge 
-s subtree 
$rev 
 651                 if [ -n "$message" ]; then 
 652                         git merge 
-Xsubtree="$prefix" --message="$message" $rev 
 654                         git merge 
-Xsubtree="$prefix" $rev 
 662         git fetch 
"$@" || exit $?
 
 670         if [ $# -ne 2 ]; then 
 671             die 
"You must provide <repository> <refspec>" 
 673         if [ -e "$dir" ]; then 
 676             echo "git push using: " $repository $refspec 
 677             git push 
$repository $(git subtree split --prefix=$prefix):refs
/heads
/$refspec 
 679             die 
"'$dir' must already exist. Try 'git subtree add'."