##// END OF EJS Templates
convert: rename sink hascommit to hascommitforsplicemap...
Mads Kiilerich -
r21634:23b24d6a default
parent child Browse files
Show More
@@ -1,443 +1,445 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import base64, errno, subprocess, os, datetime, re
9 9 import cPickle as pickle
10 10 from mercurial import util
11 11 from mercurial.i18n import _
12 12
13 13 propertycache = util.propertycache
14 14
15 15 def encodeargs(args):
16 16 def encodearg(s):
17 17 lines = base64.encodestring(s)
18 18 lines = [l.splitlines()[0] for l in lines]
19 19 return ''.join(lines)
20 20
21 21 s = pickle.dumps(args)
22 22 return encodearg(s)
23 23
24 24 def decodeargs(s):
25 25 s = base64.decodestring(s)
26 26 return pickle.loads(s)
27 27
28 28 class MissingTool(Exception):
29 29 pass
30 30
31 31 def checktool(exe, name=None, abort=True):
32 32 name = name or exe
33 33 if not util.findexe(exe):
34 34 exc = abort and util.Abort or MissingTool
35 35 raise exc(_('cannot find required "%s" tool') % name)
36 36
37 37 class NoRepo(Exception):
38 38 pass
39 39
40 40 SKIPREV = 'SKIP'
41 41
42 42 class commit(object):
43 43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 44 extra={}, sortkey=None):
45 45 self.author = author or 'unknown'
46 46 self.date = date or '0 0'
47 47 self.desc = desc
48 48 self.parents = parents
49 49 self.branch = branch
50 50 self.rev = rev
51 51 self.extra = extra
52 52 self.sortkey = sortkey
53 53
54 54 class converter_source(object):
55 55 """Conversion source interface"""
56 56
57 57 def __init__(self, ui, path=None, rev=None):
58 58 """Initialize conversion source (or raise NoRepo("message")
59 59 exception if path is not a valid repository)"""
60 60 self.ui = ui
61 61 self.path = path
62 62 self.rev = rev
63 63
64 64 self.encoding = 'utf-8'
65 65
66 66 def checkhexformat(self, revstr, mapname='splicemap'):
67 67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
68 68 such format for their revision numbering
69 69 """
70 70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
71 71 raise util.Abort(_('%s entry %s is not a valid revision'
72 72 ' identifier') % (mapname, revstr))
73 73
74 74 def before(self):
75 75 pass
76 76
77 77 def after(self):
78 78 pass
79 79
80 80 def setrevmap(self, revmap):
81 81 """set the map of already-converted revisions"""
82 82 pass
83 83
84 84 def getheads(self):
85 85 """Return a list of this repository's heads"""
86 86 raise NotImplementedError
87 87
88 88 def getfile(self, name, rev):
89 89 """Return a pair (data, mode) where data is the file content
90 90 as a string and mode one of '', 'x' or 'l'. rev is the
91 91 identifier returned by a previous call to getchanges(). Raise
92 92 IOError to indicate that name was deleted in rev.
93 93 """
94 94 raise NotImplementedError
95 95
96 96 def getchanges(self, version):
97 97 """Returns a tuple of (files, copies).
98 98
99 99 files is a sorted list of (filename, id) tuples for all files
100 100 changed between version and its first parent returned by
101 101 getcommit(). id is the source revision id of the file.
102 102
103 103 copies is a dictionary of dest: source
104 104 """
105 105 raise NotImplementedError
106 106
107 107 def getcommit(self, version):
108 108 """Return the commit object for version"""
109 109 raise NotImplementedError
110 110
111 111 def gettags(self):
112 112 """Return the tags as a dictionary of name: revision
113 113
114 114 Tag names must be UTF-8 strings.
115 115 """
116 116 raise NotImplementedError
117 117
118 118 def recode(self, s, encoding=None):
119 119 if not encoding:
120 120 encoding = self.encoding or 'utf-8'
121 121
122 122 if isinstance(s, unicode):
123 123 return s.encode("utf-8")
124 124 try:
125 125 return s.decode(encoding).encode("utf-8")
126 126 except UnicodeError:
127 127 try:
128 128 return s.decode("latin-1").encode("utf-8")
129 129 except UnicodeError:
130 130 return s.decode(encoding, "replace").encode("utf-8")
131 131
132 132 def getchangedfiles(self, rev, i):
133 133 """Return the files changed by rev compared to parent[i].
134 134
135 135 i is an index selecting one of the parents of rev. The return
136 136 value should be the list of files that are different in rev and
137 137 this parent.
138 138
139 139 If rev has no parents, i is None.
140 140
141 141 This function is only needed to support --filemap
142 142 """
143 143 raise NotImplementedError
144 144
145 145 def converted(self, rev, sinkrev):
146 146 '''Notify the source that a revision has been converted.'''
147 147 pass
148 148
149 149 def hasnativeorder(self):
150 150 """Return true if this source has a meaningful, native revision
151 151 order. For instance, Mercurial revisions are store sequentially
152 152 while there is no such global ordering with Darcs.
153 153 """
154 154 return False
155 155
156 156 def hasnativeclose(self):
157 157 """Return true if this source has ability to close branch.
158 158 """
159 159 return False
160 160
161 161 def lookuprev(self, rev):
162 162 """If rev is a meaningful revision reference in source, return
163 163 the referenced identifier in the same format used by getcommit().
164 164 return None otherwise.
165 165 """
166 166 return None
167 167
168 168 def getbookmarks(self):
169 169 """Return the bookmarks as a dictionary of name: revision
170 170
171 171 Bookmark names are to be UTF-8 strings.
172 172 """
173 173 return {}
174 174
175 175 def checkrevformat(self, revstr, mapname='splicemap'):
176 176 """revstr is a string that describes a revision in the given
177 177 source control system. Return true if revstr has correct
178 178 format.
179 179 """
180 180 return True
181 181
182 182 class converter_sink(object):
183 183 """Conversion sink (target) interface"""
184 184
185 185 def __init__(self, ui, path):
186 186 """Initialize conversion sink (or raise NoRepo("message")
187 187 exception if path is not a valid repository)
188 188
189 189 created is a list of paths to remove if a fatal error occurs
190 190 later"""
191 191 self.ui = ui
192 192 self.path = path
193 193 self.created = []
194 194
195 195 def revmapfile(self):
196 196 """Path to a file that will contain lines
197 197 source_rev_id sink_rev_id
198 198 mapping equivalent revision identifiers for each system."""
199 199 raise NotImplementedError
200 200
201 201 def authorfile(self):
202 202 """Path to a file that will contain lines
203 203 srcauthor=dstauthor
204 204 mapping equivalent authors identifiers for each system."""
205 205 return None
206 206
207 207 def putcommit(self, files, copies, parents, commit, source, revmap):
208 208 """Create a revision with all changed files listed in 'files'
209 209 and having listed parents. 'commit' is a commit object
210 210 containing at a minimum the author, date, and message for this
211 211 changeset. 'files' is a list of (path, version) tuples,
212 212 'copies' is a dictionary mapping destinations to sources,
213 213 'source' is the source repository, and 'revmap' is a mapfile
214 214 of source revisions to converted revisions. Only getfile() and
215 215 lookuprev() should be called on 'source'.
216 216
217 217 Note that the sink repository is not told to update itself to
218 218 a particular revision (or even what that revision would be)
219 219 before it receives the file data.
220 220 """
221 221 raise NotImplementedError
222 222
223 223 def puttags(self, tags):
224 224 """Put tags into sink.
225 225
226 226 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
227 227 Return a pair (tag_revision, tag_parent_revision), or (None, None)
228 228 if nothing was changed.
229 229 """
230 230 raise NotImplementedError
231 231
232 232 def setbranch(self, branch, pbranches):
233 233 """Set the current branch name. Called before the first putcommit
234 234 on the branch.
235 235 branch: branch name for subsequent commits
236 236 pbranches: (converted parent revision, parent branch) tuples"""
237 237 pass
238 238
239 239 def setfilemapmode(self, active):
240 240 """Tell the destination that we're using a filemap
241 241
242 242 Some converter_sources (svn in particular) can claim that a file
243 243 was changed in a revision, even if there was no change. This method
244 244 tells the destination that we're using a filemap and that it should
245 245 filter empty revisions.
246 246 """
247 247 pass
248 248
249 249 def before(self):
250 250 pass
251 251
252 252 def after(self):
253 253 pass
254 254
255 255 def putbookmarks(self, bookmarks):
256 256 """Put bookmarks into sink.
257 257
258 258 bookmarks: {bookmarkname: sink_rev_id, ...}
259 259 where bookmarkname is an UTF-8 string.
260 260 """
261 261 pass
262 262
263 def hascommit(self, rev):
264 """Return True if the sink contains rev"""
263 def hascommitforsplicemap(self, rev):
264 """This method is for the special needs for splicemap handling and not
265 for general use. Returns True if the sink contains rev, aborts on some
266 special cases."""
265 267 raise NotImplementedError
266 268
267 269 class commandline(object):
268 270 def __init__(self, ui, command):
269 271 self.ui = ui
270 272 self.command = command
271 273
272 274 def prerun(self):
273 275 pass
274 276
275 277 def postrun(self):
276 278 pass
277 279
278 280 def _cmdline(self, cmd, *args, **kwargs):
279 281 cmdline = [self.command, cmd] + list(args)
280 282 for k, v in kwargs.iteritems():
281 283 if len(k) == 1:
282 284 cmdline.append('-' + k)
283 285 else:
284 286 cmdline.append('--' + k.replace('_', '-'))
285 287 try:
286 288 if len(k) == 1:
287 289 cmdline.append('' + v)
288 290 else:
289 291 cmdline[-1] += '=' + v
290 292 except TypeError:
291 293 pass
292 294 cmdline = [util.shellquote(arg) for arg in cmdline]
293 295 if not self.ui.debugflag:
294 296 cmdline += ['2>', os.devnull]
295 297 cmdline = ' '.join(cmdline)
296 298 return cmdline
297 299
298 300 def _run(self, cmd, *args, **kwargs):
299 301 def popen(cmdline):
300 302 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
301 303 close_fds=util.closefds,
302 304 stdout=subprocess.PIPE)
303 305 return p
304 306 return self._dorun(popen, cmd, *args, **kwargs)
305 307
306 308 def _run2(self, cmd, *args, **kwargs):
307 309 return self._dorun(util.popen2, cmd, *args, **kwargs)
308 310
309 311 def _dorun(self, openfunc, cmd, *args, **kwargs):
310 312 cmdline = self._cmdline(cmd, *args, **kwargs)
311 313 self.ui.debug('running: %s\n' % (cmdline,))
312 314 self.prerun()
313 315 try:
314 316 return openfunc(cmdline)
315 317 finally:
316 318 self.postrun()
317 319
318 320 def run(self, cmd, *args, **kwargs):
319 321 p = self._run(cmd, *args, **kwargs)
320 322 output = p.communicate()[0]
321 323 self.ui.debug(output)
322 324 return output, p.returncode
323 325
324 326 def runlines(self, cmd, *args, **kwargs):
325 327 p = self._run(cmd, *args, **kwargs)
326 328 output = p.stdout.readlines()
327 329 p.wait()
328 330 self.ui.debug(''.join(output))
329 331 return output, p.returncode
330 332
331 333 def checkexit(self, status, output=''):
332 334 if status:
333 335 if output:
334 336 self.ui.warn(_('%s error:\n') % self.command)
335 337 self.ui.warn(output)
336 338 msg = util.explainexit(status)[0]
337 339 raise util.Abort('%s %s' % (self.command, msg))
338 340
339 341 def run0(self, cmd, *args, **kwargs):
340 342 output, status = self.run(cmd, *args, **kwargs)
341 343 self.checkexit(status, output)
342 344 return output
343 345
344 346 def runlines0(self, cmd, *args, **kwargs):
345 347 output, status = self.runlines(cmd, *args, **kwargs)
346 348 self.checkexit(status, ''.join(output))
347 349 return output
348 350
349 351 @propertycache
350 352 def argmax(self):
351 353 # POSIX requires at least 4096 bytes for ARG_MAX
352 354 argmax = 4096
353 355 try:
354 356 argmax = os.sysconf("SC_ARG_MAX")
355 357 except (AttributeError, ValueError):
356 358 pass
357 359
358 360 # Windows shells impose their own limits on command line length,
359 361 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
360 362 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
361 363 # details about cmd.exe limitations.
362 364
363 365 # Since ARG_MAX is for command line _and_ environment, lower our limit
364 366 # (and make happy Windows shells while doing this).
365 367 return argmax // 2 - 1
366 368
367 369 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
368 370 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
369 371 limit = self.argmax - cmdlen
370 372 bytes = 0
371 373 fl = []
372 374 for fn in arglist:
373 375 b = len(fn) + 3
374 376 if bytes + b < limit or len(fl) == 0:
375 377 fl.append(fn)
376 378 bytes += b
377 379 else:
378 380 yield fl
379 381 fl = [fn]
380 382 bytes = b
381 383 if fl:
382 384 yield fl
383 385
384 386 def xargs(self, arglist, cmd, *args, **kwargs):
385 387 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
386 388 self.run0(cmd, *(list(args) + l), **kwargs)
387 389
388 390 class mapfile(dict):
389 391 def __init__(self, ui, path):
390 392 super(mapfile, self).__init__()
391 393 self.ui = ui
392 394 self.path = path
393 395 self.fp = None
394 396 self.order = []
395 397 self._read()
396 398
397 399 def _read(self):
398 400 if not self.path:
399 401 return
400 402 try:
401 403 fp = open(self.path, 'r')
402 404 except IOError, err:
403 405 if err.errno != errno.ENOENT:
404 406 raise
405 407 return
406 408 for i, line in enumerate(fp):
407 409 line = line.splitlines()[0].rstrip()
408 410 if not line:
409 411 # Ignore blank lines
410 412 continue
411 413 try:
412 414 key, value = line.rsplit(' ', 1)
413 415 except ValueError:
414 416 raise util.Abort(
415 417 _('syntax error in %s(%d): key/value pair expected')
416 418 % (self.path, i + 1))
417 419 if key not in self:
418 420 self.order.append(key)
419 421 super(mapfile, self).__setitem__(key, value)
420 422 fp.close()
421 423
422 424 def __setitem__(self, key, value):
423 425 if self.fp is None:
424 426 try:
425 427 self.fp = open(self.path, 'a')
426 428 except IOError, err:
427 429 raise util.Abort(_('could not open map file %r: %s') %
428 430 (self.path, err.strerror))
429 431 self.fp.write('%s %s\n' % (key, value))
430 432 self.fp.flush()
431 433 super(mapfile, self).__setitem__(key, value)
432 434
433 435 def close(self):
434 436 if self.fp:
435 437 self.fp.close()
436 438 self.fp = None
437 439
438 440 def makedatetimestamp(t):
439 441 """Like util.makedate() but for time t instead of current time"""
440 442 delta = (datetime.datetime.utcfromtimestamp(t) -
441 443 datetime.datetime.fromtimestamp(t))
442 444 tz = delta.days * 86400 + delta.seconds
443 445 return t, tz
@@ -1,528 +1,528 b''
1 1 # convcmd - convert extension commands definition
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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from common import NoRepo, MissingTool, SKIPREV, 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 svn_source, svn_sink
14 14 from monotone import monotone_source
15 15 from gnuarch import gnuarch_source
16 16 from bzr import bzr_source
17 17 from p4 import p4_source
18 18 import filemap
19 19
20 20 import os, shutil, shlex
21 21 from mercurial import hg, util, encoding
22 22 from mercurial.i18n import _
23 23
24 24 orig_encoding = 'ascii'
25 25
26 26 def recode(s):
27 27 if isinstance(s, unicode):
28 28 return s.encode(orig_encoding, 'replace')
29 29 else:
30 30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31 31
32 32 source_converters = [
33 33 ('cvs', convert_cvs, 'branchsort'),
34 34 ('git', convert_git, 'branchsort'),
35 35 ('svn', svn_source, 'branchsort'),
36 36 ('hg', mercurial_source, 'sourcesort'),
37 37 ('darcs', darcs_source, 'branchsort'),
38 38 ('mtn', monotone_source, 'branchsort'),
39 39 ('gnuarch', gnuarch_source, 'branchsort'),
40 40 ('bzr', bzr_source, 'branchsort'),
41 41 ('p4', p4_source, 'branchsort'),
42 42 ]
43 43
44 44 sink_converters = [
45 45 ('hg', mercurial_sink),
46 46 ('svn', svn_sink),
47 47 ]
48 48
49 49 def convertsource(ui, path, type, rev):
50 50 exceptions = []
51 51 if type and type not in [s[0] for s in source_converters]:
52 52 raise util.Abort(_('%s: invalid source repository type') % type)
53 53 for name, source, sortmode in source_converters:
54 54 try:
55 55 if not type or name == type:
56 56 return source(ui, path, rev), sortmode
57 57 except (NoRepo, MissingTool), inst:
58 58 exceptions.append(inst)
59 59 if not ui.quiet:
60 60 for inst in exceptions:
61 61 ui.write("%s\n" % inst)
62 62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63 63
64 64 def convertsink(ui, path, type):
65 65 if type and type not in [s[0] for s in sink_converters]:
66 66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 67 for name, sink in sink_converters:
68 68 try:
69 69 if not type or name == type:
70 70 return sink(ui, path)
71 71 except NoRepo, inst:
72 72 ui.note(_("convert: %s\n") % inst)
73 73 except MissingTool, inst:
74 74 raise util.Abort('%s\n' % inst)
75 75 raise util.Abort(_('%s: unknown repository type') % path)
76 76
77 77 class progresssource(object):
78 78 def __init__(self, ui, source, filecount):
79 79 self.ui = ui
80 80 self.source = source
81 81 self.filecount = filecount
82 82 self.retrieved = 0
83 83
84 84 def getfile(self, file, rev):
85 85 self.retrieved += 1
86 86 self.ui.progress(_('getting files'), self.retrieved,
87 87 item=file, total=self.filecount)
88 88 return self.source.getfile(file, rev)
89 89
90 90 def lookuprev(self, rev):
91 91 return self.source.lookuprev(rev)
92 92
93 93 def close(self):
94 94 self.ui.progress(_('getting files'), None)
95 95
96 96 class converter(object):
97 97 def __init__(self, ui, source, dest, revmapfile, opts):
98 98
99 99 self.source = source
100 100 self.dest = dest
101 101 self.ui = ui
102 102 self.opts = opts
103 103 self.commitcache = {}
104 104 self.authors = {}
105 105 self.authorfile = None
106 106
107 107 # Record converted revisions persistently: maps source revision
108 108 # ID to target revision ID (both strings). (This is how
109 109 # incremental conversions work.)
110 110 self.map = mapfile(ui, revmapfile)
111 111
112 112 # Read first the dst author map if any
113 113 authorfile = self.dest.authorfile()
114 114 if authorfile and os.path.exists(authorfile):
115 115 self.readauthormap(authorfile)
116 116 # Extend/Override with new author map if necessary
117 117 if opts.get('authormap'):
118 118 self.readauthormap(opts.get('authormap'))
119 119 self.authorfile = self.dest.authorfile()
120 120
121 121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123 123
124 124 def parsesplicemap(self, path):
125 125 """ check and validate the splicemap format and
126 126 return a child/parents dictionary.
127 127 Format checking has two parts.
128 128 1. generic format which is same across all source types
129 129 2. specific format checking which may be different for
130 130 different source type. This logic is implemented in
131 131 checkrevformat function in source files like
132 132 hg.py, subversion.py etc.
133 133 """
134 134
135 135 if not path:
136 136 return {}
137 137 m = {}
138 138 try:
139 139 fp = open(path, 'r')
140 140 for i, line in enumerate(fp):
141 141 line = line.splitlines()[0].rstrip()
142 142 if not line:
143 143 # Ignore blank lines
144 144 continue
145 145 # split line
146 146 lex = shlex.shlex(line, posix=True)
147 147 lex.whitespace_split = True
148 148 lex.whitespace += ','
149 149 line = list(lex)
150 150 # check number of parents
151 151 if not (2 <= len(line) <= 3):
152 152 raise util.Abort(_('syntax error in %s(%d): child parent1'
153 153 '[,parent2] expected') % (path, i + 1))
154 154 for part in line:
155 155 self.source.checkrevformat(part)
156 156 child, p1, p2 = line[0], line[1:2], line[2:]
157 157 if p1 == p2:
158 158 m[child] = p1
159 159 else:
160 160 m[child] = p1 + p2
161 161 # if file does not exist or error reading, exit
162 162 except IOError:
163 163 raise util.Abort(_('splicemap file not found or error reading %s:')
164 164 % path)
165 165 return m
166 166
167 167
168 168 def walktree(self, heads):
169 169 '''Return a mapping that identifies the uncommitted parents of every
170 170 uncommitted changeset.'''
171 171 visit = heads
172 172 known = set()
173 173 parents = {}
174 174 while visit:
175 175 n = visit.pop(0)
176 176 if n in known or n in self.map:
177 177 continue
178 178 known.add(n)
179 179 self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
180 180 commit = self.cachecommit(n)
181 181 parents[n] = []
182 182 for p in commit.parents:
183 183 parents[n].append(p)
184 184 visit.append(p)
185 185 self.ui.progress(_('scanning'), None)
186 186
187 187 return parents
188 188
189 189 def mergesplicemap(self, parents, splicemap):
190 190 """A splicemap redefines child/parent relationships. Check the
191 191 map contains valid revision identifiers and merge the new
192 192 links in the source graph.
193 193 """
194 194 for c in sorted(splicemap):
195 195 if c not in parents:
196 if not self.dest.hascommit(self.map.get(c, c)):
196 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
197 197 # Could be in source but not converted during this run
198 198 self.ui.warn(_('splice map revision %s is not being '
199 199 'converted, ignoring\n') % c)
200 200 continue
201 201 pc = []
202 202 for p in splicemap[c]:
203 203 # We do not have to wait for nodes already in dest.
204 if self.dest.hascommit(self.map.get(p, p)):
204 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
205 205 continue
206 206 # Parent is not in dest and not being converted, not good
207 207 if p not in parents:
208 208 raise util.Abort(_('unknown splice map parent: %s') % p)
209 209 pc.append(p)
210 210 parents[c] = pc
211 211
212 212 def toposort(self, parents, sortmode):
213 213 '''Return an ordering such that every uncommitted changeset is
214 214 preceded by all its uncommitted ancestors.'''
215 215
216 216 def mapchildren(parents):
217 217 """Return a (children, roots) tuple where 'children' maps parent
218 218 revision identifiers to children ones, and 'roots' is the list of
219 219 revisions without parents. 'parents' must be a mapping of revision
220 220 identifier to its parents ones.
221 221 """
222 222 visit = sorted(parents)
223 223 seen = set()
224 224 children = {}
225 225 roots = []
226 226
227 227 while visit:
228 228 n = visit.pop(0)
229 229 if n in seen:
230 230 continue
231 231 seen.add(n)
232 232 # Ensure that nodes without parents are present in the
233 233 # 'children' mapping.
234 234 children.setdefault(n, [])
235 235 hasparent = False
236 236 for p in parents[n]:
237 237 if p not in self.map:
238 238 visit.append(p)
239 239 hasparent = True
240 240 children.setdefault(p, []).append(n)
241 241 if not hasparent:
242 242 roots.append(n)
243 243
244 244 return children, roots
245 245
246 246 # Sort functions are supposed to take a list of revisions which
247 247 # can be converted immediately and pick one
248 248
249 249 def makebranchsorter():
250 250 """If the previously converted revision has a child in the
251 251 eligible revisions list, pick it. Return the list head
252 252 otherwise. Branch sort attempts to minimize branch
253 253 switching, which is harmful for Mercurial backend
254 254 compression.
255 255 """
256 256 prev = [None]
257 257 def picknext(nodes):
258 258 next = nodes[0]
259 259 for n in nodes:
260 260 if prev[0] in parents[n]:
261 261 next = n
262 262 break
263 263 prev[0] = next
264 264 return next
265 265 return picknext
266 266
267 267 def makesourcesorter():
268 268 """Source specific sort."""
269 269 keyfn = lambda n: self.commitcache[n].sortkey
270 270 def picknext(nodes):
271 271 return sorted(nodes, key=keyfn)[0]
272 272 return picknext
273 273
274 274 def makeclosesorter():
275 275 """Close order sort."""
276 276 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
277 277 self.commitcache[n].sortkey)
278 278 def picknext(nodes):
279 279 return sorted(nodes, key=keyfn)[0]
280 280 return picknext
281 281
282 282 def makedatesorter():
283 283 """Sort revisions by date."""
284 284 dates = {}
285 285 def getdate(n):
286 286 if n not in dates:
287 287 dates[n] = util.parsedate(self.commitcache[n].date)
288 288 return dates[n]
289 289
290 290 def picknext(nodes):
291 291 return min([(getdate(n), n) for n in nodes])[1]
292 292
293 293 return picknext
294 294
295 295 if sortmode == 'branchsort':
296 296 picknext = makebranchsorter()
297 297 elif sortmode == 'datesort':
298 298 picknext = makedatesorter()
299 299 elif sortmode == 'sourcesort':
300 300 picknext = makesourcesorter()
301 301 elif sortmode == 'closesort':
302 302 picknext = makeclosesorter()
303 303 else:
304 304 raise util.Abort(_('unknown sort mode: %s') % sortmode)
305 305
306 306 children, actives = mapchildren(parents)
307 307
308 308 s = []
309 309 pendings = {}
310 310 while actives:
311 311 n = picknext(actives)
312 312 actives.remove(n)
313 313 s.append(n)
314 314
315 315 # Update dependents list
316 316 for c in children.get(n, []):
317 317 if c not in pendings:
318 318 pendings[c] = [p for p in parents[c] if p not in self.map]
319 319 try:
320 320 pendings[c].remove(n)
321 321 except ValueError:
322 322 raise util.Abort(_('cycle detected between %s and %s')
323 323 % (recode(c), recode(n)))
324 324 if not pendings[c]:
325 325 # Parents are converted, node is eligible
326 326 actives.insert(0, c)
327 327 pendings[c] = None
328 328
329 329 if len(s) != len(parents):
330 330 raise util.Abort(_("not all revisions were sorted"))
331 331
332 332 return s
333 333
334 334 def writeauthormap(self):
335 335 authorfile = self.authorfile
336 336 if authorfile:
337 337 self.ui.status(_('writing author map file %s\n') % authorfile)
338 338 ofile = open(authorfile, 'w+')
339 339 for author in self.authors:
340 340 ofile.write("%s=%s\n" % (author, self.authors[author]))
341 341 ofile.close()
342 342
343 343 def readauthormap(self, authorfile):
344 344 afile = open(authorfile, 'r')
345 345 for line in afile:
346 346
347 347 line = line.strip()
348 348 if not line or line.startswith('#'):
349 349 continue
350 350
351 351 try:
352 352 srcauthor, dstauthor = line.split('=', 1)
353 353 except ValueError:
354 354 msg = _('ignoring bad line in author map file %s: %s\n')
355 355 self.ui.warn(msg % (authorfile, line.rstrip()))
356 356 continue
357 357
358 358 srcauthor = srcauthor.strip()
359 359 dstauthor = dstauthor.strip()
360 360 if self.authors.get(srcauthor) in (None, dstauthor):
361 361 msg = _('mapping author %s to %s\n')
362 362 self.ui.debug(msg % (srcauthor, dstauthor))
363 363 self.authors[srcauthor] = dstauthor
364 364 continue
365 365
366 366 m = _('overriding mapping for author %s, was %s, will be %s\n')
367 367 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
368 368
369 369 afile.close()
370 370
371 371 def cachecommit(self, rev):
372 372 commit = self.source.getcommit(rev)
373 373 commit.author = self.authors.get(commit.author, commit.author)
374 374 # If commit.branch is None, this commit is coming from the source
375 375 # repository's default branch and destined for the default branch in the
376 376 # destination repository. For such commits, passing a literal "None"
377 377 # string to branchmap.get() below allows the user to map "None" to an
378 378 # alternate default branch in the destination repository.
379 379 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
380 380 self.commitcache[rev] = commit
381 381 return commit
382 382
383 383 def copy(self, rev):
384 384 commit = self.commitcache[rev]
385 385
386 386 changes = self.source.getchanges(rev)
387 387 if isinstance(changes, basestring):
388 388 if changes == SKIPREV:
389 389 dest = SKIPREV
390 390 else:
391 391 dest = self.map[changes]
392 392 self.map[rev] = dest
393 393 return
394 394 files, copies = changes
395 395 pbranches = []
396 396 if commit.parents:
397 397 for prev in commit.parents:
398 398 if prev not in self.commitcache:
399 399 self.cachecommit(prev)
400 400 pbranches.append((self.map[prev],
401 401 self.commitcache[prev].branch))
402 402 self.dest.setbranch(commit.branch, pbranches)
403 403 try:
404 404 parents = self.splicemap[rev]
405 405 self.ui.status(_('spliced in %s as parents of %s\n') %
406 406 (parents, rev))
407 407 parents = [self.map.get(p, p) for p in parents]
408 408 except KeyError:
409 409 parents = [b[0] for b in pbranches]
410 410 source = progresssource(self.ui, self.source, len(files))
411 411 newnode = self.dest.putcommit(files, copies, parents, commit,
412 412 source, self.map)
413 413 source.close()
414 414 self.source.converted(rev, newnode)
415 415 self.map[rev] = newnode
416 416
417 417 def convert(self, sortmode):
418 418 try:
419 419 self.source.before()
420 420 self.dest.before()
421 421 self.source.setrevmap(self.map)
422 422 self.ui.status(_("scanning source...\n"))
423 423 heads = self.source.getheads()
424 424 parents = self.walktree(heads)
425 425 self.mergesplicemap(parents, self.splicemap)
426 426 self.ui.status(_("sorting...\n"))
427 427 t = self.toposort(parents, sortmode)
428 428 num = len(t)
429 429 c = None
430 430
431 431 self.ui.status(_("converting...\n"))
432 432 for i, c in enumerate(t):
433 433 num -= 1
434 434 desc = self.commitcache[c].desc
435 435 if "\n" in desc:
436 436 desc = desc.splitlines()[0]
437 437 # convert log message to local encoding without using
438 438 # tolocal() because the encoding.encoding convert()
439 439 # uses is 'utf-8'
440 440 self.ui.status("%d %s\n" % (num, recode(desc)))
441 441 self.ui.note(_("source: %s\n") % recode(c))
442 442 self.ui.progress(_('converting'), i, unit=_('revisions'),
443 443 total=len(t))
444 444 self.copy(c)
445 445 self.ui.progress(_('converting'), None)
446 446
447 447 tags = self.source.gettags()
448 448 ctags = {}
449 449 for k in tags:
450 450 v = tags[k]
451 451 if self.map.get(v, SKIPREV) != SKIPREV:
452 452 ctags[k] = self.map[v]
453 453
454 454 if c and ctags:
455 455 nrev, tagsparent = self.dest.puttags(ctags)
456 456 if nrev and tagsparent:
457 457 # write another hash correspondence to override the previous
458 458 # one so we don't end up with extra tag heads
459 459 tagsparents = [e for e in self.map.iteritems()
460 460 if e[1] == tagsparent]
461 461 if tagsparents:
462 462 self.map[tagsparents[0][0]] = nrev
463 463
464 464 bookmarks = self.source.getbookmarks()
465 465 cbookmarks = {}
466 466 for k in bookmarks:
467 467 v = bookmarks[k]
468 468 if self.map.get(v, SKIPREV) != SKIPREV:
469 469 cbookmarks[k] = self.map[v]
470 470
471 471 if c and cbookmarks:
472 472 self.dest.putbookmarks(cbookmarks)
473 473
474 474 self.writeauthormap()
475 475 finally:
476 476 self.cleanup()
477 477
478 478 def cleanup(self):
479 479 try:
480 480 self.dest.after()
481 481 finally:
482 482 self.source.after()
483 483 self.map.close()
484 484
485 485 def convert(ui, src, dest=None, revmapfile=None, **opts):
486 486 global orig_encoding
487 487 orig_encoding = encoding.encoding
488 488 encoding.encoding = 'UTF-8'
489 489
490 490 # support --authors as an alias for --authormap
491 491 if not opts.get('authormap'):
492 492 opts['authormap'] = opts.get('authors')
493 493
494 494 if not dest:
495 495 dest = hg.defaultdest(src) + "-hg"
496 496 ui.status(_("assuming destination %s\n") % dest)
497 497
498 498 destc = convertsink(ui, dest, opts.get('dest_type'))
499 499
500 500 try:
501 501 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
502 502 opts.get('rev'))
503 503 except Exception:
504 504 for path in destc.created:
505 505 shutil.rmtree(path, True)
506 506 raise
507 507
508 508 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
509 509 sortmode = [m for m in sortmodes if opts.get(m)]
510 510 if len(sortmode) > 1:
511 511 raise util.Abort(_('more than one sort mode specified'))
512 512 sortmode = sortmode and sortmode[0] or defaultsort
513 513 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
514 514 raise util.Abort(_('--sourcesort is not supported by this data source'))
515 515 if sortmode == 'closesort' and not srcc.hasnativeclose():
516 516 raise util.Abort(_('--closesort is not supported by this data source'))
517 517
518 518 fmap = opts.get('filemap')
519 519 if fmap:
520 520 srcc = filemap.filemap_source(ui, srcc, fmap)
521 521 destc.setfilemapmode(True)
522 522
523 523 if not revmapfile:
524 524 revmapfile = destc.revmapfile()
525 525
526 526 c = converter(ui, srcc, destc, revmapfile, opts)
527 527 c.convert(sortmode)
528 528
@@ -1,448 +1,448 b''
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19
20 20
21 21 import os, time, cStringIO
22 22 from mercurial.i18n import _
23 23 from mercurial.node import bin, hex, nullid
24 24 from mercurial import hg, util, context, bookmarks, error, scmutil
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink
27 27
28 28 import re
29 29 sha1re = re.compile(r'\b[0-9a-f]{6,40}\b')
30 30
31 31 class mercurial_sink(converter_sink):
32 32 def __init__(self, ui, path):
33 33 converter_sink.__init__(self, ui, path)
34 34 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
35 35 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
36 36 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
37 37 self.lastbranch = None
38 38 if os.path.isdir(path) and len(os.listdir(path)) > 0:
39 39 try:
40 40 self.repo = hg.repository(self.ui, path)
41 41 if not self.repo.local():
42 42 raise NoRepo(_('%s is not a local Mercurial repository')
43 43 % path)
44 44 except error.RepoError, err:
45 45 ui.traceback()
46 46 raise NoRepo(err.args[0])
47 47 else:
48 48 try:
49 49 ui.status(_('initializing destination %s repository\n') % path)
50 50 self.repo = hg.repository(self.ui, path, create=True)
51 51 if not self.repo.local():
52 52 raise NoRepo(_('%s is not a local Mercurial repository')
53 53 % path)
54 54 self.created.append(path)
55 55 except error.RepoError:
56 56 ui.traceback()
57 57 raise NoRepo(_("could not create hg repository %s as sink")
58 58 % path)
59 59 self.lock = None
60 60 self.wlock = None
61 61 self.filemapmode = False
62 62
63 63 def before(self):
64 64 self.ui.debug('run hg sink pre-conversion action\n')
65 65 self.wlock = self.repo.wlock()
66 66 self.lock = self.repo.lock()
67 67
68 68 def after(self):
69 69 self.ui.debug('run hg sink post-conversion action\n')
70 70 if self.lock:
71 71 self.lock.release()
72 72 if self.wlock:
73 73 self.wlock.release()
74 74
75 75 def revmapfile(self):
76 76 return self.repo.join("shamap")
77 77
78 78 def authorfile(self):
79 79 return self.repo.join("authormap")
80 80
81 81 def setbranch(self, branch, pbranches):
82 82 if not self.clonebranches:
83 83 return
84 84
85 85 setbranch = (branch != self.lastbranch)
86 86 self.lastbranch = branch
87 87 if not branch:
88 88 branch = 'default'
89 89 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
90 90 pbranch = pbranches and pbranches[0][1] or 'default'
91 91
92 92 branchpath = os.path.join(self.path, branch)
93 93 if setbranch:
94 94 self.after()
95 95 try:
96 96 self.repo = hg.repository(self.ui, branchpath)
97 97 except Exception:
98 98 self.repo = hg.repository(self.ui, branchpath, create=True)
99 99 self.before()
100 100
101 101 # pbranches may bring revisions from other branches (merge parents)
102 102 # Make sure we have them, or pull them.
103 103 missings = {}
104 104 for b in pbranches:
105 105 try:
106 106 self.repo.lookup(b[0])
107 107 except Exception:
108 108 missings.setdefault(b[1], []).append(b[0])
109 109
110 110 if missings:
111 111 self.after()
112 112 for pbranch, heads in sorted(missings.iteritems()):
113 113 pbranchpath = os.path.join(self.path, pbranch)
114 114 prepo = hg.peer(self.ui, {}, pbranchpath)
115 115 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
116 116 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
117 117 self.before()
118 118
119 119 def _rewritetags(self, source, revmap, data):
120 120 fp = cStringIO.StringIO()
121 121 for line in data.splitlines():
122 122 s = line.split(' ', 1)
123 123 if len(s) != 2:
124 124 continue
125 125 revid = revmap.get(source.lookuprev(s[0]))
126 126 if not revid:
127 127 continue
128 128 fp.write('%s %s\n' % (revid, s[1]))
129 129 return fp.getvalue()
130 130
131 131 def putcommit(self, files, copies, parents, commit, source, revmap):
132 132
133 133 files = dict(files)
134 134 def getfilectx(repo, memctx, f):
135 135 v = files[f]
136 136 data, mode = source.getfile(f, v)
137 137 if f == '.hgtags':
138 138 data = self._rewritetags(source, revmap, data)
139 139 return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
140 140 copies.get(f))
141 141
142 142 pl = []
143 143 for p in parents:
144 144 if p not in pl:
145 145 pl.append(p)
146 146 parents = pl
147 147 nparents = len(parents)
148 148 if self.filemapmode and nparents == 1:
149 149 m1node = self.repo.changelog.read(bin(parents[0]))[0]
150 150 parent = parents[0]
151 151
152 152 if len(parents) < 2:
153 153 parents.append(nullid)
154 154 if len(parents) < 2:
155 155 parents.append(nullid)
156 156 p2 = parents.pop(0)
157 157
158 158 text = commit.desc
159 159
160 160 sha1s = re.findall(sha1re, text)
161 161 for sha1 in sha1s:
162 162 oldrev = source.lookuprev(sha1)
163 163 newrev = revmap.get(oldrev)
164 164 if newrev is not None:
165 165 text = text.replace(sha1, newrev[:len(sha1)])
166 166
167 167 extra = commit.extra.copy()
168 168 if self.branchnames and commit.branch:
169 169 extra['branch'] = commit.branch
170 170 if commit.rev:
171 171 extra['convert_revision'] = commit.rev
172 172
173 173 while parents:
174 174 p1 = p2
175 175 p2 = parents.pop(0)
176 176 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
177 177 getfilectx, commit.author, commit.date, extra)
178 178 self.repo.commitctx(ctx)
179 179 text = "(octopus merge fixup)\n"
180 180 p2 = hex(self.repo.changelog.tip())
181 181
182 182 if self.filemapmode and nparents == 1:
183 183 man = self.repo.manifest
184 184 mnode = self.repo.changelog.read(bin(p2))[0]
185 185 closed = 'close' in commit.extra
186 186 if not closed and not man.cmp(m1node, man.revision(mnode)):
187 187 self.ui.status(_("filtering out empty revision\n"))
188 188 self.repo.rollback(force=True)
189 189 return parent
190 190 return p2
191 191
192 192 def puttags(self, tags):
193 193 try:
194 194 parentctx = self.repo[self.tagsbranch]
195 195 tagparent = parentctx.node()
196 196 except error.RepoError:
197 197 parentctx = None
198 198 tagparent = nullid
199 199
200 200 oldlines = set()
201 201 for branch, heads in self.repo.branchmap().iteritems():
202 202 for h in heads:
203 203 if '.hgtags' in self.repo[h]:
204 204 oldlines.update(
205 205 set(self.repo[h]['.hgtags'].data().splitlines(True)))
206 206 oldlines = sorted(list(oldlines))
207 207
208 208 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
209 209 if newlines == oldlines:
210 210 return None, None
211 211
212 212 # if the old and new tags match, then there is nothing to update
213 213 oldtags = set()
214 214 newtags = set()
215 215 for line in oldlines:
216 216 s = line.strip().split(' ', 1)
217 217 if len(s) != 2:
218 218 continue
219 219 oldtags.add(s[1])
220 220 for line in newlines:
221 221 s = line.strip().split(' ', 1)
222 222 if len(s) != 2:
223 223 continue
224 224 if s[1] not in oldtags:
225 225 newtags.add(s[1].strip())
226 226
227 227 if not newtags:
228 228 return None, None
229 229
230 230 data = "".join(newlines)
231 231 def getfilectx(repo, memctx, f):
232 232 return context.memfilectx(f, data, False, False, None)
233 233
234 234 self.ui.status(_("updating tags\n"))
235 235 date = "%s 0" % int(time.mktime(time.gmtime()))
236 236 extra = {'branch': self.tagsbranch}
237 237 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
238 238 [".hgtags"], getfilectx, "convert-repo", date,
239 239 extra)
240 240 self.repo.commitctx(ctx)
241 241 return hex(self.repo.changelog.tip()), hex(tagparent)
242 242
243 243 def setfilemapmode(self, active):
244 244 self.filemapmode = active
245 245
246 246 def putbookmarks(self, updatedbookmark):
247 247 if not len(updatedbookmark):
248 248 return
249 249
250 250 self.ui.status(_("updating bookmarks\n"))
251 251 destmarks = self.repo._bookmarks
252 252 for bookmark in updatedbookmark:
253 253 destmarks[bookmark] = bin(updatedbookmark[bookmark])
254 254 destmarks.write()
255 255
256 def hascommit(self, rev):
256 def hascommitforsplicemap(self, rev):
257 257 if rev not in self.repo and self.clonebranches:
258 258 raise util.Abort(_('revision %s not found in destination '
259 259 'repository (lookups with clonebranches=true '
260 260 'are not implemented)') % rev)
261 261 return rev in self.repo
262 262
263 263 class mercurial_source(converter_source):
264 264 def __init__(self, ui, path, rev=None):
265 265 converter_source.__init__(self, ui, path, rev)
266 266 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
267 267 self.ignored = set()
268 268 self.saverev = ui.configbool('convert', 'hg.saverev', False)
269 269 try:
270 270 self.repo = hg.repository(self.ui, path)
271 271 # try to provoke an exception if this isn't really a hg
272 272 # repo, but some other bogus compatible-looking url
273 273 if not self.repo.local():
274 274 raise error.RepoError
275 275 except error.RepoError:
276 276 ui.traceback()
277 277 raise NoRepo(_("%s is not a local Mercurial repository") % path)
278 278 self.lastrev = None
279 279 self.lastctx = None
280 280 self._changescache = None
281 281 self.convertfp = None
282 282 # Restrict converted revisions to startrev descendants
283 283 startnode = ui.config('convert', 'hg.startrev')
284 284 hgrevs = ui.config('convert', 'hg.revs')
285 285 if hgrevs is None:
286 286 if startnode is not None:
287 287 try:
288 288 startnode = self.repo.lookup(startnode)
289 289 except error.RepoError:
290 290 raise util.Abort(_('%s is not a valid start revision')
291 291 % startnode)
292 292 startrev = self.repo.changelog.rev(startnode)
293 293 children = {startnode: 1}
294 294 for r in self.repo.changelog.descendants([startrev]):
295 295 children[self.repo.changelog.node(r)] = 1
296 296 self.keep = children.__contains__
297 297 else:
298 298 self.keep = util.always
299 299 if rev:
300 300 self._heads = [self.repo[rev].node()]
301 301 else:
302 302 self._heads = self.repo.heads()
303 303 else:
304 304 if rev or startnode is not None:
305 305 raise util.Abort(_('hg.revs cannot be combined with '
306 306 'hg.startrev or --rev'))
307 307 nodes = set()
308 308 parents = set()
309 309 for r in scmutil.revrange(self.repo, [hgrevs]):
310 310 ctx = self.repo[r]
311 311 nodes.add(ctx.node())
312 312 parents.update(p.node() for p in ctx.parents())
313 313 self.keep = nodes.__contains__
314 314 self._heads = nodes - parents
315 315
316 316 def changectx(self, rev):
317 317 if self.lastrev != rev:
318 318 self.lastctx = self.repo[rev]
319 319 self.lastrev = rev
320 320 return self.lastctx
321 321
322 322 def parents(self, ctx):
323 323 return [p for p in ctx.parents() if p and self.keep(p.node())]
324 324
325 325 def getheads(self):
326 326 return [hex(h) for h in self._heads if self.keep(h)]
327 327
328 328 def getfile(self, name, rev):
329 329 try:
330 330 fctx = self.changectx(rev)[name]
331 331 return fctx.data(), fctx.flags()
332 332 except error.LookupError, err:
333 333 raise IOError(err)
334 334
335 335 def getchanges(self, rev):
336 336 ctx = self.changectx(rev)
337 337 parents = self.parents(ctx)
338 338 if not parents:
339 339 files = sorted(ctx.manifest())
340 340 # getcopies() is not needed for roots, but it is a simple way to
341 341 # detect missing revlogs and abort on errors or populate
342 342 # self.ignored
343 343 self.getcopies(ctx, parents, files)
344 344 return [(f, rev) for f in files if f not in self.ignored], {}
345 345 if self._changescache and self._changescache[0] == rev:
346 346 m, a, r = self._changescache[1]
347 347 else:
348 348 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
349 349 # getcopies() detects missing revlogs early, run it before
350 350 # filtering the changes.
351 351 copies = self.getcopies(ctx, parents, m + a)
352 352 changes = [(name, rev) for name in m + a + r
353 353 if name not in self.ignored]
354 354 return sorted(changes), copies
355 355
356 356 def getcopies(self, ctx, parents, files):
357 357 copies = {}
358 358 for name in files:
359 359 if name in self.ignored:
360 360 continue
361 361 try:
362 362 copysource, _copynode = ctx.filectx(name).renamed()
363 363 if copysource in self.ignored:
364 364 continue
365 365 # Ignore copy sources not in parent revisions
366 366 found = False
367 367 for p in parents:
368 368 if copysource in p:
369 369 found = True
370 370 break
371 371 if not found:
372 372 continue
373 373 copies[name] = copysource
374 374 except TypeError:
375 375 pass
376 376 except error.LookupError, e:
377 377 if not self.ignoreerrors:
378 378 raise
379 379 self.ignored.add(name)
380 380 self.ui.warn(_('ignoring: %s\n') % e)
381 381 return copies
382 382
383 383 def getcommit(self, rev):
384 384 ctx = self.changectx(rev)
385 385 parents = [p.hex() for p in self.parents(ctx)]
386 386 if self.saverev:
387 387 crev = rev
388 388 else:
389 389 crev = None
390 390 return commit(author=ctx.user(),
391 391 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
392 392 desc=ctx.description(), rev=crev, parents=parents,
393 393 branch=ctx.branch(), extra=ctx.extra(),
394 394 sortkey=ctx.rev())
395 395
396 396 def gettags(self):
397 397 # This will get written to .hgtags, filter non global tags out.
398 398 tags = [t for t in self.repo.tagslist()
399 399 if self.repo.tagtype(t[0]) == 'global']
400 400 return dict([(name, hex(node)) for name, node in tags
401 401 if self.keep(node)])
402 402
403 403 def getchangedfiles(self, rev, i):
404 404 ctx = self.changectx(rev)
405 405 parents = self.parents(ctx)
406 406 if not parents and i is None:
407 407 i = 0
408 408 changes = [], ctx.manifest().keys(), []
409 409 else:
410 410 i = i or 0
411 411 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
412 412 changes = [[f for f in l if f not in self.ignored] for l in changes]
413 413
414 414 if i == 0:
415 415 self._changescache = (rev, changes)
416 416
417 417 return changes[0] + changes[1] + changes[2]
418 418
419 419 def converted(self, rev, destrev):
420 420 if self.convertfp is None:
421 421 self.convertfp = open(self.repo.join('shamap'), 'a')
422 422 self.convertfp.write('%s %s\n' % (destrev, rev))
423 423 self.convertfp.flush()
424 424
425 425 def before(self):
426 426 self.ui.debug('run hg source pre-conversion action\n')
427 427
428 428 def after(self):
429 429 self.ui.debug('run hg source post-conversion action\n')
430 430
431 431 def hasnativeorder(self):
432 432 return True
433 433
434 434 def hasnativeclose(self):
435 435 return True
436 436
437 437 def lookuprev(self, rev):
438 438 try:
439 439 return hex(self.repo.lookup(rev))
440 440 except error.RepoError:
441 441 return None
442 442
443 443 def getbookmarks(self):
444 444 return bookmarks.listbookmarks(self.repo)
445 445
446 446 def checkrevformat(self, revstr, mapname='splicemap'):
447 447 """ Mercurial, revision string is a 40 byte hex """
448 448 self.checkhexformat(revstr, mapname)
@@ -1,1310 +1,1310 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 import os, re, sys, tempfile, urllib, urllib2
6 6 import xml.dom.minidom
7 7 import cPickle as pickle
8 8
9 9 from mercurial import strutil, scmutil, util, encoding
10 10 from mercurial.i18n import _
11 11
12 12 propertycache = util.propertycache
13 13
14 14 # Subversion stuff. Works best with very recent Python SVN bindings
15 15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
16 16 # these bindings.
17 17
18 18 from cStringIO import StringIO
19 19
20 20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
21 21 from common import commandline, converter_source, converter_sink, mapfile
22 22 from common import makedatetimestamp
23 23
24 24 try:
25 25 from svn.core import SubversionException, Pool
26 26 import svn
27 27 import svn.client
28 28 import svn.core
29 29 import svn.ra
30 30 import svn.delta
31 31 import transport
32 32 import warnings
33 33 warnings.filterwarnings('ignore',
34 34 module='svn.core',
35 35 category=DeprecationWarning)
36 36
37 37 except ImportError:
38 38 svn = None
39 39
40 40 class SvnPathNotFound(Exception):
41 41 pass
42 42
43 43 def revsplit(rev):
44 44 """Parse a revision string and return (uuid, path, revnum).
45 45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
46 46 ... '/proj%20B/mytrunk/mytrunk@1')
47 47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
48 48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
49 49 ('', '', 1)
50 50 >>> revsplit('@7')
51 51 ('', '', 7)
52 52 >>> revsplit('7')
53 53 ('', '', 0)
54 54 >>> revsplit('bad')
55 55 ('', '', 0)
56 56 """
57 57 parts = rev.rsplit('@', 1)
58 58 revnum = 0
59 59 if len(parts) > 1:
60 60 revnum = int(parts[1])
61 61 parts = parts[0].split('/', 1)
62 62 uuid = ''
63 63 mod = ''
64 64 if len(parts) > 1 and parts[0].startswith('svn:'):
65 65 uuid = parts[0][4:]
66 66 mod = '/' + parts[1]
67 67 return uuid, mod, revnum
68 68
69 69 def quote(s):
70 70 # As of svn 1.7, many svn calls expect "canonical" paths. In
71 71 # theory, we should call svn.core.*canonicalize() on all paths
72 72 # before passing them to the API. Instead, we assume the base url
73 73 # is canonical and copy the behaviour of svn URL encoding function
74 74 # so we can extend it safely with new components. The "safe"
75 75 # characters were taken from the "svn_uri__char_validity" table in
76 76 # libsvn_subr/path.c.
77 77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
78 78
79 79 def geturl(path):
80 80 try:
81 81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
82 82 except SubversionException:
83 83 # svn.client.url_from_path() fails with local repositories
84 84 pass
85 85 if os.path.isdir(path):
86 86 path = os.path.normpath(os.path.abspath(path))
87 87 if os.name == 'nt':
88 88 path = '/' + util.normpath(path)
89 89 # Module URL is later compared with the repository URL returned
90 90 # by svn API, which is UTF-8.
91 91 path = encoding.tolocal(path)
92 92 path = 'file://%s' % quote(path)
93 93 return svn.core.svn_path_canonicalize(path)
94 94
95 95 def optrev(number):
96 96 optrev = svn.core.svn_opt_revision_t()
97 97 optrev.kind = svn.core.svn_opt_revision_number
98 98 optrev.value.number = number
99 99 return optrev
100 100
101 101 class changedpath(object):
102 102 def __init__(self, p):
103 103 self.copyfrom_path = p.copyfrom_path
104 104 self.copyfrom_rev = p.copyfrom_rev
105 105 self.action = p.action
106 106
107 107 def get_log_child(fp, url, paths, start, end, limit=0,
108 108 discover_changed_paths=True, strict_node_history=False):
109 109 protocol = -1
110 110 def receiver(orig_paths, revnum, author, date, message, pool):
111 111 paths = {}
112 112 if orig_paths is not None:
113 113 for k, v in orig_paths.iteritems():
114 114 paths[k] = changedpath(v)
115 115 pickle.dump((paths, revnum, author, date, message),
116 116 fp, protocol)
117 117
118 118 try:
119 119 # Use an ra of our own so that our parent can consume
120 120 # our results without confusing the server.
121 121 t = transport.SvnRaTransport(url=url)
122 122 svn.ra.get_log(t.ra, paths, start, end, limit,
123 123 discover_changed_paths,
124 124 strict_node_history,
125 125 receiver)
126 126 except IOError:
127 127 # Caller may interrupt the iteration
128 128 pickle.dump(None, fp, protocol)
129 129 except Exception, inst:
130 130 pickle.dump(str(inst), fp, protocol)
131 131 else:
132 132 pickle.dump(None, fp, protocol)
133 133 fp.close()
134 134 # With large history, cleanup process goes crazy and suddenly
135 135 # consumes *huge* amount of memory. The output file being closed,
136 136 # there is no need for clean termination.
137 137 os._exit(0)
138 138
139 139 def debugsvnlog(ui, **opts):
140 140 """Fetch SVN log in a subprocess and channel them back to parent to
141 141 avoid memory collection issues.
142 142 """
143 143 if svn is None:
144 144 raise util.Abort(_('debugsvnlog could not load Subversion python '
145 145 'bindings'))
146 146
147 147 util.setbinary(sys.stdin)
148 148 util.setbinary(sys.stdout)
149 149 args = decodeargs(sys.stdin.read())
150 150 get_log_child(sys.stdout, *args)
151 151
152 152 class logstream(object):
153 153 """Interruptible revision log iterator."""
154 154 def __init__(self, stdout):
155 155 self._stdout = stdout
156 156
157 157 def __iter__(self):
158 158 while True:
159 159 try:
160 160 entry = pickle.load(self._stdout)
161 161 except EOFError:
162 162 raise util.Abort(_('Mercurial failed to run itself, check'
163 163 ' hg executable is in PATH'))
164 164 try:
165 165 orig_paths, revnum, author, date, message = entry
166 166 except (TypeError, ValueError):
167 167 if entry is None:
168 168 break
169 169 raise util.Abort(_("log stream exception '%s'") % entry)
170 170 yield entry
171 171
172 172 def close(self):
173 173 if self._stdout:
174 174 self._stdout.close()
175 175 self._stdout = None
176 176
177 177 class directlogstream(list):
178 178 """Direct revision log iterator.
179 179 This can be used for debugging and development but it will probably leak
180 180 memory and is not suitable for real conversions."""
181 181 def __init__(self, url, paths, start, end, limit=0,
182 182 discover_changed_paths=True, strict_node_history=False):
183 183
184 184 def receiver(orig_paths, revnum, author, date, message, pool):
185 185 paths = {}
186 186 if orig_paths is not None:
187 187 for k, v in orig_paths.iteritems():
188 188 paths[k] = changedpath(v)
189 189 self.append((paths, revnum, author, date, message))
190 190
191 191 # Use an ra of our own so that our parent can consume
192 192 # our results without confusing the server.
193 193 t = transport.SvnRaTransport(url=url)
194 194 svn.ra.get_log(t.ra, paths, start, end, limit,
195 195 discover_changed_paths,
196 196 strict_node_history,
197 197 receiver)
198 198
199 199 def close(self):
200 200 pass
201 201
202 202 # Check to see if the given path is a local Subversion repo. Verify this by
203 203 # looking for several svn-specific files and directories in the given
204 204 # directory.
205 205 def filecheck(ui, path, proto):
206 206 for x in ('locks', 'hooks', 'format', 'db'):
207 207 if not os.path.exists(os.path.join(path, x)):
208 208 return False
209 209 return True
210 210
211 211 # Check to see if a given path is the root of an svn repo over http. We verify
212 212 # this by requesting a version-controlled URL we know can't exist and looking
213 213 # for the svn-specific "not found" XML.
214 214 def httpcheck(ui, path, proto):
215 215 try:
216 216 opener = urllib2.build_opener()
217 217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
218 218 data = rsp.read()
219 219 except urllib2.HTTPError, inst:
220 220 if inst.code != 404:
221 221 # Except for 404 we cannot know for sure this is not an svn repo
222 222 ui.warn(_('svn: cannot probe remote repository, assume it could '
223 223 'be a subversion repository. Use --source-type if you '
224 224 'know better.\n'))
225 225 return True
226 226 data = inst.fp.read()
227 227 except Exception:
228 228 # Could be urllib2.URLError if the URL is invalid or anything else.
229 229 return False
230 230 return '<m:human-readable errcode="160013">' in data
231 231
232 232 protomap = {'http': httpcheck,
233 233 'https': httpcheck,
234 234 'file': filecheck,
235 235 }
236 236 def issvnurl(ui, url):
237 237 try:
238 238 proto, path = url.split('://', 1)
239 239 if proto == 'file':
240 240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
241 241 and path[2:6].lower() == '%3a/'):
242 242 path = path[:2] + ':/' + path[6:]
243 243 path = urllib.url2pathname(path)
244 244 except ValueError:
245 245 proto = 'file'
246 246 path = os.path.abspath(url)
247 247 if proto == 'file':
248 248 path = util.pconvert(path)
249 249 check = protomap.get(proto, lambda *args: False)
250 250 while '/' in path:
251 251 if check(ui, path, proto):
252 252 return True
253 253 path = path.rsplit('/', 1)[0]
254 254 return False
255 255
256 256 # SVN conversion code stolen from bzr-svn and tailor
257 257 #
258 258 # Subversion looks like a versioned filesystem, branches structures
259 259 # are defined by conventions and not enforced by the tool. First,
260 260 # we define the potential branches (modules) as "trunk" and "branches"
261 261 # children directories. Revisions are then identified by their
262 262 # module and revision number (and a repository identifier).
263 263 #
264 264 # The revision graph is really a tree (or a forest). By default, a
265 265 # revision parent is the previous revision in the same module. If the
266 266 # module directory is copied/moved from another module then the
267 267 # revision is the module root and its parent the source revision in
268 268 # the parent module. A revision has at most one parent.
269 269 #
270 270 class svn_source(converter_source):
271 271 def __init__(self, ui, url, rev=None):
272 272 super(svn_source, self).__init__(ui, url, rev=rev)
273 273
274 274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
275 275 (os.path.exists(url) and
276 276 os.path.exists(os.path.join(url, '.svn'))) or
277 277 issvnurl(ui, url)):
278 278 raise NoRepo(_("%s does not look like a Subversion repository")
279 279 % url)
280 280 if svn is None:
281 281 raise MissingTool(_('could not load Subversion python bindings'))
282 282
283 283 try:
284 284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
285 285 if version < (1, 4):
286 286 raise MissingTool(_('Subversion python bindings %d.%d found, '
287 287 '1.4 or later required') % version)
288 288 except AttributeError:
289 289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
290 290 'or later required'))
291 291
292 292 self.lastrevs = {}
293 293
294 294 latest = None
295 295 try:
296 296 # Support file://path@rev syntax. Useful e.g. to convert
297 297 # deleted branches.
298 298 at = url.rfind('@')
299 299 if at >= 0:
300 300 latest = int(url[at + 1:])
301 301 url = url[:at]
302 302 except ValueError:
303 303 pass
304 304 self.url = geturl(url)
305 305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
306 306 try:
307 307 self.transport = transport.SvnRaTransport(url=self.url)
308 308 self.ra = self.transport.ra
309 309 self.ctx = self.transport.client
310 310 self.baseurl = svn.ra.get_repos_root(self.ra)
311 311 # Module is either empty or a repository path starting with
312 312 # a slash and not ending with a slash.
313 313 self.module = urllib.unquote(self.url[len(self.baseurl):])
314 314 self.prevmodule = None
315 315 self.rootmodule = self.module
316 316 self.commits = {}
317 317 self.paths = {}
318 318 self.uuid = svn.ra.get_uuid(self.ra)
319 319 except SubversionException:
320 320 ui.traceback()
321 321 raise NoRepo(_("%s does not look like a Subversion repository")
322 322 % self.url)
323 323
324 324 if rev:
325 325 try:
326 326 latest = int(rev)
327 327 except ValueError:
328 328 raise util.Abort(_('svn: revision %s is not an integer') % rev)
329 329
330 330 self.trunkname = self.ui.config('convert', 'svn.trunk',
331 331 'trunk').strip('/')
332 332 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
333 333 try:
334 334 self.startrev = int(self.startrev)
335 335 if self.startrev < 0:
336 336 self.startrev = 0
337 337 except ValueError:
338 338 raise util.Abort(_('svn: start revision %s is not an integer')
339 339 % self.startrev)
340 340
341 341 try:
342 342 self.head = self.latest(self.module, latest)
343 343 except SvnPathNotFound:
344 344 self.head = None
345 345 if not self.head:
346 346 raise util.Abort(_('no revision found in module %s')
347 347 % self.module)
348 348 self.last_changed = self.revnum(self.head)
349 349
350 350 self._changescache = None
351 351
352 352 if os.path.exists(os.path.join(url, '.svn/entries')):
353 353 self.wc = url
354 354 else:
355 355 self.wc = None
356 356 self.convertfp = None
357 357
358 358 def setrevmap(self, revmap):
359 359 lastrevs = {}
360 360 for revid in revmap.iterkeys():
361 361 uuid, module, revnum = revsplit(revid)
362 362 lastrevnum = lastrevs.setdefault(module, revnum)
363 363 if revnum > lastrevnum:
364 364 lastrevs[module] = revnum
365 365 self.lastrevs = lastrevs
366 366
367 367 def exists(self, path, optrev):
368 368 try:
369 369 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
370 370 optrev, False, self.ctx)
371 371 return True
372 372 except SubversionException:
373 373 return False
374 374
375 375 def getheads(self):
376 376
377 377 def isdir(path, revnum):
378 378 kind = self._checkpath(path, revnum)
379 379 return kind == svn.core.svn_node_dir
380 380
381 381 def getcfgpath(name, rev):
382 382 cfgpath = self.ui.config('convert', 'svn.' + name)
383 383 if cfgpath is not None and cfgpath.strip() == '':
384 384 return None
385 385 path = (cfgpath or name).strip('/')
386 386 if not self.exists(path, rev):
387 387 if self.module.endswith(path) and name == 'trunk':
388 388 # we are converting from inside this directory
389 389 return None
390 390 if cfgpath:
391 391 raise util.Abort(_('expected %s to be at %r, but not found')
392 392 % (name, path))
393 393 return None
394 394 self.ui.note(_('found %s at %r\n') % (name, path))
395 395 return path
396 396
397 397 rev = optrev(self.last_changed)
398 398 oldmodule = ''
399 399 trunk = getcfgpath('trunk', rev)
400 400 self.tags = getcfgpath('tags', rev)
401 401 branches = getcfgpath('branches', rev)
402 402
403 403 # If the project has a trunk or branches, we will extract heads
404 404 # from them. We keep the project root otherwise.
405 405 if trunk:
406 406 oldmodule = self.module or ''
407 407 self.module += '/' + trunk
408 408 self.head = self.latest(self.module, self.last_changed)
409 409 if not self.head:
410 410 raise util.Abort(_('no revision found in module %s')
411 411 % self.module)
412 412
413 413 # First head in the list is the module's head
414 414 self.heads = [self.head]
415 415 if self.tags is not None:
416 416 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
417 417
418 418 # Check if branches bring a few more heads to the list
419 419 if branches:
420 420 rpath = self.url.strip('/')
421 421 branchnames = svn.client.ls(rpath + '/' + quote(branches),
422 422 rev, False, self.ctx)
423 423 for branch in sorted(branchnames):
424 424 module = '%s/%s/%s' % (oldmodule, branches, branch)
425 425 if not isdir(module, self.last_changed):
426 426 continue
427 427 brevid = self.latest(module, self.last_changed)
428 428 if not brevid:
429 429 self.ui.note(_('ignoring empty branch %s\n') % branch)
430 430 continue
431 431 self.ui.note(_('found branch %s at %d\n') %
432 432 (branch, self.revnum(brevid)))
433 433 self.heads.append(brevid)
434 434
435 435 if self.startrev and self.heads:
436 436 if len(self.heads) > 1:
437 437 raise util.Abort(_('svn: start revision is not supported '
438 438 'with more than one branch'))
439 439 revnum = self.revnum(self.heads[0])
440 440 if revnum < self.startrev:
441 441 raise util.Abort(
442 442 _('svn: no revision found after start revision %d')
443 443 % self.startrev)
444 444
445 445 return self.heads
446 446
447 447 def getchanges(self, rev):
448 448 if self._changescache and self._changescache[0] == rev:
449 449 return self._changescache[1]
450 450 self._changescache = None
451 451 (paths, parents) = self.paths[rev]
452 452 if parents:
453 453 files, self.removed, copies = self.expandpaths(rev, paths, parents)
454 454 else:
455 455 # Perform a full checkout on roots
456 456 uuid, module, revnum = revsplit(rev)
457 457 entries = svn.client.ls(self.baseurl + quote(module),
458 458 optrev(revnum), True, self.ctx)
459 459 files = [n for n, e in entries.iteritems()
460 460 if e.kind == svn.core.svn_node_file]
461 461 copies = {}
462 462 self.removed = set()
463 463
464 464 files.sort()
465 465 files = zip(files, [rev] * len(files))
466 466
467 467 # caller caches the result, so free it here to release memory
468 468 del self.paths[rev]
469 469 return (files, copies)
470 470
471 471 def getchangedfiles(self, rev, i):
472 472 changes = self.getchanges(rev)
473 473 self._changescache = (rev, changes)
474 474 return [f[0] for f in changes[0]]
475 475
476 476 def getcommit(self, rev):
477 477 if rev not in self.commits:
478 478 uuid, module, revnum = revsplit(rev)
479 479 self.module = module
480 480 self.reparent(module)
481 481 # We assume that:
482 482 # - requests for revisions after "stop" come from the
483 483 # revision graph backward traversal. Cache all of them
484 484 # down to stop, they will be used eventually.
485 485 # - requests for revisions before "stop" come to get
486 486 # isolated branches parents. Just fetch what is needed.
487 487 stop = self.lastrevs.get(module, 0)
488 488 if revnum < stop:
489 489 stop = revnum + 1
490 490 self._fetch_revisions(revnum, stop)
491 491 if rev not in self.commits:
492 492 raise util.Abort(_('svn: revision %s not found') % revnum)
493 493 commit = self.commits[rev]
494 494 # caller caches the result, so free it here to release memory
495 495 del self.commits[rev]
496 496 return commit
497 497
498 498 def checkrevformat(self, revstr, mapname='splicemap'):
499 499 """ fails if revision format does not match the correct format"""
500 500 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
501 501 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
502 502 '{12,12}(.*)\@[0-9]+$',revstr):
503 503 raise util.Abort(_('%s entry %s is not a valid revision'
504 504 ' identifier') % (mapname, revstr))
505 505
506 506 def gettags(self):
507 507 tags = {}
508 508 if self.tags is None:
509 509 return tags
510 510
511 511 # svn tags are just a convention, project branches left in a
512 512 # 'tags' directory. There is no other relationship than
513 513 # ancestry, which is expensive to discover and makes them hard
514 514 # to update incrementally. Worse, past revisions may be
515 515 # referenced by tags far away in the future, requiring a deep
516 516 # history traversal on every calculation. Current code
517 517 # performs a single backward traversal, tracking moves within
518 518 # the tags directory (tag renaming) and recording a new tag
519 519 # everytime a project is copied from outside the tags
520 520 # directory. It also lists deleted tags, this behaviour may
521 521 # change in the future.
522 522 pendings = []
523 523 tagspath = self.tags
524 524 start = svn.ra.get_latest_revnum(self.ra)
525 525 stream = self._getlog([self.tags], start, self.startrev)
526 526 try:
527 527 for entry in stream:
528 528 origpaths, revnum, author, date, message = entry
529 529 if not origpaths:
530 530 origpaths = []
531 531 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
532 532 in origpaths.iteritems() if e.copyfrom_path]
533 533 # Apply moves/copies from more specific to general
534 534 copies.sort(reverse=True)
535 535
536 536 srctagspath = tagspath
537 537 if copies and copies[-1][2] == tagspath:
538 538 # Track tags directory moves
539 539 srctagspath = copies.pop()[0]
540 540
541 541 for source, sourcerev, dest in copies:
542 542 if not dest.startswith(tagspath + '/'):
543 543 continue
544 544 for tag in pendings:
545 545 if tag[0].startswith(dest):
546 546 tagpath = source + tag[0][len(dest):]
547 547 tag[:2] = [tagpath, sourcerev]
548 548 break
549 549 else:
550 550 pendings.append([source, sourcerev, dest])
551 551
552 552 # Filter out tags with children coming from different
553 553 # parts of the repository like:
554 554 # /tags/tag.1 (from /trunk:10)
555 555 # /tags/tag.1/foo (from /branches/foo:12)
556 556 # Here/tags/tag.1 discarded as well as its children.
557 557 # It happens with tools like cvs2svn. Such tags cannot
558 558 # be represented in mercurial.
559 559 addeds = dict((p, e.copyfrom_path) for p, e
560 560 in origpaths.iteritems()
561 561 if e.action == 'A' and e.copyfrom_path)
562 562 badroots = set()
563 563 for destroot in addeds:
564 564 for source, sourcerev, dest in pendings:
565 565 if (not dest.startswith(destroot + '/')
566 566 or source.startswith(addeds[destroot] + '/')):
567 567 continue
568 568 badroots.add(destroot)
569 569 break
570 570
571 571 for badroot in badroots:
572 572 pendings = [p for p in pendings if p[2] != badroot
573 573 and not p[2].startswith(badroot + '/')]
574 574
575 575 # Tell tag renamings from tag creations
576 576 renamings = []
577 577 for source, sourcerev, dest in pendings:
578 578 tagname = dest.split('/')[-1]
579 579 if source.startswith(srctagspath):
580 580 renamings.append([source, sourcerev, tagname])
581 581 continue
582 582 if tagname in tags:
583 583 # Keep the latest tag value
584 584 continue
585 585 # From revision may be fake, get one with changes
586 586 try:
587 587 tagid = self.latest(source, sourcerev)
588 588 if tagid and tagname not in tags:
589 589 tags[tagname] = tagid
590 590 except SvnPathNotFound:
591 591 # It happens when we are following directories
592 592 # we assumed were copied with their parents
593 593 # but were really created in the tag
594 594 # directory.
595 595 pass
596 596 pendings = renamings
597 597 tagspath = srctagspath
598 598 finally:
599 599 stream.close()
600 600 return tags
601 601
602 602 def converted(self, rev, destrev):
603 603 if not self.wc:
604 604 return
605 605 if self.convertfp is None:
606 606 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
607 607 'a')
608 608 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
609 609 self.convertfp.flush()
610 610
611 611 def revid(self, revnum, module=None):
612 612 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
613 613
614 614 def revnum(self, rev):
615 615 return int(rev.split('@')[-1])
616 616
617 617 def latest(self, path, stop=None):
618 618 """Find the latest revid affecting path, up to stop revision
619 619 number. If stop is None, default to repository latest
620 620 revision. It may return a revision in a different module,
621 621 since a branch may be moved without a change being
622 622 reported. Return None if computed module does not belong to
623 623 rootmodule subtree.
624 624 """
625 625 def findchanges(path, start, stop=None):
626 626 stream = self._getlog([path], start, stop or 1)
627 627 try:
628 628 for entry in stream:
629 629 paths, revnum, author, date, message = entry
630 630 if stop is None and paths:
631 631 # We do not know the latest changed revision,
632 632 # keep the first one with changed paths.
633 633 break
634 634 if revnum <= stop:
635 635 break
636 636
637 637 for p in paths:
638 638 if (not path.startswith(p) or
639 639 not paths[p].copyfrom_path):
640 640 continue
641 641 newpath = paths[p].copyfrom_path + path[len(p):]
642 642 self.ui.debug("branch renamed from %s to %s at %d\n" %
643 643 (path, newpath, revnum))
644 644 path = newpath
645 645 break
646 646 if not paths:
647 647 revnum = None
648 648 return revnum, path
649 649 finally:
650 650 stream.close()
651 651
652 652 if not path.startswith(self.rootmodule):
653 653 # Requests on foreign branches may be forbidden at server level
654 654 self.ui.debug('ignoring foreign branch %r\n' % path)
655 655 return None
656 656
657 657 if stop is None:
658 658 stop = svn.ra.get_latest_revnum(self.ra)
659 659 try:
660 660 prevmodule = self.reparent('')
661 661 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
662 662 self.reparent(prevmodule)
663 663 except SubversionException:
664 664 dirent = None
665 665 if not dirent:
666 666 raise SvnPathNotFound(_('%s not found up to revision %d')
667 667 % (path, stop))
668 668
669 669 # stat() gives us the previous revision on this line of
670 670 # development, but it might be in *another module*. Fetch the
671 671 # log and detect renames down to the latest revision.
672 672 revnum, realpath = findchanges(path, stop, dirent.created_rev)
673 673 if revnum is None:
674 674 # Tools like svnsync can create empty revision, when
675 675 # synchronizing only a subtree for instance. These empty
676 676 # revisions created_rev still have their original values
677 677 # despite all changes having disappeared and can be
678 678 # returned by ra.stat(), at least when stating the root
679 679 # module. In that case, do not trust created_rev and scan
680 680 # the whole history.
681 681 revnum, realpath = findchanges(path, stop)
682 682 if revnum is None:
683 683 self.ui.debug('ignoring empty branch %r\n' % realpath)
684 684 return None
685 685
686 686 if not realpath.startswith(self.rootmodule):
687 687 self.ui.debug('ignoring foreign branch %r\n' % realpath)
688 688 return None
689 689 return self.revid(revnum, realpath)
690 690
691 691 def reparent(self, module):
692 692 """Reparent the svn transport and return the previous parent."""
693 693 if self.prevmodule == module:
694 694 return module
695 695 svnurl = self.baseurl + quote(module)
696 696 prevmodule = self.prevmodule
697 697 if prevmodule is None:
698 698 prevmodule = ''
699 699 self.ui.debug("reparent to %s\n" % svnurl)
700 700 svn.ra.reparent(self.ra, svnurl)
701 701 self.prevmodule = module
702 702 return prevmodule
703 703
704 704 def expandpaths(self, rev, paths, parents):
705 705 changed, removed = set(), set()
706 706 copies = {}
707 707
708 708 new_module, revnum = revsplit(rev)[1:]
709 709 if new_module != self.module:
710 710 self.module = new_module
711 711 self.reparent(self.module)
712 712
713 713 for i, (path, ent) in enumerate(paths):
714 714 self.ui.progress(_('scanning paths'), i, item=path,
715 715 total=len(paths))
716 716 entrypath = self.getrelpath(path)
717 717
718 718 kind = self._checkpath(entrypath, revnum)
719 719 if kind == svn.core.svn_node_file:
720 720 changed.add(self.recode(entrypath))
721 721 if not ent.copyfrom_path or not parents:
722 722 continue
723 723 # Copy sources not in parent revisions cannot be
724 724 # represented, ignore their origin for now
725 725 pmodule, prevnum = revsplit(parents[0])[1:]
726 726 if ent.copyfrom_rev < prevnum:
727 727 continue
728 728 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
729 729 if not copyfrom_path:
730 730 continue
731 731 self.ui.debug("copied to %s from %s@%s\n" %
732 732 (entrypath, copyfrom_path, ent.copyfrom_rev))
733 733 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
734 734 elif kind == 0: # gone, but had better be a deleted *file*
735 735 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
736 736 pmodule, prevnum = revsplit(parents[0])[1:]
737 737 parentpath = pmodule + "/" + entrypath
738 738 fromkind = self._checkpath(entrypath, prevnum, pmodule)
739 739
740 740 if fromkind == svn.core.svn_node_file:
741 741 removed.add(self.recode(entrypath))
742 742 elif fromkind == svn.core.svn_node_dir:
743 743 oroot = parentpath.strip('/')
744 744 nroot = path.strip('/')
745 745 children = self._iterfiles(oroot, prevnum)
746 746 for childpath in children:
747 747 childpath = childpath.replace(oroot, nroot)
748 748 childpath = self.getrelpath("/" + childpath, pmodule)
749 749 if childpath:
750 750 removed.add(self.recode(childpath))
751 751 else:
752 752 self.ui.debug('unknown path in revision %d: %s\n' % \
753 753 (revnum, path))
754 754 elif kind == svn.core.svn_node_dir:
755 755 if ent.action == 'M':
756 756 # If the directory just had a prop change,
757 757 # then we shouldn't need to look for its children.
758 758 continue
759 759 if ent.action == 'R' and parents:
760 760 # If a directory is replacing a file, mark the previous
761 761 # file as deleted
762 762 pmodule, prevnum = revsplit(parents[0])[1:]
763 763 pkind = self._checkpath(entrypath, prevnum, pmodule)
764 764 if pkind == svn.core.svn_node_file:
765 765 removed.add(self.recode(entrypath))
766 766 elif pkind == svn.core.svn_node_dir:
767 767 # We do not know what files were kept or removed,
768 768 # mark them all as changed.
769 769 for childpath in self._iterfiles(pmodule, prevnum):
770 770 childpath = self.getrelpath("/" + childpath)
771 771 if childpath:
772 772 changed.add(self.recode(childpath))
773 773
774 774 for childpath in self._iterfiles(path, revnum):
775 775 childpath = self.getrelpath("/" + childpath)
776 776 if childpath:
777 777 changed.add(self.recode(childpath))
778 778
779 779 # Handle directory copies
780 780 if not ent.copyfrom_path or not parents:
781 781 continue
782 782 # Copy sources not in parent revisions cannot be
783 783 # represented, ignore their origin for now
784 784 pmodule, prevnum = revsplit(parents[0])[1:]
785 785 if ent.copyfrom_rev < prevnum:
786 786 continue
787 787 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
788 788 if not copyfrompath:
789 789 continue
790 790 self.ui.debug("mark %s came from %s:%d\n"
791 791 % (path, copyfrompath, ent.copyfrom_rev))
792 792 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
793 793 for childpath in children:
794 794 childpath = self.getrelpath("/" + childpath, pmodule)
795 795 if not childpath:
796 796 continue
797 797 copytopath = path + childpath[len(copyfrompath):]
798 798 copytopath = self.getrelpath(copytopath)
799 799 copies[self.recode(copytopath)] = self.recode(childpath)
800 800
801 801 self.ui.progress(_('scanning paths'), None)
802 802 changed.update(removed)
803 803 return (list(changed), removed, copies)
804 804
805 805 def _fetch_revisions(self, from_revnum, to_revnum):
806 806 if from_revnum < to_revnum:
807 807 from_revnum, to_revnum = to_revnum, from_revnum
808 808
809 809 self.child_cset = None
810 810
811 811 def parselogentry(orig_paths, revnum, author, date, message):
812 812 """Return the parsed commit object or None, and True if
813 813 the revision is a branch root.
814 814 """
815 815 self.ui.debug("parsing revision %d (%d changes)\n" %
816 816 (revnum, len(orig_paths)))
817 817
818 818 branched = False
819 819 rev = self.revid(revnum)
820 820 # branch log might return entries for a parent we already have
821 821
822 822 if rev in self.commits or revnum < to_revnum:
823 823 return None, branched
824 824
825 825 parents = []
826 826 # check whether this revision is the start of a branch or part
827 827 # of a branch renaming
828 828 orig_paths = sorted(orig_paths.iteritems())
829 829 root_paths = [(p, e) for p, e in orig_paths
830 830 if self.module.startswith(p)]
831 831 if root_paths:
832 832 path, ent = root_paths[-1]
833 833 if ent.copyfrom_path:
834 834 branched = True
835 835 newpath = ent.copyfrom_path + self.module[len(path):]
836 836 # ent.copyfrom_rev may not be the actual last revision
837 837 previd = self.latest(newpath, ent.copyfrom_rev)
838 838 if previd is not None:
839 839 prevmodule, prevnum = revsplit(previd)[1:]
840 840 if prevnum >= self.startrev:
841 841 parents = [previd]
842 842 self.ui.note(
843 843 _('found parent of branch %s at %d: %s\n') %
844 844 (self.module, prevnum, prevmodule))
845 845 else:
846 846 self.ui.debug("no copyfrom path, don't know what to do.\n")
847 847
848 848 paths = []
849 849 # filter out unrelated paths
850 850 for path, ent in orig_paths:
851 851 if self.getrelpath(path) is None:
852 852 continue
853 853 paths.append((path, ent))
854 854
855 855 # Example SVN datetime. Includes microseconds.
856 856 # ISO-8601 conformant
857 857 # '2007-01-04T17:35:00.902377Z'
858 858 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
859 859 if self.ui.configbool('convert', 'localtimezone'):
860 860 date = makedatetimestamp(date[0])
861 861
862 862 log = message and self.recode(message) or ''
863 863 author = author and self.recode(author) or ''
864 864 try:
865 865 branch = self.module.split("/")[-1]
866 866 if branch == self.trunkname:
867 867 branch = None
868 868 except IndexError:
869 869 branch = None
870 870
871 871 cset = commit(author=author,
872 872 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
873 873 desc=log,
874 874 parents=parents,
875 875 branch=branch,
876 876 rev=rev)
877 877
878 878 self.commits[rev] = cset
879 879 # The parents list is *shared* among self.paths and the
880 880 # commit object. Both will be updated below.
881 881 self.paths[rev] = (paths, cset.parents)
882 882 if self.child_cset and not self.child_cset.parents:
883 883 self.child_cset.parents[:] = [rev]
884 884 self.child_cset = cset
885 885 return cset, branched
886 886
887 887 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
888 888 (self.module, from_revnum, to_revnum))
889 889
890 890 try:
891 891 firstcset = None
892 892 lastonbranch = False
893 893 stream = self._getlog([self.module], from_revnum, to_revnum)
894 894 try:
895 895 for entry in stream:
896 896 paths, revnum, author, date, message = entry
897 897 if revnum < self.startrev:
898 898 lastonbranch = True
899 899 break
900 900 if not paths:
901 901 self.ui.debug('revision %d has no entries\n' % revnum)
902 902 # If we ever leave the loop on an empty
903 903 # revision, do not try to get a parent branch
904 904 lastonbranch = lastonbranch or revnum == 0
905 905 continue
906 906 cset, lastonbranch = parselogentry(paths, revnum, author,
907 907 date, message)
908 908 if cset:
909 909 firstcset = cset
910 910 if lastonbranch:
911 911 break
912 912 finally:
913 913 stream.close()
914 914
915 915 if not lastonbranch and firstcset and not firstcset.parents:
916 916 # The first revision of the sequence (the last fetched one)
917 917 # has invalid parents if not a branch root. Find the parent
918 918 # revision now, if any.
919 919 try:
920 920 firstrevnum = self.revnum(firstcset.rev)
921 921 if firstrevnum > 1:
922 922 latest = self.latest(self.module, firstrevnum - 1)
923 923 if latest:
924 924 firstcset.parents.append(latest)
925 925 except SvnPathNotFound:
926 926 pass
927 927 except SubversionException, (inst, num):
928 928 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
929 929 raise util.Abort(_('svn: branch has no revision %s')
930 930 % to_revnum)
931 931 raise
932 932
933 933 def getfile(self, file, rev):
934 934 # TODO: ra.get_file transmits the whole file instead of diffs.
935 935 if file in self.removed:
936 936 raise IOError
937 937 mode = ''
938 938 try:
939 939 new_module, revnum = revsplit(rev)[1:]
940 940 if self.module != new_module:
941 941 self.module = new_module
942 942 self.reparent(self.module)
943 943 io = StringIO()
944 944 info = svn.ra.get_file(self.ra, file, revnum, io)
945 945 data = io.getvalue()
946 946 # ra.get_file() seems to keep a reference on the input buffer
947 947 # preventing collection. Release it explicitly.
948 948 io.close()
949 949 if isinstance(info, list):
950 950 info = info[-1]
951 951 mode = ("svn:executable" in info) and 'x' or ''
952 952 mode = ("svn:special" in info) and 'l' or mode
953 953 except SubversionException, e:
954 954 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
955 955 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
956 956 if e.apr_err in notfound: # File not found
957 957 raise IOError
958 958 raise
959 959 if mode == 'l':
960 960 link_prefix = "link "
961 961 if data.startswith(link_prefix):
962 962 data = data[len(link_prefix):]
963 963 return data, mode
964 964
965 965 def _iterfiles(self, path, revnum):
966 966 """Enumerate all files in path at revnum, recursively."""
967 967 path = path.strip('/')
968 968 pool = Pool()
969 969 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
970 970 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
971 971 if path:
972 972 path += '/'
973 973 return ((path + p) for p, e in entries.iteritems()
974 974 if e.kind == svn.core.svn_node_file)
975 975
976 976 def getrelpath(self, path, module=None):
977 977 if module is None:
978 978 module = self.module
979 979 # Given the repository url of this wc, say
980 980 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
981 981 # extract the "entry" portion (a relative path) from what
982 982 # svn log --xml says, i.e.
983 983 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
984 984 # that is to say "tests/PloneTestCase.py"
985 985 if path.startswith(module):
986 986 relative = path.rstrip('/')[len(module):]
987 987 if relative.startswith('/'):
988 988 return relative[1:]
989 989 elif relative == '':
990 990 return relative
991 991
992 992 # The path is outside our tracked tree...
993 993 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
994 994 return None
995 995
996 996 def _checkpath(self, path, revnum, module=None):
997 997 if module is not None:
998 998 prevmodule = self.reparent('')
999 999 path = module + '/' + path
1000 1000 try:
1001 1001 # ra.check_path does not like leading slashes very much, it leads
1002 1002 # to PROPFIND subversion errors
1003 1003 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1004 1004 finally:
1005 1005 if module is not None:
1006 1006 self.reparent(prevmodule)
1007 1007
1008 1008 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1009 1009 strict_node_history=False):
1010 1010 # Normalize path names, svn >= 1.5 only wants paths relative to
1011 1011 # supplied URL
1012 1012 relpaths = []
1013 1013 for p in paths:
1014 1014 if not p.startswith('/'):
1015 1015 p = self.module + '/' + p
1016 1016 relpaths.append(p.strip('/'))
1017 1017 args = [self.baseurl, relpaths, start, end, limit,
1018 1018 discover_changed_paths, strict_node_history]
1019 1019 # undocumented feature: debugsvnlog can be disabled
1020 1020 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1021 1021 return directlogstream(*args)
1022 1022 arg = encodeargs(args)
1023 1023 hgexe = util.hgexecutable()
1024 1024 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1025 1025 stdin, stdout = util.popen2(util.quotecommand(cmd))
1026 1026 stdin.write(arg)
1027 1027 try:
1028 1028 stdin.close()
1029 1029 except IOError:
1030 1030 raise util.Abort(_('Mercurial failed to run itself, check'
1031 1031 ' hg executable is in PATH'))
1032 1032 return logstream(stdout)
1033 1033
1034 1034 pre_revprop_change = '''#!/bin/sh
1035 1035
1036 1036 REPOS="$1"
1037 1037 REV="$2"
1038 1038 USER="$3"
1039 1039 PROPNAME="$4"
1040 1040 ACTION="$5"
1041 1041
1042 1042 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1043 1043 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1044 1044 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1045 1045
1046 1046 echo "Changing prohibited revision property" >&2
1047 1047 exit 1
1048 1048 '''
1049 1049
1050 1050 class svn_sink(converter_sink, commandline):
1051 1051 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1052 1052 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1053 1053
1054 1054 def prerun(self):
1055 1055 if self.wc:
1056 1056 os.chdir(self.wc)
1057 1057
1058 1058 def postrun(self):
1059 1059 if self.wc:
1060 1060 os.chdir(self.cwd)
1061 1061
1062 1062 def join(self, name):
1063 1063 return os.path.join(self.wc, '.svn', name)
1064 1064
1065 1065 def revmapfile(self):
1066 1066 return self.join('hg-shamap')
1067 1067
1068 1068 def authorfile(self):
1069 1069 return self.join('hg-authormap')
1070 1070
1071 1071 def __init__(self, ui, path):
1072 1072
1073 1073 converter_sink.__init__(self, ui, path)
1074 1074 commandline.__init__(self, ui, 'svn')
1075 1075 self.delete = []
1076 1076 self.setexec = []
1077 1077 self.delexec = []
1078 1078 self.copies = []
1079 1079 self.wc = None
1080 1080 self.cwd = os.getcwd()
1081 1081
1082 1082 created = False
1083 1083 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1084 1084 self.wc = os.path.realpath(path)
1085 1085 self.run0('update')
1086 1086 else:
1087 1087 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1088 1088 path = os.path.realpath(path)
1089 1089 if os.path.isdir(os.path.dirname(path)):
1090 1090 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1091 1091 ui.status(_('initializing svn repository %r\n') %
1092 1092 os.path.basename(path))
1093 1093 commandline(ui, 'svnadmin').run0('create', path)
1094 1094 created = path
1095 1095 path = util.normpath(path)
1096 1096 if not path.startswith('/'):
1097 1097 path = '/' + path
1098 1098 path = 'file://' + path
1099 1099
1100 1100 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1101 1101 ui.status(_('initializing svn working copy %r\n')
1102 1102 % os.path.basename(wcpath))
1103 1103 self.run0('checkout', path, wcpath)
1104 1104
1105 1105 self.wc = wcpath
1106 1106 self.opener = scmutil.opener(self.wc)
1107 1107 self.wopener = scmutil.opener(self.wc)
1108 1108 self.childmap = mapfile(ui, self.join('hg-childmap'))
1109 1109 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1110 1110
1111 1111 if created:
1112 1112 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1113 1113 fp = open(hook, 'w')
1114 1114 fp.write(pre_revprop_change)
1115 1115 fp.close()
1116 1116 util.setflags(hook, False, True)
1117 1117
1118 1118 output = self.run0('info')
1119 1119 self.uuid = self.uuid_re.search(output).group(1).strip()
1120 1120
1121 1121 def wjoin(self, *names):
1122 1122 return os.path.join(self.wc, *names)
1123 1123
1124 1124 @propertycache
1125 1125 def manifest(self):
1126 1126 # As of svn 1.7, the "add" command fails when receiving
1127 1127 # already tracked entries, so we have to track and filter them
1128 1128 # ourselves.
1129 1129 m = set()
1130 1130 output = self.run0('ls', recursive=True, xml=True)
1131 1131 doc = xml.dom.minidom.parseString(output)
1132 1132 for e in doc.getElementsByTagName('entry'):
1133 1133 for n in e.childNodes:
1134 1134 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1135 1135 continue
1136 1136 name = ''.join(c.data for c in n.childNodes
1137 1137 if c.nodeType == c.TEXT_NODE)
1138 1138 # Entries are compared with names coming from
1139 1139 # mercurial, so bytes with undefined encoding. Our
1140 1140 # best bet is to assume they are in local
1141 1141 # encoding. They will be passed to command line calls
1142 1142 # later anyway, so they better be.
1143 1143 m.add(encoding.tolocal(name.encode('utf-8')))
1144 1144 break
1145 1145 return m
1146 1146
1147 1147 def putfile(self, filename, flags, data):
1148 1148 if 'l' in flags:
1149 1149 self.wopener.symlink(data, filename)
1150 1150 else:
1151 1151 try:
1152 1152 if os.path.islink(self.wjoin(filename)):
1153 1153 os.unlink(filename)
1154 1154 except OSError:
1155 1155 pass
1156 1156 self.wopener.write(filename, data)
1157 1157
1158 1158 if self.is_exec:
1159 1159 if self.is_exec(self.wjoin(filename)):
1160 1160 if 'x' not in flags:
1161 1161 self.delexec.append(filename)
1162 1162 else:
1163 1163 if 'x' in flags:
1164 1164 self.setexec.append(filename)
1165 1165 util.setflags(self.wjoin(filename), False, 'x' in flags)
1166 1166
1167 1167 def _copyfile(self, source, dest):
1168 1168 # SVN's copy command pukes if the destination file exists, but
1169 1169 # our copyfile method expects to record a copy that has
1170 1170 # already occurred. Cross the semantic gap.
1171 1171 wdest = self.wjoin(dest)
1172 1172 exists = os.path.lexists(wdest)
1173 1173 if exists:
1174 1174 fd, tempname = tempfile.mkstemp(
1175 1175 prefix='hg-copy-', dir=os.path.dirname(wdest))
1176 1176 os.close(fd)
1177 1177 os.unlink(tempname)
1178 1178 os.rename(wdest, tempname)
1179 1179 try:
1180 1180 self.run0('copy', source, dest)
1181 1181 finally:
1182 1182 self.manifest.add(dest)
1183 1183 if exists:
1184 1184 try:
1185 1185 os.unlink(wdest)
1186 1186 except OSError:
1187 1187 pass
1188 1188 os.rename(tempname, wdest)
1189 1189
1190 1190 def dirs_of(self, files):
1191 1191 dirs = set()
1192 1192 for f in files:
1193 1193 if os.path.isdir(self.wjoin(f)):
1194 1194 dirs.add(f)
1195 1195 for i in strutil.rfindall(f, '/'):
1196 1196 dirs.add(f[:i])
1197 1197 return dirs
1198 1198
1199 1199 def add_dirs(self, files):
1200 1200 add_dirs = [d for d in sorted(self.dirs_of(files))
1201 1201 if d not in self.manifest]
1202 1202 if add_dirs:
1203 1203 self.manifest.update(add_dirs)
1204 1204 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1205 1205 return add_dirs
1206 1206
1207 1207 def add_files(self, files):
1208 1208 files = [f for f in files if f not in self.manifest]
1209 1209 if files:
1210 1210 self.manifest.update(files)
1211 1211 self.xargs(files, 'add', quiet=True)
1212 1212 return files
1213 1213
1214 1214 def tidy_dirs(self, names):
1215 1215 deleted = []
1216 1216 for d in sorted(self.dirs_of(names), reverse=True):
1217 1217 wd = self.wjoin(d)
1218 1218 if os.listdir(wd) == '.svn':
1219 1219 self.run0('delete', d)
1220 1220 self.manifest.remove(d)
1221 1221 deleted.append(d)
1222 1222 return deleted
1223 1223
1224 1224 def addchild(self, parent, child):
1225 1225 self.childmap[parent] = child
1226 1226
1227 1227 def revid(self, rev):
1228 1228 return u"svn:%s@%s" % (self.uuid, rev)
1229 1229
1230 1230 def putcommit(self, files, copies, parents, commit, source, revmap):
1231 1231 for parent in parents:
1232 1232 try:
1233 1233 return self.revid(self.childmap[parent])
1234 1234 except KeyError:
1235 1235 pass
1236 1236
1237 1237 # Apply changes to working copy
1238 1238 for f, v in files:
1239 1239 try:
1240 1240 data, mode = source.getfile(f, v)
1241 1241 except IOError:
1242 1242 self.delete.append(f)
1243 1243 else:
1244 1244 self.putfile(f, mode, data)
1245 1245 if f in copies:
1246 1246 self.copies.append([copies[f], f])
1247 1247 files = [f[0] for f in files]
1248 1248
1249 1249 entries = set(self.delete)
1250 1250 files = frozenset(files)
1251 1251 entries.update(self.add_dirs(files.difference(entries)))
1252 1252 if self.copies:
1253 1253 for s, d in self.copies:
1254 1254 self._copyfile(s, d)
1255 1255 self.copies = []
1256 1256 if self.delete:
1257 1257 self.xargs(self.delete, 'delete')
1258 1258 for f in self.delete:
1259 1259 self.manifest.remove(f)
1260 1260 self.delete = []
1261 1261 entries.update(self.add_files(files.difference(entries)))
1262 1262 entries.update(self.tidy_dirs(entries))
1263 1263 if self.delexec:
1264 1264 self.xargs(self.delexec, 'propdel', 'svn:executable')
1265 1265 self.delexec = []
1266 1266 if self.setexec:
1267 1267 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1268 1268 self.setexec = []
1269 1269
1270 1270 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1271 1271 fp = os.fdopen(fd, 'w')
1272 1272 fp.write(commit.desc)
1273 1273 fp.close()
1274 1274 try:
1275 1275 output = self.run0('commit',
1276 1276 username=util.shortuser(commit.author),
1277 1277 file=messagefile,
1278 1278 encoding='utf-8')
1279 1279 try:
1280 1280 rev = self.commit_re.search(output).group(1)
1281 1281 except AttributeError:
1282 1282 if not files:
1283 1283 return parents[0]
1284 1284 self.ui.warn(_('unexpected svn output:\n'))
1285 1285 self.ui.warn(output)
1286 1286 raise util.Abort(_('unable to cope with svn output'))
1287 1287 if commit.rev:
1288 1288 self.run('propset', 'hg:convert-rev', commit.rev,
1289 1289 revprop=True, revision=rev)
1290 1290 if commit.branch and commit.branch != 'default':
1291 1291 self.run('propset', 'hg:convert-branch', commit.branch,
1292 1292 revprop=True, revision=rev)
1293 1293 for parent in parents:
1294 1294 self.addchild(parent, rev)
1295 1295 return self.revid(rev)
1296 1296 finally:
1297 1297 os.unlink(messagefile)
1298 1298
1299 1299 def puttags(self, tags):
1300 1300 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1301 1301 return None, None
1302 1302
1303 def hascommit(self, rev):
1303 def hascommitforsplicemap(self, rev):
1304 1304 # This is not correct as one can convert to an existing subversion
1305 1305 # repository and childmap would not list all revisions. Too bad.
1306 1306 if rev in self.childmap:
1307 1307 return True
1308 1308 raise util.Abort(_('splice map revision %s not found in subversion '
1309 1309 'child map (revision lookups are not implemented)')
1310 1310 % rev)
General Comments 0
You need to be logged in to leave comments. Login now