contrib/subtree: portability fix for string printing
[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> <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...>
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 prefix=
50
51 debug()
52 {
53         if [ -n "$debug" ]; then
54                 printf "%s\n" "$*" >&2
55         fi
56 }
57
58 say()
59 {
60         if [ -z "$quiet" ]; then
61                 printf "%s\n" "$*" >&2
62         fi
63 }
64
65 progress()
66 {
67         if [ -z "$quiet" ]; then
68                 printf "%s\r" "$*" >&2
69         fi
70 }
71
72 assert()
73 {
74         if "$@"; then
75                 :
76         else
77                 die "assertion failed: " "$@"
78         fi
79 }
80
81
82 #echo "Options: $*"
83
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
108
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
116
117 if [ -z "$prefix" ]; then
118         die "You must provide the --prefix option."
119 fi
120
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
127
128 dir="$(dirname "$prefix/.")"
129
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."
135         fi
136 fi
137
138 debug "command: {$command}"
139 debug "quiet: {$quiet}"
140 debug "revs: {$revs}"
141 debug "dir: {$dir}"
142 debug "opts: {$*}"
143 debug
144
145 cache_setup()
146 {
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
152 }
153
154 cache_get()
155 {
156         for oldrev in $*; do
157                 if [ -r "$cachedir/$oldrev" ]; then
158                         read newrev <"$cachedir/$oldrev"
159                         echo $newrev
160                 fi
161         done
162 }
163
164 cache_miss()
165 {
166         for oldrev in $*; do
167                 if [ ! -r "$cachedir/$oldrev" ]; then
168                         echo $oldrev
169                 fi
170         done
171 }
172
173 check_parents()
174 {
175         missed=$(cache_miss $*)
176         for miss in $missed; do
177                 if [ ! -r "$cachedir/notree/$miss" ]; then
178                         debug "  incorrect order: $miss"
179                 fi
180         done
181 }
182
183 set_notree()
184 {
185         echo "1" > "$cachedir/notree/$1"
186 }
187
188 cache_set()
189 {
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!"
196         fi
197         echo "$newrev" >"$cachedir/$oldrev"
198 }
199
200 rev_exists()
201 {
202         if git rev-parse "$1" >/dev/null 2>&1; then
203                 return 0
204         else
205                 return 1
206         fi
207 }
208
209 rev_is_descendant_of_branch()
210 {
211         newrev="$1"
212         branch="$2"
213         branch_hash=$(git rev-parse $branch)
214         match=$(git rev-list -1 $branch_hash ^$newrev)
215
216         if [ -z "$match" ]; then
217                 return 0
218         else
219                 return 1
220         fi
221 }
222
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()
227 {
228         if rev_exists "$1^"; then
229                 echo "^$1^"
230         fi
231 }
232
233 find_latest_squash()
234 {
235         debug "Looking for latest squash ($dir)..."
236         dir="$1"
237         sq=
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"
255                                         fi
256                                         debug "Squash found: $sq $sub"
257                                         echo "$sq" "$sub"
258                                         break
259                                 fi
260                                 sq=
261                                 main=
262                                 sub=
263                                 ;;
264                 esac
265         done
266 }
267
268 find_existing_splits()
269 {
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"
288                                 fi
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"
295                                 fi
296                                 main=
297                                 sub=
298                                 ;;
299                 esac
300         done
301 }
302
303 copy_commit()
304 {
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" |
309         (
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"
325 }
326
327 add_msg()
328 {
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'"
336         fi
337         cat <<-EOF
338                 $commit_message
339                 
340                 git-subtree-dir: $dir
341                 git-subtree-mainline: $latest_old
342                 git-subtree-split: $latest_new
343         EOF
344 }
345
346 add_squashed_msg()
347 {
348         if [ -n "$message" ]; then
349                 echo "$message"
350         else
351                 echo "Merge commit '$1' as '$2'"
352         fi
353 }
354
355 rejoin_msg()
356 {
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'"
364         fi
365         cat <<-EOF
366                 $commit_message
367                 
368                 git-subtree-dir: $dir
369                 git-subtree-mainline: $latest_old
370                 git-subtree-split: $latest_new
371         EOF
372 }
373
374 squash_msg()
375 {
376         dir="$1"
377         oldsub="$2"
378         newsub="$3"
379         newsub_short=$(git rev-parse --short "$newsub")
380         
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"
389         fi
390         
391         echo
392         echo "git-subtree-dir: $dir"
393         echo "git-subtree-split: $newsub"
394 }
395
396 toptree_for_commit()
397 {
398         commit="$1"
399         git log -1 --pretty=format:'%T' "$commit" -- || exit $?
400 }
401
402 subtree_for_commit()
403 {
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
414 }
415
416 tree_changed()
417 {
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
428                 fi
429         fi
430 }
431
432 new_squash_commit()
433 {
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 $?
444         fi
445 }
446
447 copy_or_skip()
448 {
449         rev="$1"
450         tree="$2"
451         newparents="$3"
452         assert [ -n "$tree" ]
453
454         identical=
455         nonidentical=
456         p=
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"
466                 fi
467                 
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
475                         fi
476                 done
477                 if [ -n "$is_new" ]; then
478                         gotparents="$gotparents $parent"
479                         p="$p -p $parent"
480                 fi
481         done
482         
483         if [ -n "$identical" ]; then
484                 echo $identical
485         else
486                 copy_commit $rev $tree "$p" || exit $?
487         fi
488 }
489
490 ensure_clean()
491 {
492         if ! git diff-index HEAD --exit-code --quiet 2>&1; then
493                 die "Working tree has modifications.  Cannot add."
494         fi
495         if ! git diff-index --cached HEAD --exit-code --quiet 2>&1; then
496                 die "Index has modifications.  Cannot add."
497         fi
498 }
499
500 ensure_valid_ref_format()
501 {
502         git check-ref-format "refs/heads/$1" ||
503             die "'$1' does not look like a ref"
504 }
505
506 cmd_add()
507 {
508         if [ -e "$dir" ]; then
509                 die "'$dir' already exists.  Cannot add."
510         fi
511
512         ensure_clean
513         
514         if [ $# -eq 1 ]; then
515             git rev-parse -q --verify "$1^{commit}" >/dev/null ||
516             die "'$1' does not refer to a commit"
517
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"
526
527             "cmd_add_repository" "$@"
528         else
529             say "error: parameters were '$@'"
530             die "Provide either a commit or a repository and commit."
531         fi
532 }
533
534 cmd_add_repository()
535 {
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 "$@"
543 }
544
545 cmd_add_commit()
546 {
547         revs=$(git rev-parse $default --revs-only "$@") || exit $?
548         set -- $revs
549         rev="$1"
550         
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 $?
555         
556         headrev=$(git rev-parse HEAD) || exit $?
557         if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
558                 headp="-p $headrev"
559         else
560                 headp=
561         fi
562         
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 $?
571         fi
572         git reset "$commit" || exit $?
573         
574         say "Added dir '$dir'"
575 }
576
577 cmd_split()
578 {
579         debug "Splitting $dir..."
580         cache_setup || exit $?
581         
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
591         fi
592         
593         if [ -n "$ignore_joins" ]; then
594                 unrevs=
595         else
596                 unrevs="$(find_existing_splits "$dir" "$revs")"
597         fi
598         
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
615                 fi
616                 createcount=$(($createcount + 1))
617                 debug "  parents: $parents"
618                 newparents=$(cache_get $parents)
619                 debug "  newparents: $newparents"
620                 
621                 tree=$(subtree_for_commit $rev "$dir")
622                 debug "  tree is: $tree"
623
624                 check_parents $parents
625                 
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
632                         fi
633                         continue
634                 fi
635
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"
645         fi
646         
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 $?
653         fi
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'."
658                         fi
659                         action='Updated'
660                 else
661                         action='Created'
662                 fi
663                 git update-ref -m 'subtree split' "refs/heads/$branch" $latest_new || exit $?
664                 say "$action branch '$branch'"
665         fi
666         echo $latest_new
667         exit 0
668 }
669
670 cmd_merge()
671 {
672         revs=$(git rev-parse $default --revs-only "$@") || exit $?
673         ensure_clean
674         
675         set -- $revs
676         if [ $# -ne 1 ]; then
677                 die "You must provide exactly one revision.  Got: '$revs'"
678         fi
679         rev="$1"
680         
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."
685                 fi
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
692                 fi
693                 new=$(new_squash_commit "$old" "$sub" "$rev") || exit $?
694                 debug "New squash commit: $new"
695                 rev="$new"
696         fi
697
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
704                 fi
705         else
706                 if [ -n "$message" ]; then
707                         git merge -Xsubtree="$prefix" --message="$message" $rev
708                 else
709                         git merge -Xsubtree="$prefix" $rev
710                 fi
711         fi
712 }
713
714 cmd_pull()
715 {
716         if [ $# -ne 2 ]; then
717             die "You must provide <repository> <ref>"
718         fi
719         ensure_clean
720         ensure_valid_ref_format "$2"
721         git fetch "$@" || exit $?
722         revs=FETCH_HEAD
723         set -- $revs
724         cmd_merge "$@"
725 }
726
727 cmd_push()
728 {
729         if [ $# -ne 2 ]; then
730             die "You must provide <repository> <ref>"
731         fi
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'."
741         fi
742 }
743
744 "cmd_$command" "$@"