docs: Description, synopsys, options and examples changes.
[git-subtree.git] / git-subtree.sh
blobce94d363dcc55493f5a49722124de75722018ff7
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 debug "Using cachedir: $cachedir" >&2
144 cache_get()
146 for oldrev in $*; do
147 if [ -r "$cachedir/$oldrev" ]; then
148 read newrev <"$cachedir/$oldrev"
149 echo $newrev
151 done
154 cache_set()
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!"
163 echo "$newrev" >"$cachedir/$oldrev"
166 rev_exists()
168 if git rev-parse "$1" >/dev/null 2>&1; then
169 return 0
170 else
171 return 1
175 rev_is_descendant_of_branch()
177 newrev="$1"
178 branch="$2"
179 branch_hash=$(git rev-parse $branch)
180 match=$(git rev-list -1 $branch_hash ^$newrev)
182 if [ -z "$match" ]; then
183 return 0
184 else
185 return 1
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()
194 if rev_exists "$1^"; then
195 echo "^$1^"
199 find_latest_squash()
201 debug "Looking for latest squash ($dir)..."
202 dir="$1"
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"
222 debug "Squash found: $sq $sub"
223 echo "$sq" "$sub"
224 break
227 main=
228 sub=
230 esac
231 done
234 find_existing_splits()
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"
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"
262 main=
263 sub=
265 esac
266 done
269 copy_commit()
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" |
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"
293 add_msg()
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'"
303 cat <<-EOF
304 $commit_message
306 git-subtree-dir: $dir
307 git-subtree-mainline: $latest_old
308 git-subtree-split: $latest_new
312 add_squashed_msg()
314 if [ -n "$message" ]; then
315 echo "$message"
316 else
317 echo "Merge commit '$1' as '$2'"
321 rejoin_msg()
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'"
331 cat <<-EOF
332 $commit_message
334 git-subtree-dir: $dir
335 git-subtree-mainline: $latest_old
336 git-subtree-split: $latest_new
340 squash_msg()
342 dir="$1"
343 oldsub="$2"
344 newsub="$3"
345 newsub_short=$(git rev-parse --short "$newsub")
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"
357 echo
358 echo "git-subtree-dir: $dir"
359 echo "git-subtree-split: $newsub"
362 toptree_for_commit()
364 commit="$1"
365 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
368 subtree_for_commit()
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
381 tree_changed()
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
397 new_squash_commit()
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 $?
412 copy_or_skip()
414 rev="$1"
415 tree="$2"
416 newparents="$3"
417 assert [ -n "$tree" ]
419 identical=
420 nonidentical=
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"
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
441 done
442 if [ -n "$is_new" ]; then
443 gotparents="$gotparents $parent"
444 p="$p -p $parent"
446 done
448 if [ -n "$identical" ]; then
449 echo $identical
450 else
451 copy_commit $rev $tree "$p" || exit $?
455 ensure_clean()
457 if ! git diff-index HEAD --exit-code --quiet 2>&1; then
458 die "Working tree has modifications. Cannot add."
460 if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
461 die "Index has modifications. Cannot add."
465 cmd_add()
467 if [ -e "$dir" ]; then
468 die "'$dir' already exists. Cannot add."
471 ensure_clean
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."
483 cmd_add_repository()
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 "$@"
494 cmd_add_commit()
496 revs=$(git rev-parse $default --revs-only "$@") || exit $?
497 set -- $revs
498 rev="$1"
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 $?
505 headrev=$(git rev-parse HEAD) || exit $?
506 if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
507 headp="-p $headrev"
508 else
509 headp=
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 $?
520 git reset "$commit" || exit $?
522 say "Added dir '$dir'"
525 cmd_split()
527 debug "Splitting $dir..."
528 cache_setup || exit $?
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
541 if [ -n "$ignore_joins" ]; then
542 unrevs=
543 else
544 unrevs="$(find_existing_splits "$dir" "$revs")"
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
564 createcount=$(($createcount + 1))
565 debug " parents: $parents"
566 newparents=$(cache_get $parents)
567 debug " newparents: $newparents"
569 tree=$(subtree_for_commit $rev "$dir")
570 debug " tree is: $tree"
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
578 continue
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"
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 $?
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'."
604 action='Updated'
605 else
606 action='Created'
608 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
609 say "$action branch '$branch'"
611 echo $latest_new
612 exit 0
615 cmd_merge()
617 revs=$(git rev-parse $default --revs-only "$@") || exit $?
618 ensure_clean
620 set -- $revs
621 if [ $# -ne 1 ]; then
622 die "You must provide exactly one revision. Got: '$revs'"
624 rev="$1"
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."
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
638 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
639 debug "New squash commit: $new"
640 rev="$new"
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
650 else
651 if [ -n "$message" ]; then
652 git merge -Xsubtree="$prefix" --message="$message" $rev
653 else
654 git merge -Xsubtree="$prefix" $rev
659 cmd_pull()
661 ensure_clean
662 git fetch "$@" || exit $?
663 revs=FETCH_HEAD
664 set -- $revs
665 cmd_merge "$@"
668 cmd_push()
670 if [ $# -ne 2 ]; then
671 die "You must provide <repository> <refspec>"
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'."
683 "cmd_$command" "$@"