]> git.ipfire.org Git - thirdparty/git.git/blame - git-subtree.sh
Added new 'push' command and 2-parameter form of 'add'.
[thirdparty/git.git] / git-subtree.sh
CommitLineData
0ca71b37
AP
1#!/bin/bash
2#
3# git-subtree.sh: split/join git repositories in subdirectories of this one
4#
b77172f8 5# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
0ca71b37 6#
96db2c04
AP
7if [ $# -eq 0 ]; then
8 set -- -h
9fi
0ca71b37 10OPTS_SPEC="\
f4f29557 11git subtree add --prefix=<prefix> <commit>
13648af5
AP
12git subtree merge --prefix=<prefix> <commit>
13git subtree pull --prefix=<prefix> <repository> <refspec...>
c00d1d11 14git subtree push --prefix=<prefix> <repository> <refspec...>
f4f29557 15git subtree split --prefix=<prefix> <commit...>
0ca71b37 16--
96db2c04
AP
17h,help show the help
18q quiet
942dce55 19d show debug messages
6e25f79f 20P,prefix= the name of the subdir to split out
2da0969a 21m,message= use the given message as the commit message for the merge commit
13648af5 22 options for 'split'
d0eb1b14 23annotate= add a prefix to commit message of new commits
43a39512
AP
24b,branch= create a new branch from the split subtree
25ignore-joins ignore prior --rejoin commits
96db2c04
AP
26onto= try connecting new tree to an existing one
27rejoin merge the new branch back into HEAD
c00d1d11 28 options for 'add', 'merge', 'pull' and 'push'
8e79043c 29squash merge subtree changes as a single commit
0ca71b37
AP
30"
31eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
33aaa697
AP
32PATH=$(git --exec-path):$PATH
33. git-sh-setup
0ca71b37
AP
34require_work_tree
35
36quiet=
43a39512 37branch=
942dce55 38debug=
0ca71b37 39command=
b77172f8
AP
40onto=
41rejoin=
96db2c04 42ignore_joins=
d0eb1b14 43annotate=
8e79043c 44squash=
2da0969a 45message=
0ca71b37
AP
46
47debug()
942dce55
AP
48{
49 if [ -n "$debug" ]; then
50 echo "$@" >&2
51 fi
52}
53
54say()
0ca71b37
AP
55{
56 if [ -z "$quiet" ]; then
57 echo "$@" >&2
58 fi
59}
60
2573354e
AP
61assert()
62{
63 if "$@"; then
64 :
65 else
66 die "assertion failed: " "$@"
67 fi
68}
69
70
0ca71b37
AP
71#echo "Options: $*"
72
73while [ $# -gt 0 ]; do
74 opt="$1"
75 shift
76 case "$opt" in
77 -q) quiet=1 ;;
942dce55 78 -d) debug=1 ;;
d0eb1b14
AP
79 --annotate) annotate="$1"; shift ;;
80 --no-annotate) annotate= ;;
43a39512 81 -b) branch="$1"; shift ;;
6e25f79f 82 -P) prefix="$1"; shift ;;
2da0969a 83 -m) message="$1"; shift ;;
9a8821ff 84 --no-prefix) prefix= ;;
b77172f8 85 --onto) onto="$1"; shift ;;
96db2c04 86 --no-onto) onto= ;;
b77172f8 87 --rejoin) rejoin=1 ;;
96db2c04
AP
88 --no-rejoin) rejoin= ;;
89 --ignore-joins) ignore_joins=1 ;;
90 --no-ignore-joins) ignore_joins= ;;
8e79043c
AP
91 --squash) squash=1 ;;
92 --no-squash) squash= ;;
0ca71b37 93 --) break ;;
43a39512 94 *) die "Unexpected option: $opt" ;;
0ca71b37
AP
95 esac
96done
97
98command="$1"
99shift
100case "$command" in
13648af5 101 add|merge|pull) default= ;;
c00d1d11 102 split|push) default="--default HEAD" ;;
0ca71b37
AP
103 *) die "Unknown command '$command'" ;;
104esac
105
9a8821ff
AP
106if [ -z "$prefix" ]; then
107 die "You must provide the --prefix option."
108fi
ec54f0d9 109
77ba3058
AP
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
ec54f0d9 116
6f2012cd 117dir="$(dirname "$prefix/.")"
9a8821ff 118
c00d1d11 119if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
13648af5
AP
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
0ca71b37 125fi
0ca71b37
AP
126
127debug "command: {$command}"
128debug "quiet: {$quiet}"
129debug "revs: {$revs}"
130debug "dir: {$dir}"
13648af5 131debug "opts: {$*}"
eb7b590c 132debug
0ca71b37
AP
133
134cache_setup()
135{
2573354e 136 cachedir="$GIT_DIR/subtree-cache/$$"
0ca71b37
AP
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
0ca71b37
AP
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"
b77172f8
AP
156 if [ "$oldrev" != "latest_old" \
157 -a "$oldrev" != "latest_new" \
158 -a -e "$cachedir/$oldrev" ]; then
0ca71b37
AP
159 die "cache for $oldrev already exists!"
160 fi
161 echo "$newrev" >"$cachedir/$oldrev"
162}
163
43a39512
AP
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
0a562948
JS
173rev_is_descendant_of_branch()
174{
175 newrev="$1"
176 branch="$2"
177 branch_hash=$(git rev-parse $branch)
6bd910a8 178 match=$(git rev-list -1 $branch_hash ^$newrev)
0a562948 179
6bd910a8 180 if [ -z "$match" ]; then
0a562948
JS
181 return 0
182 else
183 return 1
184 fi
185}
186
b9de5353
AP
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{
43a39512 192 if rev_exists "$1^"; then
b9de5353
AP
193 echo "^$1^"
194 fi
195}
196
1cc2cfff
AP
197find_latest_squash()
198{
d713e2d8 199 debug "Looking for latest squash ($dir)..."
1cc2cfff 200 dir="$1"
d713e2d8
AP
201 sq=
202 main=
203 sub=
6f2012cd 204 git log --grep="^git-subtree-dir: $dir/*\$" \
1cc2cfff
AP
205 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
206 while read a b junk; do
d713e2d8
AP
207 debug "$a $b $junk"
208 debug "{{$sq/$main/$sub}}"
1cc2cfff
AP
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
8b4a77f2
AP
232find_existing_splits()
233{
234 debug "Looking for prior splits..."
235 dir="$1"
236 revs="$2"
d713e2d8
AP
237 main=
238 sub=
6f2012cd 239 git log --grep="^git-subtree-dir: $dir/*\$" \
1a8c36dc 240 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
8b4a77f2
AP
241 while read a b junk; do
242 case "$a" in
2275f707 243 START) sq="$b" ;;
8b4a77f2
AP
244 git-subtree-mainline:) main="$b" ;;
245 git-subtree-split:) sub="$b" ;;
7ee9eef3 246 END)
2275f707 247 debug " Main is: '$main'"
1a8c36dc
AP
248 if [ -z "$main" -a -n "$sub" ]; then
249 # squash commits refer to a subtree
2275f707 250 debug " Squash: $sq from $sub"
1a8c36dc
AP
251 cache_set "$sq" "$sub"
252 fi
8b4a77f2
AP
253 if [ -n "$main" -a -n "$sub" ]; then
254 debug " Prior: $main -> $sub"
255 cache_set $main $sub
b9de5353
AP
256 try_remove_previous "$main"
257 try_remove_previous "$sub"
8b4a77f2 258 fi
7ee9eef3
AP
259 main=
260 sub=
8b4a77f2
AP
261 ;;
262 esac
263 done
264}
265
fd9500ee
AP
266copy_commit()
267{
f96bc790 268 # We're going to set some environment vars here, so
fd9500ee 269 # do it in a subshell to get rid of them safely later
a64f3a72 270 debug copy_commit "{$1}" "{$2}" "{$3}"
fd9500ee
AP
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
b77172f8
AP
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
d0eb1b14 285 (echo -n "$annotate"; cat ) |
fd9500ee
AP
286 git commit-tree "$2" $3 # reads the rest of stdin
287 ) || die "Can't copy commit $1"
288}
289
eb7b590c
AP
290add_msg()
291{
292 dir="$1"
293 latest_old="$2"
294 latest_new="$3"
2da0969a
JS
295 if [ -n "$message" ]; then
296 commit_message="$message"
297 else
298 commit_message="Add '$dir/' from commit '$latest_new'"
299 fi
eb7b590c 300 cat <<-EOF
2da0969a 301 $commit_message
eb7b590c
AP
302
303 git-subtree-dir: $dir
304 git-subtree-mainline: $latest_old
305 git-subtree-split: $latest_new
306 EOF
307}
308
2da0969a
JS
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
7ee9eef3 318rejoin_msg()
b77172f8
AP
319{
320 dir="$1"
321 latest_old="$2"
322 latest_new="$3"
2da0969a
JS
323 if [ -n "$message" ]; then
324 commit_message="$message"
325 else
326 commit_message="Split '$dir/' into commit '$latest_new'"
327 fi
b77172f8 328 cat <<-EOF
12629161 329 $commit_message
b77172f8
AP
330
331 git-subtree-dir: $dir
8b4a77f2
AP
332 git-subtree-mainline: $latest_old
333 git-subtree-split: $latest_new
b77172f8
AP
334 EOF
335}
336
1cc2cfff
AP
337squash_msg()
338{
339 dir="$1"
340 oldsub="$2"
341 newsub="$3"
1cc2cfff 342 newsub_short=$(git rev-parse --short "$newsub")
1cc2cfff 343
d713e2d8
AP
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
1cc2cfff 353
d713e2d8
AP
354 echo
355 echo "git-subtree-dir: $dir"
356 echo "git-subtree-split: $newsub"
1cc2cfff
AP
357}
358
210d0839 359toptree_for_commit()
768d6d10 360{
210d0839
AP
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" |
768d6d10
AP
370 while read mode type tree name; do
371 assert [ "$name" = "$dir" ]
8ac5eca1 372 assert [ "$type" = "tree" ]
768d6d10
AP
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
210d0839 385 ptree=$(toptree_for_commit $1)
768d6d10
AP
386 if [ "$ptree" != "$tree" ]; then
387 return 0 # changed
388 else
389 return 1 # not changed
390 fi
391 fi
392}
393
1cc2cfff
AP
394new_squash_commit()
395{
396 old="$1"
397 oldsub="$2"
398 newsub="$3"
399 tree=$(toptree_for_commit $newsub) || exit $?
d713e2d8
AP
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
1cc2cfff
AP
407}
408
d6912658
AP
409copy_or_skip()
410{
411 rev="$1"
412 tree="$2"
413 newparents="$3"
414 assert [ -n "$tree" ]
415
96db2c04 416 identical=
49cf8228 417 nonidentical=
96db2c04 418 p=
a64f3a72 419 gotparents=
d6912658
AP
420 for parent in $newparents; do
421 ptree=$(toptree_for_commit $parent) || exit $?
a64f3a72 422 [ -z "$ptree" ] && continue
d6912658 423 if [ "$ptree" = "$tree" ]; then
96db2c04
AP
424 # an identical parent could be used in place of this rev.
425 identical="$parent"
49cf8228
AP
426 else
427 nonidentical="$parent"
96db2c04 428 fi
a64f3a72
AP
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"
d6912658
AP
441 p="$p -p $parent"
442 fi
443 done
444
795e730e 445 if [ -n "$identical" ]; then
96db2c04
AP
446 echo $identical
447 else
448 copy_commit $rev $tree "$p" || exit $?
449 fi
d6912658
AP
450}
451
13648af5 452ensure_clean()
eb7b590c 453{
c00d1d11 454 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
eb7b590c
AP
455 die "Working tree has modifications. Cannot add."
456 fi
c00d1d11 457 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
eb7b590c
AP
458 die "Index has modifications. Cannot add."
459 fi
13648af5
AP
460}
461
462cmd_add()
463{
464 if [ -e "$dir" ]; then
465 die "'$dir' already exists. Cannot add."
466 fi
c00d1d11 467
13648af5
AP
468 ensure_clean
469
c00d1d11
WW
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."
eb7b590c 477 fi
c00d1d11
WW
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
eb7b590c
AP
495 rev="$1"
496
497 debug "Adding $dir as '$rev'..."
498 git read-tree --prefix="$dir" $rev || exit $?
227f7811 499 git checkout -- "$dir" || exit $?
eb7b590c
AP
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
d713e2d8
AP
508
509 if [ -n "$squash" ]; then
510 rev=$(new_squash_commit "" "" "$rev") || exit $?
2da0969a 511 commit=$(add_squashed_msg "$rev" "$dir" |
d713e2d8
AP
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
eb7b590c 517 git reset "$commit" || exit $?
d713e2d8
AP
518
519 say "Added dir '$dir'"
eb7b590c
AP
520}
521
0ca71b37
AP
522cmd_split()
523{
524 debug "Splitting $dir..."
525 cache_setup || exit $?
526
33ff583a 527 if [ -n "$onto" ]; then
847e8681 528 debug "Reading history for --onto=$onto..."
33ff583a
AP
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
2c71b7c4 533 debug " cache: $rev"
33ff583a
AP
534 cache_set $rev $rev
535 done
536 fi
537
96db2c04
AP
538 if [ -n "$ignore_joins" ]; then
539 unrevs=
540 else
541 unrevs="$(find_existing_splits "$dir" "$revs")"
542 fi
8b4a77f2 543
1f73862f
AP
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)
942dce55
AP
547 grl='git rev-list --reverse --parents $revs $unrevs'
548 revmax=$(eval "$grl" | wc -l)
549 revcount=0
550 createcount=0
551 eval "$grl" |
0ca71b37 552 while read rev parents; do
942dce55 553 revcount=$(($revcount + 1))
e2d0a450 554 say -n "$revcount/$revmax ($createcount)\r"
2c71b7c4
AP
555 debug "Processing commit: $rev"
556 exists=$(cache_get $rev)
8b4a77f2
AP
557 if [ -n "$exists" ]; then
558 debug " prior: $exists"
559 continue
560 fi
942dce55 561 createcount=$(($createcount + 1))
2c71b7c4
AP
562 debug " parents: $parents"
563 newparents=$(cache_get $parents)
564 debug " newparents: $newparents"
8b4a77f2 565
210d0839 566 tree=$(subtree_for_commit $rev "$dir")
768d6d10 567 debug " tree is: $tree"
7ee9eef3
AP
568
569 # ugly. is there no better way to tell if this is a subtree
570 # vs. a mainline commit? Does it matter?
da949cc5
JS
571 if [ -z $tree ]; then
572 cache_set $rev $rev
573 continue
574 fi
768d6d10 575
d6912658 576 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
768d6d10
AP
577 debug " newrev is: $newrev"
578 cache_set $rev $newrev
579 cache_set latest_new $newrev
580 cache_set latest_old $rev
2573354e 581 done || exit $?
b77172f8
AP
582 latest_new=$(cache_get latest_new)
583 if [ -z "$latest_new" ]; then
e25a6bf8
AP
584 die "No new revisions were found"
585 fi
b77172f8
AP
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 \
7ee9eef3 591 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
ea28d674 592 $latest_new >&2 || exit $?
b77172f8 593 fi
43a39512 594 if [ -n "$branch" ]; then
0a562948
JS
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'"
43a39512 605 fi
b77172f8 606 echo $latest_new
0ca71b37
AP
607 exit 0
608}
609
610cmd_merge()
611{
c00d1d11 612 revs=$(git rev-parse $default --revs-only "$@") || exit $?
13648af5
AP
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
1cc2cfff
AP
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
eb4fb910
AP
629 if [ "$sub" = "$rev" ]; then
630 say "Subtree is already at commit $rev."
631 exit 0
632 fi
1cc2cfff
AP
633 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
634 debug "New squash commit: $new"
635 rev="$new"
636 fi
637
349a70d5
AP
638 if [ -n "$message" ]; then
639 git merge -s subtree --message="$message" $rev
640 else
641 git merge -s subtree $rev
642 fi
13648af5
AP
643}
644
645cmd_pull()
646{
647 ensure_clean
e31d1e2f
AP
648 git fetch "$@" || exit $?
649 revs=FETCH_HEAD
c00d1d11
WW
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
0ca71b37
AP
667}
668
13648af5 669"cmd_$command" "$@"