Merge branch 'km/gc-eperm' into maint
[alt-git.git] / contrib / subtree / git-subtree.sh
blob7d7af03274ee0759c9e915a6ea7b52ecce65cf55
1 #!/bin/sh
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 add --prefix=<prefix> <repository> <commit>
13 git subtree merge --prefix=<prefix> <commit>
14 git subtree pull --prefix=<prefix> <repository> <refspec...>
15 git subtree push --prefix=<prefix> <repository> <refspec...>
16 git subtree split --prefix=<prefix> <commit...>
18 h,help show the help
19 q quiet
20 d show debug messages
21 P,prefix= the name of the subdir to split out
22 m,message= use the given message as the commit message for the merge commit
23 options for 'split'
24 annotate= add a prefix to commit message of new commits
25 b,branch= create a new branch from the split subtree
26 ignore-joins ignore prior --rejoin commits
27 onto= try connecting new tree to an existing one
28 rejoin merge the new branch back into HEAD
29 options for 'add', 'merge', 'pull' and 'push'
30 squash merge subtree changes as a single commit
32 eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
34 PATH=$PATH:$(git --exec-path)
35 . git-sh-setup
37 require_work_tree
39 quiet=
40 branch=
41 debug=
42 command=
43 onto=
44 rejoin=
45 ignore_joins=
46 annotate=
47 squash=
48 message=
50 debug()
52 if [ -n "$debug" ]; then
53 echo "$@" >&2
57 say()
59 if [ -z "$quiet" ]; then
60 echo "$@" >&2
64 assert()
66 if "$@"; then
68 else
69 die "assertion failed: " "$@"
74 #echo "Options: $*"
76 while [ $# -gt 0 ]; do
77 opt="$1"
78 shift
79 case "$opt" in
80 -q) quiet=1 ;;
81 -d) debug=1 ;;
82 --annotate) annotate="$1"; shift ;;
83 --no-annotate) annotate= ;;
84 -b) branch="$1"; shift ;;
85 -P) prefix="$1"; shift ;;
86 -m) message="$1"; shift ;;
87 --no-prefix) prefix= ;;
88 --onto) onto="$1"; shift ;;
89 --no-onto) onto= ;;
90 --rejoin) rejoin=1 ;;
91 --no-rejoin) rejoin= ;;
92 --ignore-joins) ignore_joins=1 ;;
93 --no-ignore-joins) ignore_joins= ;;
94 --squash) squash=1 ;;
95 --no-squash) squash= ;;
96 --) break ;;
97 *) die "Unexpected option: $opt" ;;
98 esac
99 done
101 command="$1"
102 shift
103 case "$command" in
104 add|merge|pull) default= ;;
105 split|push) default="--default HEAD" ;;
106 *) die "Unknown command '$command'" ;;
107 esac
109 if [ -z "$prefix" ]; then
110 die "You must provide the --prefix option."
113 case "$command" in
114 add) [ -e "$prefix" ] &&
115 die "prefix '$prefix' already exists." ;;
116 *) [ -e "$prefix" ] ||
117 die "'$prefix' does not exist; use 'git subtree add'" ;;
118 esac
120 dir="$(dirname "$prefix/.")"
122 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
123 revs=$(git rev-parse $default --revs-only "$@") || exit $?
124 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
125 if [ -n "$dirs" ]; then
126 die "Error: Use --prefix instead of bare filenames."
130 debug "command: {$command}"
131 debug "quiet: {$quiet}"
132 debug "revs: {$revs}"
133 debug "dir: {$dir}"
134 debug "opts: {$*}"
135 debug
137 cache_setup()
139 cachedir="$GIT_DIR/subtree-cache/$$"
140 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
141 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
142 mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
143 debug "Using cachedir: $cachedir" >&2
146 cache_get()
148 for oldrev in $*; do
149 if [ -r "$cachedir/$oldrev" ]; then
150 read newrev <"$cachedir/$oldrev"
151 echo $newrev
153 done
156 cache_miss()
158 for oldrev in $*; do
159 if [ ! -r "$cachedir/$oldrev" ]; then
160 echo $oldrev
162 done
165 check_parents()
167 missed=$(cache_miss $*)
168 for miss in $missed; do
169 if [ ! -r "$cachedir/notree/$miss" ]; then
170 debug " incorrect order: $miss"
172 done
175 set_notree()
177 echo "1" > "$cachedir/notree/$1"
180 cache_set()
182 oldrev="$1"
183 newrev="$2"
184 if [ "$oldrev" != "latest_old" \
185 -a "$oldrev" != "latest_new" \
186 -a -e "$cachedir/$oldrev" ]; then
187 die "cache for $oldrev already exists!"
189 echo "$newrev" >"$cachedir/$oldrev"
192 rev_exists()
194 if git rev-parse "$1" >/dev/null 2>&1; then
195 return 0
196 else
197 return 1
201 rev_is_descendant_of_branch()
203 newrev="$1"
204 branch="$2"
205 branch_hash=$(git rev-parse $branch)
206 match=$(git rev-list -1 $branch_hash ^$newrev)
208 if [ -z "$match" ]; then
209 return 0
210 else
211 return 1
215 # if a commit doesn't have a parent, this might not work. But we only want
216 # to remove the parent from the rev-list, and since it doesn't exist, it won't
217 # be there anyway, so do nothing in that case.
218 try_remove_previous()
220 if rev_exists "$1^"; then
221 echo "^$1^"
225 find_latest_squash()
227 debug "Looking for latest squash ($dir)..."
228 dir="$1"
230 main=
231 sub=
232 git log --grep="^git-subtree-dir: $dir/*\$" \
233 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
234 while read a b junk; do
235 debug "$a $b $junk"
236 debug "{{$sq/$main/$sub}}"
237 case "$a" in
238 START) sq="$b" ;;
239 git-subtree-mainline:) main="$b" ;;
240 git-subtree-split:) sub="$b" ;;
241 END)
242 if [ -n "$sub" ]; then
243 if [ -n "$main" ]; then
244 # a rejoin commit?
245 # Pretend its sub was a squash.
246 sq="$sub"
248 debug "Squash found: $sq $sub"
249 echo "$sq" "$sub"
250 break
253 main=
254 sub=
256 esac
257 done
260 find_existing_splits()
262 debug "Looking for prior splits..."
263 dir="$1"
264 revs="$2"
265 main=
266 sub=
267 git log --grep="^git-subtree-dir: $dir/*\$" \
268 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
269 while read a b junk; do
270 case "$a" in
271 START) sq="$b" ;;
272 git-subtree-mainline:) main="$b" ;;
273 git-subtree-split:) sub="$b" ;;
274 END)
275 debug " Main is: '$main'"
276 if [ -z "$main" -a -n "$sub" ]; then
277 # squash commits refer to a subtree
278 debug " Squash: $sq from $sub"
279 cache_set "$sq" "$sub"
281 if [ -n "$main" -a -n "$sub" ]; then
282 debug " Prior: $main -> $sub"
283 cache_set $main $sub
284 cache_set $sub $sub
285 try_remove_previous "$main"
286 try_remove_previous "$sub"
288 main=
289 sub=
291 esac
292 done
295 copy_commit()
297 # We're going to set some environment vars here, so
298 # do it in a subshell to get rid of them safely later
299 debug copy_commit "{$1}" "{$2}" "{$3}"
300 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
302 read GIT_AUTHOR_NAME
303 read GIT_AUTHOR_EMAIL
304 read GIT_AUTHOR_DATE
305 read GIT_COMMITTER_NAME
306 read GIT_COMMITTER_EMAIL
307 read GIT_COMMITTER_DATE
308 export GIT_AUTHOR_NAME \
309 GIT_AUTHOR_EMAIL \
310 GIT_AUTHOR_DATE \
311 GIT_COMMITTER_NAME \
312 GIT_COMMITTER_EMAIL \
313 GIT_COMMITTER_DATE
314 (printf "%s" "$annotate"; cat ) |
315 git commit-tree "$2" $3 # reads the rest of stdin
316 ) || die "Can't copy commit $1"
319 add_msg()
321 dir="$1"
322 latest_old="$2"
323 latest_new="$3"
324 if [ -n "$message" ]; then
325 commit_message="$message"
326 else
327 commit_message="Add '$dir/' from commit '$latest_new'"
329 cat <<-EOF
330 $commit_message
332 git-subtree-dir: $dir
333 git-subtree-mainline: $latest_old
334 git-subtree-split: $latest_new
338 add_squashed_msg()
340 if [ -n "$message" ]; then
341 echo "$message"
342 else
343 echo "Merge commit '$1' as '$2'"
347 rejoin_msg()
349 dir="$1"
350 latest_old="$2"
351 latest_new="$3"
352 if [ -n "$message" ]; then
353 commit_message="$message"
354 else
355 commit_message="Split '$dir/' into commit '$latest_new'"
357 cat <<-EOF
358 $commit_message
360 git-subtree-dir: $dir
361 git-subtree-mainline: $latest_old
362 git-subtree-split: $latest_new
366 squash_msg()
368 dir="$1"
369 oldsub="$2"
370 newsub="$3"
371 newsub_short=$(git rev-parse --short "$newsub")
373 if [ -n "$oldsub" ]; then
374 oldsub_short=$(git rev-parse --short "$oldsub")
375 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
376 echo
377 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
378 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
379 else
380 echo "Squashed '$dir/' content from commit $newsub_short"
383 echo
384 echo "git-subtree-dir: $dir"
385 echo "git-subtree-split: $newsub"
388 toptree_for_commit()
390 commit="$1"
391 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
394 subtree_for_commit()
396 commit="$1"
397 dir="$2"
398 git ls-tree "$commit" -- "$dir" |
399 while read mode type tree name; do
400 assert [ "$name" = "$dir" ]
401 assert [ "$type" = "tree" -o "$type" = "commit" ]
402 [ "$type" = "commit" ] && continue # ignore submodules
403 echo $tree
404 break
405 done
408 tree_changed()
410 tree=$1
411 shift
412 if [ $# -ne 1 ]; then
413 return 0 # weird parents, consider it changed
414 else
415 ptree=$(toptree_for_commit $1)
416 if [ "$ptree" != "$tree" ]; then
417 return 0 # changed
418 else
419 return 1 # not changed
424 new_squash_commit()
426 old="$1"
427 oldsub="$2"
428 newsub="$3"
429 tree=$(toptree_for_commit $newsub) || exit $?
430 if [ -n "$old" ]; then
431 squash_msg "$dir" "$oldsub" "$newsub" |
432 git commit-tree "$tree" -p "$old" || exit $?
433 else
434 squash_msg "$dir" "" "$newsub" |
435 git commit-tree "$tree" || exit $?
439 copy_or_skip()
441 rev="$1"
442 tree="$2"
443 newparents="$3"
444 assert [ -n "$tree" ]
446 identical=
447 nonidentical=
449 gotparents=
450 for parent in $newparents; do
451 ptree=$(toptree_for_commit $parent) || exit $?
452 [ -z "$ptree" ] && continue
453 if [ "$ptree" = "$tree" ]; then
454 # an identical parent could be used in place of this rev.
455 identical="$parent"
456 else
457 nonidentical="$parent"
460 # sometimes both old parents map to the same newparent;
461 # eliminate duplicates
462 is_new=1
463 for gp in $gotparents; do
464 if [ "$gp" = "$parent" ]; then
465 is_new=
466 break
468 done
469 if [ -n "$is_new" ]; then
470 gotparents="$gotparents $parent"
471 p="$p -p $parent"
473 done
475 if [ -n "$identical" ]; then
476 echo $identical
477 else
478 copy_commit $rev $tree "$p" || exit $?
482 ensure_clean()
484 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
485 die "Working tree has modifications. Cannot add."
487 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
488 die "Index has modifications. Cannot add."
492 cmd_add()
494 if [ -e "$dir" ]; then
495 die "'$dir' already exists. Cannot add."
498 ensure_clean
500 if [ $# -eq 1 ]; then
501 git rev-parse -q --verify "$1^{commit}" >/dev/null ||
502 die "'$1' does not refer to a commit"
504 "cmd_add_commit" "$@"
505 elif [ $# -eq 2 ]; then
506 # Technically we could accept a refspec here but we're
507 # just going to turn around and add FETCH_HEAD under the
508 # specified directory. Allowing a refspec might be
509 # misleading because we won't do anything with any other
510 # branches fetched via the refspec.
511 git rev-parse -q --verify "$2^{commit}" >/dev/null ||
512 die "'$2' does not refer to a commit"
514 "cmd_add_repository" "$@"
515 else
516 say "error: parameters were '$@'"
517 die "Provide either a commit or a repository and commit."
521 cmd_add_repository()
523 echo "git fetch" "$@"
524 repository=$1
525 refspec=$2
526 git fetch "$@" || exit $?
527 revs=FETCH_HEAD
528 set -- $revs
529 cmd_add_commit "$@"
532 cmd_add_commit()
534 revs=$(git rev-parse $default --revs-only "$@") || exit $?
535 set -- $revs
536 rev="$1"
538 debug "Adding $dir as '$rev'..."
539 git read-tree --prefix="$dir" $rev || exit $?
540 git checkout -- "$dir" || exit $?
541 tree=$(git write-tree) || exit $?
543 headrev=$(git rev-parse HEAD) || exit $?
544 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
545 headp="-p $headrev"
546 else
547 headp=
550 if [ -n "$squash" ]; then
551 rev=$(new_squash_commit "" "" "$rev") || exit $?
552 commit=$(add_squashed_msg "$rev" "$dir" |
553 git commit-tree $tree $headp -p "$rev") || exit $?
554 else
555 commit=$(add_msg "$dir" "$headrev" "$rev" |
556 git commit-tree $tree $headp -p "$rev") || exit $?
558 git reset "$commit" || exit $?
560 say "Added dir '$dir'"
563 cmd_split()
565 debug "Splitting $dir..."
566 cache_setup || exit $?
568 if [ -n "$onto" ]; then
569 debug "Reading history for --onto=$onto..."
570 git rev-list $onto |
571 while read rev; do
572 # the 'onto' history is already just the subdir, so
573 # any parent we find there can be used verbatim
574 debug " cache: $rev"
575 cache_set $rev $rev
576 done
579 if [ -n "$ignore_joins" ]; then
580 unrevs=
581 else
582 unrevs="$(find_existing_splits "$dir" "$revs")"
585 # We can't restrict rev-list to only $dir here, because some of our
586 # parents have the $dir contents the root, and those won't match.
587 # (and rev-list --follow doesn't seem to solve this)
588 grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
589 revmax=$(eval "$grl" | wc -l)
590 revcount=0
591 createcount=0
592 eval "$grl" |
593 while read rev parents; do
594 revcount=$(($revcount + 1))
595 say -n "$revcount/$revmax ($createcount) "
596 debug "Processing commit: $rev"
597 exists=$(cache_get $rev)
598 if [ -n "$exists" ]; then
599 debug " prior: $exists"
600 continue
602 createcount=$(($createcount + 1))
603 debug " parents: $parents"
604 newparents=$(cache_get $parents)
605 debug " newparents: $newparents"
607 tree=$(subtree_for_commit $rev "$dir")
608 debug " tree is: $tree"
610 check_parents $parents
612 # ugly. is there no better way to tell if this is a subtree
613 # vs. a mainline commit? Does it matter?
614 if [ -z $tree ]; then
615 set_notree $rev
616 if [ -n "$newparents" ]; then
617 cache_set $rev $rev
619 continue
622 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
623 debug " newrev is: $newrev"
624 cache_set $rev $newrev
625 cache_set latest_new $newrev
626 cache_set latest_old $rev
627 done || exit $?
628 latest_new=$(cache_get latest_new)
629 if [ -z "$latest_new" ]; then
630 die "No new revisions were found"
633 if [ -n "$rejoin" ]; then
634 debug "Merging split branch into HEAD..."
635 latest_old=$(cache_get latest_old)
636 git merge -s ours \
637 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
638 $latest_new >&2 || exit $?
640 if [ -n "$branch" ]; then
641 if rev_exists "refs/heads/$branch"; then
642 if ! rev_is_descendant_of_branch $latest_new $branch; then
643 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
645 action='Updated'
646 else
647 action='Created'
649 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
650 say "$action branch '$branch'"
652 echo $latest_new
653 exit 0
656 cmd_merge()
658 revs=$(git rev-parse $default --revs-only "$@") || exit $?
659 ensure_clean
661 set -- $revs
662 if [ $# -ne 1 ]; then
663 die "You must provide exactly one revision. Got: '$revs'"
665 rev="$1"
667 if [ -n "$squash" ]; then
668 first_split="$(find_latest_squash "$dir")"
669 if [ -z "$first_split" ]; then
670 die "Can't squash-merge: '$dir' was never added."
672 set $first_split
673 old=$1
674 sub=$2
675 if [ "$sub" = "$rev" ]; then
676 say "Subtree is already at commit $rev."
677 exit 0
679 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
680 debug "New squash commit: $new"
681 rev="$new"
684 version=$(git version)
685 if [ "$version" \< "git version 1.7" ]; then
686 if [ -n "$message" ]; then
687 git merge -s subtree --message="$message" $rev
688 else
689 git merge -s subtree $rev
691 else
692 if [ -n "$message" ]; then
693 git merge -Xsubtree="$prefix" --message="$message" $rev
694 else
695 git merge -Xsubtree="$prefix" $rev
700 cmd_pull()
702 ensure_clean
703 git fetch "$@" || exit $?
704 revs=FETCH_HEAD
705 set -- $revs
706 cmd_merge "$@"
709 cmd_push()
711 if [ $# -ne 2 ]; then
712 die "You must provide <repository> <refspec>"
714 if [ -e "$dir" ]; then
715 repository=$1
716 refspec=$2
717 echo "git push using: " $repository $refspec
718 localrev=$(git subtree split --prefix="$prefix") || die
719 git push $repository $localrev:refs/heads/$refspec
720 else
721 die "'$dir' must already exist. Try 'git subtree add'."
725 "cmd_$command" "$@"