##// END OF EJS Templates
convert: tell the source repository when a rev has been converted...
Bryan O'Sullivan -
r5554:2147a734 default
parent child Browse files
Show More
@@ -0,0 +1,66 b''
1 #!/bin/sh
2
3 "$TESTDIR/hghave" svn svn-bindings || exit 80
4
5 fix_path()
6 {
7 tr '\\' /
8 }
9
10 echo "[extensions]" >> $HGRCPATH
11 echo "convert = " >> $HGRCPATH
12
13 svnpath=`pwd`/svn-repo
14 svnadmin create $svnpath
15
16 cat > $svnpath/hooks/pre-revprop-change <<'EOF'
17 #!/bin/sh
18
19 REPOS="$1"
20 REV="$2"
21 USER="$3"
22 PROPNAME="$4"
23 ACTION="$5"
24
25 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
26 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
27 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
28
29 echo "Changing prohibited revision property" >&2
30 exit 1
31 EOF
32 chmod +x $svnpath/hooks/pre-revprop-change
33
34 svnurl=file://$svnpath
35 svn co $svnurl $svnpath-wc
36
37 cd $svnpath-wc
38 echo a > a
39 svn add a
40 svn ci -m'added a' a
41
42 cd ..
43
44 echo % initial roundtrip
45 hg convert -s svn -d hg $svnpath-wc $svnpath-hg | grep -v initializing
46 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
47
48 echo % second roundtrip should do nothing
49 hg convert -s svn -d hg $svnpath-wc $svnpath-hg
50 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
51
52 echo % new hg rev
53
54 hg clone $svnpath-hg $svnpath-work
55 echo b > $svnpath-work/b
56 hg --cwd $svnpath-work add b
57 hg --cwd $svnpath-work ci -mb
58
59 echo % echo hg to svn
60 hg --cwd $svnpath-hg pull -q $svnpath-work
61 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
62
63 echo % svn back to hg should do nothing
64 hg convert -s svn -d hg $svnpath-wc $svnpath-hg
65 echo % hg back to svn should do nothing
66 hg convert -s hg -d svn $svnpath-hg $svnpath-wc
@@ -0,0 +1,35 b''
1 Checked out revision 0.
2 A a
3 Adding a
4 Transmitting file data .
5 Committed revision 1.
6 % initial roundtrip
7 scanning source...
8 sorting...
9 converting...
10 0 added a
11 scanning source...
12 sorting...
13 converting...
14 % second roundtrip should do nothing
15 scanning source...
16 sorting...
17 converting...
18 scanning source...
19 sorting...
20 converting...
21 % new hg rev
22 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
23 % echo hg to svn
24 scanning source...
25 sorting...
26 converting...
27 0 b
28 % svn back to hg should do nothing
29 scanning source...
30 sorting...
31 converting...
32 % hg back to svn should do nothing
33 scanning source...
34 sorting...
35 converting...
@@ -1,382 +1,383 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from common import NoRepo, SKIPREV, converter_source, converter_sink, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import debugsvnlog, svn_source, svn_sink
14 14 import filemap
15 15
16 16 import os, shutil
17 17 from mercurial import hg, ui, util, commands
18 18 from mercurial.i18n import _
19 19
20 20 commands.norepo += " convert debugsvnlog"
21 21
22 22 source_converters = [
23 23 ('cvs', convert_cvs),
24 24 ('git', convert_git),
25 25 ('svn', svn_source),
26 26 ('hg', mercurial_source),
27 27 ('darcs', darcs_source),
28 28 ]
29 29
30 30 sink_converters = [
31 31 ('hg', mercurial_sink),
32 32 ('svn', svn_sink),
33 33 ]
34 34
35 35 def convertsource(ui, path, type, rev):
36 36 exceptions = []
37 37 for name, source in source_converters:
38 38 try:
39 39 if not type or name == type:
40 40 return source(ui, path, rev)
41 41 except NoRepo, inst:
42 42 exceptions.append(inst)
43 43 if not ui.quiet:
44 44 for inst in exceptions:
45 45 ui.write(_("%s\n") % inst)
46 46 raise util.Abort('%s: unknown repository type' % path)
47 47
48 48 def convertsink(ui, path, type):
49 49 for name, sink in sink_converters:
50 50 try:
51 51 if not type or name == type:
52 52 return sink(ui, path)
53 53 except NoRepo, inst:
54 54 ui.note(_("convert: %s\n") % inst)
55 55 raise util.Abort('%s: unknown repository type' % path)
56 56
57 57 class converter(object):
58 58 def __init__(self, ui, source, dest, revmapfile, opts):
59 59
60 60 self.source = source
61 61 self.dest = dest
62 62 self.ui = ui
63 63 self.opts = opts
64 64 self.commitcache = {}
65 65 self.authors = {}
66 66 self.authorfile = None
67 67
68 68 self.map = mapfile(ui, revmapfile)
69 69
70 70 # Read first the dst author map if any
71 71 authorfile = self.dest.authorfile()
72 72 if authorfile and os.path.exists(authorfile):
73 73 self.readauthormap(authorfile)
74 74 # Extend/Override with new author map if necessary
75 75 if opts.get('authors'):
76 76 self.readauthormap(opts.get('authors'))
77 77 self.authorfile = self.dest.authorfile()
78 78
79 79 def walktree(self, heads):
80 80 '''Return a mapping that identifies the uncommitted parents of every
81 81 uncommitted changeset.'''
82 82 visit = heads
83 83 known = {}
84 84 parents = {}
85 85 while visit:
86 86 n = visit.pop(0)
87 87 if n in known or n in self.map: continue
88 88 known[n] = 1
89 89 commit = self.cachecommit(n)
90 90 parents[n] = []
91 91 for p in commit.parents:
92 92 parents[n].append(p)
93 93 visit.append(p)
94 94
95 95 return parents
96 96
97 97 def toposort(self, parents):
98 98 '''Return an ordering such that every uncommitted changeset is
99 99 preceeded by all its uncommitted ancestors.'''
100 100 visit = parents.keys()
101 101 seen = {}
102 102 children = {}
103 103
104 104 while visit:
105 105 n = visit.pop(0)
106 106 if n in seen: continue
107 107 seen[n] = 1
108 108 # Ensure that nodes without parents are present in the 'children'
109 109 # mapping.
110 110 children.setdefault(n, [])
111 111 for p in parents[n]:
112 112 if not p in self.map:
113 113 visit.append(p)
114 114 children.setdefault(p, []).append(n)
115 115
116 116 s = []
117 117 removed = {}
118 118 visit = children.keys()
119 119 while visit:
120 120 n = visit.pop(0)
121 121 if n in removed: continue
122 122 dep = 0
123 123 if n in parents:
124 124 for p in parents[n]:
125 125 if p in self.map: continue
126 126 if p not in removed:
127 127 # we're still dependent
128 128 visit.append(n)
129 129 dep = 1
130 130 break
131 131
132 132 if not dep:
133 133 # all n's parents are in the list
134 134 removed[n] = 1
135 135 if n not in self.map:
136 136 s.append(n)
137 137 if n in children:
138 138 for c in children[n]:
139 139 visit.insert(0, c)
140 140
141 141 if self.opts.get('datesort'):
142 142 depth = {}
143 143 for n in s:
144 144 depth[n] = 0
145 145 pl = [p for p in self.commitcache[n].parents
146 146 if p not in self.map]
147 147 if pl:
148 148 depth[n] = max([depth[p] for p in pl]) + 1
149 149
150 150 s = [(depth[n], self.commitcache[n].date, n) for n in s]
151 151 s.sort()
152 152 s = [e[2] for e in s]
153 153
154 154 return s
155 155
156 156 def writeauthormap(self):
157 157 authorfile = self.authorfile
158 158 if authorfile:
159 159 self.ui.status('Writing author map file %s\n' % authorfile)
160 160 ofile = open(authorfile, 'w+')
161 161 for author in self.authors:
162 162 ofile.write("%s=%s\n" % (author, self.authors[author]))
163 163 ofile.close()
164 164
165 165 def readauthormap(self, authorfile):
166 166 afile = open(authorfile, 'r')
167 167 for line in afile:
168 168 try:
169 169 srcauthor = line.split('=')[0].strip()
170 170 dstauthor = line.split('=')[1].strip()
171 171 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
172 172 self.ui.status(
173 173 'Overriding mapping for author %s, was %s, will be %s\n'
174 174 % (srcauthor, self.authors[srcauthor], dstauthor))
175 175 else:
176 176 self.ui.debug('Mapping author %s to %s\n'
177 177 % (srcauthor, dstauthor))
178 178 self.authors[srcauthor] = dstauthor
179 179 except IndexError:
180 180 self.ui.warn(
181 181 'Ignoring bad line in author file map %s: %s\n'
182 182 % (authorfile, line))
183 183 afile.close()
184 184
185 185 def cachecommit(self, rev):
186 186 commit = self.source.getcommit(rev)
187 187 commit.author = self.authors.get(commit.author, commit.author)
188 188 self.commitcache[rev] = commit
189 189 return commit
190 190
191 191 def copy(self, rev):
192 192 commit = self.commitcache[rev]
193 193 do_copies = hasattr(self.dest, 'copyfile')
194 194 filenames = []
195 195
196 196 changes = self.source.getchanges(rev)
197 197 if isinstance(changes, basestring):
198 198 if changes == SKIPREV:
199 199 dest = SKIPREV
200 200 else:
201 201 dest = self.map[changes]
202 202 self.map[rev] = dest
203 203 return
204 204 files, copies = changes
205 205 parents = [self.map[r] for r in commit.parents]
206 206 if commit.parents:
207 207 prev = commit.parents[0]
208 208 if prev not in self.commitcache:
209 209 self.cachecommit(prev)
210 210 pbranch = self.commitcache[prev].branch
211 211 else:
212 212 pbranch = None
213 213 self.dest.setbranch(commit.branch, pbranch, parents)
214 214 for f, v in files:
215 215 filenames.append(f)
216 216 try:
217 217 data = self.source.getfile(f, v)
218 218 except IOError, inst:
219 219 self.dest.delfile(f)
220 220 else:
221 221 e = self.source.getmode(f, v)
222 222 self.dest.putfile(f, e, data)
223 223 if do_copies:
224 224 if f in copies:
225 225 copyf = copies[f]
226 226 # Merely marks that a copy happened.
227 227 self.dest.copyfile(copyf, f)
228 228
229 229 newnode = self.dest.putcommit(filenames, parents, commit)
230 self.source.converted(rev, newnode)
230 231 self.map[rev] = newnode
231 232
232 233 def convert(self):
233 234 try:
234 235 self.source.before()
235 236 self.dest.before()
236 237 self.source.setrevmap(self.map)
237 238 self.ui.status("scanning source...\n")
238 239 heads = self.source.getheads()
239 240 parents = self.walktree(heads)
240 241 self.ui.status("sorting...\n")
241 242 t = self.toposort(parents)
242 243 num = len(t)
243 244 c = None
244 245
245 246 self.ui.status("converting...\n")
246 247 for c in t:
247 248 num -= 1
248 249 desc = self.commitcache[c].desc
249 250 if "\n" in desc:
250 251 desc = desc.splitlines()[0]
251 252 self.ui.status("%d %s\n" % (num, desc))
252 253 self.copy(c)
253 254
254 255 tags = self.source.gettags()
255 256 ctags = {}
256 257 for k in tags:
257 258 v = tags[k]
258 259 if self.map.get(v, SKIPREV) != SKIPREV:
259 260 ctags[k] = self.map[v]
260 261
261 262 if c and ctags:
262 263 nrev = self.dest.puttags(ctags)
263 264 # write another hash correspondence to override the previous
264 265 # one so we don't end up with extra tag heads
265 266 if nrev:
266 267 self.map[c] = nrev
267 268
268 269 self.writeauthormap()
269 270 finally:
270 271 self.cleanup()
271 272
272 273 def cleanup(self):
273 274 try:
274 275 self.dest.after()
275 276 finally:
276 277 self.source.after()
277 278 self.map.close()
278 279
279 280 def convert(ui, src, dest=None, revmapfile=None, **opts):
280 281 """Convert a foreign SCM repository to a Mercurial one.
281 282
282 283 Accepted source formats:
283 284 - Mercurial
284 285 - CVS
285 286 - Darcs
286 287 - git
287 288 - Subversion
288 289
289 290 Accepted destination formats:
290 291 - Mercurial
291 292 - Subversion (history on branches is not preserved)
292 293
293 294 If no revision is given, all revisions will be converted. Otherwise,
294 295 convert will only import up to the named revision (given in a format
295 296 understood by the source).
296 297
297 298 If no destination directory name is specified, it defaults to the
298 299 basename of the source with '-hg' appended. If the destination
299 300 repository doesn't exist, it will be created.
300 301
301 302 If <MAPFILE> isn't given, it will be put in a default location
302 303 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
303 304 file that maps each source commit ID to the destination ID for
304 305 that revision, like so:
305 306 <source ID> <destination ID>
306 307
307 308 If the file doesn't exist, it's automatically created. It's updated
308 309 on each commit copied, so convert-repo can be interrupted and can
309 310 be run repeatedly to copy new commits.
310 311
311 312 The [username mapping] file is a simple text file that maps each source
312 313 commit author to a destination commit author. It is handy for source SCMs
313 314 that use unix logins to identify authors (eg: CVS). One line per author
314 315 mapping and the line format is:
315 316 srcauthor=whatever string you want
316 317
317 318 The filemap is a file that allows filtering and remapping of files
318 319 and directories. Comment lines start with '#'. Each line can
319 320 contain one of the following directives:
320 321
321 322 include path/to/file
322 323
323 324 exclude path/to/file
324 325
325 326 rename from/file to/file
326 327
327 328 The 'include' directive causes a file, or all files under a
328 329 directory, to be included in the destination repository, and the
329 330 exclusion of all other files and dirs not explicitely included.
330 331 The 'exclude' directive causes files or directories to be omitted.
331 332 The 'rename' directive renames a file or directory. To rename from a
332 333 subdirectory into the root of the repository, use '.' as the path to
333 334 rename to.
334 335 """
335 336
336 337 util._encoding = 'UTF-8'
337 338
338 339 if not dest:
339 340 dest = hg.defaultdest(src) + "-hg"
340 341 ui.status("assuming destination %s\n" % dest)
341 342
342 343 destc = convertsink(ui, dest, opts.get('dest_type'))
343 344
344 345 try:
345 346 srcc = convertsource(ui, src, opts.get('source_type'),
346 347 opts.get('rev'))
347 348 except Exception:
348 349 for path in destc.created:
349 350 shutil.rmtree(path, True)
350 351 raise
351 352
352 353 fmap = opts.get('filemap')
353 354 if fmap:
354 355 srcc = filemap.filemap_source(ui, srcc, fmap)
355 356 destc.setfilemapmode(True)
356 357
357 358 if not revmapfile:
358 359 try:
359 360 revmapfile = destc.revmapfile()
360 361 except:
361 362 revmapfile = os.path.join(destc, "map")
362 363
363 364 c = converter(ui, srcc, destc, revmapfile, opts)
364 365 c.convert()
365 366
366 367
367 368 cmdtable = {
368 369 "convert":
369 370 (convert,
370 371 [('A', 'authors', '', 'username mapping filename'),
371 372 ('d', 'dest-type', '', 'destination repository type'),
372 373 ('', 'filemap', '', 'remap file names using contents of file'),
373 374 ('r', 'rev', '', 'import up to target revision REV'),
374 375 ('s', 'source-type', '', 'source repository type'),
375 376 ('', 'datesort', None, 'try to sort changesets by date')],
376 377 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
377 378 "debugsvnlog":
378 379 (debugsvnlog,
379 380 [],
380 381 'hg debugsvnlog'),
381 382 }
382 383
@@ -1,292 +1,297 b''
1 1 # common code for the convert extension
2 2 import base64, errno
3 3 import cPickle as pickle
4 4 from mercurial import util
5 5 from mercurial.i18n import _
6 6
7 7 def encodeargs(args):
8 8 def encodearg(s):
9 9 lines = base64.encodestring(s)
10 10 lines = [l.splitlines()[0] for l in lines]
11 11 return ''.join(lines)
12 12
13 13 s = pickle.dumps(args)
14 14 return encodearg(s)
15 15
16 16 def decodeargs(s):
17 17 s = base64.decodestring(s)
18 18 return pickle.loads(s)
19 19
20 20 def checktool(exe, name=None):
21 21 name = name or exe
22 22 if not util.find_exe(exe):
23 23 raise util.Abort('cannot find required "%s" tool' % name)
24 24
25 25 class NoRepo(Exception): pass
26 26
27 27 SKIPREV = 'SKIP'
28 28
29 29 class commit(object):
30 30 def __init__(self, author, date, desc, parents, branch=None, rev=None,
31 31 extra={}):
32 32 self.author = author
33 33 self.date = date
34 34 self.desc = desc
35 35 self.parents = parents
36 36 self.branch = branch
37 37 self.rev = rev
38 38 self.extra = extra
39 39
40 40 class converter_source(object):
41 41 """Conversion source interface"""
42 42
43 43 def __init__(self, ui, path, rev=None):
44 44 """Initialize conversion source (or raise NoRepo("message")
45 45 exception if path is not a valid repository)"""
46 46 self.ui = ui
47 47 self.path = path
48 48 self.rev = rev
49 49
50 50 self.encoding = 'utf-8'
51 51
52 52 def before(self):
53 53 pass
54 54
55 55 def after(self):
56 56 pass
57 57
58 58 def setrevmap(self, revmap):
59 59 """set the map of already-converted revisions"""
60 60 pass
61 61
62 62 def getheads(self):
63 63 """Return a list of this repository's heads"""
64 64 raise NotImplementedError()
65 65
66 66 def getfile(self, name, rev):
67 67 """Return file contents as a string"""
68 68 raise NotImplementedError()
69 69
70 70 def getmode(self, name, rev):
71 71 """Return file mode, eg. '', 'x', or 'l'"""
72 72 raise NotImplementedError()
73 73
74 74 def getchanges(self, version):
75 75 """Returns a tuple of (files, copies)
76 76 Files is a sorted list of (filename, id) tuples for all files changed
77 77 in version, where id is the source revision id of the file.
78 78
79 79 copies is a dictionary of dest: source
80 80 """
81 81 raise NotImplementedError()
82 82
83 83 def getcommit(self, version):
84 84 """Return the commit object for version"""
85 85 raise NotImplementedError()
86 86
87 87 def gettags(self):
88 88 """Return the tags as a dictionary of name: revision"""
89 89 raise NotImplementedError()
90 90
91 91 def recode(self, s, encoding=None):
92 92 if not encoding:
93 93 encoding = self.encoding or 'utf-8'
94 94
95 95 if isinstance(s, unicode):
96 96 return s.encode("utf-8")
97 97 try:
98 98 return s.decode(encoding).encode("utf-8")
99 99 except:
100 100 try:
101 101 return s.decode("latin-1").encode("utf-8")
102 102 except:
103 103 return s.decode(encoding, "replace").encode("utf-8")
104 104
105 105 def getchangedfiles(self, rev, i):
106 106 """Return the files changed by rev compared to parent[i].
107 107
108 108 i is an index selecting one of the parents of rev. The return
109 109 value should be the list of files that are different in rev and
110 110 this parent.
111 111
112 112 If rev has no parents, i is None.
113 113
114 114 This function is only needed to support --filemap
115 115 """
116 116 raise NotImplementedError()
117 117
118 def converted(self, rev, sinkrev):
119 '''Notify the source that a revision has been converted.'''
120 pass
121
122
118 123 class converter_sink(object):
119 124 """Conversion sink (target) interface"""
120 125
121 126 def __init__(self, ui, path):
122 127 """Initialize conversion sink (or raise NoRepo("message")
123 128 exception if path is not a valid repository)
124 129
125 130 created is a list of paths to remove if a fatal error occurs
126 131 later"""
127 132 self.ui = ui
128 133 self.path = path
129 134 self.created = []
130 135
131 136 def getheads(self):
132 137 """Return a list of this repository's heads"""
133 138 raise NotImplementedError()
134 139
135 140 def revmapfile(self):
136 141 """Path to a file that will contain lines
137 142 source_rev_id sink_rev_id
138 143 mapping equivalent revision identifiers for each system."""
139 144 raise NotImplementedError()
140 145
141 146 def authorfile(self):
142 147 """Path to a file that will contain lines
143 148 srcauthor=dstauthor
144 149 mapping equivalent authors identifiers for each system."""
145 150 return None
146 151
147 152 def putfile(self, f, e, data):
148 153 """Put file for next putcommit().
149 154 f: path to file
150 155 e: '', 'x', or 'l' (regular file, executable, or symlink)
151 156 data: file contents"""
152 157 raise NotImplementedError()
153 158
154 159 def delfile(self, f):
155 160 """Delete file for next putcommit().
156 161 f: path to file"""
157 162 raise NotImplementedError()
158 163
159 164 def putcommit(self, files, parents, commit):
160 165 """Create a revision with all changed files listed in 'files'
161 166 and having listed parents. 'commit' is a commit object containing
162 167 at a minimum the author, date, and message for this changeset.
163 168 Called after putfile() and delfile() calls. Note that the sink
164 169 repository is not told to update itself to a particular revision
165 170 (or even what that revision would be) before it receives the
166 171 file data."""
167 172 raise NotImplementedError()
168 173
169 174 def puttags(self, tags):
170 175 """Put tags into sink.
171 176 tags: {tagname: sink_rev_id, ...}"""
172 177 raise NotImplementedError()
173 178
174 179 def setbranch(self, branch, pbranch, parents):
175 180 """Set the current branch name. Called before the first putfile
176 181 on the branch.
177 182 branch: branch name for subsequent commits
178 183 pbranch: branch name of parent commit
179 184 parents: destination revisions of parent"""
180 185 pass
181 186
182 187 def setfilemapmode(self, active):
183 188 """Tell the destination that we're using a filemap
184 189
185 190 Some converter_sources (svn in particular) can claim that a file
186 191 was changed in a revision, even if there was no change. This method
187 192 tells the destination that we're using a filemap and that it should
188 193 filter empty revisions.
189 194 """
190 195 pass
191 196
192 197 def before(self):
193 198 pass
194 199
195 200 def after(self):
196 201 pass
197 202
198 203
199 204 class commandline(object):
200 205 def __init__(self, ui, command):
201 206 self.ui = ui
202 207 self.command = command
203 208
204 209 def prerun(self):
205 210 pass
206 211
207 212 def postrun(self):
208 213 pass
209 214
210 215 def _run(self, cmd, *args, **kwargs):
211 216 cmdline = [self.command, cmd] + list(args)
212 217 for k, v in kwargs.iteritems():
213 218 if len(k) == 1:
214 219 cmdline.append('-' + k)
215 220 else:
216 221 cmdline.append('--' + k.replace('_', '-'))
217 222 try:
218 223 if len(k) == 1:
219 224 cmdline.append('' + v)
220 225 else:
221 226 cmdline[-1] += '=' + v
222 227 except TypeError:
223 228 pass
224 229 cmdline = [util.shellquote(arg) for arg in cmdline]
225 230 cmdline += ['<', util.nulldev]
226 231 cmdline = ' '.join(cmdline)
227 232 self.ui.debug(cmdline, '\n')
228 233
229 234 self.prerun()
230 235 try:
231 236 return util.popen(cmdline)
232 237 finally:
233 238 self.postrun()
234 239
235 240 def run(self, cmd, *args, **kwargs):
236 241 fp = self._run(cmd, *args, **kwargs)
237 242 output = fp.read()
238 243 self.ui.debug(output)
239 244 return output, fp.close()
240 245
241 246 def checkexit(self, status, output=''):
242 247 if status:
243 248 if output:
244 249 self.ui.warn(_('%s error:\n') % self.command)
245 250 self.ui.warn(output)
246 251 msg = util.explain_exit(status)[0]
247 252 raise util.Abort(_('%s %s') % (self.command, msg))
248 253
249 254 def run0(self, cmd, *args, **kwargs):
250 255 output, status = self.run(cmd, *args, **kwargs)
251 256 self.checkexit(status, output)
252 257 return output
253 258
254 259
255 260 class mapfile(dict):
256 261 def __init__(self, ui, path):
257 262 super(mapfile, self).__init__()
258 263 self.ui = ui
259 264 self.path = path
260 265 self.fp = None
261 266 self.order = []
262 267 self._read()
263 268
264 269 def _read(self):
265 270 try:
266 271 fp = open(self.path, 'r')
267 272 except IOError, err:
268 273 if err.errno != errno.ENOENT:
269 274 raise
270 275 return
271 276 for line in fp:
272 277 key, value = line[:-1].split(' ', 1)
273 278 if key not in self:
274 279 self.order.append(key)
275 280 super(mapfile, self).__setitem__(key, value)
276 281 fp.close()
277 282
278 283 def __setitem__(self, key, value):
279 284 if self.fp is None:
280 285 try:
281 286 self.fp = open(self.path, 'a')
282 287 except IOError, err:
283 288 raise util.Abort(_('could not open map file %r: %s') %
284 289 (self.path, err.strerror))
285 290 self.fp.write('%s %s\n' % (key, value))
286 291 self.fp.flush()
287 292 super(mapfile, self).__setitem__(key, value)
288 293
289 294 def close(self):
290 295 if self.fp:
291 296 self.fp.close()
292 297 self.fp = None
@@ -1,260 +1,265 b''
1 1 # hg backend for convert extension
2 2
3 3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
4 4 # the whitespace from the ends of commit messages, but new versions
5 5 # do. Changesets created by those older versions, then converted, may
6 6 # thus have different hashes for changesets that are otherwise
7 7 # identical.
8 8
9 9
10 10 import os, time
11 11 from mercurial.i18n import _
12 12 from mercurial.node import *
13 13 from mercurial import hg, lock, revlog, util
14 14
15 15 from common import NoRepo, commit, converter_source, converter_sink
16 16
17 17 class mercurial_sink(converter_sink):
18 18 def __init__(self, ui, path):
19 19 converter_sink.__init__(self, ui, path)
20 20 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
21 21 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
22 22 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
23 23 self.lastbranch = None
24 24 if os.path.isdir(path) and len(os.listdir(path)) > 0:
25 25 try:
26 26 self.repo = hg.repository(self.ui, path)
27 ui.status(_('destination %s is a Mercurial repository\n') %
28 path)
29 27 except hg.RepoError, err:
30 28 ui.print_exc()
31 29 raise NoRepo(err.args[0])
32 30 else:
33 31 try:
34 32 ui.status(_('initializing destination %s repository\n') % path)
35 33 self.repo = hg.repository(self.ui, path, create=True)
36 34 self.created.append(path)
37 35 except hg.RepoError, err:
38 36 ui.print_exc()
39 37 raise NoRepo("could not create hg repo %s as sink" % path)
40 38 self.lock = None
41 39 self.wlock = None
42 40 self.filemapmode = False
43 41
44 42 def before(self):
45 43 self.wlock = self.repo.wlock()
46 44 self.lock = self.repo.lock()
47 45 self.repo.dirstate.clear()
48 46
49 47 def after(self):
50 48 self.repo.dirstate.invalidate()
51 49 self.lock = None
52 50 self.wlock = None
53 51
54 52 def revmapfile(self):
55 53 return os.path.join(self.path, ".hg", "shamap")
56 54
57 55 def authorfile(self):
58 56 return os.path.join(self.path, ".hg", "authormap")
59 57
60 58 def getheads(self):
61 59 h = self.repo.changelog.heads()
62 60 return [ hex(x) for x in h ]
63 61
64 62 def putfile(self, f, e, data):
65 63 self.repo.wwrite(f, data, e)
66 64 if f not in self.repo.dirstate:
67 65 self.repo.dirstate.normallookup(f)
68 66
69 67 def copyfile(self, source, dest):
70 68 self.repo.copy(source, dest)
71 69
72 70 def delfile(self, f):
73 71 try:
74 72 util.unlink(self.repo.wjoin(f))
75 73 #self.repo.remove([f])
76 74 except OSError:
77 75 pass
78 76
79 77 def setbranch(self, branch, pbranch, parents):
80 78 if (not self.clonebranches) or (branch == self.lastbranch):
81 79 return
82 80
83 81 self.lastbranch = branch
84 82 self.after()
85 83 if not branch:
86 84 branch = 'default'
87 85 if not pbranch:
88 86 pbranch = 'default'
89 87
90 88 branchpath = os.path.join(self.path, branch)
91 89 try:
92 90 self.repo = hg.repository(self.ui, branchpath)
93 91 except:
94 92 if not parents:
95 93 self.repo = hg.repository(self.ui, branchpath, create=True)
96 94 else:
97 95 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
98 96 hg.clone(self.ui, os.path.join(self.path, pbranch),
99 97 branchpath, rev=parents, update=False,
100 98 stream=True)
101 99 self.repo = hg.repository(self.ui, branchpath)
102 100 self.before()
103 101
104 102 def putcommit(self, files, parents, commit):
105 103 seen = {}
106 104 pl = []
107 105 for p in parents:
108 106 if p not in seen:
109 107 pl.append(p)
110 108 seen[p] = 1
111 109 parents = pl
112 110 nparents = len(parents)
113 111 if self.filemapmode and nparents == 1:
114 112 m1node = self.repo.changelog.read(bin(parents[0]))[0]
115 113 parent = parents[0]
116 114
117 115 if len(parents) < 2: parents.append("0" * 40)
118 116 if len(parents) < 2: parents.append("0" * 40)
119 117 p2 = parents.pop(0)
120 118
121 119 text = commit.desc
122 120 extra = commit.extra.copy()
123 121 if self.branchnames and commit.branch:
124 122 extra['branch'] = commit.branch
125 123 if commit.rev:
126 124 extra['convert_revision'] = commit.rev
127 125
128 126 while parents:
129 127 p1 = p2
130 128 p2 = parents.pop(0)
131 129 a = self.repo.rawcommit(files, text, commit.author, commit.date,
132 130 bin(p1), bin(p2), extra=extra)
133 131 self.repo.dirstate.clear()
134 132 text = "(octopus merge fixup)\n"
135 133 p2 = hg.hex(self.repo.changelog.tip())
136 134
137 135 if self.filemapmode and nparents == 1:
138 136 man = self.repo.manifest
139 137 mnode = self.repo.changelog.read(bin(p2))[0]
140 138 if not man.cmp(m1node, man.revision(mnode)):
141 139 self.repo.rollback()
142 140 self.repo.dirstate.clear()
143 141 return parent
144 142 return p2
145 143
146 144 def puttags(self, tags):
147 145 try:
148 146 old = self.repo.wfile(".hgtags").read()
149 147 oldlines = old.splitlines(1)
150 148 oldlines.sort()
151 149 except:
152 150 oldlines = []
153 151
154 152 k = tags.keys()
155 153 k.sort()
156 154 newlines = []
157 155 for tag in k:
158 156 newlines.append("%s %s\n" % (tags[tag], tag))
159 157
160 158 newlines.sort()
161 159
162 160 if newlines != oldlines:
163 161 self.ui.status("updating tags\n")
164 162 f = self.repo.wfile(".hgtags", "w")
165 163 f.write("".join(newlines))
166 164 f.close()
167 165 if not oldlines: self.repo.add([".hgtags"])
168 166 date = "%s 0" % int(time.mktime(time.gmtime()))
169 167 extra = {}
170 168 if self.tagsbranch != 'default':
171 169 extra['branch'] = self.tagsbranch
172 170 try:
173 171 tagparent = self.repo.changectx(self.tagsbranch).node()
174 172 except hg.RepoError, inst:
175 173 tagparent = nullid
176 174 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
177 175 date, tagparent, nullid)
178 176 return hex(self.repo.changelog.tip())
179 177
180 178 def setfilemapmode(self, active):
181 179 self.filemapmode = active
182 180
183 181 class mercurial_source(converter_source):
184 182 def __init__(self, ui, path, rev=None):
185 183 converter_source.__init__(self, ui, path, rev)
186 184 try:
187 185 self.repo = hg.repository(self.ui, path)
188 186 # try to provoke an exception if this isn't really a hg
189 187 # repo, but some other bogus compatible-looking url
190 188 if not self.repo.local():
191 189 raise hg.RepoError()
192 190 except hg.RepoError:
193 191 ui.print_exc()
194 192 raise NoRepo("%s is not a local Mercurial repo" % path)
195 193 self.lastrev = None
196 194 self.lastctx = None
197 195 self._changescache = None
196 self.convertfp = None
198 197
199 198 def changectx(self, rev):
200 199 if self.lastrev != rev:
201 200 self.lastctx = self.repo.changectx(rev)
202 201 self.lastrev = rev
203 202 return self.lastctx
204 203
205 204 def getheads(self):
206 205 if self.rev:
207 206 return [hex(self.repo.changectx(self.rev).node())]
208 207 else:
209 208 return [hex(node) for node in self.repo.heads()]
210 209
211 210 def getfile(self, name, rev):
212 211 try:
213 212 return self.changectx(rev).filectx(name).data()
214 213 except revlog.LookupError, err:
215 214 raise IOError(err)
216 215
217 216 def getmode(self, name, rev):
218 217 m = self.changectx(rev).manifest()
219 218 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
220 219
221 220 def getchanges(self, rev):
222 221 ctx = self.changectx(rev)
223 222 if self._changescache and self._changescache[0] == rev:
224 223 m, a, r = self._changescache[1]
225 224 else:
226 225 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
227 226 changes = [(name, rev) for name in m + a + r]
228 227 changes.sort()
229 228 return (changes, self.getcopies(ctx, m + a))
230 229
231 230 def getcopies(self, ctx, files):
232 231 copies = {}
233 232 for name in files:
234 233 try:
235 234 copies[name] = ctx.filectx(name).renamed()[0]
236 235 except TypeError:
237 236 pass
238 237 return copies
239 238
240 239 def getcommit(self, rev):
241 240 ctx = self.changectx(rev)
242 241 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
243 242 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
244 243 desc=ctx.description(), rev=rev, parents=parents,
245 244 branch=ctx.branch(), extra=ctx.extra())
246 245
247 246 def gettags(self):
248 247 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
249 248 return dict([(name, hex(node)) for name, node in tags])
250 249
251 250 def getchangedfiles(self, rev, i):
252 251 ctx = self.changectx(rev)
253 252 i = i or 0
254 253 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
255 254
256 255 if i == 0:
257 256 self._changescache = (rev, changes)
258 257
259 258 return changes[0] + changes[1] + changes[2]
260 259
260 def converted(self, rev, destrev):
261 if self.convertfp is None:
262 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
263 'a')
264 self.convertfp.write('%s %s\n' % (destrev, rev))
265 self.convertfp.flush()
@@ -1,881 +1,902 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4 #
5 5 # Configuration options:
6 6 #
7 7 # convert.svn.trunk
8 8 # Relative path to the trunk (default: "trunk")
9 9 # convert.svn.branches
10 10 # Relative path to tree of branches (default: "branches")
11 11 # convert.svn.tags
12 12 # Relative path to tree of tags (default: "tags")
13 13 #
14 14 # Set these in a hgrc, or on the command line as follows:
15 15 #
16 16 # hg convert --config convert.svn.trunk=wackoname [...]
17 17
18 18 import locale
19 19 import os
20 20 import re
21 21 import sys
22 22 import cPickle as pickle
23 23 import tempfile
24 24
25 25 from mercurial import strutil, util
26 26 from mercurial.i18n import _
27 27
28 28 # Subversion stuff. Works best with very recent Python SVN bindings
29 29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 30 # these bindings.
31 31
32 32 from cStringIO import StringIO
33 33
34 34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 35 from common import commandline, converter_sink, mapfile
36 36
37 37 try:
38 38 from svn.core import SubversionException, Pool
39 39 import svn
40 40 import svn.client
41 41 import svn.core
42 42 import svn.ra
43 43 import svn.delta
44 44 import transport
45 45 except ImportError:
46 46 pass
47 47
48 48 def geturl(path):
49 49 try:
50 50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 51 except SubversionException:
52 52 pass
53 53 if os.path.isdir(path):
54 54 return 'file://%s' % os.path.normpath(os.path.abspath(path))
55 55 return path
56 56
57 57 def optrev(number):
58 58 optrev = svn.core.svn_opt_revision_t()
59 59 optrev.kind = svn.core.svn_opt_revision_number
60 60 optrev.value.number = number
61 61 return optrev
62 62
63 63 class changedpath(object):
64 64 def __init__(self, p):
65 65 self.copyfrom_path = p.copyfrom_path
66 66 self.copyfrom_rev = p.copyfrom_rev
67 67 self.action = p.action
68 68
69 69 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
70 70 strict_node_history=False):
71 71 protocol = -1
72 72 def receiver(orig_paths, revnum, author, date, message, pool):
73 73 if orig_paths is not None:
74 74 for k, v in orig_paths.iteritems():
75 75 orig_paths[k] = changedpath(v)
76 76 pickle.dump((orig_paths, revnum, author, date, message),
77 77 fp, protocol)
78 78
79 79 try:
80 80 # Use an ra of our own so that our parent can consume
81 81 # our results without confusing the server.
82 82 t = transport.SvnRaTransport(url=url)
83 83 svn.ra.get_log(t.ra, paths, start, end, limit,
84 84 discover_changed_paths,
85 85 strict_node_history,
86 86 receiver)
87 87 except SubversionException, (inst, num):
88 88 pickle.dump(num, fp, protocol)
89 89 else:
90 90 pickle.dump(None, fp, protocol)
91 91 fp.close()
92 92
93 93 def debugsvnlog(ui, **opts):
94 94 """Fetch SVN log in a subprocess and channel them back to parent to
95 95 avoid memory collection issues.
96 96 """
97 97 util.set_binary(sys.stdin)
98 98 util.set_binary(sys.stdout)
99 99 args = decodeargs(sys.stdin.read())
100 100 get_log_child(sys.stdout, *args)
101 101
102 102 # SVN conversion code stolen from bzr-svn and tailor
103 103 class svn_source(converter_source):
104 104 def __init__(self, ui, url, rev=None):
105 105 super(svn_source, self).__init__(ui, url, rev=rev)
106 106
107 107 try:
108 108 SubversionException
109 109 except NameError:
110 110 raise NoRepo('Subversion python bindings could not be loaded')
111 111
112 112 self.encoding = locale.getpreferredencoding()
113 113 self.lastrevs = {}
114 114
115 115 latest = None
116 116 try:
117 117 # Support file://path@rev syntax. Useful e.g. to convert
118 118 # deleted branches.
119 119 at = url.rfind('@')
120 120 if at >= 0:
121 121 latest = int(url[at+1:])
122 122 url = url[:at]
123 123 except ValueError, e:
124 124 pass
125 125 self.url = geturl(url)
126 126 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
127 127 try:
128 128 self.transport = transport.SvnRaTransport(url=self.url)
129 129 self.ra = self.transport.ra
130 130 self.ctx = self.transport.client
131 131 self.base = svn.ra.get_repos_root(self.ra)
132 132 self.module = self.url[len(self.base):]
133 133 self.modulemap = {} # revision, module
134 134 self.commits = {}
135 135 self.paths = {}
136 136 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
137 137 except SubversionException, e:
138 138 ui.print_exc()
139 139 raise NoRepo("%s does not look like a Subversion repo" % self.url)
140 140
141 141 if rev:
142 142 try:
143 143 latest = int(rev)
144 144 except ValueError:
145 145 raise util.Abort('svn: revision %s is not an integer' % rev)
146 146
147 147 try:
148 148 self.get_blacklist()
149 149 except IOError, e:
150 150 pass
151 151
152 152 self.last_changed = self.latest(self.module, latest)
153 153
154 154 self.head = self.revid(self.last_changed)
155 155 self._changescache = None
156 156
157 if os.path.exists(os.path.join(url, '.svn/entries')):
158 self.wc = url
159 else:
160 self.wc = None
161 self.convertfp = None
162
157 163 def setrevmap(self, revmap):
158 164 lastrevs = {}
159 165 for revid in revmap.iterkeys():
160 166 uuid, module, revnum = self.revsplit(revid)
161 167 lastrevnum = lastrevs.setdefault(module, revnum)
162 168 if revnum > lastrevnum:
163 169 lastrevs[module] = revnum
164 170 self.lastrevs = lastrevs
165 171
166 172 def exists(self, path, optrev):
167 173 try:
168 174 svn.client.ls(self.url.rstrip('/') + '/' + path,
169 175 optrev, False, self.ctx)
170 176 return True
171 177 except SubversionException, err:
172 178 return False
173 179
174 180 def getheads(self):
175 181 # detect standard /branches, /tags, /trunk layout
176 182 rev = optrev(self.last_changed)
177 183 rpath = self.url.strip('/')
178 184 cfgtrunk = self.ui.config('convert', 'svn.trunk')
179 185 cfgbranches = self.ui.config('convert', 'svn.branches')
180 186 cfgtags = self.ui.config('convert', 'svn.tags')
181 187 trunk = (cfgtrunk or 'trunk').strip('/')
182 188 branches = (cfgbranches or 'branches').strip('/')
183 189 tags = (cfgtags or 'tags').strip('/')
184 190 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
185 191 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
186 192 (trunk, branches, tags))
187 193 oldmodule = self.module
188 194 self.module += '/' + trunk
189 195 lt = self.latest(self.module, self.last_changed)
190 196 self.head = self.revid(lt)
191 197 self.heads = [self.head]
192 198 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
193 199 self.ctx)
194 200 for branch in branchnames.keys():
195 201 if oldmodule:
196 202 module = oldmodule + '/' + branches + '/' + branch
197 203 else:
198 204 module = '/' + branches + '/' + branch
199 205 brevnum = self.latest(module, self.last_changed)
200 206 brev = self.revid(brevnum, module)
201 207 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
202 208 self.heads.append(brev)
203 209
204 210 if oldmodule:
205 211 self.tags = '%s/%s' % (oldmodule, tags)
206 212 else:
207 213 self.tags = '/%s' % tags
208 214
209 215 elif cfgtrunk or cfgbranches or cfgtags:
210 216 raise util.Abort('trunk/branch/tags layout expected, but not found')
211 217 else:
212 218 self.ui.note('working with one branch\n')
213 219 self.heads = [self.head]
214 220 self.tags = tags
215 221 return self.heads
216 222
217 223 def getfile(self, file, rev):
218 224 data, mode = self._getfile(file, rev)
219 225 self.modecache[(file, rev)] = mode
220 226 return data
221 227
222 228 def getmode(self, file, rev):
223 229 return self.modecache[(file, rev)]
224 230
225 231 def getchanges(self, rev):
226 232 if self._changescache and self._changescache[0] == rev:
227 233 return self._changescache[1]
228 234 self._changescache = None
229 235 self.modecache = {}
230 236 (paths, parents) = self.paths[rev]
231 237 files, copies = self.expandpaths(rev, paths, parents)
232 238 files.sort()
233 239 files = zip(files, [rev] * len(files))
234 240
235 241 # caller caches the result, so free it here to release memory
236 242 del self.paths[rev]
237 243 return (files, copies)
238 244
239 245 def getchangedfiles(self, rev, i):
240 246 changes = self.getchanges(rev)
241 247 self._changescache = (rev, changes)
242 248 return [f[0] for f in changes[0]]
243 249
244 250 def getcommit(self, rev):
245 251 if rev not in self.commits:
246 252 uuid, module, revnum = self.revsplit(rev)
247 253 self.module = module
248 254 self.reparent(module)
249 255 stop = self.lastrevs.get(module, 0)
250 256 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
251 257 commit = self.commits[rev]
252 258 # caller caches the result, so free it here to release memory
253 259 del self.commits[rev]
254 260 return commit
255 261
256 262 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
257 263 strict_node_history=False):
258 264
259 265 def parent(fp):
260 266 while True:
261 267 entry = pickle.load(fp)
262 268 try:
263 269 orig_paths, revnum, author, date, message = entry
264 270 except:
265 271 if entry is None:
266 272 break
267 273 raise SubversionException("child raised exception", entry)
268 274 yield entry
269 275
270 276 args = [self.url, paths, start, end, limit, discover_changed_paths,
271 277 strict_node_history]
272 278 arg = encodeargs(args)
273 279 hgexe = util.hgexecutable()
274 280 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
275 281 stdin, stdout = os.popen2(cmd, 'b')
276 282
277 283 stdin.write(arg)
278 284 stdin.close()
279 285
280 286 for p in parent(stdout):
281 287 yield p
282 288
283 289 def gettags(self):
284 290 tags = {}
285 291 start = self.revnum(self.head)
286 292 try:
287 293 for entry in self.get_log([self.tags], 0, start):
288 294 orig_paths, revnum, author, date, message = entry
289 295 for path in orig_paths:
290 296 if not path.startswith(self.tags+'/'):
291 297 continue
292 298 ent = orig_paths[path]
293 299 source = ent.copyfrom_path
294 300 rev = ent.copyfrom_rev
295 301 tag = path.split('/')[-1]
296 302 tags[tag] = self.revid(rev, module=source)
297 303 except SubversionException, (inst, num):
298 304 self.ui.note('no tags found at revision %d\n' % start)
299 305 return tags
300 306
307 def converted(self, rev, destrev):
308 if not self.wc:
309 return
310 if self.convertfp is None:
311 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
312 'a')
313 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
314 self.convertfp.flush()
315
301 316 # -- helper functions --
302 317
303 318 def revid(self, revnum, module=None):
304 319 if not module:
305 320 module = self.module
306 321 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
307 322 revnum)
308 323
309 324 def revnum(self, rev):
310 325 return int(rev.split('@')[-1])
311 326
312 327 def revsplit(self, rev):
313 328 url, revnum = rev.encode(self.encoding).split('@', 1)
314 329 revnum = int(revnum)
315 330 parts = url.split('/', 1)
316 331 uuid = parts.pop(0)[4:]
317 332 mod = ''
318 333 if parts:
319 334 mod = '/' + parts[0]
320 335 return uuid, mod, revnum
321 336
322 337 def latest(self, path, stop=0):
323 338 'find the latest revision affecting path, up to stop'
324 339 if not stop:
325 340 stop = svn.ra.get_latest_revnum(self.ra)
326 341 try:
327 342 self.reparent('')
328 343 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
329 344 self.reparent(self.module)
330 345 except SubversionException:
331 346 dirent = None
332 347 if not dirent:
333 348 raise util.Abort('%s not found up to revision %d' % (path, stop))
334 349
335 350 return dirent.created_rev
336 351
337 352 def get_blacklist(self):
338 353 """Avoid certain revision numbers.
339 354 It is not uncommon for two nearby revisions to cancel each other
340 355 out, e.g. 'I copied trunk into a subdirectory of itself instead
341 356 of making a branch'. The converted repository is significantly
342 357 smaller if we ignore such revisions."""
343 358 self.blacklist = util.set()
344 359 blacklist = self.blacklist
345 360 for line in file("blacklist.txt", "r"):
346 361 if not line.startswith("#"):
347 362 try:
348 363 svn_rev = int(line.strip())
349 364 blacklist.add(svn_rev)
350 365 except ValueError, e:
351 366 pass # not an integer or a comment
352 367
353 368 def is_blacklisted(self, svn_rev):
354 369 return svn_rev in self.blacklist
355 370
356 371 def reparent(self, module):
357 372 svn_url = self.base + module
358 373 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
359 374 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
360 375
361 376 def expandpaths(self, rev, paths, parents):
362 377 def get_entry_from_path(path, module=self.module):
363 378 # Given the repository url of this wc, say
364 379 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
365 380 # extract the "entry" portion (a relative path) from what
366 381 # svn log --xml says, ie
367 382 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
368 383 # that is to say "tests/PloneTestCase.py"
369 384 if path.startswith(module):
370 385 relative = path[len(module):]
371 386 if relative.startswith('/'):
372 387 return relative[1:]
373 388 else:
374 389 return relative
375 390
376 391 # The path is outside our tracked tree...
377 392 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
378 393 return None
379 394
380 395 entries = []
381 396 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
382 397 copies = {}
383 398 revnum = self.revnum(rev)
384 399
385 400 if revnum in self.modulemap:
386 401 new_module = self.modulemap[revnum]
387 402 if new_module != self.module:
388 403 self.module = new_module
389 404 self.reparent(self.module)
390 405
391 406 for path, ent in paths:
392 407 entrypath = get_entry_from_path(path, module=self.module)
393 408 entry = entrypath.decode(self.encoding)
394 409
395 410 kind = svn.ra.check_path(self.ra, entrypath, revnum)
396 411 if kind == svn.core.svn_node_file:
397 412 if ent.copyfrom_path:
398 413 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
399 414 if copyfrom_path:
400 415 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
401 416 # It's probably important for hg that the source
402 417 # exists in the revision's parent, not just the
403 418 # ent.copyfrom_rev
404 419 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
405 420 if fromkind != 0:
406 421 copies[self.recode(entry)] = self.recode(copyfrom_path)
407 422 entries.append(self.recode(entry))
408 423 elif kind == 0: # gone, but had better be a deleted *file*
409 424 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
410 425
411 426 # if a branch is created but entries are removed in the same
412 427 # changeset, get the right fromrev
413 428 if parents:
414 429 uuid, old_module, fromrev = self.revsplit(parents[0])
415 430 else:
416 431 fromrev = revnum - 1
417 432 # might always need to be revnum - 1 in these 3 lines?
418 433 old_module = self.modulemap.get(fromrev, self.module)
419 434
420 435 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
421 436 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
422 437
423 438 def lookup_parts(p):
424 439 rc = None
425 440 parts = p.split("/")
426 441 for i in range(len(parts)):
427 442 part = "/".join(parts[:i])
428 443 info = part, copyfrom.get(part, None)
429 444 if info[1] is not None:
430 445 self.ui.debug("Found parent directory %s\n" % info[1])
431 446 rc = info
432 447 return rc
433 448
434 449 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
435 450
436 451 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
437 452
438 453 # need to remove fragment from lookup_parts and replace with copyfrom_path
439 454 if frompath is not None:
440 455 self.ui.debug("munge-o-matic\n")
441 456 self.ui.debug(entrypath + '\n')
442 457 self.ui.debug(entrypath[len(frompath):] + '\n')
443 458 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
444 459 fromrev = froment.copyfrom_rev
445 460 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
446 461
447 462 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
448 463 if fromkind == svn.core.svn_node_file: # a deleted file
449 464 entries.append(self.recode(entry))
450 465 elif fromkind == svn.core.svn_node_dir:
451 466 # print "Deleted/moved non-file:", revnum, path, ent
452 467 # children = self._find_children(path, revnum - 1)
453 468 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
454 469 # Sometimes this is tricky. For example: in
455 470 # The Subversion Repository revision 6940 a dir
456 471 # was copied and one of its files was deleted
457 472 # from the new location in the same commit. This
458 473 # code can't deal with that yet.
459 474 if ent.action == 'C':
460 475 children = self._find_children(path, fromrev)
461 476 else:
462 477 oroot = entrypath.strip('/')
463 478 nroot = path.strip('/')
464 479 children = self._find_children(oroot, fromrev)
465 480 children = [s.replace(oroot,nroot) for s in children]
466 481 # Mark all [files, not directories] as deleted.
467 482 for child in children:
468 483 # Can we move a child directory and its
469 484 # parent in the same commit? (probably can). Could
470 485 # cause problems if instead of revnum -1,
471 486 # we have to look in (copyfrom_path, revnum - 1)
472 487 entrypath = get_entry_from_path("/" + child, module=old_module)
473 488 if entrypath:
474 489 entry = self.recode(entrypath.decode(self.encoding))
475 490 if entry in copies:
476 491 # deleted file within a copy
477 492 del copies[entry]
478 493 else:
479 494 entries.append(entry)
480 495 else:
481 496 self.ui.debug('unknown path in revision %d: %s\n' % \
482 497 (revnum, path))
483 498 elif kind == svn.core.svn_node_dir:
484 499 # Should probably synthesize normal file entries
485 500 # and handle as above to clean up copy/rename handling.
486 501
487 502 # If the directory just had a prop change,
488 503 # then we shouldn't need to look for its children.
489 504 # Also this could create duplicate entries. Not sure
490 505 # whether this will matter. Maybe should make entries a set.
491 506 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
492 507 # This will fail if a directory was copied
493 508 # from another branch and then some of its files
494 509 # were deleted in the same transaction.
495 510 children = self._find_children(path, revnum)
496 511 children.sort()
497 512 for child in children:
498 513 # Can we move a child directory and its
499 514 # parent in the same commit? (probably can). Could
500 515 # cause problems if instead of revnum -1,
501 516 # we have to look in (copyfrom_path, revnum - 1)
502 517 entrypath = get_entry_from_path("/" + child, module=self.module)
503 518 # print child, self.module, entrypath
504 519 if entrypath:
505 520 # Need to filter out directories here...
506 521 kind = svn.ra.check_path(self.ra, entrypath, revnum)
507 522 if kind != svn.core.svn_node_dir:
508 523 entries.append(self.recode(entrypath))
509 524
510 525 # Copies here (must copy all from source)
511 526 # Probably not a real problem for us if
512 527 # source does not exist
513 528
514 529 # Can do this with the copy command "hg copy"
515 530 # if ent.copyfrom_path:
516 531 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
517 532 # module=self.module)
518 533 # copyto_entry = entrypath
519 534 #
520 535 # print "copy directory", copyfrom_entry, 'to', copyto_entry
521 536 #
522 537 # copies.append((copyfrom_entry, copyto_entry))
523 538
524 539 if ent.copyfrom_path:
525 540 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
526 541 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
527 542 if copyfrom_entry:
528 543 copyfrom[path] = ent
529 544 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
530 545
531 546 # Good, /probably/ a regular copy. Really should check
532 547 # to see whether the parent revision actually contains
533 548 # the directory in question.
534 549 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
535 550 children.sort()
536 551 for child in children:
537 552 entrypath = get_entry_from_path("/" + child, module=self.module)
538 553 if entrypath:
539 554 entry = entrypath.decode(self.encoding)
540 555 # print "COPY COPY From", copyfrom_entry, entry
541 556 copyto_path = path + entry[len(copyfrom_entry):]
542 557 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
543 558 # print "COPY", entry, "COPY To", copyto_entry
544 559 copies[self.recode(copyto_entry)] = self.recode(entry)
545 560 # copy from quux splort/quuxfile
546 561
547 562 return (entries, copies)
548 563
549 564 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
550 565 self.child_cset = None
551 566 def parselogentry(orig_paths, revnum, author, date, message):
552 567 self.ui.debug("parsing revision %d (%d changes)\n" %
553 568 (revnum, len(orig_paths)))
554 569
555 570 if revnum in self.modulemap:
556 571 new_module = self.modulemap[revnum]
557 572 if new_module != self.module:
558 573 self.module = new_module
559 574 self.reparent(self.module)
560 575
561 576 rev = self.revid(revnum)
562 577 # branch log might return entries for a parent we already have
563 578 if (rev in self.commits or
564 579 (revnum < self.lastrevs.get(self.module, 0))):
565 580 return
566 581
567 582 parents = []
568 583 # check whether this revision is the start of a branch
569 584 if self.module in orig_paths:
570 585 ent = orig_paths[self.module]
571 586 if ent.copyfrom_path:
572 587 # ent.copyfrom_rev may not be the actual last revision
573 588 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
574 589 self.modulemap[prev] = ent.copyfrom_path
575 590 parents = [self.revid(prev, ent.copyfrom_path)]
576 591 self.ui.note('found parent of branch %s at %d: %s\n' % \
577 592 (self.module, prev, ent.copyfrom_path))
578 593 else:
579 594 self.ui.debug("No copyfrom path, don't know what to do.\n")
580 595
581 596 self.modulemap[revnum] = self.module # track backwards in time
582 597
583 598 orig_paths = orig_paths.items()
584 599 orig_paths.sort()
585 600 paths = []
586 601 # filter out unrelated paths
587 602 for path, ent in orig_paths:
588 603 if not path.startswith(self.module):
589 604 self.ui.debug("boring@%s: %s\n" % (revnum, path))
590 605 continue
591 606 paths.append((path, ent))
592 607
593 608 self.paths[rev] = (paths, parents)
594 609
595 610 # Example SVN datetime. Includes microseconds.
596 611 # ISO-8601 conformant
597 612 # '2007-01-04T17:35:00.902377Z'
598 613 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
599 614
600 615 log = message and self.recode(message)
601 616 author = author and self.recode(author) or ''
602 617 try:
603 618 branch = self.module.split("/")[-1]
604 619 if branch == 'trunk':
605 620 branch = ''
606 621 except IndexError:
607 622 branch = None
608 623
609 624 cset = commit(author=author,
610 625 date=util.datestr(date),
611 626 desc=log,
612 627 parents=parents,
613 628 branch=branch,
614 629 rev=rev.encode('utf-8'))
615 630
616 631 self.commits[rev] = cset
617 632 if self.child_cset and not self.child_cset.parents:
618 633 self.child_cset.parents = [rev]
619 634 self.child_cset = cset
620 635
621 636 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
622 637 (self.module, from_revnum, to_revnum))
623 638
624 639 try:
625 640 for entry in self.get_log([self.module], from_revnum, to_revnum):
626 641 orig_paths, revnum, author, date, message = entry
627 642 if self.is_blacklisted(revnum):
628 643 self.ui.note('skipping blacklisted revision %d\n' % revnum)
629 644 continue
630 645 if orig_paths is None:
631 646 self.ui.debug('revision %d has no entries\n' % revnum)
632 647 continue
633 648 parselogentry(orig_paths, revnum, author, date, message)
634 649 except SubversionException, (inst, num):
635 650 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
636 651 raise NoSuchRevision(branch=self,
637 652 revision="Revision number %d" % to_revnum)
638 653 raise
639 654
640 655 def _getfile(self, file, rev):
641 656 io = StringIO()
642 657 # TODO: ra.get_file transmits the whole file instead of diffs.
643 658 mode = ''
644 659 try:
645 660 revnum = self.revnum(rev)
646 661 if self.module != self.modulemap[revnum]:
647 662 self.module = self.modulemap[revnum]
648 663 self.reparent(self.module)
649 664 info = svn.ra.get_file(self.ra, file, revnum, io)
650 665 if isinstance(info, list):
651 666 info = info[-1]
652 667 mode = ("svn:executable" in info) and 'x' or ''
653 668 mode = ("svn:special" in info) and 'l' or mode
654 669 except SubversionException, e:
655 670 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
656 671 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
657 672 if e.apr_err in notfound: # File not found
658 673 raise IOError()
659 674 raise
660 675 data = io.getvalue()
661 676 if mode == 'l':
662 677 link_prefix = "link "
663 678 if data.startswith(link_prefix):
664 679 data = data[len(link_prefix):]
665 680 return data, mode
666 681
667 682 def _find_children(self, path, revnum):
668 683 path = path.strip('/')
669 684 pool = Pool()
670 685 rpath = '/'.join([self.base, path]).strip('/')
671 686 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
672 687
673 688 pre_revprop_change = '''#!/bin/sh
674 689
675 690 REPOS="$1"
676 691 REV="$2"
677 692 USER="$3"
678 693 PROPNAME="$4"
679 694 ACTION="$5"
680 695
681 696 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
682 697 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
683 698 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
684 699
685 700 echo "Changing prohibited revision property" >&2
686 701 exit 1
687 702 '''
688 703
689 704 class svn_sink(converter_sink, commandline):
690 705 commit_re = re.compile(r'Committed revision (\d+).', re.M)
691 706
692 707 def prerun(self):
693 708 if self.wc:
694 709 os.chdir(self.wc)
695 710
696 711 def postrun(self):
697 712 if self.wc:
698 713 os.chdir(self.cwd)
699 714
700 715 def join(self, name):
701 716 return os.path.join(self.wc, '.svn', name)
702 717
703 718 def revmapfile(self):
704 719 return self.join('hg-shamap')
705 720
706 721 def authorfile(self):
707 722 return self.join('hg-authormap')
708 723
709 724 def __init__(self, ui, path):
710 725 converter_sink.__init__(self, ui, path)
711 726 commandline.__init__(self, ui, 'svn')
712 727 self.delete = []
713 728 self.wc = None
714 729 self.cwd = os.getcwd()
715 730
716 731 path = os.path.realpath(path)
717 732
718 733 created = False
719 734 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
720 735 self.wc = path
721 736 self.run0('update')
722 737 else:
723 738 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
724 739
725 740 if os.path.isdir(os.path.dirname(path)):
726 741 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
727 742 ui.status(_('initializing svn repo %r\n') %
728 743 os.path.basename(path))
729 744 commandline(ui, 'svnadmin').run0('create', path)
730 745 created = path
731 746 path = path.replace('\\', '/')
732 747 if not path.startswith('/'):
733 748 path = '/' + path
734 749 path = 'file://' + path
735 750
736 751 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
737 752 self.run0('checkout', path, wcpath)
738 753
739 754 self.wc = wcpath
740 755 self.opener = util.opener(self.wc)
741 756 self.wopener = util.opener(self.wc)
742 757 self.childmap = mapfile(ui, self.join('hg-childmap'))
743 758 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
744 759
745 760 if created:
746 761 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
747 762 fp = open(hook, 'w')
748 763 fp.write(pre_revprop_change)
749 764 fp.close()
750 765 util.set_exec(hook, True)
751 766
767 xport = transport.SvnRaTransport(url=geturl(path))
768 self.uuid = svn.ra.get_uuid(xport.ra)
769
752 770 def wjoin(self, *names):
753 771 return os.path.join(self.wc, *names)
754 772
755 773 def putfile(self, filename, flags, data):
756 774 if 'l' in flags:
757 775 self.wopener.symlink(data, filename)
758 776 else:
759 777 try:
760 778 if os.path.islink(self.wjoin(filename)):
761 779 os.unlink(filename)
762 780 except OSError:
763 781 pass
764 782 self.wopener(filename, 'w').write(data)
765 783
766 784 if self.is_exec:
767 785 was_exec = self.is_exec(self.wjoin(filename))
768 786 else:
769 787 # On filesystems not supporting execute-bit, there is no way
770 788 # to know if it is set but asking subversion. Setting it
771 789 # systematically is just as expensive and much simpler.
772 790 was_exec = 'x' not in flags
773 791
774 792 util.set_exec(self.wjoin(filename), 'x' in flags)
775 793 if was_exec:
776 794 if 'x' not in flags:
777 795 self.run0('propdel', 'svn:executable', filename)
778 796 else:
779 797 if 'x' in flags:
780 798 self.run0('propset', 'svn:executable', '*', filename)
781 799
782 800 def delfile(self, name):
783 801 self.delete.append(name)
784 802
785 803 def copyfile(self, source, dest):
786 804 # SVN's copy command pukes if the destination file exists, but
787 805 # our copyfile method expects to record a copy that has
788 806 # already occurred. Cross the semantic gap.
789 807 wdest = self.wjoin(dest)
790 808 exists = os.path.exists(wdest)
791 809 if exists:
792 810 fd, tempname = tempfile.mkstemp(
793 811 prefix='hg-copy-', dir=os.path.dirname(wdest))
794 812 os.close(fd)
795 813 os.unlink(tempname)
796 814 os.rename(wdest, tempname)
797 815 try:
798 816 self.run0('copy', source, dest)
799 817 finally:
800 818 if exists:
801 819 try:
802 820 os.unlink(wdest)
803 821 except OSError:
804 822 pass
805 823 os.rename(tempname, wdest)
806 824
807 825 def dirs_of(self, files):
808 826 dirs = set()
809 827 for f in files:
810 828 if os.path.isdir(self.wjoin(f)):
811 829 dirs.add(f)
812 830 for i in strutil.rfindall(f, '/'):
813 831 dirs.add(f[:i])
814 832 return dirs
815 833
816 834 def add_files(self, files):
817 835 add_dirs = [d for d in self.dirs_of(files)
818 836 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
819 837 if add_dirs:
820 838 add_dirs.sort()
821 839 self.run('add', non_recursive=True, quiet=True, *add_dirs)
822 840 if files:
823 841 self.run('add', quiet=True, *files)
824 842 return files.union(add_dirs)
825 843
826 844 def tidy_dirs(self, names):
827 845 dirs = list(self.dirs_of(names))
828 846 dirs.sort(reverse=True)
829 847 deleted = []
830 848 for d in dirs:
831 849 wd = self.wjoin(d)
832 850 if os.listdir(wd) == '.svn':
833 851 self.run0('delete', d)
834 852 deleted.append(d)
835 853 return deleted
836 854
837 855 def addchild(self, parent, child):
838 856 self.childmap[parent] = child
839 857
858 def revid(self, rev):
859 return u"svn:%s@%s" % (self.uuid, rev)
860
840 861 def putcommit(self, files, parents, commit):
841 862 for parent in parents:
842 863 try:
843 return self.childmap[parent]
864 return self.revid(self.childmap[parent])
844 865 except KeyError:
845 866 pass
846 867 entries = set(self.delete)
847 868 if self.delete:
848 869 self.run0('delete', *self.delete)
849 870 self.delete = []
850 871 files = util.frozenset(files)
851 872 entries.update(self.add_files(files.difference(entries)))
852 873 entries.update(self.tidy_dirs(entries))
853 874 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
854 875 fp = os.fdopen(fd, 'w')
855 876 fp.write(commit.desc)
856 877 fp.close()
857 878 try:
858 879 output = self.run0('commit',
859 880 username=util.shortuser(commit.author),
860 881 file=messagefile,
861 882 *list(entries))
862 883 try:
863 884 rev = self.commit_re.search(output).group(1)
864 885 except AttributeError:
865 886 self.ui.warn(_('unexpected svn output:\n'))
866 887 self.ui.warn(output)
867 888 raise util.Abort(_('unable to cope with svn output'))
868 889 if commit.rev:
869 890 self.run('propset', 'hg:convert-rev', commit.rev,
870 891 revprop=True, revision=rev)
871 892 if commit.branch and commit.branch != 'default':
872 893 self.run('propset', 'hg:convert-branch', commit.branch,
873 894 revprop=True, revision=rev)
874 895 for parent in parents:
875 896 self.addchild(parent, rev)
876 return rev
897 return self.revid(rev)
877 898 finally:
878 899 os.unlink(messagefile)
879 900
880 901 def puttags(self, tags):
881 902 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now