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 add --prefix=<prefix> <repository> <ref>
13 git subtree merge --prefix=<prefix> <commit>
14 git subtree pull --prefix=<prefix> <repository> <ref>
15 git subtree push --prefix=<prefix> <repository> <ref>
16 git subtree split --prefix=<prefix> <commit...>
21 P,prefix= the name of the subdir to split out
22 m,message= use the given message as the commit message for the merge commit
24 annotate= add a prefix to commit message of new commits
25 b,branch= create a new branch from the split subtree
26 ignore-joins ignore prior --rejoin commits
27 onto= try connecting new tree to an existing one
28 rejoin merge the new branch back into HEAD
29 options for 'add', 'merge', 'pull' and 'push'
30 squash merge subtree changes as a single commit
32 eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
34 PATH=$PATH:$(git --exec-path)
52 if [ -n "$debug" ]; then
59 if [ -z "$quiet" ]; then
69 die "assertion failed: " "$@"
76 while [ $# -gt 0 ]; do
82 --annotate) annotate="$1"; shift ;;
83 --no-annotate) annotate= ;;
84 -b) branch="$1"; shift ;;
85 -P) prefix="$1"; shift ;;
86 -m) message="$1"; shift ;;
87 --no-prefix) prefix= ;;
88 --onto) onto="$1"; shift ;;
91 --no-rejoin) rejoin= ;;
92 --ignore-joins) ignore_joins=1 ;;
93 --no-ignore-joins) ignore_joins= ;;
95 --no-squash) squash= ;;
97 *) die "Unexpected option: $opt" ;;
104 add|merge|pull) default= ;;
105 split|push) default="--default HEAD" ;;
106 *) die "Unknown command '$command'" ;;
109 if [ -z "$prefix" ]; then
110 die "You must provide the --prefix option."
114 add) [ -e "$prefix" ] &&
115 die "prefix '$prefix' already exists." ;;
116 *) [ -e "$prefix" ] ||
117 die "'$prefix' does not exist; use 'git subtree add'" ;;
120 dir="$(dirname "$prefix/.")"
122 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
123 revs=$(git rev-parse $default --revs-only "$@") || exit $?
124 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
125 if [ -n "$dirs" ]; then
126 die "Error: Use --prefix instead of bare filenames."
130 debug "command: {$command}"
131 debug "quiet: {$quiet}"
132 debug "revs: {$revs}"
139 cachedir="$GIT_DIR/subtree-cache/$$"
140 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
141 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
142 mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
143 debug "Using cachedir: $cachedir" >&2
149 if [ -r "$cachedir/$oldrev" ]; then
150 read newrev <"$cachedir/$oldrev"
159 if [ ! -r "$cachedir/$oldrev" ]; then
167 missed=$(cache_miss $*)
168 for miss in $missed; do
169 if [ ! -r "$cachedir/notree/$miss" ]; then
170 debug " incorrect order: $miss"
177 echo "1" > "$cachedir/notree/$1"
184 if [ "$oldrev" != "latest_old" \
185 -a "$oldrev" != "latest_new" \
186 -a -e "$cachedir/$oldrev" ]; then
187 die "cache for $oldrev already exists!"
189 echo "$newrev" >"$cachedir/$oldrev"
194 if git rev-parse "$1" >/dev/null 2>&1; then
201 rev_is_descendant_of_branch()
205 branch_hash=$(git rev-parse $branch)
206 match=$(git rev-list -1 $branch_hash ^$newrev)
208 if [ -z "$match" ]; then
215 # if a commit doesn't have a parent, this might not work. But we only want
216 # to remove the parent from the rev-list, and since it doesn't exist, it won't
217 # be there anyway, so do nothing in that case.
218 try_remove_previous()
220 if rev_exists "$1^"; then
227 debug "Looking for latest squash ($dir)..."
232 git log --grep="^git-subtree-dir: $dir/*\$" \
233 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
234 while read a b junk; do
236 debug "{{$sq/$main/$sub}}"
239 git-subtree-mainline:) main="$b" ;;
240 git-subtree-split:) sub="$b" ;;
242 if [ -n "$sub" ]; then
243 if [ -n "$main" ]; then
245 # Pretend its sub was a squash.
248 debug "Squash found: $sq $sub"
260 find_existing_splits()
262 debug "Looking for prior splits..."
267 git log --grep="^git-subtree-dir: $dir/*\$" \
268 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
269 while read a b junk; do
272 git-subtree-mainline:) main="$b" ;;
273 git-subtree-split:) sub="$b" ;;
275 debug " Main is: '$main'"
276 if [ -z "$main" -a -n "$sub" ]; then
277 # squash commits refer to a subtree
278 debug " Squash: $sq from $sub"
279 cache_set "$sq" "$sub"
281 if [ -n "$main" -a -n "$sub" ]; then
282 debug " Prior: $main -> $sub"
285 try_remove_previous "$main"
286 try_remove_previous "$sub"
297 # We're going to set some environment vars here, so
298 # do it in a subshell to get rid of them safely later
299 debug copy_commit "{$1}" "{$2}" "{$3}"
300 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
303 read GIT_AUTHOR_EMAIL
305 read GIT_COMMITTER_NAME
306 read GIT_COMMITTER_EMAIL
307 read GIT_COMMITTER_DATE
308 export GIT_AUTHOR_NAME \
312 GIT_COMMITTER_EMAIL \
314 (printf "%s" "$annotate"; cat ) |
315 git commit-tree "$2" $3 # reads the rest of stdin
316 ) || die "Can't copy commit $1"
324 if [ -n "$message" ]; then
325 commit_message="$message"
327 commit_message="Add '$dir/' from commit '$latest_new'"
332 git-subtree-dir: $dir
333 git-subtree-mainline: $latest_old
334 git-subtree-split: $latest_new
340 if [ -n "$message" ]; then
343 echo "Merge commit '$1' as '$2'"
352 if [ -n "$message" ]; then
353 commit_message="$message"
355 commit_message="Split '$dir/' into commit '$latest_new'"
360 git-subtree-dir: $dir
361 git-subtree-mainline: $latest_old
362 git-subtree-split: $latest_new
371 newsub_short=$(git rev-parse --short "$newsub")
373 if [ -n "$oldsub" ]; then
374 oldsub_short=$(git rev-parse --short "$oldsub")
375 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
377 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
378 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
380 echo "Squashed '$dir/' content from commit $newsub_short"
384 echo "git-subtree-dir: $dir"
385 echo "git-subtree-split: $newsub"
391 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
398 git ls-tree "$commit" -- "$dir" |
399 while read mode type tree name; do
400 assert [ "$name" = "$dir" ]
401 assert [ "$type" = "tree" -o "$type" = "commit" ]
402 [ "$type" = "commit" ] && continue # ignore submodules
412 if [ $# -ne 1 ]; then
413 return 0 # weird parents, consider it changed
415 ptree=$(toptree_for_commit $1)
416 if [ "$ptree" != "$tree" ]; then
419 return 1 # not changed
429 tree=$(toptree_for_commit $newsub) || exit $?
430 if [ -n "$old" ]; then
431 squash_msg "$dir" "$oldsub" "$newsub" |
432 git commit-tree "$tree" -p "$old" || exit $?
434 squash_msg "$dir" "" "$newsub" |
435 git commit-tree "$tree" || exit $?
444 assert [ -n "$tree" ]
450 for parent in $newparents; do
451 ptree=$(toptree_for_commit $parent) || exit $?
452 [ -z "$ptree" ] && continue
453 if [ "$ptree" = "$tree" ]; then
454 # an identical parent could be used in place of this rev.
457 nonidentical="$parent"
460 # sometimes both old parents map to the same newparent;
461 # eliminate duplicates
463 for gp in $gotparents; do
464 if [ "$gp" = "$parent" ]; then
469 if [ -n "$is_new" ]; then
470 gotparents="$gotparents $parent"
475 if [ -n "$identical" ]; then
478 copy_commit $rev $tree "$p" || exit $?
484 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
485 die "Working tree has modifications. Cannot add."
487 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
488 die "Index has modifications. Cannot add."
492 ensure_valid_ref_format()
494 git check-ref-format "refs/heads/$1" ||
495 die "'$1' does not look like a ref"
500 if [ -e "$dir" ]; then
501 die "'$dir' already exists. Cannot add."
506 if [ $# -eq 1 ]; then
507 git rev-parse -q --verify "$1^{commit}" >/dev/null ||
508 die "'$1' does not refer to a commit"
510 "cmd_add_commit" "$@"
511 elif [ $# -eq 2 ]; then
512 # Technically we could accept a refspec here but we're
513 # just going to turn around and add FETCH_HEAD under the
514 # specified directory. Allowing a refspec might be
515 # misleading because we won't do anything with any other
516 # branches fetched via the refspec.
517 ensure_valid_ref_format "$2"
519 "cmd_add_repository" "$@"
521 say "error: parameters were '$@'"
522 die "Provide either a commit or a repository and commit."
528 echo "git fetch" "$@"
531 git fetch "$@" || exit $?
539 revs=$(git rev-parse $default --revs-only "$@") || exit $?
543 debug "Adding $dir as '$rev'..."
544 git read-tree --prefix="$dir" $rev || exit $?
545 git checkout -- "$dir" || exit $?
546 tree=$(git write-tree) || exit $?
548 headrev=$(git rev-parse HEAD) || exit $?
549 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
555 if [ -n "$squash" ]; then
556 rev=$(new_squash_commit "" "" "$rev") || exit $?
557 commit=$(add_squashed_msg "$rev" "$dir" |
558 git commit-tree $tree $headp -p "$rev") || exit $?
560 commit=$(add_msg "$dir" "$headrev" "$rev" |
561 git commit-tree $tree $headp -p "$rev") || exit $?
563 git reset "$commit" || exit $?
565 say "Added dir '$dir'"
570 debug "Splitting $dir..."
571 cache_setup || exit $?
573 if [ -n "$onto" ]; then
574 debug "Reading history for --onto=$onto..."
577 # the 'onto' history is already just the subdir, so
578 # any parent we find there can be used verbatim
584 if [ -n "$ignore_joins" ]; then
587 unrevs="$(find_existing_splits "$dir" "$revs")"
590 # We can't restrict rev-list to only $dir here, because some of our
591 # parents have the $dir contents the root, and those won't match.
592 # (and rev-list --follow doesn't seem to solve this)
593 grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
594 revmax=$(eval "$grl" | wc -l)
598 while read rev parents; do
599 revcount=$(($revcount + 1))
600 say -n "$revcount/$revmax ($createcount)
\r"
601 debug "Processing commit: $rev"
602 exists=$(cache_get $rev)
603 if [ -n "$exists" ]; then
604 debug " prior: $exists"
607 createcount=$(($createcount + 1))
608 debug " parents: $parents"
609 newparents=$(cache_get $parents)
610 debug " newparents: $newparents"
612 tree=$(subtree_for_commit $rev "$dir")
613 debug " tree is: $tree"
615 check_parents $parents
617 # ugly. is there no better way to tell if this is a subtree
618 # vs. a mainline commit? Does it matter?
619 if [ -z $tree ]; then
621 if [ -n "$newparents" ]; then
627 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
628 debug " newrev is: $newrev"
629 cache_set $rev $newrev
630 cache_set latest_new $newrev
631 cache_set latest_old $rev
633 latest_new=$(cache_get latest_new)
634 if [ -z "$latest_new" ]; then
635 die "No new revisions were found"
638 if [ -n "$rejoin" ]; then
639 debug "Merging split branch into HEAD..."
640 latest_old=$(cache_get latest_old)
642 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
643 $latest_new >&2 || exit $?
645 if [ -n "$branch" ]; then
646 if rev_exists "refs/heads/$branch"; then
647 if ! rev_is_descendant_of_branch $latest_new $branch; then
648 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
654 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
655 say "$action branch '$branch'"
663 revs=$(git rev-parse $default --revs-only "$@") || exit $?
667 if [ $# -ne 1 ]; then
668 die "You must provide exactly one revision. Got: '$revs'"
672 if [ -n "$squash" ]; then
673 first_split="$(find_latest_squash "$dir")"
674 if [ -z "$first_split" ]; then
675 die "Can't squash-merge: '$dir' was never added."
680 if [ "$sub" = "$rev" ]; then
681 say "Subtree is already at commit $rev."
684 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
685 debug "New squash commit: $new"
689 version=$(git version)
690 if [ "$version" \< "git version 1.7" ]; then
691 if [ -n "$message" ]; then
692 git merge -s subtree --message="$message" $rev
694 git merge -s subtree $rev
697 if [ -n "$message" ]; then
698 git merge -Xsubtree="$prefix" --message="$message" $rev
700 git merge -Xsubtree="$prefix" $rev
707 if [ $# -ne 2 ]; then
708 die "You must provide <repository> <ref>"
711 ensure_valid_ref_format "$2"
712 git fetch "$@" || exit $?
720 if [ $# -ne 2 ]; then
721 die "You must provide <repository> <ref>"
723 ensure_valid_ref_format "$2"
724 if [ -e "$dir" ]; then
727 echo "git push using: " $repository $refspec
728 localrev=$(git subtree split --prefix="$prefix") || die
729 git push $repository $localrev:refs/heads/$refspec
731 die "'$dir' must already exist. Try 'git subtree add'."