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> <commit>
13 git subtree merge --prefix=<prefix> <commit>
14 git subtree pull --prefix=<prefix> <repository> <refspec...>
15 git subtree push --prefix=<prefix> <repository> <refspec...>
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."
494 if [ -e "$dir" ]; then
495 die "'$dir' already exists. Cannot add."
500 if [ $# -eq 1 ]; then
501 git rev-parse -q --verify "$1^{commit}" >/dev/null ||
502 die "'$1' does not refer to a commit"
504 "cmd_add_commit" "$@"
505 elif [ $# -eq 2 ]; then
506 # Technically we could accept a refspec here but we're
507 # just going to turn around and add FETCH_HEAD under the
508 # specified directory. Allowing a refspec might be
509 # misleading because we won't do anything with any other
510 # branches fetched via the refspec.
511 git rev-parse -q --verify "$2^{commit}" >/dev/null ||
512 die "'$2' does not refer to a commit"
514 "cmd_add_repository" "$@"
516 say "error: parameters were '$@'"
517 die "Provide either a commit or a repository and commit."
523 echo "git fetch" "$@"
526 git fetch "$@" || exit $?
534 revs=$(git rev-parse $default --revs-only "$@") || exit $?
538 debug "Adding $dir as '$rev'..."
539 git read-tree --prefix="$dir" $rev || exit $?
540 git checkout -- "$dir" || exit $?
541 tree=$(git write-tree) || exit $?
543 headrev=$(git rev-parse HEAD) || exit $?
544 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
550 if [ -n "$squash" ]; then
551 rev=$(new_squash_commit "" "" "$rev") || exit $?
552 commit=$(add_squashed_msg "$rev" "$dir" |
553 git commit-tree $tree $headp -p "$rev") || exit $?
555 commit=$(add_msg "$dir" "$headrev" "$rev" |
556 git commit-tree $tree $headp -p "$rev") || exit $?
558 git reset "$commit" || exit $?
560 say "Added dir '$dir'"
565 debug "Splitting $dir..."
566 cache_setup || exit $?
568 if [ -n "$onto" ]; then
569 debug "Reading history for --onto=$onto..."
572 # the 'onto' history is already just the subdir, so
573 # any parent we find there can be used verbatim
579 if [ -n "$ignore_joins" ]; then
582 unrevs="$(find_existing_splits "$dir" "$revs")"
585 # We can't restrict rev-list to only $dir here, because some of our
586 # parents have the $dir contents the root, and those won't match.
587 # (and rev-list --follow doesn't seem to solve this)
588 grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
589 revmax=$(eval "$grl" | wc -l)
593 while read rev parents; do
594 revcount=$(($revcount + 1))
595 say -n "$revcount/$revmax ($createcount)
\r"
596 debug "Processing commit: $rev"
597 exists=$(cache_get $rev)
598 if [ -n "$exists" ]; then
599 debug " prior: $exists"
602 createcount=$(($createcount + 1))
603 debug " parents: $parents"
604 newparents=$(cache_get $parents)
605 debug " newparents: $newparents"
607 tree=$(subtree_for_commit $rev "$dir")
608 debug " tree is: $tree"
610 check_parents $parents
612 # ugly. is there no better way to tell if this is a subtree
613 # vs. a mainline commit? Does it matter?
614 if [ -z $tree ]; then
616 if [ -n "$newparents" ]; then
622 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
623 debug " newrev is: $newrev"
624 cache_set $rev $newrev
625 cache_set latest_new $newrev
626 cache_set latest_old $rev
628 latest_new=$(cache_get latest_new)
629 if [ -z "$latest_new" ]; then
630 die "No new revisions were found"
633 if [ -n "$rejoin" ]; then
634 debug "Merging split branch into HEAD..."
635 latest_old=$(cache_get latest_old)
637 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
638 $latest_new >&2 || exit $?
640 if [ -n "$branch" ]; then
641 if rev_exists "refs/heads/$branch"; then
642 if ! rev_is_descendant_of_branch $latest_new $branch; then
643 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
649 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
650 say "$action branch '$branch'"
658 revs=$(git rev-parse $default --revs-only "$@") || exit $?
662 if [ $# -ne 1 ]; then
663 die "You must provide exactly one revision. Got: '$revs'"
667 if [ -n "$squash" ]; then
668 first_split="$(find_latest_squash "$dir")"
669 if [ -z "$first_split" ]; then
670 die "Can't squash-merge: '$dir' was never added."
675 if [ "$sub" = "$rev" ]; then
676 say "Subtree is already at commit $rev."
679 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
680 debug "New squash commit: $new"
684 version=$(git version)
685 if [ "$version" \< "git version 1.7" ]; then
686 if [ -n "$message" ]; then
687 git merge -s subtree --message="$message" $rev
689 git merge -s subtree $rev
692 if [ -n "$message" ]; then
693 git merge -Xsubtree="$prefix" --message="$message" $rev
695 git merge -Xsubtree="$prefix" $rev
703 git fetch "$@" || exit $?
711 if [ $# -ne 2 ]; then
712 die "You must provide <repository> <refspec>"
714 if [ -e "$dir" ]; then
717 echo "git push using: " $repository $refspec
718 localrev=$(git subtree split --prefix="$prefix") || die
719 git push $repository $localrev:refs/heads/$refspec
721 die "'$dir' must already exist. Try 'git subtree add'."