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#
7if [ $# -eq 0 ]; then
8 set -- -h
9fi
10OPTS_SPEC="\
11git subtree split [options...] <commit...> -- <path>
12git subtree merge
13
14git subtree does foo and bar!
15--
16h,help show the help
17q quiet
18v verbose
19onto= try connecting new tree to an existing one
20rejoin merge the new branch back into HEAD
21ignore-joins ignore prior --rejoin commits
22"
23eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
24. git-sh-setup
25require_work_tree
26
27quiet=
28command=
29onto=
30rejoin=
31ignore_joins=
32
33debug()
34{
35 if [ -z "$quiet" ]; then
36 echo "$@" >&2
37 fi
38}
39
40assert()
41{
42 if "$@"; then
43 :
44 else
45 die "assertion failed: " "$@"
46 fi
47}
48
49
50#echo "Options: $*"
51
52while [ $# -gt 0 ]; do
53 opt="$1"
54 shift
55 case "$opt" in
56 -q) quiet=1 ;;
57 --onto) onto="$1"; shift ;;
58 --no-onto) onto= ;;
59 --rejoin) rejoin=1 ;;
60 --no-rejoin) rejoin= ;;
61 --ignore-joins) ignore_joins=1 ;;
62 --no-ignore-joins) ignore_joins= ;;
63 --) break ;;
64 esac
65done
66
67command="$1"
68shift
69case "$command" in
70 split|merge) ;;
71 *) die "Unknown command '$command'" ;;
72esac
73
74revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $?
75dirs="$(git rev-parse --sq --no-revs --no-flags "$@")" || exit $?
76
77#echo "dirs is {$dirs}"
78eval $(echo set -- $dirs)
79if [ "$#" -ne 1 ]; then
80 die "Must provide exactly one subtree dir (got $#)"
81fi
82dir="$1"
83
84debug "command: {$command}"
85debug "quiet: {$quiet}"
86debug "revs: {$revs}"
87debug "dir: {$dir}"
88
89cache_setup()
90{
91 cachedir="$GIT_DIR/subtree-cache/$$"
92 rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
93 mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
94 debug "Using cachedir: $cachedir" >&2
95}
96
97cache_get()
98{
99 for oldrev in $*; do
100 if [ -r "$cachedir/$oldrev" ]; then
101 read newrev <"$cachedir/$oldrev"
102 echo $newrev
103 fi
104 done
105}
106
107cache_set()
108{
109 oldrev="$1"
110 newrev="$2"
111 if [ "$oldrev" != "latest_old" \
112 -a "$oldrev" != "latest_new" \
113 -a -e "$cachedir/$oldrev" ]; then
114 die "cache for $oldrev already exists!"
115 fi
116 echo "$newrev" >"$cachedir/$oldrev"
117}
118
119find_existing_splits()
120{
121 debug "Looking for prior splits..."
122 dir="$1"
123 revs="$2"
124 git log --grep="^git-subtree-dir: $dir\$" \
125 --pretty=format:'%s%n%n%b%nEND' "$revs" |
126 while read a b junk; do
127 case "$a" in
128 git-subtree-mainline:) main="$b" ;;
129 git-subtree-split:) sub="$b" ;;
130 *)
131 if [ -n "$main" -a -n "$sub" ]; then
132 debug " Prior: $main -> $sub"
133 cache_set $main $sub
134 echo "^$main^ ^$sub^"
135 main=
136 sub=
137 fi
138 ;;
139 esac
140 done
141}
142
143copy_commit()
144{
145 # We're doing to set some environment vars here, so
146 # do it in a subshell to get rid of them safely later
147 git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
148 (
149 read GIT_AUTHOR_NAME
150 read GIT_AUTHOR_EMAIL
151 read GIT_AUTHOR_DATE
152 read GIT_COMMITTER_NAME
153 read GIT_COMMITTER_EMAIL
154 read GIT_COMMITTER_DATE
155 export GIT_AUTHOR_NAME \
156 GIT_AUTHOR_EMAIL \
157 GIT_AUTHOR_DATE \
158 GIT_COMMITTER_NAME \
159 GIT_COMMITTER_EMAIL \
160 GIT_COMMITTER_DATE
161 (echo -n '*'; cat ) | # FIXME
162 git commit-tree "$2" $3 # reads the rest of stdin
163 ) || die "Can't copy commit $1"
164}
165
166merge_msg()
167{
168 dir="$1"
169 latest_old="$2"
170 latest_new="$3"
171 cat <<-EOF
172 Split '$dir/' into commit '$latest_new'
173
174 git-subtree-dir: $dir
175 git-subtree-mainline: $latest_old
176 git-subtree-split: $latest_new
177 EOF
178}
179
180toptree_for_commit()
181{
182 commit="$1"
183 git log -1 --pretty=format:'%T' "$commit" -- || exit $?
184}
185
186subtree_for_commit()
187{
188 commit="$1"
189 dir="$2"
190 git ls-tree "$commit" -- "$dir" |
191 while read mode type tree name; do
192 assert [ "$name" = "$dir" ]
193 echo $tree
194 break
195 done
196}
197
198tree_changed()
199{
200 tree=$1
201 shift
202 if [ $# -ne 1 ]; then
203 return 0 # weird parents, consider it changed
204 else
205 ptree=$(toptree_for_commit $1)
206 if [ "$ptree" != "$tree" ]; then
207 return 0 # changed
208 else
209 return 1 # not changed
210 fi
211 fi
212}
213
214copy_or_skip()
215{
216 rev="$1"
217 tree="$2"
218 newparents="$3"
219 assert [ -n "$tree" ]
220
221 identical=
222 p=
223 for parent in $newparents; do
224 ptree=$(toptree_for_commit $parent) || exit $?
225 if [ "$ptree" = "$tree" ]; then
226 # an identical parent could be used in place of this rev.
227 identical="$parent"
228 fi
229 if [ -n "$ptree" ]; then
230 parentmatch="$parentmatch$parent"
231 p="$p -p $parent"
232 fi
233 done
234
235 if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
236 echo $identical
237 else
238 copy_commit $rev $tree "$p" || exit $?
239 fi
240}
241
242cmd_split()
243{
244 debug "Splitting $dir..."
245 cache_setup || exit $?
246
247 if [ -n "$onto" ]; then
248 debug "Reading history for --onto=$onto..."
249 git rev-list $onto |
250 while read rev; do
251 # the 'onto' history is already just the subdir, so
252 # any parent we find there can be used verbatim
253 debug " cache: $rev"
254 cache_set $rev $rev
255 done
256 fi
257
258 if [ -n "$ignore_joins" ]; then
259 unrevs=
260 else
261 unrevs="$(find_existing_splits "$dir" "$revs")"
262 fi
263
264 debug "git rev-list --reverse $revs $unrevs"
265 git rev-list --reverse --parents $revs $unrevsx |
266 while read rev parents; do
267 debug
268 debug "Processing commit: $rev"
269 exists=$(cache_get $rev)
270 if [ -n "$exists" ]; then
271 debug " prior: $exists"
272 continue
273 fi
274 debug " parents: $parents"
275 newparents=$(cache_get $parents)
276 debug " newparents: $newparents"
277
278 tree=$(subtree_for_commit $rev "$dir")
279 debug " tree is: $tree"
280 [ -z $tree ] && continue
281
282 newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
283 debug " newrev is: $newrev"
284 cache_set $rev $newrev
285 cache_set latest_new $newrev
286 cache_set latest_old $rev
287 done || exit $?
288 latest_new=$(cache_get latest_new)
289 if [ -z "$latest_new" ]; then
290 die "No new revisions were found"
291 fi
292
293 if [ -n "$rejoin" ]; then
294 debug "Merging split branch into HEAD..."
295 latest_old=$(cache_get latest_old)
296 git merge -s ours \
297 -m "$(merge_msg $dir $latest_old $latest_new)" \
298 $latest_new >&2
299 fi
300 echo $latest_new
301 exit 0
302}
303
304cmd_merge()
305{
306 die "merge command not implemented yet"
307}
308
309"cmd_$command"