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)
53 if [ -n "$debug" ]; then
60 if [ -z "$quiet" ]; then
70 die "assertion failed: " "$@"
77 while [ $# -gt 0 ]; do
83 --annotate) annotate="$1"; shift ;;
84 --no-annotate) annotate= ;;
85 -b) branch="$1"; shift ;;
86 -P) prefix="$1"; shift ;;
87 -m) message="$1"; shift ;;
88 --no-prefix) prefix= ;;
89 --onto) onto="$1"; shift ;;
92 --no-rejoin) rejoin= ;;
93 --ignore-joins) ignore_joins=1 ;;
94 --no-ignore-joins) ignore_joins= ;;
96 --no-squash) squash= ;;
98 *) die "Unexpected option: $opt" ;;
105 add|merge|pull) default= ;;
106 split|push) default="--default HEAD" ;;
107 *) die "Unknown command '$command'" ;;
110 if [ -z "$prefix" ]; then
111 die "You must provide the --prefix option."
115 add) [ -e "$prefix" ] &&
116 die "prefix '$prefix' already exists." ;;
117 *) [ -e "$prefix" ] ||
118 die "'$prefix' does not exist; use 'git subtree add'" ;;
121 dir="$(dirname "$prefix/.")"
123 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
124 revs=$(git rev-parse $default --revs-only "$@") || exit $?
125 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
126 if [ -n "$dirs" ]; then
127 die "Error: Use --prefix instead of bare filenames."
131 debug "command: {$command}"
132 debug "quiet: {$quiet}"
133 debug "revs: {$revs}"
140 cachedir="$GIT_DIR/subtree-cache/$$"
141 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
142 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
143 mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
144 debug "Using cachedir: $cachedir" >&2
150 if [ -r "$cachedir/$oldrev" ]; then
151 read newrev <"$cachedir/$oldrev"
160 if [ ! -r "$cachedir/$oldrev" ]; then
168 missed=$(cache_miss $*)
169 for miss in $missed; do
170 if [ ! -r "$cachedir/notree/$miss" ]; then
171 debug " incorrect order: $miss"
178 echo "1" > "$cachedir/notree/$1"
185 if [ "$oldrev" != "latest_old" \
186 -a "$oldrev" != "latest_new" \
187 -a -e "$cachedir/$oldrev" ]; then
188 die "cache for $oldrev already exists!"
190 echo "$newrev" >"$cachedir/$oldrev"
195 if git rev-parse "$1" >/dev/null 2>&1; then
202 rev_is_descendant_of_branch()
206 branch_hash=$(git rev-parse $branch)
207 match=$(git rev-list -1 $branch_hash ^$newrev)
209 if [ -z "$match" ]; then
216 # if a commit doesn't have a parent, this might not work. But we only want
217 # to remove the parent from the rev-list, and since it doesn't exist, it won't
218 # be there anyway, so do nothing in that case.
219 try_remove_previous()
221 if rev_exists "$1^"; then
228 debug "Looking for latest squash ($dir)..."
233 git log --grep="^git-subtree-dir: $dir/*\$" \
234 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
235 while read a b junk; do
237 debug "{{$sq/$main/$sub}}"
240 git-subtree-mainline:) main="$b" ;;
241 git-subtree-split:) sub="$b" ;;
243 if [ -n "$sub" ]; then
244 if [ -n "$main" ]; then
246 # Pretend its sub was a squash.
249 debug "Squash found: $sq $sub"
261 find_existing_splits()
263 debug "Looking for prior splits..."
268 git log --grep="^git-subtree-dir: $dir/*\$" \
269 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
270 while read a b junk; do
273 git-subtree-mainline:) main="$b" ;;
274 git-subtree-split:) sub="$b" ;;
276 debug " Main is: '$main'"
277 if [ -z "$main" -a -n "$sub" ]; then
278 # squash commits refer to a subtree
279 debug " Squash: $sq from $sub"
280 cache_set "$sq" "$sub"
282 if [ -n "$main" -a -n "$sub" ]; then
283 debug " Prior: $main -> $sub"
286 try_remove_previous "$main"
287 try_remove_previous "$sub"
298 # We're going to set some environment vars here, so
299 # do it in a subshell to get rid of them safely later
300 debug copy_commit "{$1}" "{$2}" "{$3}"
301 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
304 read GIT_AUTHOR_EMAIL
306 read GIT_COMMITTER_NAME
307 read GIT_COMMITTER_EMAIL
308 read GIT_COMMITTER_DATE
309 export GIT_AUTHOR_NAME \
313 GIT_COMMITTER_EMAIL \
315 (printf "%s" "$annotate"; cat ) |
316 git commit-tree "$2" $3 # reads the rest of stdin
317 ) || die "Can't copy commit $1"
325 if [ -n "$message" ]; then
326 commit_message="$message"
328 commit_message="Add '$dir/' from commit '$latest_new'"
333 git-subtree-dir: $dir
334 git-subtree-mainline: $latest_old
335 git-subtree-split: $latest_new
341 if [ -n "$message" ]; then
344 echo "Merge commit '$1' as '$2'"
353 if [ -n "$message" ]; then
354 commit_message="$message"
356 commit_message="Split '$dir/' into commit '$latest_new'"
361 git-subtree-dir: $dir
362 git-subtree-mainline: $latest_old
363 git-subtree-split: $latest_new
372 newsub_short=$(git rev-parse --short "$newsub")
374 if [ -n "$oldsub" ]; then
375 oldsub_short=$(git rev-parse --short "$oldsub")
376 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
378 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
379 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
381 echo "Squashed '$dir/' content from commit $newsub_short"
385 echo "git-subtree-dir: $dir"
386 echo "git-subtree-split: $newsub"
392 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
399 git ls-tree "$commit" -- "$dir" |
400 while read mode type tree name; do
401 assert [ "$name" = "$dir" ]
402 assert [ "$type" = "tree" -o "$type" = "commit" ]
403 [ "$type" = "commit" ] && continue # ignore submodules
413 if [ $# -ne 1 ]; then
414 return 0 # weird parents, consider it changed
416 ptree=$(toptree_for_commit $1)
417 if [ "$ptree" != "$tree" ]; then
420 return 1 # not changed
430 tree=$(toptree_for_commit $newsub) || exit $?
431 if [ -n "$old" ]; then
432 squash_msg "$dir" "$oldsub" "$newsub" |
433 git commit-tree "$tree" -p "$old" || exit $?
435 squash_msg "$dir" "" "$newsub" |
436 git commit-tree "$tree" || exit $?
445 assert [ -n "$tree" ]
451 for parent in $newparents; do
452 ptree=$(toptree_for_commit $parent) || exit $?
453 [ -z "$ptree" ] && continue
454 if [ "$ptree" = "$tree" ]; then
455 # an identical parent could be used in place of this rev.
458 nonidentical="$parent"
461 # sometimes both old parents map to the same newparent;
462 # eliminate duplicates
464 for gp in $gotparents; do
465 if [ "$gp" = "$parent" ]; then
470 if [ -n "$is_new" ]; then
471 gotparents="$gotparents $parent"
476 if [ -n "$identical" ]; then
479 copy_commit $rev $tree "$p" || exit $?
485 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
486 die "Working tree has modifications. Cannot add."
488 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
489 die "Index has modifications. Cannot add."
493 ensure_valid_ref_format()
495 git check-ref-format "refs/heads/$1" ||
496 die "'$1' does not look like a ref"
501 if [ -e "$dir" ]; then
502 die "'$dir' already exists. Cannot add."
507 if [ $# -eq 1 ]; then
508 git rev-parse -q --verify "$1^{commit}" >/dev/null ||
509 die "'$1' does not refer to a commit"
511 "cmd_add_commit" "$@"
512 elif [ $# -eq 2 ]; then
513 # Technically we could accept a refspec here but we're
514 # just going to turn around and add FETCH_HEAD under the
515 # specified directory. Allowing a refspec might be
516 # misleading because we won't do anything with any other
517 # branches fetched via the refspec.
518 ensure_valid_ref_format "$2"
520 "cmd_add_repository" "$@"
522 say "error: parameters were '$@'"
523 die "Provide either a commit or a repository and commit."
529 echo "git fetch" "$@"
532 git fetch "$@" || exit $?
540 revs=$(git rev-parse $default --revs-only "$@") || exit $?
544 debug "Adding $dir as '$rev'..."
545 git read-tree --prefix="$dir" $rev || exit $?
546 git checkout -- "$dir" || exit $?
547 tree=$(git write-tree) || exit $?
549 headrev=$(git rev-parse HEAD) || exit $?
550 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
556 if [ -n "$squash" ]; then
557 rev=$(new_squash_commit "" "" "$rev") || exit $?
558 commit=$(add_squashed_msg "$rev" "$dir" |
559 git commit-tree $tree $headp -p "$rev") || exit $?
561 commit=$(add_msg "$dir" "$headrev" "$rev" |
562 git commit-tree $tree $headp -p "$rev") || exit $?
564 git reset "$commit" || exit $?
566 say "Added dir '$dir'"
571 debug "Splitting $dir..."
572 cache_setup || exit $?
574 if [ -n "$onto" ]; then
575 debug "Reading history for --onto=$onto..."
578 # the 'onto' history is already just the subdir, so
579 # any parent we find there can be used verbatim
585 if [ -n "$ignore_joins" ]; then
588 unrevs="$(find_existing_splits "$dir" "$revs")"
591 # We can't restrict rev-list to only $dir here, because some of our
592 # parents have the $dir contents the root, and those won't match.
593 # (and rev-list --follow doesn't seem to solve this)
594 grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
595 revmax=$(eval "$grl" | wc -l)
599 while read rev parents; do
600 revcount=$(($revcount + 1))
601 say -n "$revcount/$revmax ($createcount)
\r"
602 debug "Processing commit: $rev"
603 exists=$(cache_get $rev)
604 if [ -n "$exists" ]; then
605 debug " prior: $exists"
608 createcount=$(($createcount + 1))
609 debug " parents: $parents"
610 newparents=$(cache_get $parents)
611 debug " newparents: $newparents"
613 tree=$(subtree_for_commit $rev "$dir")
614 debug " tree is: $tree"
616 check_parents $parents
618 # ugly. is there no better way to tell if this is a subtree
619 # vs. a mainline commit? Does it matter?
620 if [ -z $tree ]; then
622 if [ -n "$newparents" ]; then
628 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
629 debug " newrev is: $newrev"
630 cache_set $rev $newrev
631 cache_set latest_new $newrev
632 cache_set latest_old $rev
634 latest_new=$(cache_get latest_new)
635 if [ -z "$latest_new" ]; then
636 die "No new revisions were found"
639 if [ -n "$rejoin" ]; then
640 debug "Merging split branch into HEAD..."
641 latest_old=$(cache_get latest_old)
643 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
644 $latest_new >&2 || exit $?
646 if [ -n "$branch" ]; then
647 if rev_exists "refs/heads/$branch"; then
648 if ! rev_is_descendant_of_branch $latest_new $branch; then
649 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
655 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
656 say "$action branch '$branch'"
664 revs=$(git rev-parse $default --revs-only "$@") || exit $?
668 if [ $# -ne 1 ]; then
669 die "You must provide exactly one revision. Got: '$revs'"
673 if [ -n "$squash" ]; then
674 first_split="$(find_latest_squash "$dir")"
675 if [ -z "$first_split" ]; then
676 die "Can't squash-merge: '$dir' was never added."
681 if [ "$sub" = "$rev" ]; then
682 say "Subtree is already at commit $rev."
685 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
686 debug "New squash commit: $new"
690 version=$(git version)
691 if [ "$version" \< "git version 1.7" ]; then
692 if [ -n "$message" ]; then
693 git merge -s subtree --message="$message" $rev
695 git merge -s subtree $rev
698 if [ -n "$message" ]; then
699 git merge -Xsubtree="$prefix" --message="$message" $rev
701 git merge -Xsubtree="$prefix" $rev
708 if [ $# -ne 2 ]; then
709 die "You must provide <repository> <ref>"
712 ensure_valid_ref_format "$2"
713 git fetch "$@" || exit $?
721 if [ $# -ne 2 ]; then
722 die "You must provide <repository> <ref>"
724 ensure_valid_ref_format "$2"
725 if [ -e "$dir" ]; then
728 echo "git push using: " $repository $refspec
729 localrev=$(git subtree split --prefix="$prefix") || die
730 git push $repository $localrev:refs/heads/$refspec
732 die "'$dir' must already exist. Try 'git subtree add'."