Code

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