Use 'git merge -Xsubtree' when git version >= 1.7.0.
[git-subtree.git] / git-subtree.sh
blobb7c350107e6edc8032eba76d5d74e56628bffd82
1 #!/bin/bash
3 # git-subtree.sh: split/join git repositories in subdirectories of this one
5 # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
7 if [ $# -eq 0 ]; then
8 set -- -h
9 fi
10 OPTS_SPEC="\
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...>
17 h,help show the help
18 q quiet
19 d show debug messages
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
22 options for 'split'
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 $?)
32 PATH=$(git --exec-path):$PATH
33 . git-sh-setup
34 require_work_tree
36 quiet=
37 branch=
38 debug=
39 command=
40 onto=
41 rejoin=
42 ignore_joins=
43 annotate=
44 squash=
45 message=
47 debug()
49 if [ -n "$debug" ]; then
50 echo "$@" >&2
54 say()
56 if [ -z "$quiet" ]; then
57 echo "$@" >&2
61 assert()
63 if "$@"; then
65 else
66 die "assertion failed: " "$@"
71 #echo "Options: $*"
73 while [ $# -gt 0 ]; do
74 opt="$1"
75 shift
76 case "$opt" in
77 -q) quiet=1 ;;
78 -d) debug=1 ;;
79 --annotate) annotate="$1"; shift ;;
80 --no-annotate) annotate= ;;
81 -b) branch="$1"; shift ;;
82 -P) prefix="$1"; shift ;;
83 -m) message="$1"; shift ;;
84 --no-prefix) prefix= ;;
85 --onto) onto="$1"; shift ;;
86 --no-onto) onto= ;;
87 --rejoin) rejoin=1 ;;
88 --no-rejoin) rejoin= ;;
89 --ignore-joins) ignore_joins=1 ;;
90 --no-ignore-joins) ignore_joins= ;;
91 --squash) squash=1 ;;
92 --no-squash) squash= ;;
93 --) break ;;
94 *) die "Unexpected option: $opt" ;;
95 esac
96 done
98 command="$1"
99 shift
100 case "$command" in
101 add|merge|pull) default= ;;
102 split|push) default="--default HEAD" ;;
103 *) die "Unknown command '$command'" ;;
104 esac
106 if [ -z "$prefix" ]; then
107 die "You must provide the --prefix option."
110 case "$command" in
111 add) [ -e "$prefix" ] &&
112 die "prefix '$prefix' already exists." ;;
113 *) [ -e "$prefix" ] ||
114 die "'$prefix' does not exist; use 'git subtree add'" ;;
115 esac
117 dir="$(dirname "$prefix/.")"
119 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
120 revs=$(git rev-parse $default --revs-only "$@") || exit $?
121 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
122 if [ -n "$dirs" ]; then
123 die "Error: Use --prefix instead of bare filenames."
127 debug "command: {$command}"
128 debug "quiet: {$quiet}"
129 debug "revs: {$revs}"
130 debug "dir: {$dir}"
131 debug "opts: {$*}"
132 debug
134 cache_setup()
136 cachedir="$GIT_DIR/subtree-cache/$$"
137 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
138 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
139 debug "Using cachedir: $cachedir" >&2
142 cache_get()
144 for oldrev in $*; do
145 if [ -r "$cachedir/$oldrev" ]; then
146 read newrev <"$cachedir/$oldrev"
147 echo $newrev
149 done
152 cache_set()
154 oldrev="$1"
155 newrev="$2"
156 if [ "$oldrev" != "latest_old" \
157 -a "$oldrev" != "latest_new" \
158 -a -e "$cachedir/$oldrev" ]; then
159 die "cache for $oldrev already exists!"
161 echo "$newrev" >"$cachedir/$oldrev"
164 rev_exists()
166 if git rev-parse "$1" >/dev/null 2>&1; then
167 return 0
168 else
169 return 1
173 rev_is_descendant_of_branch()
175 newrev="$1"
176 branch="$2"
177 branch_hash=$(git rev-parse $branch)
178 match=$(git rev-list -1 $branch_hash ^$newrev)
180 if [ -z "$match" ]; then
181 return 0
182 else
183 return 1
187 # if a commit doesn't have a parent, this might not work. But we only want
188 # to remove the parent from the rev-list, and since it doesn't exist, it won't
189 # be there anyway, so do nothing in that case.
190 try_remove_previous()
192 if rev_exists "$1^"; then
193 echo "^$1^"
197 find_latest_squash()
199 debug "Looking for latest squash ($dir)..."
200 dir="$1"
202 main=
203 sub=
204 git log --grep="^git-subtree-dir: $dir/*\$" \
205 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
206 while read a b junk; do
207 debug "$a $b $junk"
208 debug "{{$sq/$main/$sub}}"
209 case "$a" in
210 START) sq="$b" ;;
211 git-subtree-mainline:) main="$b" ;;
212 git-subtree-split:) sub="$b" ;;
213 END)
214 if [ -n "$sub" ]; then
215 if [ -n "$main" ]; then
216 # a rejoin commit?
217 # Pretend its sub was a squash.
218 sq="$sub"
220 debug "Squash found: $sq $sub"
221 echo "$sq" "$sub"
222 break
225 main=
226 sub=
228 esac
229 done
232 find_existing_splits()
234 debug "Looking for prior splits..."
235 dir="$1"
236 revs="$2"
237 main=
238 sub=
239 git log --grep="^git-subtree-dir: $dir/*\$" \
240 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
241 while read a b junk; do
242 case "$a" in
243 START) sq="$b" ;;
244 git-subtree-mainline:) main="$b" ;;
245 git-subtree-split:) sub="$b" ;;
246 END)
247 debug " Main is: '$main'"
248 if [ -z "$main" -a -n "$sub" ]; then
249 # squash commits refer to a subtree
250 debug " Squash: $sq from $sub"
251 cache_set "$sq" "$sub"
253 if [ -n "$main" -a -n "$sub" ]; then
254 debug " Prior: $main -> $sub"
255 cache_set $main $sub
256 try_remove_previous "$main"
257 try_remove_previous "$sub"
259 main=
260 sub=
262 esac
263 done
266 copy_commit()
268 # We're going to set some environment vars here, so
269 # do it in a subshell to get rid of them safely later
270 debug copy_commit "{$1}" "{$2}" "{$3}"
271 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
273 read GIT_AUTHOR_NAME
274 read GIT_AUTHOR_EMAIL
275 read GIT_AUTHOR_DATE
276 read GIT_COMMITTER_NAME
277 read GIT_COMMITTER_EMAIL
278 read GIT_COMMITTER_DATE
279 export GIT_AUTHOR_NAME \
280 GIT_AUTHOR_EMAIL \
281 GIT_AUTHOR_DATE \
282 GIT_COMMITTER_NAME \
283 GIT_COMMITTER_EMAIL \
284 GIT_COMMITTER_DATE
285 (echo -n "$annotate"; cat ) |
286 git commit-tree "$2" $3 # reads the rest of stdin
287 ) || die "Can't copy commit $1"
290 add_msg()
292 dir="$1"
293 latest_old="$2"
294 latest_new="$3"
295 if [ -n "$message" ]; then
296 commit_message="$message"
297 else
298 commit_message="Add '$dir/' from commit '$latest_new'"
300 cat <<-EOF
301 $commit_message
303 git-subtree-dir: $dir
304 git-subtree-mainline: $latest_old
305 git-subtree-split: $latest_new
309 add_squashed_msg()
311 if [ -n "$message" ]; then
312 echo "$message"
313 else
314 echo "Merge commit '$1' as '$2'"
318 rejoin_msg()
320 dir="$1"
321 latest_old="$2"
322 latest_new="$3"
323 if [ -n "$message" ]; then
324 commit_message="$message"
325 else
326 commit_message="Split '$dir/' into commit '$latest_new'"
328 cat <<-EOF
329 $commit_message
331 git-subtree-dir: $dir
332 git-subtree-mainline: $latest_old
333 git-subtree-split: $latest_new
337 squash_msg()
339 dir="$1"
340 oldsub="$2"
341 newsub="$3"
342 newsub_short=$(git rev-parse --short "$newsub")
344 if [ -n "$oldsub" ]; then
345 oldsub_short=$(git rev-parse --short "$oldsub")
346 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
347 echo
348 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
349 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
350 else
351 echo "Squashed '$dir/' content from commit $newsub_short"
354 echo
355 echo "git-subtree-dir: $dir"
356 echo "git-subtree-split: $newsub"
359 toptree_for_commit()
361 commit="$1"
362 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
365 subtree_for_commit()
367 commit="$1"
368 dir="$2"
369 git ls-tree "$commit" -- "$dir" |
370 while read mode type tree name; do
371 assert [ "$name" = "$dir" ]
372 assert [ "$type" = "tree" ]
373 echo $tree
374 break
375 done
378 tree_changed()
380 tree=$1
381 shift
382 if [ $# -ne 1 ]; then
383 return 0 # weird parents, consider it changed
384 else
385 ptree=$(toptree_for_commit $1)
386 if [ "$ptree" != "$tree" ]; then
387 return 0 # changed
388 else
389 return 1 # not changed
394 new_squash_commit()
396 old="$1"
397 oldsub="$2"
398 newsub="$3"
399 tree=$(toptree_for_commit $newsub) || exit $?
400 if [ -n "$old" ]; then
401 squash_msg "$dir" "$oldsub" "$newsub" |
402 git commit-tree "$tree" -p "$old" || exit $?
403 else
404 squash_msg "$dir" "" "$newsub" |
405 git commit-tree "$tree" || exit $?
409 copy_or_skip()
411 rev="$1"
412 tree="$2"
413 newparents="$3"
414 assert [ -n "$tree" ]
416 identical=
417 nonidentical=
419 gotparents=
420 for parent in $newparents; do
421 ptree=$(toptree_for_commit $parent) || exit $?
422 [ -z "$ptree" ] && continue
423 if [ "$ptree" = "$tree" ]; then
424 # an identical parent could be used in place of this rev.
425 identical="$parent"
426 else
427 nonidentical="$parent"
430 # sometimes both old parents map to the same newparent;
431 # eliminate duplicates
432 is_new=1
433 for gp in $gotparents; do
434 if [ "$gp" = "$parent" ]; then
435 is_new=
436 break
438 done
439 if [ -n "$is_new" ]; then
440 gotparents="$gotparents $parent"
441 p="$p -p $parent"
443 done
445 if [ -n "$identical" ]; then
446 echo $identical
447 else
448 copy_commit $rev $tree "$p" || exit $?
452 ensure_clean()
454 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
455 die "Working tree has modifications. Cannot add."
457 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
458 die "Index has modifications. Cannot add."
462 cmd_add()
464 if [ -e "$dir" ]; then
465 die "'$dir' already exists. Cannot add."
468 ensure_clean
470 if [ $# -eq 1 ]; then
471 "cmd_add_commit" "$@"
472 elif [ $# -eq 2 ]; then
473 "cmd_add_repository" "$@"
474 else
475 say "error: parameters were '$@'"
476 die "Provide either a refspec or a repository and refspec."
480 cmd_add_repository()
482 echo "git fetch" "$@"
483 repository=$1
484 refspec=$2
485 git fetch "$@" || exit $?
486 revs=FETCH_HEAD
487 set -- $revs
488 cmd_add_commit "$@"
491 cmd_add_commit()
493 revs=$(git rev-parse $default --revs-only "$@") || exit $?
494 set -- $revs
495 rev="$1"
497 debug "Adding $dir as '$rev'..."
498 git read-tree --prefix="$dir" $rev || exit $?
499 git checkout -- "$dir" || exit $?
500 tree=$(git write-tree) || exit $?
502 headrev=$(git rev-parse HEAD) || exit $?
503 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
504 headp="-p $headrev"
505 else
506 headp=
509 if [ -n "$squash" ]; then
510 rev=$(new_squash_commit "" "" "$rev") || exit $?
511 commit=$(add_squashed_msg "$rev" "$dir" |
512 git commit-tree $tree $headp -p "$rev") || exit $?
513 else
514 commit=$(add_msg "$dir" "$headrev" "$rev" |
515 git commit-tree $tree $headp -p "$rev") || exit $?
517 git reset "$commit" || exit $?
519 say "Added dir '$dir'"
522 cmd_split()
524 debug "Splitting $dir..."
525 cache_setup || exit $?
527 if [ -n "$onto" ]; then
528 debug "Reading history for --onto=$onto..."
529 git rev-list $onto |
530 while read rev; do
531 # the 'onto' history is already just the subdir, so
532 # any parent we find there can be used verbatim
533 debug " cache: $rev"
534 cache_set $rev $rev
535 done
538 if [ -n "$ignore_joins" ]; then
539 unrevs=
540 else
541 unrevs="$(find_existing_splits "$dir" "$revs")"
544 # We can't restrict rev-list to only $dir here, because some of our
545 # parents have the $dir contents the root, and those won't match.
546 # (and rev-list --follow doesn't seem to solve this)
547 grl='git rev-list --reverse --parents $revs $unrevs'
548 revmax=$(eval "$grl" | wc -l)
549 revcount=0
550 createcount=0
551 eval "$grl" |
552 while read rev parents; do
553 revcount=$(($revcount + 1))
554 say -n "$revcount/$revmax ($createcount) "
555 debug "Processing commit: $rev"
556 exists=$(cache_get $rev)
557 if [ -n "$exists" ]; then
558 debug " prior: $exists"
559 continue
561 createcount=$(($createcount + 1))
562 debug " parents: $parents"
563 newparents=$(cache_get $parents)
564 debug " newparents: $newparents"
566 tree=$(subtree_for_commit $rev "$dir")
567 debug " tree is: $tree"
569 # ugly. is there no better way to tell if this is a subtree
570 # vs. a mainline commit? Does it matter?
571 if [ -z $tree ]; then
572 cache_set $rev $rev
573 continue
576 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
577 debug " newrev is: $newrev"
578 cache_set $rev $newrev
579 cache_set latest_new $newrev
580 cache_set latest_old $rev
581 done || exit $?
582 latest_new=$(cache_get latest_new)
583 if [ -z "$latest_new" ]; then
584 die "No new revisions were found"
587 if [ -n "$rejoin" ]; then
588 debug "Merging split branch into HEAD..."
589 latest_old=$(cache_get latest_old)
590 git merge -s ours \
591 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
592 $latest_new >&2 || exit $?
594 if [ -n "$branch" ]; then
595 if rev_exists "refs/heads/$branch"; then
596 if ! rev_is_descendant_of_branch $latest_new $branch; then
597 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
599 action='Updated'
600 else
601 action='Created'
603 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
604 say "$action branch '$branch'"
606 echo $latest_new
607 exit 0
610 cmd_merge()
612 revs=$(git rev-parse $default --revs-only "$@") || exit $?
613 ensure_clean
615 set -- $revs
616 if [ $# -ne 1 ]; then
617 die "You must provide exactly one revision. Got: '$revs'"
619 rev="$1"
621 if [ -n "$squash" ]; then
622 first_split="$(find_latest_squash "$dir")"
623 if [ -z "$first_split" ]; then
624 die "Can't squash-merge: '$dir' was never added."
626 set $first_split
627 old=$1
628 sub=$2
629 if [ "$sub" = "$rev" ]; then
630 say "Subtree is already at commit $rev."
631 exit 0
633 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
634 debug "New squash commit: $new"
635 rev="$new"
638 version=$(git version)
639 if [ "$version" \< "git version 1.7" ]; then
640 if [ -n "$message" ]; then
641 git merge -s subtree --message="$message" $rev
642 else
643 git merge -s subtree $rev
645 else
646 if [ -n "$message" ]; then
647 git merge -Xsubtree="$prefix" --message="$message" $rev
648 else
649 git merge -Xsubtree="$prefix" $rev
654 cmd_pull()
656 ensure_clean
657 git fetch "$@" || exit $?
658 revs=FETCH_HEAD
659 set -- $revs
660 cmd_merge "$@"
663 cmd_push()
665 if [ $# -ne 2 ]; then
666 die "You must provide <repository> <refspec>"
668 if [ -e "$dir" ]; then
669 repository=$1
670 refspec=$2
671 echo "git push using: " $repository $refspec
672 git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
673 else
674 die "'$dir' must already exist. Try 'git subtree add'."
678 "cmd_$command" "$@"