It's also okay if an expected tree object is actually a commit.
[git-subtree.git] / git-subtree.sh
blobfa4e3e3661d4b0d092c35e019b160d1e4d4080c2
1 #!/bin/bash
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 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...>
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
31 eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
33 PATH=$PATH:$(git --exec-path)
34 . git-sh-setup
36 require_work_tree
38 quiet=
39 branch=
40 debug=
41 command=
42 onto=
43 rejoin=
44 ignore_joins=
45 annotate=
46 squash=
47 message=
49 debug()
51 if [ -n "$debug" ]; then
52 echo "$@" >&2
56 say()
58 if [ -z "$quiet" ]; then
59 echo "$@" >&2
63 assert()
65 if "$@"; then
67 else
68 die "assertion failed: " "$@"
73 #echo "Options: $*"
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
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
108 if [ -z "$prefix" ]; then
109 die "You must provide the --prefix option."
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
119 dir="$(dirname "$prefix/.")"
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."
129 debug "command: {$command}"
130 debug "quiet: {$quiet}"
131 debug "revs: {$revs}"
132 debug "dir: {$dir}"
133 debug "opts: {$*}"
134 debug
136 cache_setup()
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 mkdir -p "$cachedir/notree" || die "Can't create new cachedir: $cachedir/notree"
142 debug "Using cachedir: $cachedir" >&2
145 cache_get()
147 for oldrev in $*; do
148 if [ -r "$cachedir/$oldrev" ]; then
149 read newrev <"$cachedir/$oldrev"
150 echo $newrev
152 done
155 cache_miss()
157 for oldrev in $*; do
158 if [ ! -r "$cachedir/$oldrev" ]; then
159 echo $oldrev
161 done
164 check_parents()
166 missed=$(cache_miss $*)
167 for miss in $missed; do
168 if [ ! -r "$cachedir/notree/$miss" ]; then
169 debug " incorrect order: $miss"
171 done
174 set_notree()
176 echo "1" > "$cachedir/notree/$1"
179 cache_set()
181 oldrev="$1"
182 newrev="$2"
183 if [ "$oldrev" != "latest_old" \
184 -a "$oldrev" != "latest_new" \
185 -a -e "$cachedir/$oldrev" ]; then
186 die "cache for $oldrev already exists!"
188 echo "$newrev" >"$cachedir/$oldrev"
191 rev_exists()
193 if git rev-parse "$1" >/dev/null 2>&1; then
194 return 0
195 else
196 return 1
200 rev_is_descendant_of_branch()
202 newrev="$1"
203 branch="$2"
204 branch_hash=$(git rev-parse $branch)
205 match=$(git rev-list -1 $branch_hash ^$newrev)
207 if [ -z "$match" ]; then
208 return 0
209 else
210 return 1
214 # if a commit doesn't have a parent, this might not work. But we only want
215 # to remove the parent from the rev-list, and since it doesn't exist, it won't
216 # be there anyway, so do nothing in that case.
217 try_remove_previous()
219 if rev_exists "$1^"; then
220 echo "^$1^"
224 find_latest_squash()
226 debug "Looking for latest squash ($dir)..."
227 dir="$1"
229 main=
230 sub=
231 git log --grep="^git-subtree-dir: $dir/*\$" \
232 --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
233 while read a b junk; do
234 debug "$a $b $junk"
235 debug "{{$sq/$main/$sub}}"
236 case "$a" in
237 START) sq="$b" ;;
238 git-subtree-mainline:) main="$b" ;;
239 git-subtree-split:) sub="$b" ;;
240 END)
241 if [ -n "$sub" ]; then
242 if [ -n "$main" ]; then
243 # a rejoin commit?
244 # Pretend its sub was a squash.
245 sq="$sub"
247 debug "Squash found: $sq $sub"
248 echo "$sq" "$sub"
249 break
252 main=
253 sub=
255 esac
256 done
259 find_existing_splits()
261 debug "Looking for prior splits..."
262 dir="$1"
263 revs="$2"
264 main=
265 sub=
266 git log --grep="^git-subtree-dir: $dir/*\$" \
267 --pretty=format:'START %H%n%s%n%n%b%nEND%n' $revs |
268 while read a b junk; do
269 case "$a" in
270 START) sq="$b" ;;
271 git-subtree-mainline:) main="$b" ;;
272 git-subtree-split:) sub="$b" ;;
273 END)
274 debug " Main is: '$main'"
275 if [ -z "$main" -a -n "$sub" ]; then
276 # squash commits refer to a subtree
277 debug " Squash: $sq from $sub"
278 cache_set "$sq" "$sub"
280 if [ -n "$main" -a -n "$sub" ]; then
281 debug " Prior: $main -> $sub"
282 cache_set $main $sub
283 cache_set $sub $sub
284 try_remove_previous "$main"
285 try_remove_previous "$sub"
287 main=
288 sub=
290 esac
291 done
294 copy_commit()
296 # We're going to set some environment vars here, so
297 # do it in a subshell to get rid of them safely later
298 debug copy_commit "{$1}" "{$2}" "{$3}"
299 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
301 read GIT_AUTHOR_NAME
302 read GIT_AUTHOR_EMAIL
303 read GIT_AUTHOR_DATE
304 read GIT_COMMITTER_NAME
305 read GIT_COMMITTER_EMAIL
306 read GIT_COMMITTER_DATE
307 export GIT_AUTHOR_NAME \
308 GIT_AUTHOR_EMAIL \
309 GIT_AUTHOR_DATE \
310 GIT_COMMITTER_NAME \
311 GIT_COMMITTER_EMAIL \
312 GIT_COMMITTER_DATE
313 (echo -n "$annotate"; cat ) |
314 git commit-tree "$2" $3 # reads the rest of stdin
315 ) || die "Can't copy commit $1"
318 add_msg()
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="Add '$dir/' from commit '$latest_new'"
328 cat <<-EOF
329 $commit_message
331 git-subtree-dir: $dir
332 git-subtree-mainline: $latest_old
333 git-subtree-split: $latest_new
337 add_squashed_msg()
339 if [ -n "$message" ]; then
340 echo "$message"
341 else
342 echo "Merge commit '$1' as '$2'"
346 rejoin_msg()
348 dir="$1"
349 latest_old="$2"
350 latest_new="$3"
351 if [ -n "$message" ]; then
352 commit_message="$message"
353 else
354 commit_message="Split '$dir/' into commit '$latest_new'"
356 cat <<-EOF
357 $commit_message
359 git-subtree-dir: $dir
360 git-subtree-mainline: $latest_old
361 git-subtree-split: $latest_new
365 squash_msg()
367 dir="$1"
368 oldsub="$2"
369 newsub="$3"
370 newsub_short=$(git rev-parse --short "$newsub")
372 if [ -n "$oldsub" ]; then
373 oldsub_short=$(git rev-parse --short "$oldsub")
374 echo "Squashed '$dir/' changes from $oldsub_short..$newsub_short"
375 echo
376 git log --pretty=tformat:'%h %s' "$oldsub..$newsub"
377 git log --pretty=tformat:'REVERT: %h %s' "$newsub..$oldsub"
378 else
379 echo "Squashed '$dir/' content from commit $newsub_short"
382 echo
383 echo "git-subtree-dir: $dir"
384 echo "git-subtree-split: $newsub"
387 toptree_for_commit()
389 commit="$1"
390 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
393 subtree_for_commit()
395 commit="$1"
396 dir="$2"
397 git ls-tree "$commit" -- "$dir" |
398 while read mode type tree name; do
399 assert [ "$name" = "$dir" ]
400 assert [ "$type" = "tree" -o "$type" = "commit" ]
401 echo $tree
402 break
403 done
406 tree_changed()
408 tree=$1
409 shift
410 if [ $# -ne 1 ]; then
411 return 0 # weird parents, consider it changed
412 else
413 ptree=$(toptree_for_commit $1)
414 if [ "$ptree" != "$tree" ]; then
415 return 0 # changed
416 else
417 return 1 # not changed
422 new_squash_commit()
424 old="$1"
425 oldsub="$2"
426 newsub="$3"
427 tree=$(toptree_for_commit $newsub) || exit $?
428 if [ -n "$old" ]; then
429 squash_msg "$dir" "$oldsub" "$newsub" |
430 git commit-tree "$tree" -p "$old" || exit $?
431 else
432 squash_msg "$dir" "" "$newsub" |
433 git commit-tree "$tree" || exit $?
437 copy_or_skip()
439 rev="$1"
440 tree="$2"
441 newparents="$3"
442 assert [ -n "$tree" ]
444 identical=
445 nonidentical=
447 gotparents=
448 for parent in $newparents; do
449 ptree=$(toptree_for_commit $parent) || exit $?
450 [ -z "$ptree" ] && continue
451 if [ "$ptree" = "$tree" ]; then
452 # an identical parent could be used in place of this rev.
453 identical="$parent"
454 else
455 nonidentical="$parent"
458 # sometimes both old parents map to the same newparent;
459 # eliminate duplicates
460 is_new=1
461 for gp in $gotparents; do
462 if [ "$gp" = "$parent" ]; then
463 is_new=
464 break
466 done
467 if [ -n "$is_new" ]; then
468 gotparents="$gotparents $parent"
469 p="$p -p $parent"
471 done
473 if [ -n "$identical" ]; then
474 echo $identical
475 else
476 copy_commit $rev $tree "$p" || exit $?
480 ensure_clean()
482 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
483 die "Working tree has modifications. Cannot add."
485 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
486 die "Index has modifications. Cannot add."
490 cmd_add()
492 if [ -e "$dir" ]; then
493 die "'$dir' already exists. Cannot add."
496 ensure_clean
498 if [ $# -eq 1 ]; then
499 "cmd_add_commit" "$@"
500 elif [ $# -eq 2 ]; then
501 "cmd_add_repository" "$@"
502 else
503 say "error: parameters were '$@'"
504 die "Provide either a refspec or a repository and refspec."
508 cmd_add_repository()
510 echo "git fetch" "$@"
511 repository=$1
512 refspec=$2
513 git fetch "$@" || exit $?
514 revs=FETCH_HEAD
515 set -- $revs
516 cmd_add_commit "$@"
519 cmd_add_commit()
521 revs=$(git rev-parse $default --revs-only "$@") || exit $?
522 set -- $revs
523 rev="$1"
525 debug "Adding $dir as '$rev'..."
526 git read-tree --prefix="$dir" $rev || exit $?
527 git checkout -- "$dir" || exit $?
528 tree=$(git write-tree) || exit $?
530 headrev=$(git rev-parse HEAD) || exit $?
531 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
532 headp="-p $headrev"
533 else
534 headp=
537 if [ -n "$squash" ]; then
538 rev=$(new_squash_commit "" "" "$rev") || exit $?
539 commit=$(add_squashed_msg "$rev" "$dir" |
540 git commit-tree $tree $headp -p "$rev") || exit $?
541 else
542 commit=$(add_msg "$dir" "$headrev" "$rev" |
543 git commit-tree $tree $headp -p "$rev") || exit $?
545 git reset "$commit" || exit $?
547 say "Added dir '$dir'"
550 cmd_split()
552 debug "Splitting $dir..."
553 cache_setup || exit $?
555 if [ -n "$onto" ]; then
556 debug "Reading history for --onto=$onto..."
557 git rev-list $onto |
558 while read rev; do
559 # the 'onto' history is already just the subdir, so
560 # any parent we find there can be used verbatim
561 debug " cache: $rev"
562 cache_set $rev $rev
563 done
566 if [ -n "$ignore_joins" ]; then
567 unrevs=
568 else
569 unrevs="$(find_existing_splits "$dir" "$revs")"
572 # We can't restrict rev-list to only $dir here, because some of our
573 # parents have the $dir contents the root, and those won't match.
574 # (and rev-list --follow doesn't seem to solve this)
575 grl='git rev-list --topo-order --reverse --parents $revs $unrevs'
576 revmax=$(eval "$grl" | wc -l)
577 revcount=0
578 createcount=0
579 eval "$grl" |
580 while read rev parents; do
581 revcount=$(($revcount + 1))
582 say -n "$revcount/$revmax ($createcount) "
583 debug "Processing commit: $rev"
584 exists=$(cache_get $rev)
585 if [ -n "$exists" ]; then
586 debug " prior: $exists"
587 continue
589 createcount=$(($createcount + 1))
590 debug " parents: $parents"
591 newparents=$(cache_get $parents)
592 debug " newparents: $newparents"
594 tree=$(subtree_for_commit $rev "$dir")
595 debug " tree is: $tree"
597 check_parents $parents
599 # ugly. is there no better way to tell if this is a subtree
600 # vs. a mainline commit? Does it matter?
601 if [ -z $tree ]; then
602 set_notree $rev
603 if [ -n "$newparents" ]; then
604 cache_set $rev $rev
606 continue
609 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
610 debug " newrev is: $newrev"
611 cache_set $rev $newrev
612 cache_set latest_new $newrev
613 cache_set latest_old $rev
614 done || exit $?
615 latest_new=$(cache_get latest_new)
616 if [ -z "$latest_new" ]; then
617 die "No new revisions were found"
620 if [ -n "$rejoin" ]; then
621 debug "Merging split branch into HEAD..."
622 latest_old=$(cache_get latest_old)
623 git merge -s ours \
624 -m "$(rejoin_msg $dir $latest_old $latest_new)" \
625 $latest_new >&2 || exit $?
627 if [ -n "$branch" ]; then
628 if rev_exists "refs/heads/$branch"; then
629 if ! rev_is_descendant_of_branch $latest_new $branch; then
630 die "Branch '$branch' is not an ancestor of commit '$latest_new'."
632 action='Updated'
633 else
634 action='Created'
636 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
637 say "$action branch '$branch'"
639 echo $latest_new
640 exit 0
643 cmd_merge()
645 revs=$(git rev-parse $default --revs-only "$@") || exit $?
646 ensure_clean
648 set -- $revs
649 if [ $# -ne 1 ]; then
650 die "You must provide exactly one revision. Got: '$revs'"
652 rev="$1"
654 if [ -n "$squash" ]; then
655 first_split="$(find_latest_squash "$dir")"
656 if [ -z "$first_split" ]; then
657 die "Can't squash-merge: '$dir' was never added."
659 set $first_split
660 old=$1
661 sub=$2
662 if [ "$sub" = "$rev" ]; then
663 say "Subtree is already at commit $rev."
664 exit 0
666 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
667 debug "New squash commit: $new"
668 rev="$new"
671 version=$(git version)
672 if [ "$version" \< "git version 1.7" ]; then
673 if [ -n "$message" ]; then
674 git merge -s subtree --message="$message" $rev
675 else
676 git merge -s subtree $rev
678 else
679 if [ -n "$message" ]; then
680 git merge -Xsubtree="$prefix" --message="$message" $rev
681 else
682 git merge -Xsubtree="$prefix" $rev
687 cmd_pull()
689 ensure_clean
690 git fetch "$@" || exit $?
691 revs=FETCH_HEAD
692 set -- $revs
693 cmd_merge "$@"
696 cmd_push()
698 if [ $# -ne 2 ]; then
699 die "You must provide <repository> <refspec>"
701 if [ -e "$dir" ]; then
702 repository=$1
703 refspec=$2
704 echo "git push using: " $repository $refspec
705 git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
706 else
707 die "'$dir' must already exist. Try 'git subtree add'."
711 "cmd_$command" "$@"