]>
Commit | Line | Data |
---|---|---|
0ca71b37 AP |
1 | #!/bin/bash |
2 | # | |
3 | # git-subtree.sh: split/join git repositories in subdirectories of this one | |
4 | # | |
b77172f8 | 5 | # Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com> |
0ca71b37 | 6 | # |
96db2c04 AP |
7 | if [ $# -eq 0 ]; then |
8 | set -- -h | |
9 | fi | |
0ca71b37 | 10 | OPTS_SPEC="\ |
13648af5 AP |
11 | git subtree add --prefix=<prefix> <commit> |
12 | git subtree split [options...] --prefix=<prefix> <commit...> | |
13 | git subtree merge --prefix=<prefix> <commit> | |
14 | git subtree pull --prefix=<prefix> <repository> <refspec...> | |
0ca71b37 | 15 | -- |
96db2c04 AP |
16 | h,help show the help |
17 | q quiet | |
9a8821ff | 18 | prefix= the name of the subdir to split out |
13648af5 | 19 | options for 'split' |
d0eb1b14 | 20 | annotate= add a prefix to commit message of new commits |
96db2c04 AP |
21 | onto= try connecting new tree to an existing one |
22 | rejoin merge the new branch back into HEAD | |
23 | ignore-joins ignore prior --rejoin commits | |
0ca71b37 AP |
24 | " |
25 | eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?) | |
26 | . git-sh-setup | |
27 | require_work_tree | |
28 | ||
29 | quiet= | |
30 | command= | |
b77172f8 AP |
31 | onto= |
32 | rejoin= | |
96db2c04 | 33 | ignore_joins= |
d0eb1b14 | 34 | annotate= |
0ca71b37 AP |
35 | |
36 | debug() | |
37 | { | |
38 | if [ -z "$quiet" ]; then | |
39 | echo "$@" >&2 | |
40 | fi | |
41 | } | |
42 | ||
2573354e AP |
43 | assert() |
44 | { | |
45 | if "$@"; then | |
46 | : | |
47 | else | |
48 | die "assertion failed: " "$@" | |
49 | fi | |
50 | } | |
51 | ||
52 | ||
0ca71b37 AP |
53 | #echo "Options: $*" |
54 | ||
55 | while [ $# -gt 0 ]; do | |
56 | opt="$1" | |
57 | shift | |
58 | case "$opt" in | |
59 | -q) quiet=1 ;; | |
d0eb1b14 AP |
60 | --annotate) annotate="$1"; shift ;; |
61 | --no-annotate) annotate= ;; | |
9a8821ff AP |
62 | --prefix) prefix="$1"; shift ;; |
63 | --no-prefix) prefix= ;; | |
b77172f8 | 64 | --onto) onto="$1"; shift ;; |
96db2c04 | 65 | --no-onto) onto= ;; |
b77172f8 | 66 | --rejoin) rejoin=1 ;; |
96db2c04 AP |
67 | --no-rejoin) rejoin= ;; |
68 | --ignore-joins) ignore_joins=1 ;; | |
69 | --no-ignore-joins) ignore_joins= ;; | |
0ca71b37 AP |
70 | --) break ;; |
71 | esac | |
72 | done | |
73 | ||
74 | command="$1" | |
75 | shift | |
76 | case "$command" in | |
13648af5 | 77 | add|merge|pull) default= ;; |
eb7b590c | 78 | split) default="--default HEAD" ;; |
0ca71b37 AP |
79 | *) die "Unknown command '$command'" ;; |
80 | esac | |
81 | ||
9a8821ff AP |
82 | if [ -z "$prefix" ]; then |
83 | die "You must provide the --prefix option." | |
84 | fi | |
85 | dir="$prefix" | |
86 | ||
13648af5 AP |
87 | if [ "$command" != "pull" ]; then |
88 | revs=$(git rev-parse $default --revs-only "$@") || exit $? | |
89 | dirs="$(git rev-parse --no-revs --no-flags "$@")" || exit $? | |
90 | if [ -n "$dirs" ]; then | |
91 | die "Error: Use --prefix instead of bare filenames." | |
92 | fi | |
0ca71b37 | 93 | fi |
0ca71b37 AP |
94 | |
95 | debug "command: {$command}" | |
96 | debug "quiet: {$quiet}" | |
97 | debug "revs: {$revs}" | |
98 | debug "dir: {$dir}" | |
13648af5 | 99 | debug "opts: {$*}" |
eb7b590c | 100 | debug |
0ca71b37 AP |
101 | |
102 | cache_setup() | |
103 | { | |
2573354e | 104 | cachedir="$GIT_DIR/subtree-cache/$$" |
0ca71b37 AP |
105 | rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir" |
106 | mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir" | |
107 | debug "Using cachedir: $cachedir" >&2 | |
0ca71b37 AP |
108 | } |
109 | ||
110 | cache_get() | |
111 | { | |
112 | for oldrev in $*; do | |
113 | if [ -r "$cachedir/$oldrev" ]; then | |
114 | read newrev <"$cachedir/$oldrev" | |
115 | echo $newrev | |
116 | fi | |
117 | done | |
118 | } | |
119 | ||
120 | cache_set() | |
121 | { | |
122 | oldrev="$1" | |
123 | newrev="$2" | |
b77172f8 AP |
124 | if [ "$oldrev" != "latest_old" \ |
125 | -a "$oldrev" != "latest_new" \ | |
126 | -a -e "$cachedir/$oldrev" ]; then | |
0ca71b37 AP |
127 | die "cache for $oldrev already exists!" |
128 | fi | |
129 | echo "$newrev" >"$cachedir/$oldrev" | |
130 | } | |
131 | ||
b9de5353 AP |
132 | # if a commit doesn't have a parent, this might not work. But we only want |
133 | # to remove the parent from the rev-list, and since it doesn't exist, it won't | |
134 | # be there anyway, so do nothing in that case. | |
135 | try_remove_previous() | |
136 | { | |
137 | if git rev-parse "$1^" >/dev/null 2>&1; then | |
138 | echo "^$1^" | |
139 | fi | |
140 | } | |
141 | ||
8b4a77f2 AP |
142 | find_existing_splits() |
143 | { | |
144 | debug "Looking for prior splits..." | |
145 | dir="$1" | |
146 | revs="$2" | |
147 | git log --grep="^git-subtree-dir: $dir\$" \ | |
86de04c6 | 148 | --pretty=format:'%s%n%n%b%nEND' $revs | |
8b4a77f2 AP |
149 | while read a b junk; do |
150 | case "$a" in | |
151 | git-subtree-mainline:) main="$b" ;; | |
152 | git-subtree-split:) sub="$b" ;; | |
153 | *) | |
154 | if [ -n "$main" -a -n "$sub" ]; then | |
155 | debug " Prior: $main -> $sub" | |
156 | cache_set $main $sub | |
b9de5353 AP |
157 | try_remove_previous "$main" |
158 | try_remove_previous "$sub" | |
8b4a77f2 AP |
159 | main= |
160 | sub= | |
161 | fi | |
162 | ;; | |
163 | esac | |
164 | done | |
165 | } | |
166 | ||
fd9500ee AP |
167 | copy_commit() |
168 | { | |
169 | # We're doing to set some environment vars here, so | |
170 | # do it in a subshell to get rid of them safely later | |
171 | git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" | | |
172 | ( | |
173 | read GIT_AUTHOR_NAME | |
174 | read GIT_AUTHOR_EMAIL | |
175 | read GIT_AUTHOR_DATE | |
176 | read GIT_COMMITTER_NAME | |
177 | read GIT_COMMITTER_EMAIL | |
178 | read GIT_COMMITTER_DATE | |
b77172f8 AP |
179 | export GIT_AUTHOR_NAME \ |
180 | GIT_AUTHOR_EMAIL \ | |
181 | GIT_AUTHOR_DATE \ | |
182 | GIT_COMMITTER_NAME \ | |
183 | GIT_COMMITTER_EMAIL \ | |
184 | GIT_COMMITTER_DATE | |
d0eb1b14 | 185 | (echo -n "$annotate"; cat ) | |
fd9500ee AP |
186 | git commit-tree "$2" $3 # reads the rest of stdin |
187 | ) || die "Can't copy commit $1" | |
188 | } | |
189 | ||
eb7b590c AP |
190 | add_msg() |
191 | { | |
192 | dir="$1" | |
193 | latest_old="$2" | |
194 | latest_new="$3" | |
195 | cat <<-EOF | |
196 | Add '$dir/' from commit '$latest_new' | |
197 | ||
198 | git-subtree-dir: $dir | |
199 | git-subtree-mainline: $latest_old | |
200 | git-subtree-split: $latest_new | |
201 | EOF | |
202 | } | |
203 | ||
b77172f8 AP |
204 | merge_msg() |
205 | { | |
206 | dir="$1" | |
207 | latest_old="$2" | |
208 | latest_new="$3" | |
209 | cat <<-EOF | |
8b4a77f2 | 210 | Split '$dir/' into commit '$latest_new' |
b77172f8 AP |
211 | |
212 | git-subtree-dir: $dir | |
8b4a77f2 AP |
213 | git-subtree-mainline: $latest_old |
214 | git-subtree-split: $latest_new | |
b77172f8 AP |
215 | EOF |
216 | } | |
217 | ||
210d0839 | 218 | toptree_for_commit() |
768d6d10 | 219 | { |
210d0839 AP |
220 | commit="$1" |
221 | git log -1 --pretty=format:'%T' "$commit" -- || exit $? | |
222 | } | |
223 | ||
224 | subtree_for_commit() | |
225 | { | |
226 | commit="$1" | |
227 | dir="$2" | |
228 | git ls-tree "$commit" -- "$dir" | | |
768d6d10 AP |
229 | while read mode type tree name; do |
230 | assert [ "$name" = "$dir" ] | |
231 | echo $tree | |
232 | break | |
233 | done | |
234 | } | |
235 | ||
236 | tree_changed() | |
237 | { | |
238 | tree=$1 | |
239 | shift | |
240 | if [ $# -ne 1 ]; then | |
241 | return 0 # weird parents, consider it changed | |
242 | else | |
210d0839 | 243 | ptree=$(toptree_for_commit $1) |
768d6d10 AP |
244 | if [ "$ptree" != "$tree" ]; then |
245 | return 0 # changed | |
246 | else | |
247 | return 1 # not changed | |
248 | fi | |
249 | fi | |
250 | } | |
251 | ||
d6912658 AP |
252 | copy_or_skip() |
253 | { | |
254 | rev="$1" | |
255 | tree="$2" | |
256 | newparents="$3" | |
257 | assert [ -n "$tree" ] | |
258 | ||
96db2c04 AP |
259 | identical= |
260 | p= | |
d6912658 AP |
261 | for parent in $newparents; do |
262 | ptree=$(toptree_for_commit $parent) || exit $? | |
263 | if [ "$ptree" = "$tree" ]; then | |
96db2c04 AP |
264 | # an identical parent could be used in place of this rev. |
265 | identical="$parent" | |
266 | fi | |
267 | if [ -n "$ptree" ]; then | |
268 | parentmatch="$parentmatch$parent" | |
d6912658 AP |
269 | p="$p -p $parent" |
270 | fi | |
271 | done | |
272 | ||
96db2c04 AP |
273 | if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then |
274 | echo $identical | |
275 | else | |
276 | copy_commit $rev $tree "$p" || exit $? | |
277 | fi | |
d6912658 AP |
278 | } |
279 | ||
13648af5 | 280 | ensure_clean() |
eb7b590c | 281 | { |
eb7b590c AP |
282 | if ! git diff-index HEAD --exit-code --quiet; then |
283 | die "Working tree has modifications. Cannot add." | |
284 | fi | |
285 | if ! git diff-index --cached HEAD --exit-code --quiet; then | |
286 | die "Index has modifications. Cannot add." | |
287 | fi | |
13648af5 AP |
288 | } |
289 | ||
290 | cmd_add() | |
291 | { | |
292 | if [ -e "$dir" ]; then | |
293 | die "'$dir' already exists. Cannot add." | |
294 | fi | |
295 | ensure_clean | |
296 | ||
eb7b590c AP |
297 | set -- $revs |
298 | if [ $# -ne 1 ]; then | |
299 | die "You must provide exactly one revision. Got: '$revs'" | |
300 | fi | |
301 | rev="$1" | |
302 | ||
303 | debug "Adding $dir as '$rev'..." | |
304 | git read-tree --prefix="$dir" $rev || exit $? | |
305 | git checkout "$dir" || exit $? | |
306 | tree=$(git write-tree) || exit $? | |
307 | ||
308 | headrev=$(git rev-parse HEAD) || exit $? | |
309 | if [ -n "$headrev" -a "$headrev" != "$rev" ]; then | |
310 | headp="-p $headrev" | |
311 | else | |
312 | headp= | |
313 | fi | |
314 | commit=$(add_msg "$dir" "$headrev" "$rev" | | |
315 | git commit-tree $tree $headp -p "$rev") || exit $? | |
316 | git reset "$commit" || exit $? | |
317 | } | |
318 | ||
0ca71b37 AP |
319 | cmd_split() |
320 | { | |
321 | debug "Splitting $dir..." | |
322 | cache_setup || exit $? | |
323 | ||
33ff583a | 324 | if [ -n "$onto" ]; then |
847e8681 | 325 | debug "Reading history for --onto=$onto..." |
33ff583a AP |
326 | git rev-list $onto | |
327 | while read rev; do | |
328 | # the 'onto' history is already just the subdir, so | |
329 | # any parent we find there can be used verbatim | |
2c71b7c4 | 330 | debug " cache: $rev" |
33ff583a AP |
331 | cache_set $rev $rev |
332 | done | |
333 | fi | |
334 | ||
96db2c04 AP |
335 | if [ -n "$ignore_joins" ]; then |
336 | unrevs= | |
337 | else | |
338 | unrevs="$(find_existing_splits "$dir" "$revs")" | |
339 | fi | |
8b4a77f2 | 340 | |
1f73862f AP |
341 | # We can't restrict rev-list to only $dir here, because some of our |
342 | # parents have the $dir contents the root, and those won't match. | |
343 | # (and rev-list --follow doesn't seem to solve this) | |
9a8821ff | 344 | git rev-list --reverse --parents $revs $unrevs | |
0ca71b37 | 345 | while read rev parents; do |
2573354e | 346 | debug |
2c71b7c4 AP |
347 | debug "Processing commit: $rev" |
348 | exists=$(cache_get $rev) | |
8b4a77f2 AP |
349 | if [ -n "$exists" ]; then |
350 | debug " prior: $exists" | |
351 | continue | |
352 | fi | |
2c71b7c4 AP |
353 | debug " parents: $parents" |
354 | newparents=$(cache_get $parents) | |
355 | debug " newparents: $newparents" | |
8b4a77f2 | 356 | |
210d0839 | 357 | tree=$(subtree_for_commit $rev "$dir") |
768d6d10 AP |
358 | debug " tree is: $tree" |
359 | [ -z $tree ] && continue | |
360 | ||
d6912658 | 361 | newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $? |
768d6d10 AP |
362 | debug " newrev is: $newrev" |
363 | cache_set $rev $newrev | |
364 | cache_set latest_new $newrev | |
365 | cache_set latest_old $rev | |
2573354e | 366 | done || exit $? |
b77172f8 AP |
367 | latest_new=$(cache_get latest_new) |
368 | if [ -z "$latest_new" ]; then | |
e25a6bf8 AP |
369 | die "No new revisions were found" |
370 | fi | |
b77172f8 AP |
371 | |
372 | if [ -n "$rejoin" ]; then | |
373 | debug "Merging split branch into HEAD..." | |
374 | latest_old=$(cache_get latest_old) | |
375 | git merge -s ours \ | |
376 | -m "$(merge_msg $dir $latest_old $latest_new)" \ | |
847e8681 | 377 | $latest_new >&2 |
b77172f8 AP |
378 | fi |
379 | echo $latest_new | |
0ca71b37 AP |
380 | exit 0 |
381 | } | |
382 | ||
383 | cmd_merge() | |
384 | { | |
13648af5 AP |
385 | ensure_clean |
386 | ||
387 | set -- $revs | |
388 | if [ $# -ne 1 ]; then | |
389 | die "You must provide exactly one revision. Got: '$revs'" | |
390 | fi | |
391 | rev="$1" | |
392 | ||
393 | git merge -s subtree $rev | |
394 | } | |
395 | ||
396 | cmd_pull() | |
397 | { | |
398 | ensure_clean | |
399 | set -x | |
400 | git pull -s subtree "$@" | |
0ca71b37 AP |
401 | } |
402 | ||
13648af5 | 403 | "cmd_$command" "$@" |