contrib/git-subtree: Use /bin/sh interpreter instead of /bin/bash
[git] / contrib / subtree / git-subtree.sh
1 #!/bin/sh
2 #
3 # git-subtree.sh: split/join git repositories in subdirectories of this one
4 #
5 # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
6 #
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...>
17 --
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
31 "
32 eval "$(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)"
33
34 PATH=$PATH:$(git --exec-path)
35 . git-sh-setup
36
37 require_work_tree
38
39 quiet=
40 branch=
41 debug=
42 command=
43 onto=
44 rejoin=
45 ignore_joins=
46 annotate=
47 squash=
48 message=
49
50 debug()
51 {
52         if [ -n "$debug" ]; then
53                 echo "$@" >&2
54         fi
55 }
56
57 say()
58 {
59         if [ -z "$quiet" ]; then
60                 echo "$@" >&2
61         fi
62 }
63
64 assert()
65 {
66         if "$@"; then
67                 :
68         else
69                 die "assertion failed: " "$@"
70         fi
71 }
72
73
74 #echo "Options: $*"
75
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
100
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
108
109 if [ -z "$prefix" ]; then
110         die "You must provide the --prefix option."
111 fi
112
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
119
120 dir="$(dirname "$prefix/.")"
121
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."
127         fi
128 fi
129
130 debug "command: {$command}"
131 debug "quiet: {$quiet}"
132 debug "revs: {$revs}"
133 debug "dir: {$dir}"
134 debug "opts: {$*}"
135 debug
136
137 cache_setup()
138 {
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
144 }
145
146 cache_get()
147 {
148         for oldrev in $*; do
149                 if [ -r "$cachedir/$oldrev" ]; then
150                         read newrev <"$cachedir/$oldrev"
151                         echo $newrev
152                 fi
153         done
154 }
155
156 cache_miss()
157 {
158         for oldrev in $*; do
159                 if [ ! -r "$cachedir/$oldrev" ]; then
160                         echo $oldrev
161                 fi
162         done
163 }
164
165 check_parents()
166 {
167         missed=$(cache_miss $*)
168         for miss in $missed; do
169                 if [ ! -r "$cachedir/notree/$miss" ]; then
170                         debug "  incorrect order: $miss"
171                 fi
172         done
173 }
174
175 set_notree()
176 {
177         echo "1" > "$cachedir/notree/$1"
178 }
179
180 cache_set()
181 {
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!"
188         fi
189         echo "$newrev" >"$cachedir/$oldrev"
190 }
191
192 rev_exists()
193 {
194         if git rev-parse "$1" >/dev/null 2>&1; then
195                 return 0
196         else
197                 return 1
198         fi
199 }
200
201 rev_is_descendant_of_branch()
202 {
203         newrev="$1"
204         branch="$2"
205         branch_hash=$(git rev-parse $branch)
206         match=$(git rev-list -1 $branch_hash ^$newrev)
207
208         if [ -z "$match" ]; then
209                 return 0
210         else
211                 return 1
212         fi
213 }
214
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()
219 {
220         if rev_exists "$1^"; then
221                 echo "^$1^"
222         fi
223 }
224
225 find_latest_squash()
226 {
227         debug "Looking for latest squash ($dir)..."
228         dir="$1"
229         sq=
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"
247                                         fi
248                                         debug "Squash found: $sq $sub"
249                                         echo "$sq" "$sub"
250                                         break
251                                 fi
252                                 sq=
253                                 main=
254                                 sub=
255                                 ;;
256                 esac
257         done
258 }
259
260 find_existing_splits()
261 {
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"
280                                 fi
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"
287                                 fi
288                                 main=
289                                 sub=
290                                 ;;
291                 esac
292         done
293 }
294
295 copy_commit()
296 {
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" |
301         (
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                 (echo -n "$annotate"; cat ) |
315                 git commit-tree "$2" $3  # reads the rest of stdin
316         ) || die "Can't copy commit $1"
317 }
318
319 add_msg()
320 {
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'"
328         fi
329         cat <<-EOF
330                 $commit_message
331                 
332                 git-subtree-dir: $dir
333                 git-subtree-mainline: $latest_old
334                 git-subtree-split: $latest_new
335         EOF
336 }
337
338 add_squashed_msg()
339 {
340         if [ -n "$message" ]; then
341                 echo "$message"
342         else
343                 echo "Merge commit '$1' as '$2'"
344         fi
345 }
346
347 rejoin_msg()
348 {
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'"
356         fi
357         cat <<-EOF
358                 $commit_message
359                 
360                 git-subtree-dir: $dir
361                 git-subtree-mainline: $latest_old
362                 git-subtree-split: $latest_new
363         EOF
364 }
365
366 squash_msg()
367 {
368         dir="$1"
369         oldsub="$2"
370         newsub="$3"
371         newsub_short=$(git rev-parse --short "$newsub")
372         
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"
381         fi
382         
383         echo
384         echo "git-subtree-dir: $dir"
385         echo "git-subtree-split: $newsub"
386 }
387
388 toptree_for_commit()
389 {
390         commit="$1"
391         git log -1 --pretty=format:'%T' "$commit" -- || exit $?
392 }
393
394 subtree_for_commit()
395 {
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
406 }
407
408 tree_changed()
409 {
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
420                 fi
421         fi
422 }
423
424 new_squash_commit()
425 {
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 $?
436         fi
437 }
438
439 copy_or_skip()
440 {
441         rev="$1"
442         tree="$2"
443         newparents="$3"
444         assert [ -n "$tree" ]
445
446         identical=
447         nonidentical=
448         p=
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"
458                 fi
459                 
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
467                         fi
468                 done
469                 if [ -n "$is_new" ]; then
470                         gotparents="$gotparents $parent"
471                         p="$p -p $parent"
472                 fi
473         done
474         
475         if [ -n "$identical" ]; then
476                 echo $identical
477         else
478                 copy_commit $rev $tree "$p" || exit $?
479         fi
480 }
481
482 ensure_clean()
483 {
484         if ! git diff-index HEAD --exit-code --quiet 2>&1; then
485                 die "Working tree has modifications.  Cannot add."
486         fi
487         if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
488                 die "Index has modifications.  Cannot add."
489         fi
490 }
491
492 cmd_add()
493 {
494         if [ -e "$dir" ]; then
495                 die "'$dir' already exists.  Cannot add."
496         fi
497
498         ensure_clean
499         
500         if [ $# -eq 1 ]; then
501             git rev-parse -q --verify "$1^{commit}" >/dev/null ||
502             die "'$1' does not refer to a commit"
503
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"
513
514             "cmd_add_repository" "$@"
515         else
516             say "error: parameters were '$@'"
517             die "Provide either a commit or a repository and commit."
518         fi
519 }
520
521 cmd_add_repository()
522 {
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 "$@"
530 }
531
532 cmd_add_commit()
533 {
534         revs=$(git rev-parse $default --revs-only "$@") || exit $?
535         set -- $revs
536         rev="$1"
537         
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 $?
542         
543         headrev=$(git rev-parse HEAD) || exit $?
544         if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
545                 headp="-p $headrev"
546         else
547                 headp=
548         fi
549         
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 $?
557         fi
558         git reset "$commit" || exit $?
559         
560         say "Added dir '$dir'"
561 }
562
563 cmd_split()
564 {
565         debug "Splitting $dir..."
566         cache_setup || exit $?
567         
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
577         fi
578         
579         if [ -n "$ignore_joins" ]; then
580                 unrevs=
581         else
582                 unrevs="$(find_existing_splits "$dir" "$revs")"
583         fi
584         
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)\r"
596                 debug "Processing commit: $rev"
597                 exists=$(cache_get $rev)
598                 if [ -n "$exists" ]; then
599                         debug "  prior: $exists"
600                         continue
601                 fi
602                 createcount=$(($createcount + 1))
603                 debug "  parents: $parents"
604                 newparents=$(cache_get $parents)
605                 debug "  newparents: $newparents"
606                 
607                 tree=$(subtree_for_commit $rev "$dir")
608                 debug "  tree is: $tree"
609
610                 check_parents $parents
611                 
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
618                         fi
619                         continue
620                 fi
621
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"
631         fi
632         
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 $?
639         fi
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'."
644                         fi
645                         action='Updated'
646                 else
647                         action='Created'
648                 fi
649                 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
650                 say "$action branch '$branch'"
651         fi
652         echo $latest_new
653         exit 0
654 }
655
656 cmd_merge()
657 {
658         revs=$(git rev-parse $default --revs-only "$@") || exit $?
659         ensure_clean
660         
661         set -- $revs
662         if [ $# -ne 1 ]; then
663                 die "You must provide exactly one revision.  Got: '$revs'"
664         fi
665         rev="$1"
666         
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."
671                 fi
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
678                 fi
679                 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
680                 debug "New squash commit: $new"
681                 rev="$new"
682         fi
683
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
690                 fi
691         else
692                 if [ -n "$message" ]; then
693                         git merge -Xsubtree="$prefix" --message="$message" $rev
694                 else
695                         git merge -Xsubtree="$prefix" $rev
696                 fi
697         fi
698 }
699
700 cmd_pull()
701 {
702         ensure_clean
703         git fetch "$@" || exit $?
704         revs=FETCH_HEAD
705         set -- $revs
706         cmd_merge "$@"
707 }
708
709 cmd_push()
710 {
711         if [ $# -ne 2 ]; then
712             die "You must provide <repository> <refspec>"
713         fi
714         if [ -e "$dir" ]; then
715             repository=$1
716             refspec=$2
717             echo "git push using: " $repository $refspec
718             git push $repository $(git subtree split --prefix=$prefix):refs/heads/$refspec
719         else
720             die "'$dir' must already exist. Try 'git subtree add'."
721         fi
722 }
723
724 "cmd_$command" "$@"