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