##// END OF EJS Templates
convert: add function to test if file is from source...
Durham Goode -
r26035:86598f4f default
parent child Browse files
Show More
@@ -1,471 +1,478 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 phases, 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 if abort:
35 35 exc = util.Abort
36 36 else:
37 37 exc = MissingTool
38 38 raise exc(_('cannot find required "%s" tool') % name)
39 39
40 40 class NoRepo(Exception):
41 41 pass
42 42
43 43 SKIPREV = 'SKIP'
44 44
45 45 class commit(object):
46 46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
47 47 extra={}, sortkey=None, saverev=True, phase=phases.draft):
48 48 self.author = author or 'unknown'
49 49 self.date = date or '0 0'
50 50 self.desc = desc
51 51 self.parents = parents
52 52 self.branch = branch
53 53 self.rev = rev
54 54 self.extra = extra
55 55 self.sortkey = sortkey
56 56 self.saverev = saverev
57 57 self.phase = phase
58 58
59 59 class converter_source(object):
60 60 """Conversion source interface"""
61 61
62 62 def __init__(self, ui, path=None, revs=None):
63 63 """Initialize conversion source (or raise NoRepo("message")
64 64 exception if path is not a valid repository)"""
65 65 self.ui = ui
66 66 self.path = path
67 67 self.revs = revs
68 68
69 69 self.encoding = 'utf-8'
70 70
71 71 def checkhexformat(self, revstr, mapname='splicemap'):
72 72 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
73 73 such format for their revision numbering
74 74 """
75 75 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
76 76 raise util.Abort(_('%s entry %s is not a valid revision'
77 77 ' identifier') % (mapname, revstr))
78 78
79 79 def before(self):
80 80 pass
81 81
82 82 def after(self):
83 83 pass
84 84
85 def targetfilebelongstosource(self, targetfilename):
86 """Returns true if the given targetfile belongs to the source repo. This
87 is useful when only a subdirectory of the target belongs to the source
88 repo."""
89 # For normal full repo converts, this is always True.
90 return True
91
85 92 def setrevmap(self, revmap):
86 93 """set the map of already-converted revisions"""
87 94 pass
88 95
89 96 def getheads(self):
90 97 """Return a list of this repository's heads"""
91 98 raise NotImplementedError
92 99
93 100 def getfile(self, name, rev):
94 101 """Return a pair (data, mode) where data is the file content
95 102 as a string and mode one of '', 'x' or 'l'. rev is the
96 103 identifier returned by a previous call to getchanges().
97 104 Data is None if file is missing/deleted in rev.
98 105 """
99 106 raise NotImplementedError
100 107
101 108 def getchanges(self, version, full):
102 109 """Returns a tuple of (files, copies, cleanp2).
103 110
104 111 files is a sorted list of (filename, id) tuples for all files
105 112 changed between version and its first parent returned by
106 113 getcommit(). If full, all files in that revision is returned.
107 114 id is the source revision id of the file.
108 115
109 116 copies is a dictionary of dest: source
110 117
111 118 cleanp2 is the set of files filenames that are clean against p2.
112 119 (Files that are clean against p1 are already not in files (unless
113 120 full). This makes it possible to handle p2 clean files similarly.)
114 121 """
115 122 raise NotImplementedError
116 123
117 124 def getcommit(self, version):
118 125 """Return the commit object for version"""
119 126 raise NotImplementedError
120 127
121 128 def numcommits(self):
122 129 """Return the number of commits in this source.
123 130
124 131 If unknown, return None.
125 132 """
126 133 return None
127 134
128 135 def gettags(self):
129 136 """Return the tags as a dictionary of name: revision
130 137
131 138 Tag names must be UTF-8 strings.
132 139 """
133 140 raise NotImplementedError
134 141
135 142 def recode(self, s, encoding=None):
136 143 if not encoding:
137 144 encoding = self.encoding or 'utf-8'
138 145
139 146 if isinstance(s, unicode):
140 147 return s.encode("utf-8")
141 148 try:
142 149 return s.decode(encoding).encode("utf-8")
143 150 except UnicodeError:
144 151 try:
145 152 return s.decode("latin-1").encode("utf-8")
146 153 except UnicodeError:
147 154 return s.decode(encoding, "replace").encode("utf-8")
148 155
149 156 def getchangedfiles(self, rev, i):
150 157 """Return the files changed by rev compared to parent[i].
151 158
152 159 i is an index selecting one of the parents of rev. The return
153 160 value should be the list of files that are different in rev and
154 161 this parent.
155 162
156 163 If rev has no parents, i is None.
157 164
158 165 This function is only needed to support --filemap
159 166 """
160 167 raise NotImplementedError
161 168
162 169 def converted(self, rev, sinkrev):
163 170 '''Notify the source that a revision has been converted.'''
164 171 pass
165 172
166 173 def hasnativeorder(self):
167 174 """Return true if this source has a meaningful, native revision
168 175 order. For instance, Mercurial revisions are store sequentially
169 176 while there is no such global ordering with Darcs.
170 177 """
171 178 return False
172 179
173 180 def hasnativeclose(self):
174 181 """Return true if this source has ability to close branch.
175 182 """
176 183 return False
177 184
178 185 def lookuprev(self, rev):
179 186 """If rev is a meaningful revision reference in source, return
180 187 the referenced identifier in the same format used by getcommit().
181 188 return None otherwise.
182 189 """
183 190 return None
184 191
185 192 def getbookmarks(self):
186 193 """Return the bookmarks as a dictionary of name: revision
187 194
188 195 Bookmark names are to be UTF-8 strings.
189 196 """
190 197 return {}
191 198
192 199 def checkrevformat(self, revstr, mapname='splicemap'):
193 200 """revstr is a string that describes a revision in the given
194 201 source control system. Return true if revstr has correct
195 202 format.
196 203 """
197 204 return True
198 205
199 206 class converter_sink(object):
200 207 """Conversion sink (target) interface"""
201 208
202 209 def __init__(self, ui, path):
203 210 """Initialize conversion sink (or raise NoRepo("message")
204 211 exception if path is not a valid repository)
205 212
206 213 created is a list of paths to remove if a fatal error occurs
207 214 later"""
208 215 self.ui = ui
209 216 self.path = path
210 217 self.created = []
211 218
212 219 def revmapfile(self):
213 220 """Path to a file that will contain lines
214 221 source_rev_id sink_rev_id
215 222 mapping equivalent revision identifiers for each system."""
216 223 raise NotImplementedError
217 224
218 225 def authorfile(self):
219 226 """Path to a file that will contain lines
220 227 srcauthor=dstauthor
221 228 mapping equivalent authors identifiers for each system."""
222 229 return None
223 230
224 231 def putcommit(self, files, copies, parents, commit, source, revmap, full,
225 232 cleanp2):
226 233 """Create a revision with all changed files listed in 'files'
227 234 and having listed parents. 'commit' is a commit object
228 235 containing at a minimum the author, date, and message for this
229 236 changeset. 'files' is a list of (path, version) tuples,
230 237 'copies' is a dictionary mapping destinations to sources,
231 238 'source' is the source repository, and 'revmap' is a mapfile
232 239 of source revisions to converted revisions. Only getfile() and
233 240 lookuprev() should be called on 'source'. 'full' means that 'files'
234 241 is complete and all other files should be removed.
235 242 'cleanp2' is a set of the filenames that are unchanged from p2
236 243 (only in the common merge case where there two parents).
237 244
238 245 Note that the sink repository is not told to update itself to
239 246 a particular revision (or even what that revision would be)
240 247 before it receives the file data.
241 248 """
242 249 raise NotImplementedError
243 250
244 251 def puttags(self, tags):
245 252 """Put tags into sink.
246 253
247 254 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
248 255 Return a pair (tag_revision, tag_parent_revision), or (None, None)
249 256 if nothing was changed.
250 257 """
251 258 raise NotImplementedError
252 259
253 260 def setbranch(self, branch, pbranches):
254 261 """Set the current branch name. Called before the first putcommit
255 262 on the branch.
256 263 branch: branch name for subsequent commits
257 264 pbranches: (converted parent revision, parent branch) tuples"""
258 265 pass
259 266
260 267 def setfilemapmode(self, active):
261 268 """Tell the destination that we're using a filemap
262 269
263 270 Some converter_sources (svn in particular) can claim that a file
264 271 was changed in a revision, even if there was no change. This method
265 272 tells the destination that we're using a filemap and that it should
266 273 filter empty revisions.
267 274 """
268 275 pass
269 276
270 277 def before(self):
271 278 pass
272 279
273 280 def after(self):
274 281 pass
275 282
276 283 def putbookmarks(self, bookmarks):
277 284 """Put bookmarks into sink.
278 285
279 286 bookmarks: {bookmarkname: sink_rev_id, ...}
280 287 where bookmarkname is an UTF-8 string.
281 288 """
282 289 pass
283 290
284 291 def hascommitfrommap(self, rev):
285 292 """Return False if a rev mentioned in a filemap is known to not be
286 293 present."""
287 294 raise NotImplementedError
288 295
289 296 def hascommitforsplicemap(self, rev):
290 297 """This method is for the special needs for splicemap handling and not
291 298 for general use. Returns True if the sink contains rev, aborts on some
292 299 special cases."""
293 300 raise NotImplementedError
294 301
295 302 class commandline(object):
296 303 def __init__(self, ui, command):
297 304 self.ui = ui
298 305 self.command = command
299 306
300 307 def prerun(self):
301 308 pass
302 309
303 310 def postrun(self):
304 311 pass
305 312
306 313 def _cmdline(self, cmd, *args, **kwargs):
307 314 cmdline = [self.command, cmd] + list(args)
308 315 for k, v in kwargs.iteritems():
309 316 if len(k) == 1:
310 317 cmdline.append('-' + k)
311 318 else:
312 319 cmdline.append('--' + k.replace('_', '-'))
313 320 try:
314 321 if len(k) == 1:
315 322 cmdline.append('' + v)
316 323 else:
317 324 cmdline[-1] += '=' + v
318 325 except TypeError:
319 326 pass
320 327 cmdline = [util.shellquote(arg) for arg in cmdline]
321 328 if not self.ui.debugflag:
322 329 cmdline += ['2>', os.devnull]
323 330 cmdline = ' '.join(cmdline)
324 331 return cmdline
325 332
326 333 def _run(self, cmd, *args, **kwargs):
327 334 def popen(cmdline):
328 335 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
329 336 close_fds=util.closefds,
330 337 stdout=subprocess.PIPE)
331 338 return p
332 339 return self._dorun(popen, cmd, *args, **kwargs)
333 340
334 341 def _run2(self, cmd, *args, **kwargs):
335 342 return self._dorun(util.popen2, cmd, *args, **kwargs)
336 343
337 344 def _dorun(self, openfunc, cmd, *args, **kwargs):
338 345 cmdline = self._cmdline(cmd, *args, **kwargs)
339 346 self.ui.debug('running: %s\n' % (cmdline,))
340 347 self.prerun()
341 348 try:
342 349 return openfunc(cmdline)
343 350 finally:
344 351 self.postrun()
345 352
346 353 def run(self, cmd, *args, **kwargs):
347 354 p = self._run(cmd, *args, **kwargs)
348 355 output = p.communicate()[0]
349 356 self.ui.debug(output)
350 357 return output, p.returncode
351 358
352 359 def runlines(self, cmd, *args, **kwargs):
353 360 p = self._run(cmd, *args, **kwargs)
354 361 output = p.stdout.readlines()
355 362 p.wait()
356 363 self.ui.debug(''.join(output))
357 364 return output, p.returncode
358 365
359 366 def checkexit(self, status, output=''):
360 367 if status:
361 368 if output:
362 369 self.ui.warn(_('%s error:\n') % self.command)
363 370 self.ui.warn(output)
364 371 msg = util.explainexit(status)[0]
365 372 raise util.Abort('%s %s' % (self.command, msg))
366 373
367 374 def run0(self, cmd, *args, **kwargs):
368 375 output, status = self.run(cmd, *args, **kwargs)
369 376 self.checkexit(status, output)
370 377 return output
371 378
372 379 def runlines0(self, cmd, *args, **kwargs):
373 380 output, status = self.runlines(cmd, *args, **kwargs)
374 381 self.checkexit(status, ''.join(output))
375 382 return output
376 383
377 384 @propertycache
378 385 def argmax(self):
379 386 # POSIX requires at least 4096 bytes for ARG_MAX
380 387 argmax = 4096
381 388 try:
382 389 argmax = os.sysconf("SC_ARG_MAX")
383 390 except (AttributeError, ValueError):
384 391 pass
385 392
386 393 # Windows shells impose their own limits on command line length,
387 394 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
388 395 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
389 396 # details about cmd.exe limitations.
390 397
391 398 # Since ARG_MAX is for command line _and_ environment, lower our limit
392 399 # (and make happy Windows shells while doing this).
393 400 return argmax // 2 - 1
394 401
395 402 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
396 403 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
397 404 limit = self.argmax - cmdlen
398 405 bytes = 0
399 406 fl = []
400 407 for fn in arglist:
401 408 b = len(fn) + 3
402 409 if bytes + b < limit or len(fl) == 0:
403 410 fl.append(fn)
404 411 bytes += b
405 412 else:
406 413 yield fl
407 414 fl = [fn]
408 415 bytes = b
409 416 if fl:
410 417 yield fl
411 418
412 419 def xargs(self, arglist, cmd, *args, **kwargs):
413 420 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
414 421 self.run0(cmd, *(list(args) + l), **kwargs)
415 422
416 423 class mapfile(dict):
417 424 def __init__(self, ui, path):
418 425 super(mapfile, self).__init__()
419 426 self.ui = ui
420 427 self.path = path
421 428 self.fp = None
422 429 self.order = []
423 430 self._read()
424 431
425 432 def _read(self):
426 433 if not self.path:
427 434 return
428 435 try:
429 436 fp = open(self.path, 'r')
430 437 except IOError as err:
431 438 if err.errno != errno.ENOENT:
432 439 raise
433 440 return
434 441 for i, line in enumerate(fp):
435 442 line = line.splitlines()[0].rstrip()
436 443 if not line:
437 444 # Ignore blank lines
438 445 continue
439 446 try:
440 447 key, value = line.rsplit(' ', 1)
441 448 except ValueError:
442 449 raise util.Abort(
443 450 _('syntax error in %s(%d): key/value pair expected')
444 451 % (self.path, i + 1))
445 452 if key not in self:
446 453 self.order.append(key)
447 454 super(mapfile, self).__setitem__(key, value)
448 455 fp.close()
449 456
450 457 def __setitem__(self, key, value):
451 458 if self.fp is None:
452 459 try:
453 460 self.fp = open(self.path, 'a')
454 461 except IOError as err:
455 462 raise util.Abort(_('could not open map file %r: %s') %
456 463 (self.path, err.strerror))
457 464 self.fp.write('%s %s\n' % (key, value))
458 465 self.fp.flush()
459 466 super(mapfile, self).__setitem__(key, value)
460 467
461 468 def close(self):
462 469 if self.fp:
463 470 self.fp.close()
464 471 self.fp = None
465 472
466 473 def makedatetimestamp(t):
467 474 """Like util.makedate() but for time t instead of current time"""
468 475 delta = (datetime.datetime.utcfromtimestamp(t) -
469 476 datetime.datetime.fromtimestamp(t))
470 477 tz = delta.days * 86400 + delta.seconds
471 478 return t, tz
@@ -1,576 +1,579 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 def mapbranch(branch, branchmap):
33 33 '''
34 34 >>> bmap = {'default': 'branch1'}
35 35 >>> for i in ['', None]:
36 36 ... mapbranch(i, bmap)
37 37 'branch1'
38 38 'branch1'
39 39 >>> bmap = {'None': 'branch2'}
40 40 >>> for i in ['', None]:
41 41 ... mapbranch(i, bmap)
42 42 'branch2'
43 43 'branch2'
44 44 >>> bmap = {'None': 'branch3', 'default': 'branch4'}
45 45 >>> for i in ['None', '', None, 'default', 'branch5']:
46 46 ... mapbranch(i, bmap)
47 47 'branch3'
48 48 'branch4'
49 49 'branch4'
50 50 'branch4'
51 51 'branch5'
52 52 '''
53 53 # If branch is None or empty, this commit is coming from the source
54 54 # repository's default branch and destined for the default branch in the
55 55 # destination repository. For such commits, using a literal "default"
56 56 # in branchmap below allows the user to map "default" to an alternate
57 57 # default branch in the destination repository.
58 58 branch = branchmap.get(branch or 'default', branch)
59 59 # At some point we used "None" literal to denote the default branch,
60 60 # attempt to use that for backward compatibility.
61 61 if (not branch):
62 62 branch = branchmap.get(str(None), branch)
63 63 return branch
64 64
65 65 source_converters = [
66 66 ('cvs', convert_cvs, 'branchsort'),
67 67 ('git', convert_git, 'branchsort'),
68 68 ('svn', svn_source, 'branchsort'),
69 69 ('hg', mercurial_source, 'sourcesort'),
70 70 ('darcs', darcs_source, 'branchsort'),
71 71 ('mtn', monotone_source, 'branchsort'),
72 72 ('gnuarch', gnuarch_source, 'branchsort'),
73 73 ('bzr', bzr_source, 'branchsort'),
74 74 ('p4', p4_source, 'branchsort'),
75 75 ]
76 76
77 77 sink_converters = [
78 78 ('hg', mercurial_sink),
79 79 ('svn', svn_sink),
80 80 ]
81 81
82 82 def convertsource(ui, path, type, revs):
83 83 exceptions = []
84 84 if type and type not in [s[0] for s in source_converters]:
85 85 raise util.Abort(_('%s: invalid source repository type') % type)
86 86 for name, source, sortmode in source_converters:
87 87 try:
88 88 if not type or name == type:
89 89 return source(ui, path, revs), sortmode
90 90 except (NoRepo, MissingTool) as inst:
91 91 exceptions.append(inst)
92 92 if not ui.quiet:
93 93 for inst in exceptions:
94 94 ui.write("%s\n" % inst)
95 95 raise util.Abort(_('%s: missing or unsupported repository') % path)
96 96
97 97 def convertsink(ui, path, type):
98 98 if type and type not in [s[0] for s in sink_converters]:
99 99 raise util.Abort(_('%s: invalid destination repository type') % type)
100 100 for name, sink in sink_converters:
101 101 try:
102 102 if not type or name == type:
103 103 return sink(ui, path)
104 104 except NoRepo as inst:
105 105 ui.note(_("convert: %s\n") % inst)
106 106 except MissingTool as inst:
107 107 raise util.Abort('%s\n' % inst)
108 108 raise util.Abort(_('%s: unknown repository type') % path)
109 109
110 110 class progresssource(object):
111 111 def __init__(self, ui, source, filecount):
112 112 self.ui = ui
113 113 self.source = source
114 114 self.filecount = filecount
115 115 self.retrieved = 0
116 116
117 117 def getfile(self, file, rev):
118 118 self.retrieved += 1
119 119 self.ui.progress(_('getting files'), self.retrieved,
120 120 item=file, total=self.filecount)
121 121 return self.source.getfile(file, rev)
122 122
123 def targetfilebelongstosource(self, targetfilename):
124 return self.source.targetfilebelongstosource(targetfilename)
125
123 126 def lookuprev(self, rev):
124 127 return self.source.lookuprev(rev)
125 128
126 129 def close(self):
127 130 self.ui.progress(_('getting files'), None)
128 131
129 132 class converter(object):
130 133 def __init__(self, ui, source, dest, revmapfile, opts):
131 134
132 135 self.source = source
133 136 self.dest = dest
134 137 self.ui = ui
135 138 self.opts = opts
136 139 self.commitcache = {}
137 140 self.authors = {}
138 141 self.authorfile = None
139 142
140 143 # Record converted revisions persistently: maps source revision
141 144 # ID to target revision ID (both strings). (This is how
142 145 # incremental conversions work.)
143 146 self.map = mapfile(ui, revmapfile)
144 147
145 148 # Read first the dst author map if any
146 149 authorfile = self.dest.authorfile()
147 150 if authorfile and os.path.exists(authorfile):
148 151 self.readauthormap(authorfile)
149 152 # Extend/Override with new author map if necessary
150 153 if opts.get('authormap'):
151 154 self.readauthormap(opts.get('authormap'))
152 155 self.authorfile = self.dest.authorfile()
153 156
154 157 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
155 158 self.branchmap = mapfile(ui, opts.get('branchmap'))
156 159
157 160 def parsesplicemap(self, path):
158 161 """ check and validate the splicemap format and
159 162 return a child/parents dictionary.
160 163 Format checking has two parts.
161 164 1. generic format which is same across all source types
162 165 2. specific format checking which may be different for
163 166 different source type. This logic is implemented in
164 167 checkrevformat function in source files like
165 168 hg.py, subversion.py etc.
166 169 """
167 170
168 171 if not path:
169 172 return {}
170 173 m = {}
171 174 try:
172 175 fp = open(path, 'r')
173 176 for i, line in enumerate(fp):
174 177 line = line.splitlines()[0].rstrip()
175 178 if not line:
176 179 # Ignore blank lines
177 180 continue
178 181 # split line
179 182 lex = shlex.shlex(line, posix=True)
180 183 lex.whitespace_split = True
181 184 lex.whitespace += ','
182 185 line = list(lex)
183 186 # check number of parents
184 187 if not (2 <= len(line) <= 3):
185 188 raise util.Abort(_('syntax error in %s(%d): child parent1'
186 189 '[,parent2] expected') % (path, i + 1))
187 190 for part in line:
188 191 self.source.checkrevformat(part)
189 192 child, p1, p2 = line[0], line[1:2], line[2:]
190 193 if p1 == p2:
191 194 m[child] = p1
192 195 else:
193 196 m[child] = p1 + p2
194 197 # if file does not exist or error reading, exit
195 198 except IOError:
196 199 raise util.Abort(_('splicemap file not found or error reading %s:')
197 200 % path)
198 201 return m
199 202
200 203
201 204 def walktree(self, heads):
202 205 '''Return a mapping that identifies the uncommitted parents of every
203 206 uncommitted changeset.'''
204 207 visit = heads
205 208 known = set()
206 209 parents = {}
207 210 numcommits = self.source.numcommits()
208 211 while visit:
209 212 n = visit.pop(0)
210 213 if n in known:
211 214 continue
212 215 if n in self.map:
213 216 m = self.map[n]
214 217 if m == SKIPREV or self.dest.hascommitfrommap(m):
215 218 continue
216 219 known.add(n)
217 220 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
218 221 total=numcommits)
219 222 commit = self.cachecommit(n)
220 223 parents[n] = []
221 224 for p in commit.parents:
222 225 parents[n].append(p)
223 226 visit.append(p)
224 227 self.ui.progress(_('scanning'), None)
225 228
226 229 return parents
227 230
228 231 def mergesplicemap(self, parents, splicemap):
229 232 """A splicemap redefines child/parent relationships. Check the
230 233 map contains valid revision identifiers and merge the new
231 234 links in the source graph.
232 235 """
233 236 for c in sorted(splicemap):
234 237 if c not in parents:
235 238 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
236 239 # Could be in source but not converted during this run
237 240 self.ui.warn(_('splice map revision %s is not being '
238 241 'converted, ignoring\n') % c)
239 242 continue
240 243 pc = []
241 244 for p in splicemap[c]:
242 245 # We do not have to wait for nodes already in dest.
243 246 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
244 247 continue
245 248 # Parent is not in dest and not being converted, not good
246 249 if p not in parents:
247 250 raise util.Abort(_('unknown splice map parent: %s') % p)
248 251 pc.append(p)
249 252 parents[c] = pc
250 253
251 254 def toposort(self, parents, sortmode):
252 255 '''Return an ordering such that every uncommitted changeset is
253 256 preceded by all its uncommitted ancestors.'''
254 257
255 258 def mapchildren(parents):
256 259 """Return a (children, roots) tuple where 'children' maps parent
257 260 revision identifiers to children ones, and 'roots' is the list of
258 261 revisions without parents. 'parents' must be a mapping of revision
259 262 identifier to its parents ones.
260 263 """
261 264 visit = sorted(parents)
262 265 seen = set()
263 266 children = {}
264 267 roots = []
265 268
266 269 while visit:
267 270 n = visit.pop(0)
268 271 if n in seen:
269 272 continue
270 273 seen.add(n)
271 274 # Ensure that nodes without parents are present in the
272 275 # 'children' mapping.
273 276 children.setdefault(n, [])
274 277 hasparent = False
275 278 for p in parents[n]:
276 279 if p not in self.map:
277 280 visit.append(p)
278 281 hasparent = True
279 282 children.setdefault(p, []).append(n)
280 283 if not hasparent:
281 284 roots.append(n)
282 285
283 286 return children, roots
284 287
285 288 # Sort functions are supposed to take a list of revisions which
286 289 # can be converted immediately and pick one
287 290
288 291 def makebranchsorter():
289 292 """If the previously converted revision has a child in the
290 293 eligible revisions list, pick it. Return the list head
291 294 otherwise. Branch sort attempts to minimize branch
292 295 switching, which is harmful for Mercurial backend
293 296 compression.
294 297 """
295 298 prev = [None]
296 299 def picknext(nodes):
297 300 next = nodes[0]
298 301 for n in nodes:
299 302 if prev[0] in parents[n]:
300 303 next = n
301 304 break
302 305 prev[0] = next
303 306 return next
304 307 return picknext
305 308
306 309 def makesourcesorter():
307 310 """Source specific sort."""
308 311 keyfn = lambda n: self.commitcache[n].sortkey
309 312 def picknext(nodes):
310 313 return sorted(nodes, key=keyfn)[0]
311 314 return picknext
312 315
313 316 def makeclosesorter():
314 317 """Close order sort."""
315 318 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
316 319 self.commitcache[n].sortkey)
317 320 def picknext(nodes):
318 321 return sorted(nodes, key=keyfn)[0]
319 322 return picknext
320 323
321 324 def makedatesorter():
322 325 """Sort revisions by date."""
323 326 dates = {}
324 327 def getdate(n):
325 328 if n not in dates:
326 329 dates[n] = util.parsedate(self.commitcache[n].date)
327 330 return dates[n]
328 331
329 332 def picknext(nodes):
330 333 return min([(getdate(n), n) for n in nodes])[1]
331 334
332 335 return picknext
333 336
334 337 if sortmode == 'branchsort':
335 338 picknext = makebranchsorter()
336 339 elif sortmode == 'datesort':
337 340 picknext = makedatesorter()
338 341 elif sortmode == 'sourcesort':
339 342 picknext = makesourcesorter()
340 343 elif sortmode == 'closesort':
341 344 picknext = makeclosesorter()
342 345 else:
343 346 raise util.Abort(_('unknown sort mode: %s') % sortmode)
344 347
345 348 children, actives = mapchildren(parents)
346 349
347 350 s = []
348 351 pendings = {}
349 352 while actives:
350 353 n = picknext(actives)
351 354 actives.remove(n)
352 355 s.append(n)
353 356
354 357 # Update dependents list
355 358 for c in children.get(n, []):
356 359 if c not in pendings:
357 360 pendings[c] = [p for p in parents[c] if p not in self.map]
358 361 try:
359 362 pendings[c].remove(n)
360 363 except ValueError:
361 364 raise util.Abort(_('cycle detected between %s and %s')
362 365 % (recode(c), recode(n)))
363 366 if not pendings[c]:
364 367 # Parents are converted, node is eligible
365 368 actives.insert(0, c)
366 369 pendings[c] = None
367 370
368 371 if len(s) != len(parents):
369 372 raise util.Abort(_("not all revisions were sorted"))
370 373
371 374 return s
372 375
373 376 def writeauthormap(self):
374 377 authorfile = self.authorfile
375 378 if authorfile:
376 379 self.ui.status(_('writing author map file %s\n') % authorfile)
377 380 ofile = open(authorfile, 'w+')
378 381 for author in self.authors:
379 382 ofile.write("%s=%s\n" % (author, self.authors[author]))
380 383 ofile.close()
381 384
382 385 def readauthormap(self, authorfile):
383 386 afile = open(authorfile, 'r')
384 387 for line in afile:
385 388
386 389 line = line.strip()
387 390 if not line or line.startswith('#'):
388 391 continue
389 392
390 393 try:
391 394 srcauthor, dstauthor = line.split('=', 1)
392 395 except ValueError:
393 396 msg = _('ignoring bad line in author map file %s: %s\n')
394 397 self.ui.warn(msg % (authorfile, line.rstrip()))
395 398 continue
396 399
397 400 srcauthor = srcauthor.strip()
398 401 dstauthor = dstauthor.strip()
399 402 if self.authors.get(srcauthor) in (None, dstauthor):
400 403 msg = _('mapping author %s to %s\n')
401 404 self.ui.debug(msg % (srcauthor, dstauthor))
402 405 self.authors[srcauthor] = dstauthor
403 406 continue
404 407
405 408 m = _('overriding mapping for author %s, was %s, will be %s\n')
406 409 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
407 410
408 411 afile.close()
409 412
410 413 def cachecommit(self, rev):
411 414 commit = self.source.getcommit(rev)
412 415 commit.author = self.authors.get(commit.author, commit.author)
413 416 commit.branch = mapbranch(commit.branch, self.branchmap)
414 417 self.commitcache[rev] = commit
415 418 return commit
416 419
417 420 def copy(self, rev):
418 421 commit = self.commitcache[rev]
419 422 full = self.opts.get('full')
420 423 changes = self.source.getchanges(rev, full)
421 424 if isinstance(changes, basestring):
422 425 if changes == SKIPREV:
423 426 dest = SKIPREV
424 427 else:
425 428 dest = self.map[changes]
426 429 self.map[rev] = dest
427 430 return
428 431 files, copies, cleanp2 = changes
429 432 pbranches = []
430 433 if commit.parents:
431 434 for prev in commit.parents:
432 435 if prev not in self.commitcache:
433 436 self.cachecommit(prev)
434 437 pbranches.append((self.map[prev],
435 438 self.commitcache[prev].branch))
436 439 self.dest.setbranch(commit.branch, pbranches)
437 440 try:
438 441 parents = self.splicemap[rev]
439 442 self.ui.status(_('spliced in %s as parents of %s\n') %
440 443 (parents, rev))
441 444 parents = [self.map.get(p, p) for p in parents]
442 445 except KeyError:
443 446 parents = [b[0] for b in pbranches]
444 447 if len(pbranches) != 2:
445 448 cleanp2 = set()
446 449 if len(parents) < 3:
447 450 source = progresssource(self.ui, self.source, len(files))
448 451 else:
449 452 # For an octopus merge, we end up traversing the list of
450 453 # changed files N-1 times. This tweak to the number of
451 454 # files makes it so the progress bar doesn't overflow
452 455 # itself.
453 456 source = progresssource(self.ui, self.source,
454 457 len(files) * (len(parents) - 1))
455 458 newnode = self.dest.putcommit(files, copies, parents, commit,
456 459 source, self.map, full, cleanp2)
457 460 source.close()
458 461 self.source.converted(rev, newnode)
459 462 self.map[rev] = newnode
460 463
461 464 def convert(self, sortmode):
462 465 try:
463 466 self.source.before()
464 467 self.dest.before()
465 468 self.source.setrevmap(self.map)
466 469 self.ui.status(_("scanning source...\n"))
467 470 heads = self.source.getheads()
468 471 parents = self.walktree(heads)
469 472 self.mergesplicemap(parents, self.splicemap)
470 473 self.ui.status(_("sorting...\n"))
471 474 t = self.toposort(parents, sortmode)
472 475 num = len(t)
473 476 c = None
474 477
475 478 self.ui.status(_("converting...\n"))
476 479 for i, c in enumerate(t):
477 480 num -= 1
478 481 desc = self.commitcache[c].desc
479 482 if "\n" in desc:
480 483 desc = desc.splitlines()[0]
481 484 # convert log message to local encoding without using
482 485 # tolocal() because the encoding.encoding convert()
483 486 # uses is 'utf-8'
484 487 self.ui.status("%d %s\n" % (num, recode(desc)))
485 488 self.ui.note(_("source: %s\n") % recode(c))
486 489 self.ui.progress(_('converting'), i, unit=_('revisions'),
487 490 total=len(t))
488 491 self.copy(c)
489 492 self.ui.progress(_('converting'), None)
490 493
491 494 if not self.ui.configbool('convert', 'skiptags'):
492 495 tags = self.source.gettags()
493 496 ctags = {}
494 497 for k in tags:
495 498 v = tags[k]
496 499 if self.map.get(v, SKIPREV) != SKIPREV:
497 500 ctags[k] = self.map[v]
498 501
499 502 if c and ctags:
500 503 nrev, tagsparent = self.dest.puttags(ctags)
501 504 if nrev and tagsparent:
502 505 # write another hash correspondence to override the
503 506 # previous one so we don't end up with extra tag heads
504 507 tagsparents = [e for e in self.map.iteritems()
505 508 if e[1] == tagsparent]
506 509 if tagsparents:
507 510 self.map[tagsparents[0][0]] = nrev
508 511
509 512 bookmarks = self.source.getbookmarks()
510 513 cbookmarks = {}
511 514 for k in bookmarks:
512 515 v = bookmarks[k]
513 516 if self.map.get(v, SKIPREV) != SKIPREV:
514 517 cbookmarks[k] = self.map[v]
515 518
516 519 if c and cbookmarks:
517 520 self.dest.putbookmarks(cbookmarks)
518 521
519 522 self.writeauthormap()
520 523 finally:
521 524 self.cleanup()
522 525
523 526 def cleanup(self):
524 527 try:
525 528 self.dest.after()
526 529 finally:
527 530 self.source.after()
528 531 self.map.close()
529 532
530 533 def convert(ui, src, dest=None, revmapfile=None, **opts):
531 534 global orig_encoding
532 535 orig_encoding = encoding.encoding
533 536 encoding.encoding = 'UTF-8'
534 537
535 538 # support --authors as an alias for --authormap
536 539 if not opts.get('authormap'):
537 540 opts['authormap'] = opts.get('authors')
538 541
539 542 if not dest:
540 543 dest = hg.defaultdest(src) + "-hg"
541 544 ui.status(_("assuming destination %s\n") % dest)
542 545
543 546 destc = convertsink(ui, dest, opts.get('dest_type'))
544 547
545 548 try:
546 549 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
547 550 opts.get('rev'))
548 551 except Exception:
549 552 for path in destc.created:
550 553 shutil.rmtree(path, True)
551 554 raise
552 555
553 556 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
554 557 sortmode = [m for m in sortmodes if opts.get(m)]
555 558 if len(sortmode) > 1:
556 559 raise util.Abort(_('more than one sort mode specified'))
557 560 if sortmode:
558 561 sortmode = sortmode[0]
559 562 else:
560 563 sortmode = defaultsort
561 564
562 565 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
563 566 raise util.Abort(_('--sourcesort is not supported by this data source'))
564 567 if sortmode == 'closesort' and not srcc.hasnativeclose():
565 568 raise util.Abort(_('--closesort is not supported by this data source'))
566 569
567 570 fmap = opts.get('filemap')
568 571 if fmap:
569 572 srcc = filemap.filemap_source(ui, srcc, fmap)
570 573 destc.setfilemapmode(True)
571 574
572 575 if not revmapfile:
573 576 revmapfile = destc.revmapfile()
574 577
575 578 c = converter(ui, srcc, destc, revmapfile, opts)
576 579 c.convert(sortmode)
General Comments 0
You need to be logged in to leave comments. Login now