]> Tony Duckles's Git Repositories (git.nynim.org) - dotfiles.git/blob - bin/git-subtree
.term_colorpalette: Tweak 'blue' and 'white' colors
[dotfiles.git] / bin / git-subtree
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 #
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...>
16 --
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
30 "
31 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
32
33 PATH=$PATH:$(git --exec-path)
34 . git-sh-setup
35
36 require_work_tree
37
38 quiet=
39 branch=
40 debug=
41 command=
42 onto=
43 rejoin=
44 ignore_joins=
45 annotate=
46 squash=
47 message=
48
49 debug()
50 {
51 if [ -n "$debug" ]; then
52 echo "$@" >&2
53 fi
54 }
55
56 say()
57 {
58 if [ -z "$quiet" ]; then
59 echo "$@" >&2
60 fi
61 }
62
63 assert()
64 {
65 if "$@"; then
66 :
67 else
68 die "assertion failed: " "$@"
69 fi
70 }
71
72
73 #echo "Options: $*"
74
75 while [ $# -gt 0 ]; do
76 opt="$1"
77 shift
78 case "$opt" in
79 -q) quiet=1 ;;
80 -d) debug=1 ;;
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 ;;
88 --no-onto) onto= ;;
89 --rejoin) rejoin=1 ;;
90 --no-rejoin) rejoin= ;;
91 --ignore-joins) ignore_joins=1 ;;
92 --no-ignore-joins) ignore_joins= ;;
93 --squash) squash=1 ;;
94 --no-squash) squash= ;;
95 --) break ;;
96 *) die "Unexpected option: $opt" ;;
97 esac
98 done
99
100 command="$1"
101 shift
102 case "$command" in
103 add|merge|pull) default= ;;
104 split|push) default="--default HEAD" ;;
105 *) die "Unknown command '$command'" ;;
106 esac
107
108 if [ -z "$prefix" ]; then
109 die "You must provide the --prefix option."
110 fi
111
112 case "$command" in
113 add) [ -e "$prefix" ] &&
114 die "prefix '$prefix' already exists." ;;
115 *) [ -e "$prefix" ] ||
116 die "'$prefix' does not exist; use 'git subtree add'" ;;
117 esac
118
119 dir="$(dirname "$prefix/.")"
120
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."
126 fi
127 fi
128
129 debug "command: {$command}"
130 debug "quiet: {$quiet}"
131 debug "revs: {$revs}"
132 debug "dir: {$dir}"
133 debug "opts: {$*}"
134 debug
135
136 cache_setup()
137 {
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
142 }
143
144 cache_get()
145 {
146 for oldrev in $*; do
147 if [ -r "$cachedir/$oldrev" ]; then
148 read newrev <"$cachedir/$oldrev"
149 echo $newrev
150 fi
151 done
152 }
153
154 cache_set()
155 {
156 oldrev="$1"
157 newrev="$2"
158 if [ "$oldrev" != "latest_old" \
159 -a "$oldrev" != "latest_new" \
160 -a -e "$cachedir/$oldrev" ]; then
161 die "cache for $oldrev already exists!"
162 fi
163 echo "$newrev" >"$cachedir/$oldrev"
164 }
165
166 rev_exists()
167 {
168 if git rev-parse "$1" >/dev/null 2>&1; then
169 return 0
170 else
171 return 1
172 fi
173 }
174
175 rev_is_descendant_of_branch()
176 {
177 newrev="$1"
178 branch="$2"
179 branch_hash=$(git rev-parse $branch)
180 match=$(git rev-list -1 $branch_hash ^$newrev)
181
182 if [ -z "$match" ]; then
183 return 0
184 else
185 return 1
186 fi
187 }
188
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()
193 {
194 if rev_exists "$1^"; then
195 echo "^$1^"
196 fi
197 }
198
199 find_latest_squash()
200 {
201 debug "Looking for latest squash ($dir)..."
202 dir="$1"
203 sq=
204 main=
205 sub=
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
209 debug "$a $b $junk"
210 debug "{{$sq/$main/$sub}}"
211 case "$a" in
212 START) sq="$b" ;;
213 git-subtree-mainline:) main="$b" ;;
214 git-subtree-split:) sub="$b" ;;
215 END)
216 if [ -n "$sub" ]; then
217 if [ -n "$main" ]; then
218 # a rejoin commit?
219 # Pretend its sub was a squash.
220 sq="$sub"
221 fi
222 debug "Squash found: $sq $sub"
223 echo "$sq" "$sub"
224 break
225 fi
226 sq=
227 main=
228 sub=
229 ;;
230 esac
231 done
232 }
233
234 find_existing_splits()
235 {
236 debug "Looking for prior splits..."
237 dir="$1"
238 revs="$2"
239 main=
240 sub=
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
244 case "$a" in
245 START) sq="$b" ;;
246 git-subtree-mainline:) main="$b" ;;
247 git-subtree-split:) sub="$b" ;;
248 END)
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"
254 fi
255 if [ -n "$main" -a -n "$sub" ]; then
256 debug " Prior: $main -> $sub"
257 cache_set $main $sub
258 cache_set $sub $sub
259 try_remove_previous "$main"
260 try_remove_previous "$sub"
261 fi
262 main=
263 sub=
264 ;;
265 esac
266 done
267 }
268
269 copy_commit()
270 {
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" |
275 (
276 read GIT_AUTHOR_NAME
277 read GIT_AUTHOR_EMAIL
278 read GIT_AUTHOR_DATE
279 read GIT_COMMITTER_NAME
280 read GIT_COMMITTER_EMAIL
281 read GIT_COMMITTER_DATE
282 export GIT_AUTHOR_NAME \
283 GIT_AUTHOR_EMAIL \
284 GIT_AUTHOR_DATE \
285 GIT_COMMITTER_NAME \
286 GIT_COMMITTER_EMAIL \
287 GIT_COMMITTER_DATE
288 (echo -n "$annotate"; cat ) |
289 git commit-tree "$2" $3 # reads the rest of stdin
290 ) || die "Can't copy commit $1"
291 }
292
293 add_msg()
294 {
295 dir="$1"
296 latest_old="$2"
297 latest_new="$3"
298 if [ -n "$message" ]; then
299 commit_message="$message"
300 else
301 commit_message="Add '$dir/' from commit '$latest_new'"
302 fi
303 cat <<-EOF
304 $commit_message
305
306 git-subtree-dir: $dir
307 git-subtree-mainline: $latest_old
308 git-subtree-split: $latest_new
309 EOF
310 }
311
312 add_squashed_msg()
313 {
314 if [ -n "$message" ]; then
315 echo "$message"
316 else
317 echo "Merge commit '$1' as '$2'"
318 fi
319 }
320
321 rejoin_msg()
322 {
323 dir="$1"
324 latest_old="$2"
325 latest_new="$3"
326 if [ -n "$message" ]; then
327 commit_message="$message"
328 else
329 commit_message="Split '$dir/' into commit '$latest_new'"
330 fi
331 cat <<-EOF
332 $commit_message
333
334 git-subtree-dir: $dir
335 git-subtree-mainline: $latest_old
336 git-subtree-split: $latest_new
337 EOF
338 }
339
340 squash_msg()
341 {
342 dir="$1"
343 oldsub="$2"
344 newsub="$3"
345 newsub_short=$(git rev-parse --short "$newsub")
346
347 if [ -n "$oldsub" ]; then
348 oldsub_short=$(git rev-parse --short "$oldsub")
349 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
350 echo
351 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
352 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
353 else
354 echo "Squashed '$dir/' content from commit $newsub_short"
355 fi
356
357 echo
358 echo "git-subtree-dir: $dir"
359 echo "git-subtree-split: $newsub"
360 }
361
362 toptree_for_commit()
363 {
364 commit="$1"
365 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
366 }
367
368 subtree_for_commit()
369 {
370 commit="$1"
371 dir="$2"
372 git ls-tree "$commit" -- "$dir" |
373 while read mode type tree name; do
374 assert [ "$name" = "$dir" ]
375 assert [ "$type" = "tree" ]
376 echo $tree
377 break
378 done
379 }
380
381 tree_changed()
382 {
383 tree=$1
384 shift
385 if [ $# -ne 1 ]; then
386 return 0 # weird parents, consider it changed
387 else
388 ptree=$(toptree_for_commit $1)
389 if [ "$ptree" != "$tree" ]; then
390 return 0 # changed
391 else
392 return 1 # not changed
393 fi
394 fi
395 }
396
397 new_squash_commit()
398 {
399 old="$1"
400 oldsub="$2"
401 newsub="$3"
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 $?
406 else
407 squash_msg "$dir" "" "$newsub" |
408 git commit-tree "$tree" || exit $?
409 fi
410 }
411
412 copy_or_skip()
413 {
414 rev="$1"
415 tree="$2"
416 newparents="$3"
417 assert [ -n "$tree" ]
418
419 identical=
420 nonidentical=
421 p=
422 gotparents=
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.
428 identical="$parent"
429 else
430 nonidentical="$parent"
431 fi
432
433 # sometimes both old parents map to the same newparent;
434 # eliminate duplicates
435 is_new=1
436 for gp in $gotparents; do
437 if [ "$gp" = "$parent" ]; then
438 is_new=
439 break
440 fi
441 done
442 if [ -n "$is_new" ]; then
443 gotparents="$gotparents $parent"
444 p="$p -p $parent"
445 fi
446 done
447
448 if [ -n "$identical" ]; then
449 echo $identical
450 else
451 copy_commit $rev $tree "$p" || exit $?
452 fi
453 }
454
455 ensure_clean()
456 {
457 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
458 die "Working tree has modifications. Cannot add."
459 fi
460 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
461 die "Index has modifications. Cannot add."
462 fi
463 }
464
465 cmd_add()
466 {
467 if [ -e "$dir" ]; then
468 die "'$dir' already exists. Cannot add."
469 fi
470
471 ensure_clean
472
473 if [ $# -eq 1 ]; then
474 "cmd_add_commit" "$@"
475 elif [ $# -eq 2 ]; then
476 "cmd_add_repository" "$@"
477 else
478 say "error: parameters were '$@'"
479 die "Provide either a refspec or a repository and refspec."
480 fi
481 }
482
483 cmd_add_repository()
484 {
485 echo "git fetch" "$@"
486 repository=$1
487 refspec=$2
488 git fetch "$@" || exit $?
489 revs=FETCH_HEAD
490 set -- $revs
491 cmd_add_commit "$@"
492 }
493
494 cmd_add_commit()
495 {
496 revs=$(git rev-parse $default --revs-only "$@") || exit $?
497 set -- $revs
498 rev="$1"
499
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 $?
504
505 headrev=$(git rev-parse HEAD) || exit $?
506 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
507 headp="-p $headrev"
508 else
509 headp=
510 fi
511
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 $?
516 else
517 commit=$(add_msg "$dir" "$headrev" "$rev" |
518 git commit-tree $tree $headp -p "$rev") || exit $?
519 fi
520 git reset "$commit" || exit $?
521
522 say "Added dir '$dir'"
523 }
524
525 cmd_split()
526 {
527 debug "Splitting $dir..."
528 cache_setup || exit $?
529
530 if [ -n "$onto" ]; then
531 debug "Reading history for --onto=$onto..."
532 git rev-list $onto |
533 while read rev; do
534 # the 'onto' history is already just the subdir, so
535 # any parent we find there can be used verbatim
536 debug " cache: $rev"
537 cache_set $rev $rev
538 done
539 fi
540
541 if [ -n "$ignore_joins" ]; then
542 unrevs=
543 else
544 unrevs="$(find_existing_splits "$dir" "$revs")"
545 fi
546
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)
552 revcount=0
553 createcount=0
554 eval "$grl" |
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"
562 continue
563 fi
564 createcount=$(($createcount + 1))
565 debug " parents: $parents"
566 newparents=$(cache_get $parents)
567 debug " newparents: $newparents"
568
569 tree=$(subtree_for_commit $rev "$dir")
570 debug " tree is: $tree"
571
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
576 cache_set $rev $rev
577 fi
578 continue
579 fi
580
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
586 done || exit $?
587 latest_new=$(cache_get latest_new)
588 if [ -z "$latest_new" ]; then
589 die "No new revisions were found"
590 fi
591
592 if [ -n "$rejoin" ]; then
593 debug "Merging split branch into HEAD..."
594 latest_old=$(cache_get latest_old)
595 git merge -s ours \
596 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
597 $latest_new >&2 || exit $?
598 fi
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'."
603 fi
604 action='Updated'
605 else
606 action='Created'
607 fi
608 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
609 say "$action branch '$branch'"
610 fi
611 echo $latest_new
612 exit 0
613 }
614
615 cmd_merge()
616 {
617 revs=$(git rev-parse $default --revs-only "$@") || exit $?
618 ensure_clean
619
620 set -- $revs
621 if [ $# -ne 1 ]; then
622 die "You must provide exactly one revision. Got: '$revs'"
623 fi
624 rev="$1"
625
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."
630 fi
631 set $first_split
632 old=$1
633 sub=$2
634 if [ "$sub" = "$rev" ]; then
635 say "Subtree is already at commit $rev."
636 exit 0
637 fi
638 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
639 debug "New squash commit: $new"
640 rev="$new"
641 fi
642
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
647 else
648 git merge -s subtree $rev
649 fi
650 else
651 if [ -n "$message" ]; then
652 git merge -Xsubtree="$prefix" --message="$message" $rev
653 else
654 git merge -Xsubtree="$prefix" $rev
655 fi
656 fi
657 }
658
659 cmd_pull()
660 {
661 ensure_clean
662 git fetch "$@" || exit $?
663 revs=FETCH_HEAD
664 set -- $revs
665 cmd_merge "$@"
666 }
667
668 cmd_push()
669 {
670 if [ $# -ne 2 ]; then
671 die "You must provide <repository> <refspec>"
672 fi
673 if [ -e "$dir" ]; then
674 repository=$1
675 refspec=$2
676 echo "git push using: " $repository $refspec
677 git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
678 else
679 die "'$dir' must already exist. Try 'git subtree add'."
680 fi
681 }
682
683 "cmd_$command" "$@"