2 # Copyright (c) 2008 Benoit Sigoure <tsuna@lrde.epita.fr>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 svn-merge2git [options] [refspec]
20 a,all Do the work on all local the branches
21 d,debug Turn on debug mode (useful if you're hacking the script)
22 n,dry-run Do the entire processing without actually changing anything
23 r,remote Include remote branches (create a local branch for each of them)
24 v,verbose Be more verbose
33 # BRE (Basic RegExp) compatible with `git rev-list --grep' and `sed'. The RE
34 # *must* capture the revision merged in its first group.
35 merge_pattern
='[Mm]erge.*[0-9][0-9]*:\([0-9][0-9]*\)'
37 # BRE which is used to exclude commits whose line that matches of
38 # $merge_pattern also match this pattern.
39 exclude_pattern
='Finish'
41 # BRE which is used to exclude matches in the commit log of potential merge
43 log_exclude_pattern
='Finish.*merge'
45 # extract_svn_branch_name <string>
46 # --------------------------------
47 # Find the string the name of a SVN branch. Put the result in
48 # $svn_branch_name. Assumes SVN "stdlayout".
49 extract_svn_branch_name
()
53 fatal
'extract_svn_branch_name called with empty argument';;
55 extract_svn_branch_name_
'branches' "$1";;
57 extract_svn_branch_name_
'tags' "$1"
58 warn
"found a merge from tag '$svn_branch_name'";;
60 svn_branch_name
='trunk';;
64 # extract_svn_branch_name_ <kind> <string>
65 # ----------------------------------------
66 # Helper of extract_svn_branch_name below to factor some code.
67 # <kind> is probably either 'branches' or 'tags' (for SVN "stdlayout").
68 # Put the result in $svn_branch_name.
69 extract_svn_branch_name_
()
71 # XXX: Assumes that a branch name does contain a whitespace. Fragile.
72 sed_tmp
="s|.*/\\($1/[^ ]*\\).*|\\1|"
73 svn_branch_name
=`echo "$2" | sed "$sed_tmp"`
80 # print <msg> on stderr and exit 1
89 # print <msg> on stderr
92 echo "$me: warning: $*" >&2
93 warnings
=$
(($warnings + 1))
98 # Print <msg> when verbose mode is enabled.
101 $opt_verbose && echo "$*"
106 # Print <msg> when debug mode is enabled.
109 $opt_debug && echo "$*"
112 # find_merge_parent <ref> <merge-line>
113 # ------------------------------------
114 # Return (in $merge_parent) the sha1 of the commit that has been merged in by
115 # <ref>. <merge-line> must be a line extracted from the commit message of
116 # <ref> and will be used to extract the SVN revision merged. For instance, if
117 # <ref> is a SVN merge of merge-line='Merge -r42:51 in branch foo', this
118 # function puts the sha1 of the first commit the revision of which is <= 51
119 # which happens to be in branch foo in $merge_parent.
120 # If the name of the branch being merged couldn't be found, $merge_parent
121 # contains 'unknown'.
124 # Find the first line that matches $merge_pattern, do the substitution and
125 # quit. Ignore all the other lines.
126 sed_tmp
="s/.*$merge_pattern.*/\\1/"
127 svn_merge_to
=`echo "$2" | sed "$sed_tmp"`
128 case $svn_merge_to in #(
130 fatal
"invalid SVN revision '$svn_merge_to' found in $1";;
132 # Now $svn_merge_to is not necessarily a commit that took part of the
133 # merge. For instance, you can merge -r42:51 https://.../branches/foo
134 # even if the last commit in branch foo is at r46. So it's utterly
135 # important that we find the last commit on the branch being merged the
136 # revision of which must be <= $svn_merge_to (which is 51 in this example).
137 extract_svn_branch_name
"$2"
138 if test -z "$svn_branch_name"; then
139 merge_parent
='unknown'
142 svn_merge_parent
=`git rev-list --all --header \
143 --grep="^ *git-svn-id: .*/$svn_branch_name@[0-9]* [-0-9a-f]*\\$" |
144 perl -we 'my $rev = 0; my $t = 0;
146 m{^ *git-svn-id: .*/\Q'"$svn_branch_name"'\E@([0-9]+) [-0-9a-f]+$}
149 $rev = $t if $rev < $t and $t <= '"$svn_merge_to"';
153 test $rv -eq 0 || fatal
"perl returned $rv"
154 if $opt_verbose; then
155 if test "$svn_merge_to" -eq "$svn_merge_parent"; then
158 verb_tmp
=" (in fact r$svn_merge_parent)"
161 verb
" $1 is merging SVN r$svn_merge_to$verb_tmp from branch $svn_branch_name"
162 # Now find the sha1 of the merge parent.
163 merge_parent
=`git rev-list --all \
164 --grep="^ *git-svn-id: .*@$svn_merge_parent [-0-9a-f]*\\$"`
166 test $rv -eq 0 || fatal
"git rev-list returned $rv"
169 # create_graft <ref> <merge-parent>
170 # ---------------------------------
171 # Add <merge-parent> as 2nd parent of the commit designated by <ref>.
174 # --parents will print $1 along with its current parents.
175 grafted_commit
=`git rev-list --no-walk --parents "$1"`
177 test $rv -eq 0 || fatal
"git rev-list returned $rv"
178 graft_merge_parent
=$2
180 case $grafted_commit in #(
181 *"$graft_merge_parent"*)
182 debug
" not grafting commit $1: $graft_merge_parent is already a parent ($grafted_commit)"
186 graft
="$grafted_commit $graft_merge_parent"
187 existing_graft
=`grep "^$1" "$GIT_DIR/info/grafts"`
188 if test $?
-eq 0; then
189 if test x
"$existing_graft" != x
"$graft"; then
190 fatal
"$1 is already graft ($existing_graft)\
191 and the graft is different than what I was going to graft ($graft)"
193 debug
" not grafting commit $1: already properly grafted"
196 debug
" grafting commit $1: add parent $graft_merge_parent"
197 $opt_dryrun && return 0
198 nconverted
=$
((nconverted
+ 1))
199 echo >>"$GIT_DIR/info/grafts" "$graft" \
200 || fatal
"Failed to add a graft in $GIT_DIR/info/grafts"
205 # Make *all* the grafts part of the actual history.
208 $opt_dryrun && return 0
209 git filter-branch
--parent-filter cat -- --all
214 # Find all the merge mentionned in the commit messages and make them become
219 verb
" >> Processing merges in the history of $refspec"
221 git rev-list
--no-walk "$refspec" >/dev
/null \
222 || fatal
"'$refspec' does not seem to be a valid refspec"
224 git rev-list
--grep="$merge_pattern" "$refspec" >"$tmp_buf"
226 test $rv -eq 0 || fatal
"git rev-list failed and returned $rv"
227 while read commit
; do
228 merge_log
=`git log --no-walk "$commit"`
230 test $rv -eq 0 || fatal
"git log returned $rv"
231 merge_line
=`echo "$merge_log" | sed "/$merge_pattern/!d;//q"`
233 # Maybe skip the commit if it matches $exclude_pattern or
234 # $log_exclude_pattern (in which case it's not a merge)
236 && echo "$merge_line" |
grep -- "$exclude_pattern" >/dev
/null
; then
237 verb
" skipping $commit whose log merge-line is: $merge_line"
240 if $has_log_exclude \
241 && echo "$merge_log" |
grep -- "$log_exclude_pattern" >/dev
/null
; then
242 verb
" skipping $commit whose log is:"
243 $opt_verbose && echo "$merge_log" |
sed 's/^/ | /'
247 nmerge
=$
(($nmerge + 1))
248 verb
" $commit is a merge commit, log says:
251 find_merge_parent
"$commit" "$merge_line"
253 case $merge_parent in #(
255 warn
"could not find the merge parent of $commit"
258 fatal
"invalid merge_parent: '$merge_parent'";;
261 create_graft
"$commit" "$merge_parent"
262 test $?
-eq 0 || fatal
"failed to create a graft for commit $commit"
266 # ------------------ #
267 # `main' starts here #
268 # ------------------ #
270 test -d "$TMPDIR" || fatal
"TMPDIR='$TMPDIR' is not a directory"
271 test -w "$TMPDIR" || fatal
"TMPDIR='$TMPDIR' is not writable"
272 tmp_buf
=`mktemp "$TMPDIR/$me.XXXXXX"`
273 # Clean up temp file upon exit.
274 trap "exit_status=$?; rm -f $tmp_buf; exit \$exit_status" 0
276 # Parse the options passed to the script.
277 # Initialize the defaults
306 git for-each-ref
--shell --format='ref=%(refname)' refs
/heads
>"$tmp_buf"
308 test $rv -eq 0 || fatal
"git for-each-ref failed and returned $rv"
317 git for-each-ref
--shell --format='ref=%(refname)' refs
/remotes
>"$tmp_buf"
319 test $rv -eq 0 || fatal
"git for-each-ref failed and returned $rv"
323 branch
=`basename "$ref"`
325 HEAD
) # Skip branches named `HEAD' (which does happen)
326 continue;; # because they create ambiguities.
328 # if the local $branch does not already exist, we create one
329 exists
=`git rev-list --no-walk refs/heads/"$branch" 2>/dev/null`
330 if test -z "$exists"; then # the $branch does not locally exist
331 verb
"creating branch '$branch' from '$ref'"
332 git branch
"$branch" "$ref" \
333 || fatal
"could not create branch '$branch' from '$ref'"
334 else # there already is a local $branch
335 sha1
=`git rev-list --no-walk "$ref"`
336 # Maybe the existing local $branch is identical to the remote $ref?
337 if test "$sha1" = "$exists"; then # OK, local = remote
338 verb
"branch '$branch' is already properly initialized to '$ref'"
339 set "$@" "refs/heads/$branch"
340 else # KO, local != remote
341 warn
"there already exists a local branch '$branch'
342 and it is at $exists whereas the remote branch '$ref'
343 is at $sha1 so I'm skipping it..."
349 # No refspec given => work on HEAD
350 test -z "$*" && set HEAD
352 if test -z "$exclude_pattern"; then
358 if test -z "$log_exclude_pattern"; then
359 has_log_exclude
=false
369 echo ">>> processed $nconverted/$nmerge merges in $refspec"
374 test $warnings -eq 0 || warn
"job completed with $warnings warnings"