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'."