1#!/bin/bash
2#
3# git-subtree.sh: split/join git repositories in subdirectories of this one
4#
5# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
6#
7if [ $# -eq 0 ]; then
8 set -- -h
9fi
10OPTS_SPEC="\
11git subtree add --prefix=<prefix> <commit>
12git subtree merge --prefix=<prefix> <commit>
13git subtree pull --prefix=<prefix> <repository> <refspec...>
14git subtree push --prefix=<prefix> <repository> <refspec...>
15git subtree split --prefix=<prefix> <commit...>
16--
17h,help show the help
18q quiet
19d show debug messages
20P,prefix= the name of the subdir to split out
21m,message= use the given message as the commit message for the merge commit
22 options for 'split'
23annotate= add a prefix to commit message of new commits
24b,branch= create a new branch from the split subtree
25ignore-joins ignore prior --rejoin commits
26onto= try connecting new tree to an existing one
27rejoin merge the new branch back into HEAD
28 options for 'add', 'merge', 'pull' and 'push'
29squash merge subtree changes as a single commit
30"
31eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
32
33OPATH=$PATH
34PATH=$(git --exec-path):$PATH
35. git-sh-setup
36PATH=$OPATH # apparently needed for some versions of msysgit
37
38require_work_tree
39
40quiet=
41branch=
42debug=
43command=
44onto=
45rejoin=
46ignore_joins=
47annotate=
48squash=
49message=
50
51debug()
52{
53 if [ -n "$debug" ]; then
54 echo "$@" >&2
55 fi
56}
57
58say()
59{
60 if [ -z "$quiet" ]; then
61 echo "$@" >&2
62 fi
63}
64
65assert()
66{
67 if "$@"; then
68 :
69 else
70 die "assertion failed: " "$@"
71 fi
72}
73
74
75#echo "Options: $*"
76
77while [ $# -gt 0 ]; do
78 opt="$1"
79 shift
80 case "$opt" in
81 -q) quiet=1 ;;
82 -d) debug=1 ;;
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 ;;
90 --no-onto) onto= ;;
91 --rejoin) rejoin=1 ;;
92 --no-rejoin) rejoin= ;;
93 --ignore-joins) ignore_joins=1 ;;
94 --no-ignore-joins) ignore_joins= ;;
95 --squash) squash=1 ;;
96 --no-squash) squash= ;;
97 --) break ;;
98 *) die "Unexpected option: $opt" ;;
99 esac
100done
101
102command="$1"
103shift
104case "$command" in
105 add|merge|pull) default= ;;
106 split|push) default="--default HEAD" ;;
107 *) die "Unknown command '$command'" ;;
108esac
109
110if [ -z "$prefix" ]; then
111 die "You must provide the --prefix option."
112fi
113
114case "$command" in
115 add) [ -e "$prefix" ] &&
116 die "prefix '$prefix' already exists." ;;
117 *) [ -e "$prefix" ] ||
118 die "'$prefix' does not exist; use 'git subtree add'" ;;
119esac
120
121dir="$(dirname "$prefix/.")"
122
123if [ "$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."
128 fi
129fi
130
131debug "command: {$command}"
132debug "quiet: {$quiet}"
133debug "revs: {$revs}"
134debug "dir: {$dir}"
135debug "opts: {$*}"
136debug
137
138cache_setup()
139{
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 debug "Using cachedir: $cachedir" >&2
144}
145
146cache_get()
147{
148 for oldrev in $*; do
149 if [ -r "$cachedir/$oldrev" ]; then
150 read newrev <"$cachedir/$oldrev"
151 echo $newrev
152 fi
153 done
154}
155
156cache_set()
157{
158 oldrev="$1"
159 newrev="$2"
160 if [ "$oldrev" != "latest_old" \
161 -a "$oldrev" != "latest_new" \
162 -a -e "$cachedir/$oldrev" ]; then
163 die "cache for $oldrev already exists!"
164 fi
165 echo "$newrev" >"$cachedir/$oldrev"
166}
167
168rev_exists()
169{
170 if git rev-parse "$1" >/dev/null 2>&1; then
171 return 0
172 else
173 return 1
174 fi
175}
176
177rev_is_descendant_of_branch()
178{
179 newrev="$1"
180 branch="$2"
181 branch_hash=$(git rev-parse $branch)
182 match=$(git rev-list -1 $branch_hash ^$newrev)
183
184 if [ -z "$match" ]; then
185 return 0
186 else
187 return 1
188 fi
189}
190
191# if a commit doesn't have a parent, this might not work. But we only want
192# to remove the parent from the rev-list, and since it doesn't exist, it won't
193# be there anyway, so do nothing in that case.
194try_remove_previous()
195{
196 if rev_exists "$1^"; then
197 echo "^$1^"
198 fi
199}
200
201find_latest_squash()
202{
203 debug "Looking for latest squash ($dir)..."
204 dir="$1"
205 sq=
206 main=
207 sub=
208 git log --grep="^git-subtree-dir: $dir/*\$" \
209 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
210 while read a b junk; do
211 debug "$a $b $junk"
212 debug "{{$sq/$main/$sub}}"
213 case "$a" in
214 START) sq="$b" ;;
215 git-subtree-mainline:) main="$b" ;;
216 git-subtree-split:) sub="$b" ;;
217 END)
218 if [ -n "$sub" ]; then
219 if [ -n "$main" ]; then
220 # a rejoin commit?
221 # Pretend its sub was a squash.
222 sq="$sub"
223 fi
224 debug "Squash found: $sq $sub"
225 echo "$sq" "$sub"
226 break
227 fi
228 sq=
229 main=
230 sub=
231 ;;
232 esac
233 done
234}
235
236find_existing_splits()
237{
238 debug "Looking for prior splits..."
239 dir="$1"
240 revs="$2"
241 main=
242 sub=
243 git log --grep="^git-subtree-dir: $dir/*\$" \
244 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
245 while read a b junk; do
246 case "$a" in
247 START) sq="$b" ;;
248 git-subtree-mainline:) main="$b" ;;
249 git-subtree-split:) sub="$b" ;;
250 END)
251 debug " Main is: '$main'"
252 if [ -z "$main" -a -n "$sub" ]; then
253 # squash commits refer to a subtree
254 debug " Squash: $sq from $sub"
255 cache_set "$sq" "$sub"
256 fi
257 if [ -n "$main" -a -n "$sub" ]; then
258 debug " Prior: $main -> $sub"
259 cache_set $main $sub
260 cache_set $sub $sub
261 try_remove_previous "$main"
262 try_remove_previous "$sub"
263 fi
264 main=
265 sub=
266 ;;
267 esac
268 done
269}
270
271copy_commit()
272{
273 # We're going to set some environment vars here, so
274 # do it in a subshell to get rid of them safely later
275 debug copy_commit "{$1}" "{$2}" "{$3}"
276 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
277 (
278 read GIT_AUTHOR_NAME
279 read GIT_AUTHOR_EMAIL
280 read GIT_AUTHOR_DATE
281 read GIT_COMMITTER_NAME
282 read GIT_COMMITTER_EMAIL
283 read GIT_COMMITTER_DATE
284 export GIT_AUTHOR_NAME \
285 GIT_AUTHOR_EMAIL \
286 GIT_AUTHOR_DATE \
287 GIT_COMMITTER_NAME \
288 GIT_COMMITTER_EMAIL \
289 GIT_COMMITTER_DATE
290 (echo -n "$annotate"; cat ) |
291 git commit-tree "$2" $3 # reads the rest of stdin
292 ) || die "Can't copy commit $1"
293}
294
295add_msg()
296{
297 dir="$1"
298 latest_old="$2"
299 latest_new="$3"
300 if [ -n "$message" ]; then
301 commit_message="$message"
302 else
303 commit_message="Add '$dir/' from commit '$latest_new'"
304 fi
305 cat <<-EOF
306 $commit_message
307
308 git-subtree-dir: $dir
309 git-subtree-mainline: $latest_old
310 git-subtree-split: $latest_new
311 EOF
312}
313
314add_squashed_msg()
315{
316 if [ -n "$message" ]; then
317 echo "$message"
318 else
319 echo "Merge commit '$1' as '$2'"
320 fi
321}
322
323rejoin_msg()
324{
325 dir="$1"
326 latest_old="$2"
327 latest_new="$3"
328 if [ -n "$message" ]; then
329 commit_message="$message"
330 else
331 commit_message="Split '$dir/' into commit '$latest_new'"
332 fi
333 cat <<-EOF
334 $commit_message
335
336 git-subtree-dir: $dir
337 git-subtree-mainline: $latest_old
338 git-subtree-split: $latest_new
339 EOF
340}
341
342squash_msg()
343{
344 dir="$1"
345 oldsub="$2"
346 newsub="$3"
347 newsub_short=$(git rev-parse --short "$newsub")
348
349 if [ -n "$oldsub" ]; then
350 oldsub_short=$(git rev-parse --short "$oldsub")
351 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
352 echo
353 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
354 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
355 else
356 echo "Squashed '$dir/' content from commit $newsub_short"
357 fi
358
359 echo
360 echo "git-subtree-dir: $dir"
361 echo "git-subtree-split: $newsub"
362}
363
364toptree_for_commit()
365{
366 commit="$1"
367 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
368}
369
370subtree_for_commit()
371{
372 commit="$1"
373 dir="$2"
374 git ls-tree "$commit" -- "$dir" |
375 while read mode type tree name; do
376 assert [ "$name" = "$dir" ]
377 assert [ "$type" = "tree" ]
378 echo $tree
379 break
380 done
381}
382
383tree_changed()
384{
385 tree=$1
386 shift
387 if [ $# -ne 1 ]; then
388 return 0 # weird parents, consider it changed
389 else
390 ptree=$(toptree_for_commit $1)
391 if [ "$ptree" != "$tree" ]; then
392 return 0 # changed
393 else
394 return 1 # not changed
395 fi
396 fi
397}
398
399new_squash_commit()
400{
401 old="$1"
402 oldsub="$2"
403 newsub="$3"
404 tree=$(toptree_for_commit $newsub) || exit $?
405 if [ -n "$old" ]; then
406 squash_msg "$dir" "$oldsub" "$newsub" |
407 git commit-tree "$tree" -p "$old" || exit $?
408 else
409 squash_msg "$dir" "" "$newsub" |
410 git commit-tree "$tree" || exit $?
411 fi
412}
413
414copy_or_skip()
415{
416 rev="$1"
417 tree="$2"
418 newparents="$3"
419 assert [ -n "$tree" ]
420
421 identical=
422 nonidentical=
423 p=
424 gotparents=
425 for parent in $newparents; do
426 ptree=$(toptree_for_commit $parent) || exit $?
427 [ -z "$ptree" ] && continue
428 if [ "$ptree" = "$tree" ]; then
429 # an identical parent could be used in place of this rev.
430 identical="$parent"
431 else
432 nonidentical="$parent"
433 fi
434
435 # sometimes both old parents map to the same newparent;
436 # eliminate duplicates
437 is_new=1
438 for gp in $gotparents; do
439 if [ "$gp" = "$parent" ]; then
440 is_new=
441 break
442 fi
443 done
444 if [ -n "$is_new" ]; then
445 gotparents="$gotparents $parent"
446 p="$p -p $parent"
447 fi
448 done
449
450 if [ -n "$identical" ]; then
451 echo $identical
452 else
453 copy_commit $rev $tree "$p" || exit $?
454 fi
455}
456
457ensure_clean()
458{
459 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
460 die "Working tree has modifications. Cannot add."
461 fi
462 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
463 die "Index has modifications. Cannot add."
464 fi
465}
466
467cmd_add()
468{
469 if [ -e "$dir" ]; then
470 die "'$dir' already exists. Cannot add."
471 fi
472
473 ensure_clean
474
475 if [ $# -eq 1 ]; then
476 "cmd_add_commit" "$@"
477 elif [ $# -eq 2 ]; then
478 "cmd_add_repository" "$@"
479 else
480 say "error: parameters were '$@'"
481 die "Provide either a refspec or a repository and refspec."
482 fi
483}
484
485cmd_add_repository()
486{
487 echo "git fetch" "$@"
488 repository=$1
489 refspec=$2
490 git fetch "$@" || exit $?
491 revs=FETCH_HEAD
492 set -- $revs
493 cmd_add_commit "$@"
494}
495
496cmd_add_commit()
497{
498 revs=$(git rev-parse $default --revs-only "$@") || exit $?
499 set -- $revs
500 rev="$1"
501
502 debug "Adding $dir as '$rev'..."
503 git read-tree --prefix="$dir" $rev || exit $?
504 git checkout -- "$dir" || exit $?
505 tree=$(git write-tree) || exit $?
506
507 headrev=$(git rev-parse HEAD) || exit $?
508 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
509 headp="-p $headrev"
510 else
511 headp=
512 fi
513
514 if [ -n "$squash" ]; then
515 rev=$(new_squash_commit "" "" "$rev") || exit $?
516 commit=$(add_squashed_msg "$rev" "$dir" |
517 git commit-tree $tree $headp -p "$rev") || exit $?
518 else
519 commit=$(add_msg "$dir" "$headrev" "$rev" |
520 git commit-tree $tree $headp -p "$rev") || exit $?
521 fi
522 git reset "$commit" || exit $?
523
524 say "Added dir '$dir'"
525}
526
527cmd_split()
528{
529 debug "Splitting $dir..."
530 cache_setup || exit $?
531
532 if [ -n "$onto" ]; then
533 debug "Reading history for --onto=$onto..."
534 git rev-list $onto |
535 while read rev; do
536 # the 'onto' history is already just the subdir, so
537 # any parent we find there can be used verbatim
538 debug " cache: $rev"
539 cache_set $rev $rev
540 done
541 fi
542
543 if [ -n "$ignore_joins" ]; then
544 unrevs=
545 else
546 unrevs="$(find_existing_splits "$dir" "$revs")"
547 fi
548
549 # We can't restrict rev-list to only $dir here, because some of our
550 # parents have the $dir contents the root, and those won't match.
551 # (and rev-list --follow doesn't seem to solve this)
552 grl='git rev-list --reverse --parents $revs $unrevs'
553 revmax=$(eval "$grl" | wc -l)
554 revcount=0
555 createcount=0
556 eval "$grl" |
557 while read rev parents; do
558 revcount=$(($revcount + 1))
559 say -n "$revcount/$revmax ($createcount)
"
560 debug "Processing commit: $rev"
561 exists=$(cache_get $rev)
562 if [ -n "$exists" ]; then
563 debug " prior: $exists"
564 continue
565 fi
566 createcount=$(($createcount + 1))
567 debug " parents: $parents"
568 newparents=$(cache_get $parents)
569 debug " newparents: $newparents"
570
571 tree=$(subtree_for_commit $rev "$dir")
572 debug " tree is: $tree"
573
574 # ugly. is there no better way to tell if this is a subtree
575 # vs. a mainline commit? Does it matter?
576 if [ -z $tree ]; then
577 if [ -n "$newparents" ]; then
578 cache_set $rev $rev
579 fi
580 continue
581 fi
582
583 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
584 debug " newrev is: $newrev"
585 cache_set $rev $newrev
586 cache_set latest_new $newrev
587 cache_set latest_old $rev
588 done || exit $?
589 latest_new=$(cache_get latest_new)
590 if [ -z "$latest_new" ]; then
591 die "No new revisions were found"
592 fi
593
594 if [ -n "$rejoin" ]; then
595 debug "Merging split branch into HEAD..."
596 latest_old=$(cache_get latest_old)
597 git merge -s ours \
598 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
599 $latest_new >&2 || exit $?
600 fi
601 if [ -n "$branch" ]; then
602 if rev_exists "refs/heads/$branch"; then
603 if ! rev_is_descendant_of_branch $latest_new $branch; then
604 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
605 fi
606 action='Updated'
607 else
608 action='Created'
609 fi
610 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
611 say "$action branch '$branch'"
612 fi
613 echo $latest_new
614 exit 0
615}
616
617cmd_merge()
618{
619 revs=$(git rev-parse $default --revs-only "$@") || exit $?
620 ensure_clean
621
622 set -- $revs
623 if [ $# -ne 1 ]; then
624 die "You must provide exactly one revision. Got: '$revs'"
625 fi
626 rev="$1"
627
628 if [ -n "$squash" ]; then
629 first_split="$(find_latest_squash "$dir")"
630 if [ -z "$first_split" ]; then
631 die "Can't squash-merge: '$dir' was never added."
632 fi
633 set $first_split
634 old=$1
635 sub=$2
636 if [ "$sub" = "$rev" ]; then
637 say "Subtree is already at commit $rev."
638 exit 0
639 fi
640 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
641 debug "New squash commit: $new"
642 rev="$new"
643 fi
644
645 version=$(git version)
646 if [ "$version" \< "git version 1.7" ]; then
647 if [ -n "$message" ]; then
648 git merge -s subtree --message="$message" $rev
649 else
650 git merge -s subtree $rev
651 fi
652 else
653 if [ -n "$message" ]; then
654 git merge -Xsubtree="$prefix" --message="$message" $rev
655 else
656 git merge -Xsubtree="$prefix" $rev
657 fi
658 fi
659}
660
661cmd_pull()
662{
663 ensure_clean
664 git fetch "$@" || exit $?
665 revs=FETCH_HEAD
666 set -- $revs
667 cmd_merge "$@"
668}
669
670cmd_push()
671{
672 if [ $# -ne 2 ]; then
673 die "You must provide <repository> <refspec>"
674 fi
675 if [ -e "$dir" ]; then
676 repository=$1
677 refspec=$2
678 echo "git push using: " $repository $refspec
679 git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
680 else
681 die "'$dir' must already exist. Try 'git subtree add'."
682 fi
683}
684
685"cmd_$command" "$@"