Code

2dc99e82cd0aa3c23587c77dbe4eac76d355385b
[git.git] / git-subtree.sh
1 #!/bin/bash
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 split [options...] <--prefix=prefix> <commit...>
13 git subtree merge 
14 --
15 h,help        show the help
16 q             quiet
17 prefix=       the name of the subdir to split out
18 onto=         try connecting new tree to an existing one
19 rejoin        merge the new branch back into HEAD
20 ignore-joins  ignore prior --rejoin commits
21 "
22 eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
23 . git-sh-setup
24 require_work_tree
26 quiet=
27 command=
28 onto=
29 rejoin=
30 ignore_joins=
32 debug()
33 {
34         if [ -z "$quiet" ]; then
35                 echo "$@" >&2
36         fi
37 }
39 assert()
40 {
41         if "$@"; then
42                 :
43         else
44                 die "assertion failed: " "$@"
45         fi
46 }
49 #echo "Options: $*"
51 while [ $# -gt 0 ]; do
52         opt="$1"
53         shift
54         case "$opt" in
55                 -q) quiet=1 ;;
56                 --prefix) prefix="$1"; shift ;;
57                 --no-prefix) prefix= ;;
58                 --onto) onto="$1"; shift ;;
59                 --no-onto) onto= ;;
60                 --rejoin) rejoin=1 ;;
61                 --no-rejoin) rejoin= ;;
62                 --ignore-joins) ignore_joins=1 ;;
63                 --no-ignore-joins) ignore_joins= ;;
64                 --) break ;;
65         esac
66 done
68 command="$1"
69 shift
70 case "$command" in
71         add|merge) default= ;;
72         split) default="--default HEAD" ;;
73         *) die "Unknown command '$command'" ;;
74 esac
76 revs=$(git rev-parse $default --revs-only "$@") || exit $?
78 if [ -z "$prefix" ]; then
79         die "You must provide the --prefix option."
80 fi
81 dir="$prefix"
83 dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $?
84 if [ -n "$dirs" ]; then
85         die "Error: Use --prefix instead of bare filenames."
86 fi
88 debug "command: {$command}"
89 debug "quiet: {$quiet}"
90 debug "revs: {$revs}"
91 debug "dir: {$dir}"
92 debug
94 cache_setup()
95 {
96         cachedir="$GIT_DIR/subtree-cache/$$"
97         rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
98         mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
99         debug "Using cachedir: $cachedir" >&2
102 cache_get()
104         for oldrev in $*; do
105                 if [ -r "$cachedir/$oldrev" ]; then
106                         read newrev <"$cachedir/$oldrev"
107                         echo $newrev
108                 fi
109         done
112 cache_set()
114         oldrev="$1"
115         newrev="$2"
116         if [ "$oldrev" != "latest_old" \
117              -a "$oldrev" != "latest_new" \
118              -a -e "$cachedir/$oldrev" ]; then
119                 die "cache for $oldrev already exists!"
120         fi
121         echo "$newrev" >"$cachedir/$oldrev"
124 find_existing_splits()
126         debug "Looking for prior splits..."
127         dir="$1"
128         revs="$2"
129         git log --grep="^git-subtree-dir: $dir\$" \
130                 --pretty=format:'%s%n%n%b%nEND' "$revs" |
131         while read a b junk; do
132                 case "$a" in
133                         git-subtree-mainline:) main="$b" ;;
134                         git-subtree-split:) sub="$b" ;;
135                         *)
136                                 if [ -n "$main" -a -n "$sub" ]; then
137                                         debug "  Prior: $main -> $sub"
138                                         cache_set $main $sub
139                                         echo "^$main^ ^$sub^"
140                                         main=
141                                         sub=
142                                 fi
143                                 ;;
144                 esac
145         done
148 copy_commit()
150         # We're doing to set some environment vars here, so
151         # do it in a subshell to get rid of them safely later
152         git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
153         (
154                 read GIT_AUTHOR_NAME
155                 read GIT_AUTHOR_EMAIL
156                 read GIT_AUTHOR_DATE
157                 read GIT_COMMITTER_NAME
158                 read GIT_COMMITTER_EMAIL
159                 read GIT_COMMITTER_DATE
160                 export  GIT_AUTHOR_NAME \
161                         GIT_AUTHOR_EMAIL \
162                         GIT_AUTHOR_DATE \
163                         GIT_COMMITTER_NAME \
164                         GIT_COMMITTER_EMAIL \
165                         GIT_COMMITTER_DATE
166                 (echo -n '*'; cat ) |  # FIXME
167                 git commit-tree "$2" $3  # reads the rest of stdin
168         ) || die "Can't copy commit $1"
171 add_msg()
173         dir="$1"
174         latest_old="$2"
175         latest_new="$3"
176         cat <<-EOF
177                 Add '$dir/' from commit '$latest_new'
178                 
179                 git-subtree-dir: $dir
180                 git-subtree-mainline: $latest_old
181                 git-subtree-split: $latest_new
182         EOF
185 merge_msg()
187         dir="$1"
188         latest_old="$2"
189         latest_new="$3"
190         cat <<-EOF
191                 Split '$dir/' into commit '$latest_new'
192                 
193                 git-subtree-dir: $dir
194                 git-subtree-mainline: $latest_old
195                 git-subtree-split: $latest_new
196         EOF
199 toptree_for_commit()
201         commit="$1"
202         git log -1 --pretty=format:'%T' "$commit" -- || exit $?
205 subtree_for_commit()
207         commit="$1"
208         dir="$2"
209         git ls-tree "$commit" -- "$dir" |
210         while read mode type tree name; do
211                 assert [ "$name" = "$dir" ]
212                 echo $tree
213                 break
214         done
217 tree_changed()
219         tree=$1
220         shift
221         if [ $# -ne 1 ]; then
222                 return 0   # weird parents, consider it changed
223         else
224                 ptree=$(toptree_for_commit $1)
225                 if [ "$ptree" != "$tree" ]; then
226                         return 0   # changed
227                 else
228                         return 1   # not changed
229                 fi
230         fi
233 copy_or_skip()
235         rev="$1"
236         tree="$2"
237         newparents="$3"
238         assert [ -n "$tree" ]
240         identical=
241         p=
242         for parent in $newparents; do
243                 ptree=$(toptree_for_commit $parent) || exit $?
244                 if [ "$ptree" = "$tree" ]; then
245                         # an identical parent could be used in place of this rev.
246                         identical="$parent"
247                 fi
248                 if [ -n "$ptree" ]; then
249                         parentmatch="$parentmatch$parent"
250                         p="$p -p $parent"
251                 fi
252         done
253         
254         if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
255                 echo $identical
256         else
257                 copy_commit $rev $tree "$p" || exit $?
258         fi
261 cmd_add()
263         if [ -e "$dir" ]; then
264                 die "'$dir' already exists.  Cannot add."
265         fi
266         if ! git diff-index HEAD --exit-code --quiet; then
267                 die "Working tree has modifications.  Cannot add."
268         fi
269         if ! git diff-index --cached HEAD --exit-code --quiet; then
270                 die "Index has modifications.  Cannot add."
271         fi
272         set -- $revs
273         if [ $# -ne 1 ]; then
274                 die "You must provide exactly one revision.  Got: '$revs'"
275         fi
276         rev="$1"
277         
278         debug "Adding $dir as '$rev'..."
279         git read-tree --prefix="$dir" $rev || exit $?
280         git checkout "$dir" || exit $?
281         tree=$(git write-tree) || exit $?
282         
283         headrev=$(git rev-parse HEAD) || exit $?
284         if [ -n "$headrev" -a "$headrev" != "$rev" ]; then
285                 headp="-p $headrev"
286         else
287                 headp=
288         fi
289         commit=$(add_msg "$dir" "$headrev" "$rev" |
290                  git commit-tree $tree $headp -p "$rev") || exit $?
291         git reset "$commit" || exit $?
294 cmd_split()
296         debug "Splitting $dir..."
297         cache_setup || exit $?
298         
299         if [ -n "$onto" ]; then
300                 debug "Reading history for --onto=$onto..."
301                 git rev-list $onto |
302                 while read rev; do
303                         # the 'onto' history is already just the subdir, so
304                         # any parent we find there can be used verbatim
305                         debug "  cache: $rev"
306                         cache_set $rev $rev
307                 done
308         fi
309         
310         if [ -n "$ignore_joins" ]; then
311                 unrevs=
312         else
313                 unrevs="$(find_existing_splits "$dir" "$revs")"
314         fi
315         
316         # We can't restrict rev-list to only "$dir" here, because that leaves out
317         # critical information about commit parents.
318         debug "git rev-list --reverse --parents $revs $unrevs"
319         git rev-list --reverse --parents $revs $unrevs |
320         while read rev parents; do
321                 debug
322                 debug "Processing commit: $rev"
323                 exists=$(cache_get $rev)
324                 if [ -n "$exists" ]; then
325                         debug "  prior: $exists"
326                         continue
327                 fi
328                 debug "  parents: $parents"
329                 newparents=$(cache_get $parents)
330                 debug "  newparents: $newparents"
331                 
332                 tree=$(subtree_for_commit $rev "$dir")
333                 debug "  tree is: $tree"
334                 [ -z $tree ] && continue
336                 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
337                 debug "  newrev is: $newrev"
338                 cache_set $rev $newrev
339                 cache_set latest_new $newrev
340                 cache_set latest_old $rev
341         done || exit $?
342         latest_new=$(cache_get latest_new)
343         if [ -z "$latest_new" ]; then
344                 die "No new revisions were found"
345         fi
346         
347         if [ -n "$rejoin" ]; then
348                 debug "Merging split branch into HEAD..."
349                 latest_old=$(cache_get latest_old)
350                 git merge -s ours \
351                         -m "$(merge_msg $dir $latest_old $latest_new)" \
352                         $latest_new >&2
353         fi
354         echo $latest_new
355         exit 0
358 cmd_merge()
360         die "merge command not implemented yet"
363 "cmd_$command"