501c6dc2f1a8e8a646f535074c8e95f7bc445439
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 $?)
32PATH=$(git --exec-path):$PATH
33. git-sh-setup
34require_work_tree
35
36quiet=
37branch=
38debug=
39command=
40onto=
41rejoin=
42ignore_joins=
43annotate=
44squash=
45message=
46
47debug()
48{
49 if [ -n "$debug" ]; then
50 echo "$@" >&2
51 fi
52}
53
54say()
55{
56 if [ -z "$quiet" ]; then
57 echo "$@" >&2
58 fi
59}
60
61assert()
62{
63 if "$@"; then
64 :
65 else
66 die "assertion failed: " "$@"
67 fi
68}
69
70
71#echo "Options: $*"
72
73while [ $# -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
96done
97
98command="$1"
99shift
100case "$command" in
101 add|merge|pull) default= ;;
102 split|push) default="--default HEAD" ;;
103 *) die "Unknown command '$command'" ;;
104esac
105
106if [ -z "$prefix" ]; then
107 die "You must provide the --prefix option."
108fi
109
110case "$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'" ;;
115esac
116
117dir="$(dirname "$prefix/.")"
118
119if [ "$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."
124 fi
125fi
126
127debug "command: {$command}"
128debug "quiet: {$quiet}"
129debug "revs: {$revs}"
130debug "dir: {$dir}"
131debug "opts: {$*}"
132debug
133
134cache_setup()
135{
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
140}
141
142cache_get()
143{
144 for oldrev in $*; do
145 if [ -r "$cachedir/$oldrev" ]; then
146 read newrev <"$cachedir/$oldrev"
147 echo $newrev
148 fi
149 done
150}
151
152cache_set()
153{
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!"
160 fi
161 echo "$newrev" >"$cachedir/$oldrev"
162}
163
164rev_exists()
165{
166 if git rev-parse "$1" >/dev/null 2>&1; then
167 return 0
168 else
169 return 1
170 fi
171}
172
173rev_is_descendant_of_branch()
174{
175 newrev="$1"
176 branch="$2"
177 branch_hash=$(git rev-parse $branch)
178 match=$(git rev-list -1 $branch_hash ^$newrev)
179
180 if [ -z "$match" ]; then
181 return 0
182 else
183 return 1
184 fi
185}
186
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.
190try_remove_previous()
191{
192 if rev_exists "$1^"; then
193 echo "^$1^"
194 fi
195}
196
197find_latest_squash()
198{
199 debug "Looking for latest squash ($dir)..."
200 dir="$1"
201 sq=
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"
219 fi
220 debug "Squash found: $sq $sub"
221 echo "$sq" "$sub"
222 break
223 fi
224 sq=
225 main=
226 sub=
227 ;;
228 esac
229 done
230}
231
232find_existing_splits()
233{
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"
252 fi
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"
258 fi
259 main=
260 sub=
261 ;;
262 esac
263 done
264}
265
266copy_commit()
267{
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" |
272 (
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"
288}
289
290add_msg()
291{
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'"
299 fi
300 cat <<-EOF
301 $commit_message
302
303 git-subtree-dir: $dir
304 git-subtree-mainline: $latest_old
305 git-subtree-split: $latest_new
306 EOF
307}
308
309add_squashed_msg()
310{
311 if [ -n "$message" ]; then
312 echo "$message"
313 else
314 echo "Merge commit '$1' as '$2'"
315 fi
316}
317
318rejoin_msg()
319{
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'"
327 fi
328 cat <<-EOF
329 $commit_message
330
331 git-subtree-dir: $dir
332 git-subtree-mainline: $latest_old
333 git-subtree-split: $latest_new
334 EOF
335}
336
337squash_msg()
338{
339 dir="$1"
340 oldsub="$2"
341 newsub="$3"
342 newsub_short=$(git rev-parse --short "$newsub")
343
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"
352 fi
353
354 echo
355 echo "git-subtree-dir: $dir"
356 echo "git-subtree-split: $newsub"
357}
358
359toptree_for_commit()
360{
361 commit="$1"
362 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
363}
364
365subtree_for_commit()
366{
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
376}
377
378tree_changed()
379{
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
390 fi
391 fi
392}
393
394new_squash_commit()
395{
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 $?
406 fi
407}
408
409copy_or_skip()
410{
411 rev="$1"
412 tree="$2"
413 newparents="$3"
414 assert [ -n "$tree" ]
415
416 identical=
417 nonidentical=
418 p=
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"
428 fi
429
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
437 fi
438 done
439 if [ -n "$is_new" ]; then
440 gotparents="$gotparents $parent"
441 p="$p -p $parent"
442 fi
443 done
444
445 if [ -n "$identical" ]; then
446 echo $identical
447 else
448 copy_commit $rev $tree "$p" || exit $?
449 fi
450}
451
452ensure_clean()
453{
454 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
455 die "Working tree has modifications. Cannot add."
456 fi
457 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
458 die "Index has modifications. Cannot add."
459 fi
460}
461
462cmd_add()
463{
464 if [ -e "$dir" ]; then
465 die "'$dir' already exists. Cannot add."
466 fi
467
468 ensure_clean
469
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."
477 fi
478}
479
480cmd_add_repository()
481{
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 "$@"
489}
490
491cmd_add_commit()
492{
493 revs=$(git rev-parse $default --revs-only "$@") || exit $?
494 set -- $revs
495 rev="$1"
496
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 $?
501
502 headrev=$(git rev-parse HEAD) || exit $?
503 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
504 headp="-p $headrev"
505 else
506 headp=
507 fi
508
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 $?
516 fi
517 git reset "$commit" || exit $?
518
519 say "Added dir '$dir'"
520}
521
522cmd_split()
523{
524 debug "Splitting $dir..."
525 cache_setup || exit $?
526
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
536 fi
537
538 if [ -n "$ignore_joins" ]; then
539 unrevs=
540 else
541 unrevs="$(find_existing_splits "$dir" "$revs")"
542 fi
543
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
560 fi
561 createcount=$(($createcount + 1))
562 debug " parents: $parents"
563 newparents=$(cache_get $parents)
564 debug " newparents: $newparents"
565
566 tree=$(subtree_for_commit $rev "$dir")
567 debug " tree is: $tree"
568
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
574 fi
575
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"
585 fi
586
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 $?
593 fi
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'."
598 fi
599 action='Updated'
600 else
601 action='Created'
602 fi
603 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
604 say "$action branch '$branch'"
605 fi
606 echo $latest_new
607 exit 0
608}
609
610cmd_merge()
611{
612 revs=$(git rev-parse $default --revs-only "$@") || exit $?
613 ensure_clean
614
615 set -- $revs
616 if [ $# -ne 1 ]; then
617 die "You must provide exactly one revision. Got: '$revs'"
618 fi
619 rev="$1"
620
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."
625 fi
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
632 fi
633 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
634 debug "New squash commit: $new"
635 rev="$new"
636 fi
637
638 if [ -n "$message" ]; then
639 git merge -s subtree --message="$message" $rev
640 else
641 git merge -s subtree $rev
642 fi
643}
644
645cmd_pull()
646{
647 ensure_clean
648 git fetch "$@" || exit $?
649 revs=FETCH_HEAD
650 set -- $revs
651 cmd_merge "$@"
652}
653
654cmd_push()
655{
656 if [ $# -ne 2 ]; then
657 die "You must provide <repository> <refspec>"
658 fi
659 if [ -e "$dir" ]; then
660 repository=$1
661 refspec=$2
662 echo "git push using: " $repository $refspec
663 git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
664 else
665 die "'$dir' must already exist. Try 'git subtree add'."
666 fi
667}
668
669"cmd_$command" "$@"