contrib/subtree: portability fix for string printing
[git/mjg.git] / contrib / subtree / git-subtree.sh
blobd4dae7ab5b05b937df0dbe57edc1dcc5138a4608
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> <ref>
13 git subtree merge --prefix=<prefix> <commit>
14 git subtree pull --prefix=<prefix> <repository> <ref>
15 git subtree push --prefix=<prefix> <repository> <ref>
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=
49 prefix=
51 debug()
53 if [ -n "$debug" ]; then
54 printf "%s\n" "$*" >&2
58 say()
60 if [ -z "$quiet" ]; then
61 printf "%s\n" "$*" >&2
65 progress()
67 if [ -z "$quiet" ]; then
68 printf "%s\r" "$*" >&2
72 assert()
74 if "$@"; then
76 else
77 die "assertion failed: " "$@"
82 #echo "Options: $*"
84 while [ $# -gt 0 ]; do
85 opt="$1"
86 shift
87 case "$opt" in
88 -q) quiet=1 ;;
89 -d) debug=1 ;;
90 --annotate) annotate="$1"; shift ;;
91 --no-annotate) annotate= ;;
92 -b) branch="$1"; shift ;;
93 -P) prefix="$1"; shift ;;
94 -m) message="$1"; shift ;;
95 --no-prefix) prefix= ;;
96 --onto) onto="$1"; shift ;;
97 --no-onto) onto= ;;
98 --rejoin) rejoin=1 ;;
99 --no-rejoin) rejoin= ;;
100 --ignore-joins) ignore_joins=1 ;;
101 --no-ignore-joins) ignore_joins= ;;
102 --squash) squash=1 ;;
103 --no-squash) squash= ;;
104 --) break ;;
105 *) die "Unexpected option: $opt" ;;
106 esac
107 done
109 command="$1"
110 shift
111 case "$command" in
112 add|merge|pull) default= ;;
113 split|push) default="--default HEAD" ;;
114 *) die "Unknown command '$command'" ;;
115 esac
117 if [ -z "$prefix" ]; then
118 die "You must provide the --prefix option."
121 case "$command" in
122 add) [ -e "$prefix" ] &&
123 die "prefix '$prefix' already exists." ;;
124 *) [ -e "$prefix" ] ||
125 die "'$prefix' does not exist; use 'git subtree add'" ;;
126 esac
128 dir="$(dirname "$prefix/.")"
130 if [ "$command" != "pull" -a "$command" != "add" -a "$command" != "push" ]; then
131 revs=$(git rev-parse $default --revs-only "$@") || exit $?
132 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
133 if [ -n "$dirs" ]; then
134 die "Error: Use --prefix instead of bare filenames."
138 debug "command: {$command}"
139 debug "quiet: {$quiet}"
140 debug "revs: {$revs}"
141 debug "dir: {$dir}"
142 debug "opts: {$*}"
143 debug
145 cache_setup()
147 cachedir="$GIT_DIR/subtree-cache/$$"
148 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
149 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
150 mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
151 debug "Using cachedir: $cachedir" >&2
154 cache_get()
156 for oldrev in $*; do
157 if [ -r "$cachedir/$oldrev" ]; then
158 read newrev <"$cachedir/$oldrev"
159 echo $newrev
161 done
164 cache_miss()
166 for oldrev in $*; do
167 if [ ! -r "$cachedir/$oldrev" ]; then
168 echo $oldrev
170 done
173 check_parents()
175 missed=$(cache_miss $*)
176 for miss in $missed; do
177 if [ ! -r "$cachedir/notree/$miss" ]; then
178 debug " incorrect order: $miss"
180 done
183 set_notree()
185 echo "1" > "$cachedir/notree/$1"
188 cache_set()
190 oldrev="$1"
191 newrev="$2"
192 if [ "$oldrev" != "latest_old" \
193 -a "$oldrev" != "latest_new" \
194 -a -e "$cachedir/$oldrev" ]; then
195 die "cache for $oldrev already exists!"
197 echo "$newrev" >"$cachedir/$oldrev"
200 rev_exists()
202 if git rev-parse "$1" >/dev/null 2>&1; then
203 return 0
204 else
205 return 1
209 rev_is_descendant_of_branch()
211 newrev="$1"
212 branch="$2"
213 branch_hash=$(git rev-parse $branch)
214 match=$(git rev-list -1 $branch_hash ^$newrev)
216 if [ -z "$match" ]; then
217 return 0
218 else
219 return 1
223 # if a commit doesn't have a parent, this might not work. But we only want
224 # to remove the parent from the rev-list, and since it doesn't exist, it won't
225 # be there anyway, so do nothing in that case.
226 try_remove_previous()
228 if rev_exists "$1^"; then
229 echo "^$1^"
233 find_latest_squash()
235 debug "Looking for latest squash ($dir)..."
236 dir="$1"
238 main=
239 sub=
240 git log --grep="^git-subtree-dir: $dir/*\$" \
241 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
242 while read a b junk; do
243 debug "$a $b $junk"
244 debug "{{$sq/$main/$sub}}"
245 case "$a" in
246 START) sq="$b" ;;
247 git-subtree-mainline:) main="$b" ;;
248 git-subtree-split:) sub="$b" ;;
249 END)
250 if [ -n "$sub" ]; then
251 if [ -n "$main" ]; then
252 # a rejoin commit?
253 # Pretend its sub was a squash.
254 sq="$sub"
256 debug "Squash found: $sq $sub"
257 echo "$sq" "$sub"
258 break
261 main=
262 sub=
264 esac
265 done
268 find_existing_splits()
270 debug "Looking for prior splits..."
271 dir="$1"
272 revs="$2"
273 main=
274 sub=
275 git log --grep="^git-subtree-dir: $dir/*\$" \
276 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
277 while read a b junk; do
278 case "$a" in
279 START) sq="$b" ;;
280 git-subtree-mainline:) main="$b" ;;
281 git-subtree-split:) sub="$b" ;;
282 END)
283 debug " Main is: '$main'"
284 if [ -z "$main" -a -n "$sub" ]; then
285 # squash commits refer to a subtree
286 debug " Squash: $sq from $sub"
287 cache_set "$sq" "$sub"
289 if [ -n "$main" -a -n "$sub" ]; then
290 debug " Prior: $main -> $sub"
291 cache_set $main $sub
292 cache_set $sub $sub
293 try_remove_previous "$main"
294 try_remove_previous "$sub"
296 main=
297 sub=
299 esac
300 done
303 copy_commit()
305 # We're going to set some environment vars here, so
306 # do it in a subshell to get rid of them safely later
307 debug copy_commit "{$1}" "{$2}" "{$3}"
308 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%B' "$1" |
310 read GIT_AUTHOR_NAME
311 read GIT_AUTHOR_EMAIL
312 read GIT_AUTHOR_DATE
313 read GIT_COMMITTER_NAME
314 read GIT_COMMITTER_EMAIL
315 read GIT_COMMITTER_DATE
316 export GIT_AUTHOR_NAME \
317 GIT_AUTHOR_EMAIL \
318 GIT_AUTHOR_DATE \
319 GIT_COMMITTER_NAME \
320 GIT_COMMITTER_EMAIL \
321 GIT_COMMITTER_DATE
322 (printf "%s" "$annotate"; cat ) |
323 git commit-tree "$2" $3 # reads the rest of stdin
324 ) || die "Can't copy commit $1"
327 add_msg()
329 dir="$1"
330 latest_old="$2"
331 latest_new="$3"
332 if [ -n "$message" ]; then
333 commit_message="$message"
334 else
335 commit_message="Add '$dir/' from commit '$latest_new'"
337 cat <<-EOF
338 $commit_message
340 git-subtree-dir: $dir
341 git-subtree-mainline: $latest_old
342 git-subtree-split: $latest_new
346 add_squashed_msg()
348 if [ -n "$message" ]; then
349 echo "$message"
350 else
351 echo "Merge commit '$1' as '$2'"
355 rejoin_msg()
357 dir="$1"
358 latest_old="$2"
359 latest_new="$3"
360 if [ -n "$message" ]; then
361 commit_message="$message"
362 else
363 commit_message="Split '$dir/' into commit '$latest_new'"
365 cat <<-EOF
366 $commit_message
368 git-subtree-dir: $dir
369 git-subtree-mainline: $latest_old
370 git-subtree-split: $latest_new
374 squash_msg()
376 dir="$1"
377 oldsub="$2"
378 newsub="$3"
379 newsub_short=$(git rev-parse --short "$newsub")
381 if [ -n "$oldsub" ]; then
382 oldsub_short=$(git rev-parse --short "$oldsub")
383 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
384 echo
385 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
386 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
387 else
388 echo "Squashed '$dir/' content from commit $newsub_short"
391 echo
392 echo "git-subtree-dir: $dir"
393 echo "git-subtree-split: $newsub"
396 toptree_for_commit()
398 commit="$1"
399 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
402 subtree_for_commit()
404 commit="$1"
405 dir="$2"
406 git ls-tree "$commit" -- "$dir" |
407 while read mode type tree name; do
408 assert [ "$name" = "$dir" ]
409 assert [ "$type" = "tree" -o "$type" = "commit" ]
410 [ "$type" = "commit" ] && continue # ignore submodules
411 echo $tree
412 break
413 done
416 tree_changed()
418 tree=$1
419 shift
420 if [ $# -ne 1 ]; then
421 return 0 # weird parents, consider it changed
422 else
423 ptree=$(toptree_for_commit $1)
424 if [ "$ptree" != "$tree" ]; then
425 return 0 # changed
426 else
427 return 1 # not changed
432 new_squash_commit()
434 old="$1"
435 oldsub="$2"
436 newsub="$3"
437 tree=$(toptree_for_commit $newsub) || exit $?
438 if [ -n "$old" ]; then
439 squash_msg "$dir" "$oldsub" "$newsub" |
440 git commit-tree "$tree" -p "$old" || exit $?
441 else
442 squash_msg "$dir" "" "$newsub" |
443 git commit-tree "$tree" || exit $?
447 copy_or_skip()
449 rev="$1"
450 tree="$2"
451 newparents="$3"
452 assert [ -n "$tree" ]
454 identical=
455 nonidentical=
457 gotparents=
458 for parent in $newparents; do
459 ptree=$(toptree_for_commit $parent) || exit $?
460 [ -z "$ptree" ] && continue
461 if [ "$ptree" = "$tree" ]; then
462 # an identical parent could be used in place of this rev.
463 identical="$parent"
464 else
465 nonidentical="$parent"
468 # sometimes both old parents map to the same newparent;
469 # eliminate duplicates
470 is_new=1
471 for gp in $gotparents; do
472 if [ "$gp" = "$parent" ]; then
473 is_new=
474 break
476 done
477 if [ -n "$is_new" ]; then
478 gotparents="$gotparents $parent"
479 p="$p -p $parent"
481 done
483 if [ -n "$identical" ]; then
484 echo $identical
485 else
486 copy_commit $rev $tree "$p" || exit $?
490 ensure_clean()
492 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
493 die "Working tree has modifications. Cannot add."
495 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
496 die "Index has modifications. Cannot add."
500 ensure_valid_ref_format()
502 git check-ref-format "refs/heads/$1" ||
503 die "'$1' does not look like a ref"
506 cmd_add()
508 if [ -e "$dir" ]; then
509 die "'$dir' already exists. Cannot add."
512 ensure_clean
514 if [ $# -eq 1 ]; then
515 git rev-parse -q --verify "$1^{commit}" >/dev/null ||
516 die "'$1' does not refer to a commit"
518 "cmd_add_commit" "$@"
519 elif [ $# -eq 2 ]; then
520 # Technically we could accept a refspec here but we're
521 # just going to turn around and add FETCH_HEAD under the
522 # specified directory. Allowing a refspec might be
523 # misleading because we won't do anything with any other
524 # branches fetched via the refspec.
525 ensure_valid_ref_format "$2"
527 "cmd_add_repository" "$@"
528 else
529 say "error: parameters were '$@'"
530 die "Provide either a commit or a repository and commit."
534 cmd_add_repository()
536 echo "git fetch" "$@"
537 repository=$1
538 refspec=$2
539 git fetch "$@" || exit $?
540 revs=FETCH_HEAD
541 set -- $revs
542 cmd_add_commit "$@"
545 cmd_add_commit()
547 revs=$(git rev-parse $default --revs-only "$@") || exit $?
548 set -- $revs
549 rev="$1"
551 debug "Adding $dir as '$rev'..."
552 git read-tree --prefix="$dir" $rev || exit $?
553 git checkout -- "$dir" || exit $?
554 tree=$(git write-tree) || exit $?
556 headrev=$(git rev-parse HEAD) || exit $?
557 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
558 headp="-p $headrev"
559 else
560 headp=
563 if [ -n "$squash" ]; then
564 rev=$(new_squash_commit "" "" "$rev") || exit $?
565 commit=$(add_squashed_msg "$rev" "$dir" |
566 git commit-tree $tree $headp -p "$rev") || exit $?
567 else
568 revp=$(peel_committish "$rev") &&
569 commit=$(add_msg "$dir" "$headrev" "$rev" |
570 git commit-tree $tree $headp -p "$revp") || exit $?
572 git reset "$commit" || exit $?
574 say "Added dir '$dir'"
577 cmd_split()
579 debug "Splitting $dir..."
580 cache_setup || exit $?
582 if [ -n "$onto" ]; then
583 debug "Reading history for --onto=$onto..."
584 git rev-list $onto |
585 while read rev; do
586 # the 'onto' history is already just the subdir, so
587 # any parent we find there can be used verbatim
588 debug " cache: $rev"
589 cache_set $rev $rev
590 done
593 if [ -n "$ignore_joins" ]; then
594 unrevs=
595 else
596 unrevs="$(find_existing_splits "$dir" "$revs")"
599 # We can't restrict rev-list to only $dir here, because some of our
600 # parents have the $dir contents the root, and those won't match.
601 # (and rev-list --follow doesn't seem to solve this)
602 grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
603 revmax=$(eval "$grl" | wc -l)
604 revcount=0
605 createcount=0
606 eval "$grl" |
607 while read rev parents; do
608 revcount=$(($revcount + 1))
609 progress "$revcount/$revmax ($createcount)"
610 debug "Processing commit: $rev"
611 exists=$(cache_get $rev)
612 if [ -n "$exists" ]; then
613 debug " prior: $exists"
614 continue
616 createcount=$(($createcount + 1))
617 debug " parents: $parents"
618 newparents=$(cache_get $parents)
619 debug " newparents: $newparents"
621 tree=$(subtree_for_commit $rev "$dir")
622 debug " tree is: $tree"
624 check_parents $parents
626 # ugly. is there no better way to tell if this is a subtree
627 # vs. a mainline commit? Does it matter?
628 if [ -z $tree ]; then
629 set_notree $rev
630 if [ -n "$newparents" ]; then
631 cache_set $rev $rev
633 continue
636 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
637 debug " newrev is: $newrev"
638 cache_set $rev $newrev
639 cache_set latest_new $newrev
640 cache_set latest_old $rev
641 done || exit $?
642 latest_new=$(cache_get latest_new)
643 if [ -z "$latest_new" ]; then
644 die "No new revisions were found"
647 if [ -n "$rejoin" ]; then
648 debug "Merging split branch into HEAD..."
649 latest_old=$(cache_get latest_old)
650 git merge -s ours \
651 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
652 $latest_new >&2 || exit $?
654 if [ -n "$branch" ]; then
655 if rev_exists "refs/heads/$branch"; then
656 if ! rev_is_descendant_of_branch $latest_new $branch; then
657 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
659 action='Updated'
660 else
661 action='Created'
663 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
664 say "$action branch '$branch'"
666 echo $latest_new
667 exit 0
670 cmd_merge()
672 revs=$(git rev-parse $default --revs-only "$@") || exit $?
673 ensure_clean
675 set -- $revs
676 if [ $# -ne 1 ]; then
677 die "You must provide exactly one revision. Got: '$revs'"
679 rev="$1"
681 if [ -n "$squash" ]; then
682 first_split="$(find_latest_squash "$dir")"
683 if [ -z "$first_split" ]; then
684 die "Can't squash-merge: '$dir' was never added."
686 set $first_split
687 old=$1
688 sub=$2
689 if [ "$sub" = "$rev" ]; then
690 say "Subtree is already at commit $rev."
691 exit 0
693 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
694 debug "New squash commit: $new"
695 rev="$new"
698 version=$(git version)
699 if [ "$version" \< "git version 1.7" ]; then
700 if [ -n "$message" ]; then
701 git merge -s subtree --message="$message" $rev
702 else
703 git merge -s subtree $rev
705 else
706 if [ -n "$message" ]; then
707 git merge -Xsubtree="$prefix" --message="$message" $rev
708 else
709 git merge -Xsubtree="$prefix" $rev
714 cmd_pull()
716 if [ $# -ne 2 ]; then
717 die "You must provide <repository> <ref>"
719 ensure_clean
720 ensure_valid_ref_format "$2"
721 git fetch "$@" || exit $?
722 revs=FETCH_HEAD
723 set -- $revs
724 cmd_merge "$@"
727 cmd_push()
729 if [ $# -ne 2 ]; then
730 die "You must provide <repository> <ref>"
732 ensure_valid_ref_format "$2"
733 if [ -e "$dir" ]; then
734 repository=$1
735 refspec=$2
736 echo "git push using: " $repository $refspec
737 localrev=$(git subtree split --prefix="$prefix") || die
738 git push $repository $localrev:refs/heads/$refspec
739 else
740 die "'$dir' must already exist. Try 'git subtree add'."
744 "cmd_$command" "$@"