##// END OF EJS Templates
convert: keep converted hg parents that are outside convert.hg.revs (BC)...
Mads Kiilerich -
r28900:b65966f5 default
parent child Browse files
Show More
@@ -1,492 +1,494 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 from __future__ import absolute_import
8 8
9 9 import base64
10 10 import cPickle as pickle
11 11 import datetime
12 12 import errno
13 13 import os
14 14 import re
15 15 import subprocess
16 16
17 17 from mercurial import (
18 18 error,
19 19 phases,
20 20 util,
21 21 )
22 22 from mercurial.i18n import _
23 23
24 24 propertycache = util.propertycache
25 25
26 26 def encodeargs(args):
27 27 def encodearg(s):
28 28 lines = base64.encodestring(s)
29 29 lines = [l.splitlines()[0] for l in lines]
30 30 return ''.join(lines)
31 31
32 32 s = pickle.dumps(args)
33 33 return encodearg(s)
34 34
35 35 def decodeargs(s):
36 36 s = base64.decodestring(s)
37 37 return pickle.loads(s)
38 38
39 39 class MissingTool(Exception):
40 40 pass
41 41
42 42 def checktool(exe, name=None, abort=True):
43 43 name = name or exe
44 44 if not util.findexe(exe):
45 45 if abort:
46 46 exc = error.Abort
47 47 else:
48 48 exc = MissingTool
49 49 raise exc(_('cannot find required "%s" tool') % name)
50 50
51 51 class NoRepo(Exception):
52 52 pass
53 53
54 54 SKIPREV = 'SKIP'
55 55
56 56 class commit(object):
57 57 def __init__(self, author, date, desc, parents, branch=None, rev=None,
58 extra={}, sortkey=None, saverev=True, phase=phases.draft):
58 extra={}, sortkey=None, saverev=True, phase=phases.draft,
59 optparents=None):
59 60 self.author = author or 'unknown'
60 61 self.date = date or '0 0'
61 62 self.desc = desc
62 self.parents = parents
63 self.parents = parents # will be converted and used as parents
64 self.optparents = optparents or [] # will be used if already converted
63 65 self.branch = branch
64 66 self.rev = rev
65 67 self.extra = extra
66 68 self.sortkey = sortkey
67 69 self.saverev = saverev
68 70 self.phase = phase
69 71
70 72 class converter_source(object):
71 73 """Conversion source interface"""
72 74
73 75 def __init__(self, ui, path=None, revs=None):
74 76 """Initialize conversion source (or raise NoRepo("message")
75 77 exception if path is not a valid repository)"""
76 78 self.ui = ui
77 79 self.path = path
78 80 self.revs = revs
79 81
80 82 self.encoding = 'utf-8'
81 83
82 84 def checkhexformat(self, revstr, mapname='splicemap'):
83 85 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
84 86 such format for their revision numbering
85 87 """
86 88 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
87 89 raise error.Abort(_('%s entry %s is not a valid revision'
88 90 ' identifier') % (mapname, revstr))
89 91
90 92 def before(self):
91 93 pass
92 94
93 95 def after(self):
94 96 pass
95 97
96 98 def targetfilebelongstosource(self, targetfilename):
97 99 """Returns true if the given targetfile belongs to the source repo. This
98 100 is useful when only a subdirectory of the target belongs to the source
99 101 repo."""
100 102 # For normal full repo converts, this is always True.
101 103 return True
102 104
103 105 def setrevmap(self, revmap):
104 106 """set the map of already-converted revisions"""
105 107 pass
106 108
107 109 def getheads(self):
108 110 """Return a list of this repository's heads"""
109 111 raise NotImplementedError
110 112
111 113 def getfile(self, name, rev):
112 114 """Return a pair (data, mode) where data is the file content
113 115 as a string and mode one of '', 'x' or 'l'. rev is the
114 116 identifier returned by a previous call to getchanges().
115 117 Data is None if file is missing/deleted in rev.
116 118 """
117 119 raise NotImplementedError
118 120
119 121 def getchanges(self, version, full):
120 122 """Returns a tuple of (files, copies, cleanp2).
121 123
122 124 files is a sorted list of (filename, id) tuples for all files
123 125 changed between version and its first parent returned by
124 126 getcommit(). If full, all files in that revision is returned.
125 127 id is the source revision id of the file.
126 128
127 129 copies is a dictionary of dest: source
128 130
129 131 cleanp2 is the set of files filenames that are clean against p2.
130 132 (Files that are clean against p1 are already not in files (unless
131 133 full). This makes it possible to handle p2 clean files similarly.)
132 134 """
133 135 raise NotImplementedError
134 136
135 137 def getcommit(self, version):
136 138 """Return the commit object for version"""
137 139 raise NotImplementedError
138 140
139 141 def numcommits(self):
140 142 """Return the number of commits in this source.
141 143
142 144 If unknown, return None.
143 145 """
144 146 return None
145 147
146 148 def gettags(self):
147 149 """Return the tags as a dictionary of name: revision
148 150
149 151 Tag names must be UTF-8 strings.
150 152 """
151 153 raise NotImplementedError
152 154
153 155 def recode(self, s, encoding=None):
154 156 if not encoding:
155 157 encoding = self.encoding or 'utf-8'
156 158
157 159 if isinstance(s, unicode):
158 160 return s.encode("utf-8")
159 161 try:
160 162 return s.decode(encoding).encode("utf-8")
161 163 except UnicodeError:
162 164 try:
163 165 return s.decode("latin-1").encode("utf-8")
164 166 except UnicodeError:
165 167 return s.decode(encoding, "replace").encode("utf-8")
166 168
167 169 def getchangedfiles(self, rev, i):
168 170 """Return the files changed by rev compared to parent[i].
169 171
170 172 i is an index selecting one of the parents of rev. The return
171 173 value should be the list of files that are different in rev and
172 174 this parent.
173 175
174 176 If rev has no parents, i is None.
175 177
176 178 This function is only needed to support --filemap
177 179 """
178 180 raise NotImplementedError
179 181
180 182 def converted(self, rev, sinkrev):
181 183 '''Notify the source that a revision has been converted.'''
182 184 pass
183 185
184 186 def hasnativeorder(self):
185 187 """Return true if this source has a meaningful, native revision
186 188 order. For instance, Mercurial revisions are store sequentially
187 189 while there is no such global ordering with Darcs.
188 190 """
189 191 return False
190 192
191 193 def hasnativeclose(self):
192 194 """Return true if this source has ability to close branch.
193 195 """
194 196 return False
195 197
196 198 def lookuprev(self, rev):
197 199 """If rev is a meaningful revision reference in source, return
198 200 the referenced identifier in the same format used by getcommit().
199 201 return None otherwise.
200 202 """
201 203 return None
202 204
203 205 def getbookmarks(self):
204 206 """Return the bookmarks as a dictionary of name: revision
205 207
206 208 Bookmark names are to be UTF-8 strings.
207 209 """
208 210 return {}
209 211
210 212 def checkrevformat(self, revstr, mapname='splicemap'):
211 213 """revstr is a string that describes a revision in the given
212 214 source control system. Return true if revstr has correct
213 215 format.
214 216 """
215 217 return True
216 218
217 219 class converter_sink(object):
218 220 """Conversion sink (target) interface"""
219 221
220 222 def __init__(self, ui, path):
221 223 """Initialize conversion sink (or raise NoRepo("message")
222 224 exception if path is not a valid repository)
223 225
224 226 created is a list of paths to remove if a fatal error occurs
225 227 later"""
226 228 self.ui = ui
227 229 self.path = path
228 230 self.created = []
229 231
230 232 def revmapfile(self):
231 233 """Path to a file that will contain lines
232 234 source_rev_id sink_rev_id
233 235 mapping equivalent revision identifiers for each system."""
234 236 raise NotImplementedError
235 237
236 238 def authorfile(self):
237 239 """Path to a file that will contain lines
238 240 srcauthor=dstauthor
239 241 mapping equivalent authors identifiers for each system."""
240 242 return None
241 243
242 244 def putcommit(self, files, copies, parents, commit, source, revmap, full,
243 245 cleanp2):
244 246 """Create a revision with all changed files listed in 'files'
245 247 and having listed parents. 'commit' is a commit object
246 248 containing at a minimum the author, date, and message for this
247 249 changeset. 'files' is a list of (path, version) tuples,
248 250 'copies' is a dictionary mapping destinations to sources,
249 251 'source' is the source repository, and 'revmap' is a mapfile
250 252 of source revisions to converted revisions. Only getfile() and
251 253 lookuprev() should be called on 'source'. 'full' means that 'files'
252 254 is complete and all other files should be removed.
253 255 'cleanp2' is a set of the filenames that are unchanged from p2
254 256 (only in the common merge case where there two parents).
255 257
256 258 Note that the sink repository is not told to update itself to
257 259 a particular revision (or even what that revision would be)
258 260 before it receives the file data.
259 261 """
260 262 raise NotImplementedError
261 263
262 264 def puttags(self, tags):
263 265 """Put tags into sink.
264 266
265 267 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
266 268 Return a pair (tag_revision, tag_parent_revision), or (None, None)
267 269 if nothing was changed.
268 270 """
269 271 raise NotImplementedError
270 272
271 273 def setbranch(self, branch, pbranches):
272 274 """Set the current branch name. Called before the first putcommit
273 275 on the branch.
274 276 branch: branch name for subsequent commits
275 277 pbranches: (converted parent revision, parent branch) tuples"""
276 278 pass
277 279
278 280 def setfilemapmode(self, active):
279 281 """Tell the destination that we're using a filemap
280 282
281 283 Some converter_sources (svn in particular) can claim that a file
282 284 was changed in a revision, even if there was no change. This method
283 285 tells the destination that we're using a filemap and that it should
284 286 filter empty revisions.
285 287 """
286 288 pass
287 289
288 290 def before(self):
289 291 pass
290 292
291 293 def after(self):
292 294 pass
293 295
294 296 def putbookmarks(self, bookmarks):
295 297 """Put bookmarks into sink.
296 298
297 299 bookmarks: {bookmarkname: sink_rev_id, ...}
298 300 where bookmarkname is an UTF-8 string.
299 301 """
300 302 pass
301 303
302 304 def hascommitfrommap(self, rev):
303 305 """Return False if a rev mentioned in a filemap is known to not be
304 306 present."""
305 307 raise NotImplementedError
306 308
307 309 def hascommitforsplicemap(self, rev):
308 310 """This method is for the special needs for splicemap handling and not
309 311 for general use. Returns True if the sink contains rev, aborts on some
310 312 special cases."""
311 313 raise NotImplementedError
312 314
313 315 class commandline(object):
314 316 def __init__(self, ui, command):
315 317 self.ui = ui
316 318 self.command = command
317 319
318 320 def prerun(self):
319 321 pass
320 322
321 323 def postrun(self):
322 324 pass
323 325
324 326 def _cmdline(self, cmd, *args, **kwargs):
325 327 cmdline = [self.command, cmd] + list(args)
326 328 for k, v in kwargs.iteritems():
327 329 if len(k) == 1:
328 330 cmdline.append('-' + k)
329 331 else:
330 332 cmdline.append('--' + k.replace('_', '-'))
331 333 try:
332 334 if len(k) == 1:
333 335 cmdline.append('' + v)
334 336 else:
335 337 cmdline[-1] += '=' + v
336 338 except TypeError:
337 339 pass
338 340 cmdline = [util.shellquote(arg) for arg in cmdline]
339 341 if not self.ui.debugflag:
340 342 cmdline += ['2>', os.devnull]
341 343 cmdline = ' '.join(cmdline)
342 344 return cmdline
343 345
344 346 def _run(self, cmd, *args, **kwargs):
345 347 def popen(cmdline):
346 348 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
347 349 close_fds=util.closefds,
348 350 stdout=subprocess.PIPE)
349 351 return p
350 352 return self._dorun(popen, cmd, *args, **kwargs)
351 353
352 354 def _run2(self, cmd, *args, **kwargs):
353 355 return self._dorun(util.popen2, cmd, *args, **kwargs)
354 356
355 357 def _run3(self, cmd, *args, **kwargs):
356 358 return self._dorun(util.popen3, cmd, *args, **kwargs)
357 359
358 360 def _dorun(self, openfunc, cmd, *args, **kwargs):
359 361 cmdline = self._cmdline(cmd, *args, **kwargs)
360 362 self.ui.debug('running: %s\n' % (cmdline,))
361 363 self.prerun()
362 364 try:
363 365 return openfunc(cmdline)
364 366 finally:
365 367 self.postrun()
366 368
367 369 def run(self, cmd, *args, **kwargs):
368 370 p = self._run(cmd, *args, **kwargs)
369 371 output = p.communicate()[0]
370 372 self.ui.debug(output)
371 373 return output, p.returncode
372 374
373 375 def runlines(self, cmd, *args, **kwargs):
374 376 p = self._run(cmd, *args, **kwargs)
375 377 output = p.stdout.readlines()
376 378 p.wait()
377 379 self.ui.debug(''.join(output))
378 380 return output, p.returncode
379 381
380 382 def checkexit(self, status, output=''):
381 383 if status:
382 384 if output:
383 385 self.ui.warn(_('%s error:\n') % self.command)
384 386 self.ui.warn(output)
385 387 msg = util.explainexit(status)[0]
386 388 raise error.Abort('%s %s' % (self.command, msg))
387 389
388 390 def run0(self, cmd, *args, **kwargs):
389 391 output, status = self.run(cmd, *args, **kwargs)
390 392 self.checkexit(status, output)
391 393 return output
392 394
393 395 def runlines0(self, cmd, *args, **kwargs):
394 396 output, status = self.runlines(cmd, *args, **kwargs)
395 397 self.checkexit(status, ''.join(output))
396 398 return output
397 399
398 400 @propertycache
399 401 def argmax(self):
400 402 # POSIX requires at least 4096 bytes for ARG_MAX
401 403 argmax = 4096
402 404 try:
403 405 argmax = os.sysconf("SC_ARG_MAX")
404 406 except (AttributeError, ValueError):
405 407 pass
406 408
407 409 # Windows shells impose their own limits on command line length,
408 410 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
409 411 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
410 412 # details about cmd.exe limitations.
411 413
412 414 # Since ARG_MAX is for command line _and_ environment, lower our limit
413 415 # (and make happy Windows shells while doing this).
414 416 return argmax // 2 - 1
415 417
416 418 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
417 419 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
418 420 limit = self.argmax - cmdlen
419 421 bytes = 0
420 422 fl = []
421 423 for fn in arglist:
422 424 b = len(fn) + 3
423 425 if bytes + b < limit or len(fl) == 0:
424 426 fl.append(fn)
425 427 bytes += b
426 428 else:
427 429 yield fl
428 430 fl = [fn]
429 431 bytes = b
430 432 if fl:
431 433 yield fl
432 434
433 435 def xargs(self, arglist, cmd, *args, **kwargs):
434 436 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
435 437 self.run0(cmd, *(list(args) + l), **kwargs)
436 438
437 439 class mapfile(dict):
438 440 def __init__(self, ui, path):
439 441 super(mapfile, self).__init__()
440 442 self.ui = ui
441 443 self.path = path
442 444 self.fp = None
443 445 self.order = []
444 446 self._read()
445 447
446 448 def _read(self):
447 449 if not self.path:
448 450 return
449 451 try:
450 452 fp = open(self.path, 'r')
451 453 except IOError as err:
452 454 if err.errno != errno.ENOENT:
453 455 raise
454 456 return
455 457 for i, line in enumerate(fp):
456 458 line = line.splitlines()[0].rstrip()
457 459 if not line:
458 460 # Ignore blank lines
459 461 continue
460 462 try:
461 463 key, value = line.rsplit(' ', 1)
462 464 except ValueError:
463 465 raise error.Abort(
464 466 _('syntax error in %s(%d): key/value pair expected')
465 467 % (self.path, i + 1))
466 468 if key not in self:
467 469 self.order.append(key)
468 470 super(mapfile, self).__setitem__(key, value)
469 471 fp.close()
470 472
471 473 def __setitem__(self, key, value):
472 474 if self.fp is None:
473 475 try:
474 476 self.fp = open(self.path, 'a')
475 477 except IOError as err:
476 478 raise error.Abort(_('could not open map file %r: %s') %
477 479 (self.path, err.strerror))
478 480 self.fp.write('%s %s\n' % (key, value))
479 481 self.fp.flush()
480 482 super(mapfile, self).__setitem__(key, value)
481 483
482 484 def close(self):
483 485 if self.fp:
484 486 self.fp.close()
485 487 self.fp = None
486 488
487 489 def makedatetimestamp(t):
488 490 """Like util.makedate() but for time t instead of current time"""
489 491 delta = (datetime.datetime.utcfromtimestamp(t) -
490 492 datetime.datetime.fromtimestamp(t))
491 493 tz = delta.days * 86400 + delta.seconds
492 494 return t, tz
@@ -1,608 +1,611 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 from __future__ import absolute_import
8 8
9 9 import os
10 10 import shlex
11 11 import shutil
12 12
13 13 from mercurial import (
14 14 encoding,
15 15 error,
16 16 hg,
17 17 util,
18 18 )
19 19 from mercurial.i18n import _
20 20
21 21 from . import (
22 22 bzr,
23 23 common,
24 24 cvs,
25 25 darcs,
26 26 filemap,
27 27 git,
28 28 gnuarch,
29 29 hg as hgconvert,
30 30 monotone,
31 31 p4,
32 32 subversion,
33 33 )
34 34
35 35 mapfile = common.mapfile
36 36 MissingTool = common.MissingTool
37 37 NoRepo = common.NoRepo
38 38 SKIPREV = common.SKIPREV
39 39
40 40 bzr_source = bzr.bzr_source
41 41 convert_cvs = cvs.convert_cvs
42 42 convert_git = git.convert_git
43 43 darcs_source = darcs.darcs_source
44 44 gnuarch_source = gnuarch.gnuarch_source
45 45 mercurial_sink = hgconvert.mercurial_sink
46 46 mercurial_source = hgconvert.mercurial_source
47 47 monotone_source = monotone.monotone_source
48 48 p4_source = p4.p4_source
49 49 svn_sink = subversion.svn_sink
50 50 svn_source = subversion.svn_source
51 51
52 52 orig_encoding = 'ascii'
53 53
54 54 def recode(s):
55 55 if isinstance(s, unicode):
56 56 return s.encode(orig_encoding, 'replace')
57 57 else:
58 58 return s.decode('utf-8').encode(orig_encoding, 'replace')
59 59
60 60 def mapbranch(branch, branchmap):
61 61 '''
62 62 >>> bmap = {'default': 'branch1'}
63 63 >>> for i in ['', None]:
64 64 ... mapbranch(i, bmap)
65 65 'branch1'
66 66 'branch1'
67 67 >>> bmap = {'None': 'branch2'}
68 68 >>> for i in ['', None]:
69 69 ... mapbranch(i, bmap)
70 70 'branch2'
71 71 'branch2'
72 72 >>> bmap = {'None': 'branch3', 'default': 'branch4'}
73 73 >>> for i in ['None', '', None, 'default', 'branch5']:
74 74 ... mapbranch(i, bmap)
75 75 'branch3'
76 76 'branch4'
77 77 'branch4'
78 78 'branch4'
79 79 'branch5'
80 80 '''
81 81 # If branch is None or empty, this commit is coming from the source
82 82 # repository's default branch and destined for the default branch in the
83 83 # destination repository. For such commits, using a literal "default"
84 84 # in branchmap below allows the user to map "default" to an alternate
85 85 # default branch in the destination repository.
86 86 branch = branchmap.get(branch or 'default', branch)
87 87 # At some point we used "None" literal to denote the default branch,
88 88 # attempt to use that for backward compatibility.
89 89 if (not branch):
90 90 branch = branchmap.get(str(None), branch)
91 91 return branch
92 92
93 93 source_converters = [
94 94 ('cvs', convert_cvs, 'branchsort'),
95 95 ('git', convert_git, 'branchsort'),
96 96 ('svn', svn_source, 'branchsort'),
97 97 ('hg', mercurial_source, 'sourcesort'),
98 98 ('darcs', darcs_source, 'branchsort'),
99 99 ('mtn', monotone_source, 'branchsort'),
100 100 ('gnuarch', gnuarch_source, 'branchsort'),
101 101 ('bzr', bzr_source, 'branchsort'),
102 102 ('p4', p4_source, 'branchsort'),
103 103 ]
104 104
105 105 sink_converters = [
106 106 ('hg', mercurial_sink),
107 107 ('svn', svn_sink),
108 108 ]
109 109
110 110 def convertsource(ui, path, type, revs):
111 111 exceptions = []
112 112 if type and type not in [s[0] for s in source_converters]:
113 113 raise error.Abort(_('%s: invalid source repository type') % type)
114 114 for name, source, sortmode in source_converters:
115 115 try:
116 116 if not type or name == type:
117 117 return source(ui, path, revs), sortmode
118 118 except (NoRepo, MissingTool) as inst:
119 119 exceptions.append(inst)
120 120 if not ui.quiet:
121 121 for inst in exceptions:
122 122 ui.write("%s\n" % inst)
123 123 raise error.Abort(_('%s: missing or unsupported repository') % path)
124 124
125 125 def convertsink(ui, path, type):
126 126 if type and type not in [s[0] for s in sink_converters]:
127 127 raise error.Abort(_('%s: invalid destination repository type') % type)
128 128 for name, sink in sink_converters:
129 129 try:
130 130 if not type or name == type:
131 131 return sink(ui, path)
132 132 except NoRepo as inst:
133 133 ui.note(_("convert: %s\n") % inst)
134 134 except MissingTool as inst:
135 135 raise error.Abort('%s\n' % inst)
136 136 raise error.Abort(_('%s: unknown repository type') % path)
137 137
138 138 class progresssource(object):
139 139 def __init__(self, ui, source, filecount):
140 140 self.ui = ui
141 141 self.source = source
142 142 self.filecount = filecount
143 143 self.retrieved = 0
144 144
145 145 def getfile(self, file, rev):
146 146 self.retrieved += 1
147 147 self.ui.progress(_('getting files'), self.retrieved,
148 148 item=file, total=self.filecount, unit=_('files'))
149 149 return self.source.getfile(file, rev)
150 150
151 151 def targetfilebelongstosource(self, targetfilename):
152 152 return self.source.targetfilebelongstosource(targetfilename)
153 153
154 154 def lookuprev(self, rev):
155 155 return self.source.lookuprev(rev)
156 156
157 157 def close(self):
158 158 self.ui.progress(_('getting files'), None)
159 159
160 160 class converter(object):
161 161 def __init__(self, ui, source, dest, revmapfile, opts):
162 162
163 163 self.source = source
164 164 self.dest = dest
165 165 self.ui = ui
166 166 self.opts = opts
167 167 self.commitcache = {}
168 168 self.authors = {}
169 169 self.authorfile = None
170 170
171 171 # Record converted revisions persistently: maps source revision
172 172 # ID to target revision ID (both strings). (This is how
173 173 # incremental conversions work.)
174 174 self.map = mapfile(ui, revmapfile)
175 175
176 176 # Read first the dst author map if any
177 177 authorfile = self.dest.authorfile()
178 178 if authorfile and os.path.exists(authorfile):
179 179 self.readauthormap(authorfile)
180 180 # Extend/Override with new author map if necessary
181 181 if opts.get('authormap'):
182 182 self.readauthormap(opts.get('authormap'))
183 183 self.authorfile = self.dest.authorfile()
184 184
185 185 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
186 186 self.branchmap = mapfile(ui, opts.get('branchmap'))
187 187
188 188 def parsesplicemap(self, path):
189 189 """ check and validate the splicemap format and
190 190 return a child/parents dictionary.
191 191 Format checking has two parts.
192 192 1. generic format which is same across all source types
193 193 2. specific format checking which may be different for
194 194 different source type. This logic is implemented in
195 195 checkrevformat function in source files like
196 196 hg.py, subversion.py etc.
197 197 """
198 198
199 199 if not path:
200 200 return {}
201 201 m = {}
202 202 try:
203 203 fp = open(path, 'r')
204 204 for i, line in enumerate(fp):
205 205 line = line.splitlines()[0].rstrip()
206 206 if not line:
207 207 # Ignore blank lines
208 208 continue
209 209 # split line
210 210 lex = shlex.shlex(line, posix=True)
211 211 lex.whitespace_split = True
212 212 lex.whitespace += ','
213 213 line = list(lex)
214 214 # check number of parents
215 215 if not (2 <= len(line) <= 3):
216 216 raise error.Abort(_('syntax error in %s(%d): child parent1'
217 217 '[,parent2] expected') % (path, i + 1))
218 218 for part in line:
219 219 self.source.checkrevformat(part)
220 220 child, p1, p2 = line[0], line[1:2], line[2:]
221 221 if p1 == p2:
222 222 m[child] = p1
223 223 else:
224 224 m[child] = p1 + p2
225 225 # if file does not exist or error reading, exit
226 226 except IOError:
227 227 raise error.Abort(_('splicemap file not found or error reading %s:')
228 228 % path)
229 229 return m
230 230
231 231
232 232 def walktree(self, heads):
233 233 '''Return a mapping that identifies the uncommitted parents of every
234 234 uncommitted changeset.'''
235 235 visit = heads
236 236 known = set()
237 237 parents = {}
238 238 numcommits = self.source.numcommits()
239 239 while visit:
240 240 n = visit.pop(0)
241 241 if n in known:
242 242 continue
243 243 if n in self.map:
244 244 m = self.map[n]
245 245 if m == SKIPREV or self.dest.hascommitfrommap(m):
246 246 continue
247 247 known.add(n)
248 248 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
249 249 total=numcommits)
250 250 commit = self.cachecommit(n)
251 251 parents[n] = []
252 252 for p in commit.parents:
253 253 parents[n].append(p)
254 254 visit.append(p)
255 255 self.ui.progress(_('scanning'), None)
256 256
257 257 return parents
258 258
259 259 def mergesplicemap(self, parents, splicemap):
260 260 """A splicemap redefines child/parent relationships. Check the
261 261 map contains valid revision identifiers and merge the new
262 262 links in the source graph.
263 263 """
264 264 for c in sorted(splicemap):
265 265 if c not in parents:
266 266 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
267 267 # Could be in source but not converted during this run
268 268 self.ui.warn(_('splice map revision %s is not being '
269 269 'converted, ignoring\n') % c)
270 270 continue
271 271 pc = []
272 272 for p in splicemap[c]:
273 273 # We do not have to wait for nodes already in dest.
274 274 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
275 275 continue
276 276 # Parent is not in dest and not being converted, not good
277 277 if p not in parents:
278 278 raise error.Abort(_('unknown splice map parent: %s') % p)
279 279 pc.append(p)
280 280 parents[c] = pc
281 281
282 282 def toposort(self, parents, sortmode):
283 283 '''Return an ordering such that every uncommitted changeset is
284 284 preceded by all its uncommitted ancestors.'''
285 285
286 286 def mapchildren(parents):
287 287 """Return a (children, roots) tuple where 'children' maps parent
288 288 revision identifiers to children ones, and 'roots' is the list of
289 289 revisions without parents. 'parents' must be a mapping of revision
290 290 identifier to its parents ones.
291 291 """
292 292 visit = sorted(parents)
293 293 seen = set()
294 294 children = {}
295 295 roots = []
296 296
297 297 while visit:
298 298 n = visit.pop(0)
299 299 if n in seen:
300 300 continue
301 301 seen.add(n)
302 302 # Ensure that nodes without parents are present in the
303 303 # 'children' mapping.
304 304 children.setdefault(n, [])
305 305 hasparent = False
306 306 for p in parents[n]:
307 307 if p not in self.map:
308 308 visit.append(p)
309 309 hasparent = True
310 310 children.setdefault(p, []).append(n)
311 311 if not hasparent:
312 312 roots.append(n)
313 313
314 314 return children, roots
315 315
316 316 # Sort functions are supposed to take a list of revisions which
317 317 # can be converted immediately and pick one
318 318
319 319 def makebranchsorter():
320 320 """If the previously converted revision has a child in the
321 321 eligible revisions list, pick it. Return the list head
322 322 otherwise. Branch sort attempts to minimize branch
323 323 switching, which is harmful for Mercurial backend
324 324 compression.
325 325 """
326 326 prev = [None]
327 327 def picknext(nodes):
328 328 next = nodes[0]
329 329 for n in nodes:
330 330 if prev[0] in parents[n]:
331 331 next = n
332 332 break
333 333 prev[0] = next
334 334 return next
335 335 return picknext
336 336
337 337 def makesourcesorter():
338 338 """Source specific sort."""
339 339 keyfn = lambda n: self.commitcache[n].sortkey
340 340 def picknext(nodes):
341 341 return sorted(nodes, key=keyfn)[0]
342 342 return picknext
343 343
344 344 def makeclosesorter():
345 345 """Close order sort."""
346 346 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
347 347 self.commitcache[n].sortkey)
348 348 def picknext(nodes):
349 349 return sorted(nodes, key=keyfn)[0]
350 350 return picknext
351 351
352 352 def makedatesorter():
353 353 """Sort revisions by date."""
354 354 dates = {}
355 355 def getdate(n):
356 356 if n not in dates:
357 357 dates[n] = util.parsedate(self.commitcache[n].date)
358 358 return dates[n]
359 359
360 360 def picknext(nodes):
361 361 return min([(getdate(n), n) for n in nodes])[1]
362 362
363 363 return picknext
364 364
365 365 if sortmode == 'branchsort':
366 366 picknext = makebranchsorter()
367 367 elif sortmode == 'datesort':
368 368 picknext = makedatesorter()
369 369 elif sortmode == 'sourcesort':
370 370 picknext = makesourcesorter()
371 371 elif sortmode == 'closesort':
372 372 picknext = makeclosesorter()
373 373 else:
374 374 raise error.Abort(_('unknown sort mode: %s') % sortmode)
375 375
376 376 children, actives = mapchildren(parents)
377 377
378 378 s = []
379 379 pendings = {}
380 380 while actives:
381 381 n = picknext(actives)
382 382 actives.remove(n)
383 383 s.append(n)
384 384
385 385 # Update dependents list
386 386 for c in children.get(n, []):
387 387 if c not in pendings:
388 388 pendings[c] = [p for p in parents[c] if p not in self.map]
389 389 try:
390 390 pendings[c].remove(n)
391 391 except ValueError:
392 392 raise error.Abort(_('cycle detected between %s and %s')
393 393 % (recode(c), recode(n)))
394 394 if not pendings[c]:
395 395 # Parents are converted, node is eligible
396 396 actives.insert(0, c)
397 397 pendings[c] = None
398 398
399 399 if len(s) != len(parents):
400 400 raise error.Abort(_("not all revisions were sorted"))
401 401
402 402 return s
403 403
404 404 def writeauthormap(self):
405 405 authorfile = self.authorfile
406 406 if authorfile:
407 407 self.ui.status(_('writing author map file %s\n') % authorfile)
408 408 ofile = open(authorfile, 'w+')
409 409 for author in self.authors:
410 410 ofile.write("%s=%s\n" % (author, self.authors[author]))
411 411 ofile.close()
412 412
413 413 def readauthormap(self, authorfile):
414 414 afile = open(authorfile, 'r')
415 415 for line in afile:
416 416
417 417 line = line.strip()
418 418 if not line or line.startswith('#'):
419 419 continue
420 420
421 421 try:
422 422 srcauthor, dstauthor = line.split('=', 1)
423 423 except ValueError:
424 424 msg = _('ignoring bad line in author map file %s: %s\n')
425 425 self.ui.warn(msg % (authorfile, line.rstrip()))
426 426 continue
427 427
428 428 srcauthor = srcauthor.strip()
429 429 dstauthor = dstauthor.strip()
430 430 if self.authors.get(srcauthor) in (None, dstauthor):
431 431 msg = _('mapping author %s to %s\n')
432 432 self.ui.debug(msg % (srcauthor, dstauthor))
433 433 self.authors[srcauthor] = dstauthor
434 434 continue
435 435
436 436 m = _('overriding mapping for author %s, was %s, will be %s\n')
437 437 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
438 438
439 439 afile.close()
440 440
441 441 def cachecommit(self, rev):
442 442 commit = self.source.getcommit(rev)
443 443 commit.author = self.authors.get(commit.author, commit.author)
444 444 commit.branch = mapbranch(commit.branch, self.branchmap)
445 445 self.commitcache[rev] = commit
446 446 return commit
447 447
448 448 def copy(self, rev):
449 449 commit = self.commitcache[rev]
450 450 full = self.opts.get('full')
451 451 changes = self.source.getchanges(rev, full)
452 452 if isinstance(changes, basestring):
453 453 if changes == SKIPREV:
454 454 dest = SKIPREV
455 455 else:
456 456 dest = self.map[changes]
457 457 self.map[rev] = dest
458 458 return
459 459 files, copies, cleanp2 = changes
460 460 pbranches = []
461 461 if commit.parents:
462 462 for prev in commit.parents:
463 463 if prev not in self.commitcache:
464 464 self.cachecommit(prev)
465 465 pbranches.append((self.map[prev],
466 466 self.commitcache[prev].branch))
467 467 self.dest.setbranch(commit.branch, pbranches)
468 468 try:
469 469 parents = self.splicemap[rev]
470 470 self.ui.status(_('spliced in %s as parents of %s\n') %
471 471 (_(' and ').join(parents), rev))
472 472 parents = [self.map.get(p, p) for p in parents]
473 473 except KeyError:
474 474 parents = [b[0] for b in pbranches]
475 parents.extend(self.map[x]
476 for x in commit.optparents
477 if x in self.map)
475 478 if len(pbranches) != 2:
476 479 cleanp2 = set()
477 480 if len(parents) < 3:
478 481 source = progresssource(self.ui, self.source, len(files))
479 482 else:
480 483 # For an octopus merge, we end up traversing the list of
481 484 # changed files N-1 times. This tweak to the number of
482 485 # files makes it so the progress bar doesn't overflow
483 486 # itself.
484 487 source = progresssource(self.ui, self.source,
485 488 len(files) * (len(parents) - 1))
486 489 newnode = self.dest.putcommit(files, copies, parents, commit,
487 490 source, self.map, full, cleanp2)
488 491 source.close()
489 492 self.source.converted(rev, newnode)
490 493 self.map[rev] = newnode
491 494
492 495 def convert(self, sortmode):
493 496 try:
494 497 self.source.before()
495 498 self.dest.before()
496 499 self.source.setrevmap(self.map)
497 500 self.ui.status(_("scanning source...\n"))
498 501 heads = self.source.getheads()
499 502 parents = self.walktree(heads)
500 503 self.mergesplicemap(parents, self.splicemap)
501 504 self.ui.status(_("sorting...\n"))
502 505 t = self.toposort(parents, sortmode)
503 506 num = len(t)
504 507 c = None
505 508
506 509 self.ui.status(_("converting...\n"))
507 510 for i, c in enumerate(t):
508 511 num -= 1
509 512 desc = self.commitcache[c].desc
510 513 if "\n" in desc:
511 514 desc = desc.splitlines()[0]
512 515 # convert log message to local encoding without using
513 516 # tolocal() because the encoding.encoding convert()
514 517 # uses is 'utf-8'
515 518 self.ui.status("%d %s\n" % (num, recode(desc)))
516 519 self.ui.note(_("source: %s\n") % recode(c))
517 520 self.ui.progress(_('converting'), i, unit=_('revisions'),
518 521 total=len(t))
519 522 self.copy(c)
520 523 self.ui.progress(_('converting'), None)
521 524
522 525 if not self.ui.configbool('convert', 'skiptags'):
523 526 tags = self.source.gettags()
524 527 ctags = {}
525 528 for k in tags:
526 529 v = tags[k]
527 530 if self.map.get(v, SKIPREV) != SKIPREV:
528 531 ctags[k] = self.map[v]
529 532
530 533 if c and ctags:
531 534 nrev, tagsparent = self.dest.puttags(ctags)
532 535 if nrev and tagsparent:
533 536 # write another hash correspondence to override the
534 537 # previous one so we don't end up with extra tag heads
535 538 tagsparents = [e for e in self.map.iteritems()
536 539 if e[1] == tagsparent]
537 540 if tagsparents:
538 541 self.map[tagsparents[0][0]] = nrev
539 542
540 543 bookmarks = self.source.getbookmarks()
541 544 cbookmarks = {}
542 545 for k in bookmarks:
543 546 v = bookmarks[k]
544 547 if self.map.get(v, SKIPREV) != SKIPREV:
545 548 cbookmarks[k] = self.map[v]
546 549
547 550 if c and cbookmarks:
548 551 self.dest.putbookmarks(cbookmarks)
549 552
550 553 self.writeauthormap()
551 554 finally:
552 555 self.cleanup()
553 556
554 557 def cleanup(self):
555 558 try:
556 559 self.dest.after()
557 560 finally:
558 561 self.source.after()
559 562 self.map.close()
560 563
561 564 def convert(ui, src, dest=None, revmapfile=None, **opts):
562 565 global orig_encoding
563 566 orig_encoding = encoding.encoding
564 567 encoding.encoding = 'UTF-8'
565 568
566 569 # support --authors as an alias for --authormap
567 570 if not opts.get('authormap'):
568 571 opts['authormap'] = opts.get('authors')
569 572
570 573 if not dest:
571 574 dest = hg.defaultdest(src) + "-hg"
572 575 ui.status(_("assuming destination %s\n") % dest)
573 576
574 577 destc = convertsink(ui, dest, opts.get('dest_type'))
575 578
576 579 try:
577 580 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
578 581 opts.get('rev'))
579 582 except Exception:
580 583 for path in destc.created:
581 584 shutil.rmtree(path, True)
582 585 raise
583 586
584 587 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
585 588 sortmode = [m for m in sortmodes if opts.get(m)]
586 589 if len(sortmode) > 1:
587 590 raise error.Abort(_('more than one sort mode specified'))
588 591 if sortmode:
589 592 sortmode = sortmode[0]
590 593 else:
591 594 sortmode = defaultsort
592 595
593 596 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
594 597 raise error.Abort(_('--sourcesort is not supported by this data source')
595 598 )
596 599 if sortmode == 'closesort' and not srcc.hasnativeclose():
597 600 raise error.Abort(_('--closesort is not supported by this data source'))
598 601
599 602 fmap = opts.get('filemap')
600 603 if fmap:
601 604 srcc = filemap.filemap_source(ui, srcc, fmap)
602 605 destc.setfilemapmode(True)
603 606
604 607 if not revmapfile:
605 608 revmapfile = destc.revmapfile()
606 609
607 610 c = converter(ui, srcc, destc, revmapfile, opts)
608 611 c.convert(sortmode)
@@ -1,652 +1,655 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 from __future__ import absolute_import
20 20
21 21 import os
22 22 import re
23 23 import time
24 24
25 25 from mercurial import (
26 26 bookmarks,
27 27 context,
28 28 error,
29 29 exchange,
30 30 hg,
31 31 lock as lockmod,
32 32 merge as mergemod,
33 33 node as nodemod,
34 34 phases,
35 35 scmutil,
36 36 util,
37 37 )
38 38 stringio = util.stringio
39 39
40 40 from mercurial.i18n import _
41 41 from . import common
42 42 mapfile = common.mapfile
43 43 NoRepo = common.NoRepo
44 44
45 45 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
46 46
47 47 class mercurial_sink(common.converter_sink):
48 48 def __init__(self, ui, path):
49 49 common.converter_sink.__init__(self, ui, path)
50 50 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
51 51 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
52 52 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
53 53 self.lastbranch = None
54 54 if os.path.isdir(path) and len(os.listdir(path)) > 0:
55 55 try:
56 56 self.repo = hg.repository(self.ui, path)
57 57 if not self.repo.local():
58 58 raise NoRepo(_('%s is not a local Mercurial repository')
59 59 % path)
60 60 except error.RepoError as err:
61 61 ui.traceback()
62 62 raise NoRepo(err.args[0])
63 63 else:
64 64 try:
65 65 ui.status(_('initializing destination %s repository\n') % path)
66 66 self.repo = hg.repository(self.ui, path, create=True)
67 67 if not self.repo.local():
68 68 raise NoRepo(_('%s is not a local Mercurial repository')
69 69 % path)
70 70 self.created.append(path)
71 71 except error.RepoError:
72 72 ui.traceback()
73 73 raise NoRepo(_("could not create hg repository %s as sink")
74 74 % path)
75 75 self.lock = None
76 76 self.wlock = None
77 77 self.filemapmode = False
78 78 self.subrevmaps = {}
79 79
80 80 def before(self):
81 81 self.ui.debug('run hg sink pre-conversion action\n')
82 82 self.wlock = self.repo.wlock()
83 83 self.lock = self.repo.lock()
84 84
85 85 def after(self):
86 86 self.ui.debug('run hg sink post-conversion action\n')
87 87 if self.lock:
88 88 self.lock.release()
89 89 if self.wlock:
90 90 self.wlock.release()
91 91
92 92 def revmapfile(self):
93 93 return self.repo.join("shamap")
94 94
95 95 def authorfile(self):
96 96 return self.repo.join("authormap")
97 97
98 98 def setbranch(self, branch, pbranches):
99 99 if not self.clonebranches:
100 100 return
101 101
102 102 setbranch = (branch != self.lastbranch)
103 103 self.lastbranch = branch
104 104 if not branch:
105 105 branch = 'default'
106 106 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
107 107 if pbranches:
108 108 pbranch = pbranches[0][1]
109 109 else:
110 110 pbranch = 'default'
111 111
112 112 branchpath = os.path.join(self.path, branch)
113 113 if setbranch:
114 114 self.after()
115 115 try:
116 116 self.repo = hg.repository(self.ui, branchpath)
117 117 except Exception:
118 118 self.repo = hg.repository(self.ui, branchpath, create=True)
119 119 self.before()
120 120
121 121 # pbranches may bring revisions from other branches (merge parents)
122 122 # Make sure we have them, or pull them.
123 123 missings = {}
124 124 for b in pbranches:
125 125 try:
126 126 self.repo.lookup(b[0])
127 127 except Exception:
128 128 missings.setdefault(b[1], []).append(b[0])
129 129
130 130 if missings:
131 131 self.after()
132 132 for pbranch, heads in sorted(missings.iteritems()):
133 133 pbranchpath = os.path.join(self.path, pbranch)
134 134 prepo = hg.peer(self.ui, {}, pbranchpath)
135 135 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
136 136 exchange.pull(self.repo, prepo,
137 137 [prepo.lookup(h) for h in heads])
138 138 self.before()
139 139
140 140 def _rewritetags(self, source, revmap, data):
141 141 fp = stringio()
142 142 for line in data.splitlines():
143 143 s = line.split(' ', 1)
144 144 if len(s) != 2:
145 145 continue
146 146 revid = revmap.get(source.lookuprev(s[0]))
147 147 if not revid:
148 148 if s[0] == nodemod.nullhex:
149 149 revid = s[0]
150 150 else:
151 151 continue
152 152 fp.write('%s %s\n' % (revid, s[1]))
153 153 return fp.getvalue()
154 154
155 155 def _rewritesubstate(self, source, data):
156 156 fp = stringio()
157 157 for line in data.splitlines():
158 158 s = line.split(' ', 1)
159 159 if len(s) != 2:
160 160 continue
161 161
162 162 revid = s[0]
163 163 subpath = s[1]
164 164 if revid != nodemod.nullhex:
165 165 revmap = self.subrevmaps.get(subpath)
166 166 if revmap is None:
167 167 revmap = mapfile(self.ui,
168 168 self.repo.wjoin(subpath, '.hg/shamap'))
169 169 self.subrevmaps[subpath] = revmap
170 170
171 171 # It is reasonable that one or more of the subrepos don't
172 172 # need to be converted, in which case they can be cloned
173 173 # into place instead of converted. Therefore, only warn
174 174 # once.
175 175 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
176 176 if len(revmap) == 0:
177 177 sub = self.repo.wvfs.reljoin(subpath, '.hg')
178 178
179 179 if self.repo.wvfs.exists(sub):
180 180 self.ui.warn(msg % subpath)
181 181
182 182 newid = revmap.get(revid)
183 183 if not newid:
184 184 if len(revmap) > 0:
185 185 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
186 186 (revid, subpath))
187 187 else:
188 188 revid = newid
189 189
190 190 fp.write('%s %s\n' % (revid, subpath))
191 191
192 192 return fp.getvalue()
193 193
194 194 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
195 195 """Calculates the files from p2 that we need to pull in when merging p1
196 196 and p2, given that the merge is coming from the given source.
197 197
198 198 This prevents us from losing files that only exist in the target p2 and
199 199 that don't come from the source repo (like if you're merging multiple
200 200 repositories together).
201 201 """
202 202 anc = [p1ctx.ancestor(p2ctx)]
203 203 # Calculate what files are coming from p2
204 204 actions, diverge, rename = mergemod.calculateupdates(
205 205 self.repo, p1ctx, p2ctx, anc,
206 206 True, # branchmerge
207 207 True, # force
208 208 False, # acceptremote
209 209 False, # followcopies
210 210 )
211 211
212 212 for file, (action, info, msg) in actions.iteritems():
213 213 if source.targetfilebelongstosource(file):
214 214 # If the file belongs to the source repo, ignore the p2
215 215 # since it will be covered by the existing fileset.
216 216 continue
217 217
218 218 # If the file requires actual merging, abort. We don't have enough
219 219 # context to resolve merges correctly.
220 220 if action in ['m', 'dm', 'cd', 'dc']:
221 221 raise error.Abort(_("unable to convert merge commit "
222 222 "since target parents do not merge cleanly (file "
223 223 "%s, parents %s and %s)") % (file, p1ctx,
224 224 p2ctx))
225 225 elif action == 'k':
226 226 # 'keep' means nothing changed from p1
227 227 continue
228 228 else:
229 229 # Any other change means we want to take the p2 version
230 230 yield file
231 231
232 232 def putcommit(self, files, copies, parents, commit, source, revmap, full,
233 233 cleanp2):
234 234 files = dict(files)
235 235
236 236 def getfilectx(repo, memctx, f):
237 237 if p2ctx and f in p2files and f not in copies:
238 238 self.ui.debug('reusing %s from p2\n' % f)
239 239 try:
240 240 return p2ctx[f]
241 241 except error.ManifestLookupError:
242 242 # If the file doesn't exist in p2, then we're syncing a
243 243 # delete, so just return None.
244 244 return None
245 245 try:
246 246 v = files[f]
247 247 except KeyError:
248 248 return None
249 249 data, mode = source.getfile(f, v)
250 250 if data is None:
251 251 return None
252 252 if f == '.hgtags':
253 253 data = self._rewritetags(source, revmap, data)
254 254 if f == '.hgsubstate':
255 255 data = self._rewritesubstate(source, data)
256 256 return context.memfilectx(self.repo, f, data, 'l' in mode,
257 257 'x' in mode, copies.get(f))
258 258
259 259 pl = []
260 260 for p in parents:
261 261 if p not in pl:
262 262 pl.append(p)
263 263 parents = pl
264 264 nparents = len(parents)
265 265 if self.filemapmode and nparents == 1:
266 266 m1node = self.repo.changelog.read(nodemod.bin(parents[0]))[0]
267 267 parent = parents[0]
268 268
269 269 if len(parents) < 2:
270 270 parents.append(nodemod.nullid)
271 271 if len(parents) < 2:
272 272 parents.append(nodemod.nullid)
273 273 p2 = parents.pop(0)
274 274
275 275 text = commit.desc
276 276
277 277 sha1s = re.findall(sha1re, text)
278 278 for sha1 in sha1s:
279 279 oldrev = source.lookuprev(sha1)
280 280 newrev = revmap.get(oldrev)
281 281 if newrev is not None:
282 282 text = text.replace(sha1, newrev[:len(sha1)])
283 283
284 284 extra = commit.extra.copy()
285 285
286 286 sourcename = self.repo.ui.config('convert', 'hg.sourcename')
287 287 if sourcename:
288 288 extra['convert_source'] = sourcename
289 289
290 290 for label in ('source', 'transplant_source', 'rebase_source',
291 291 'intermediate-source'):
292 292 node = extra.get(label)
293 293
294 294 if node is None:
295 295 continue
296 296
297 297 # Only transplant stores its reference in binary
298 298 if label == 'transplant_source':
299 299 node = nodemod.hex(node)
300 300
301 301 newrev = revmap.get(node)
302 302 if newrev is not None:
303 303 if label == 'transplant_source':
304 304 newrev = nodemod.bin(newrev)
305 305
306 306 extra[label] = newrev
307 307
308 308 if self.branchnames and commit.branch:
309 309 extra['branch'] = commit.branch
310 310 if commit.rev and commit.saverev:
311 311 extra['convert_revision'] = commit.rev
312 312
313 313 while parents:
314 314 p1 = p2
315 315 p2 = parents.pop(0)
316 316 p1ctx = self.repo[p1]
317 317 p2ctx = None
318 318 if p2 != nodemod.nullid:
319 319 p2ctx = self.repo[p2]
320 320 fileset = set(files)
321 321 if full:
322 322 fileset.update(self.repo[p1])
323 323 fileset.update(self.repo[p2])
324 324
325 325 if p2ctx:
326 326 p2files = set(cleanp2)
327 327 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
328 328 p2files.add(file)
329 329 fileset.add(file)
330 330
331 331 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
332 332 getfilectx, commit.author, commit.date, extra)
333 333
334 334 # We won't know if the conversion changes the node until after the
335 335 # commit, so copy the source's phase for now.
336 336 self.repo.ui.setconfig('phases', 'new-commit',
337 337 phases.phasenames[commit.phase], 'convert')
338 338
339 339 with self.repo.transaction("convert") as tr:
340 340 node = nodemod.hex(self.repo.commitctx(ctx))
341 341
342 342 # If the node value has changed, but the phase is lower than
343 343 # draft, set it back to draft since it hasn't been exposed
344 344 # anywhere.
345 345 if commit.rev != node:
346 346 ctx = self.repo[node]
347 347 if ctx.phase() < phases.draft:
348 348 phases.retractboundary(self.repo, tr, phases.draft,
349 349 [ctx.node()])
350 350
351 351 text = "(octopus merge fixup)\n"
352 352 p2 = node
353 353
354 354 if self.filemapmode and nparents == 1:
355 355 man = self.repo.manifest
356 356 mnode = self.repo.changelog.read(nodemod.bin(p2))[0]
357 357 closed = 'close' in commit.extra
358 358 if not closed and not man.cmp(m1node, man.revision(mnode)):
359 359 self.ui.status(_("filtering out empty revision\n"))
360 360 self.repo.rollback(force=True)
361 361 return parent
362 362 return p2
363 363
364 364 def puttags(self, tags):
365 365 try:
366 366 parentctx = self.repo[self.tagsbranch]
367 367 tagparent = parentctx.node()
368 368 except error.RepoError:
369 369 parentctx = None
370 370 tagparent = nodemod.nullid
371 371
372 372 oldlines = set()
373 373 for branch, heads in self.repo.branchmap().iteritems():
374 374 for h in heads:
375 375 if '.hgtags' in self.repo[h]:
376 376 oldlines.update(
377 377 set(self.repo[h]['.hgtags'].data().splitlines(True)))
378 378 oldlines = sorted(list(oldlines))
379 379
380 380 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
381 381 if newlines == oldlines:
382 382 return None, None
383 383
384 384 # if the old and new tags match, then there is nothing to update
385 385 oldtags = set()
386 386 newtags = set()
387 387 for line in oldlines:
388 388 s = line.strip().split(' ', 1)
389 389 if len(s) != 2:
390 390 continue
391 391 oldtags.add(s[1])
392 392 for line in newlines:
393 393 s = line.strip().split(' ', 1)
394 394 if len(s) != 2:
395 395 continue
396 396 if s[1] not in oldtags:
397 397 newtags.add(s[1].strip())
398 398
399 399 if not newtags:
400 400 return None, None
401 401
402 402 data = "".join(newlines)
403 403 def getfilectx(repo, memctx, f):
404 404 return context.memfilectx(repo, f, data, False, False, None)
405 405
406 406 self.ui.status(_("updating tags\n"))
407 407 date = "%s 0" % int(time.mktime(time.gmtime()))
408 408 extra = {'branch': self.tagsbranch}
409 409 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
410 410 [".hgtags"], getfilectx, "convert-repo", date,
411 411 extra)
412 412 node = self.repo.commitctx(ctx)
413 413 return nodemod.hex(node), nodemod.hex(tagparent)
414 414
415 415 def setfilemapmode(self, active):
416 416 self.filemapmode = active
417 417
418 418 def putbookmarks(self, updatedbookmark):
419 419 if not len(updatedbookmark):
420 420 return
421 421 wlock = lock = tr = None
422 422 try:
423 423 wlock = self.repo.wlock()
424 424 lock = self.repo.lock()
425 425 tr = self.repo.transaction('bookmark')
426 426 self.ui.status(_("updating bookmarks\n"))
427 427 destmarks = self.repo._bookmarks
428 428 for bookmark in updatedbookmark:
429 429 destmarks[bookmark] = nodemod.bin(updatedbookmark[bookmark])
430 430 destmarks.recordchange(tr)
431 431 tr.close()
432 432 finally:
433 433 lockmod.release(lock, wlock, tr)
434 434
435 435 def hascommitfrommap(self, rev):
436 436 # the exact semantics of clonebranches is unclear so we can't say no
437 437 return rev in self.repo or self.clonebranches
438 438
439 439 def hascommitforsplicemap(self, rev):
440 440 if rev not in self.repo and self.clonebranches:
441 441 raise error.Abort(_('revision %s not found in destination '
442 442 'repository (lookups with clonebranches=true '
443 443 'are not implemented)') % rev)
444 444 return rev in self.repo
445 445
446 446 class mercurial_source(common.converter_source):
447 447 def __init__(self, ui, path, revs=None):
448 448 common.converter_source.__init__(self, ui, path, revs)
449 449 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
450 450 self.ignored = set()
451 451 self.saverev = ui.configbool('convert', 'hg.saverev', False)
452 452 try:
453 453 self.repo = hg.repository(self.ui, path)
454 454 # try to provoke an exception if this isn't really a hg
455 455 # repo, but some other bogus compatible-looking url
456 456 if not self.repo.local():
457 457 raise error.RepoError
458 458 except error.RepoError:
459 459 ui.traceback()
460 460 raise NoRepo(_("%s is not a local Mercurial repository") % path)
461 461 self.lastrev = None
462 462 self.lastctx = None
463 463 self._changescache = None, None
464 464 self.convertfp = None
465 465 # Restrict converted revisions to startrev descendants
466 466 startnode = ui.config('convert', 'hg.startrev')
467 467 hgrevs = ui.config('convert', 'hg.revs')
468 468 if hgrevs is None:
469 469 if startnode is not None:
470 470 try:
471 471 startnode = self.repo.lookup(startnode)
472 472 except error.RepoError:
473 473 raise error.Abort(_('%s is not a valid start revision')
474 474 % startnode)
475 475 startrev = self.repo.changelog.rev(startnode)
476 476 children = {startnode: 1}
477 477 for r in self.repo.changelog.descendants([startrev]):
478 478 children[self.repo.changelog.node(r)] = 1
479 479 self.keep = children.__contains__
480 480 else:
481 481 self.keep = util.always
482 482 if revs:
483 483 self._heads = [self.repo[r].node() for r in revs]
484 484 else:
485 485 self._heads = self.repo.heads()
486 486 else:
487 487 if revs or startnode is not None:
488 488 raise error.Abort(_('hg.revs cannot be combined with '
489 489 'hg.startrev or --rev'))
490 490 nodes = set()
491 491 parents = set()
492 492 for r in scmutil.revrange(self.repo, [hgrevs]):
493 493 ctx = self.repo[r]
494 494 nodes.add(ctx.node())
495 495 parents.update(p.node() for p in ctx.parents())
496 496 self.keep = nodes.__contains__
497 497 self._heads = nodes - parents
498 498
499 499 def _changectx(self, rev):
500 500 if self.lastrev != rev:
501 501 self.lastctx = self.repo[rev]
502 502 self.lastrev = rev
503 503 return self.lastctx
504 504
505 505 def _parents(self, ctx):
506 506 return [p for p in ctx.parents() if p and self.keep(p.node())]
507 507
508 508 def getheads(self):
509 509 return [nodemod.hex(h) for h in self._heads if self.keep(h)]
510 510
511 511 def getfile(self, name, rev):
512 512 try:
513 513 fctx = self._changectx(rev)[name]
514 514 return fctx.data(), fctx.flags()
515 515 except error.LookupError:
516 516 return None, None
517 517
518 518 def _changedfiles(self, ctx1, ctx2):
519 519 ma, r = [], []
520 520 maappend = ma.append
521 521 rappend = r.append
522 522 d = ctx1.manifest().diff(ctx2.manifest())
523 523 for f, ((node1, flag1), (node2, flag2)) in d.iteritems():
524 524 if node2 is None:
525 525 rappend(f)
526 526 else:
527 527 maappend(f)
528 528 return ma, r
529 529
530 530 def getchanges(self, rev, full):
531 531 ctx = self._changectx(rev)
532 532 parents = self._parents(ctx)
533 533 if full or not parents:
534 534 files = copyfiles = ctx.manifest()
535 535 if parents:
536 536 if self._changescache[0] == rev:
537 537 ma, r = self._changescache[1]
538 538 else:
539 539 ma, r = self._changedfiles(parents[0], ctx)
540 540 if not full:
541 541 files = ma + r
542 542 copyfiles = ma
543 543 # _getcopies() is also run for roots and before filtering so missing
544 544 # revlogs are detected early
545 545 copies = self._getcopies(ctx, parents, copyfiles)
546 546 cleanp2 = set()
547 547 if len(parents) == 2:
548 548 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
549 549 for f, value in d.iteritems():
550 550 if value is None:
551 551 cleanp2.add(f)
552 552 changes = [(f, rev) for f in files if f not in self.ignored]
553 553 changes.sort()
554 554 return changes, copies, cleanp2
555 555
556 556 def _getcopies(self, ctx, parents, files):
557 557 copies = {}
558 558 for name in files:
559 559 if name in self.ignored:
560 560 continue
561 561 try:
562 562 copysource, _copynode = ctx.filectx(name).renamed()
563 563 if copysource in self.ignored:
564 564 continue
565 565 # Ignore copy sources not in parent revisions
566 566 found = False
567 567 for p in parents:
568 568 if copysource in p:
569 569 found = True
570 570 break
571 571 if not found:
572 572 continue
573 573 copies[name] = copysource
574 574 except TypeError:
575 575 pass
576 576 except error.LookupError as e:
577 577 if not self.ignoreerrors:
578 578 raise
579 579 self.ignored.add(name)
580 580 self.ui.warn(_('ignoring: %s\n') % e)
581 581 return copies
582 582
583 583 def getcommit(self, rev):
584 584 ctx = self._changectx(rev)
585 parents = [p.hex() for p in self._parents(ctx)]
585 _parents = self._parents(ctx)
586 parents = [p.hex() for p in _parents]
587 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
586 588 crev = rev
587 589
588 590 return common.commit(author=ctx.user(),
589 591 date=util.datestr(ctx.date(),
590 592 '%Y-%m-%d %H:%M:%S %1%2'),
591 593 desc=ctx.description(),
592 594 rev=crev,
593 595 parents=parents,
596 optparents=optparents,
594 597 branch=ctx.branch(),
595 598 extra=ctx.extra(),
596 599 sortkey=ctx.rev(),
597 600 saverev=self.saverev,
598 601 phase=ctx.phase())
599 602
600 603 def gettags(self):
601 604 # This will get written to .hgtags, filter non global tags out.
602 605 tags = [t for t in self.repo.tagslist()
603 606 if self.repo.tagtype(t[0]) == 'global']
604 607 return dict([(name, nodemod.hex(node)) for name, node in tags
605 608 if self.keep(node)])
606 609
607 610 def getchangedfiles(self, rev, i):
608 611 ctx = self._changectx(rev)
609 612 parents = self._parents(ctx)
610 613 if not parents and i is None:
611 614 i = 0
612 615 ma, r = ctx.manifest().keys(), []
613 616 else:
614 617 i = i or 0
615 618 ma, r = self._changedfiles(parents[i], ctx)
616 619 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
617 620
618 621 if i == 0:
619 622 self._changescache = (rev, (ma, r))
620 623
621 624 return ma + r
622 625
623 626 def converted(self, rev, destrev):
624 627 if self.convertfp is None:
625 628 self.convertfp = open(self.repo.join('shamap'), 'a')
626 629 self.convertfp.write('%s %s\n' % (destrev, rev))
627 630 self.convertfp.flush()
628 631
629 632 def before(self):
630 633 self.ui.debug('run hg source pre-conversion action\n')
631 634
632 635 def after(self):
633 636 self.ui.debug('run hg source post-conversion action\n')
634 637
635 638 def hasnativeorder(self):
636 639 return True
637 640
638 641 def hasnativeclose(self):
639 642 return True
640 643
641 644 def lookuprev(self, rev):
642 645 try:
643 646 return nodemod.hex(self.repo.lookup(rev))
644 647 except (error.RepoError, error.LookupError):
645 648 return None
646 649
647 650 def getbookmarks(self):
648 651 return bookmarks.listbookmarks(self.repo)
649 652
650 653 def checkrevformat(self, revstr, mapname='splicemap'):
651 654 """ Mercurial, revision string is a 40 byte hex """
652 655 self.checkhexformat(revstr, mapname)
@@ -1,245 +1,245 b''
1 1
2 2 $ cat >> $HGRCPATH <<EOF
3 3 > [extensions]
4 4 > convert =
5 5 > [convert]
6 6 > hg.saverev = yes
7 7 > EOF
8 8
9 9 $ glog()
10 10 > {
11 11 > hg -R "$1" log -G --template '{rev} "{desc}" files: {files}\n'
12 12 > }
13 13
14 14 $ hg init source
15 15 $ cd source
16 16
17 17 $ echo a > a
18 18 $ echo b > b
19 19 $ echo f > f
20 20 $ hg ci -d '0 0' -qAm '0: add a b f'
21 21 $ echo c > c
22 22 $ hg move f d
23 23 $ hg ci -d '1 0' -qAm '1: add c, move f to d'
24 24 $ hg copy a e
25 25 $ echo b >> b
26 26 $ hg ci -d '2 0' -qAm '2: copy e from a, change b'
27 27 $ hg up -C 0
28 28 2 files updated, 0 files merged, 3 files removed, 0 files unresolved
29 29 $ echo a >> a
30 30 $ hg ci -d '3 0' -qAm '3: change a'
31 31 $ hg merge
32 32 merging a and e to e
33 33 3 files updated, 1 files merged, 1 files removed, 0 files unresolved
34 34 (branch merge, don't forget to commit)
35 35 $ hg ci -d '4 0' -qAm '4: merge 2 and 3'
36 36 $ echo a >> a
37 37 $ hg ci -d '5 0' -qAm '5: change a'
38 38 $ cd ..
39 39
40 40 Convert from null revision
41 41
42 42 $ hg convert --config convert.hg.startrev=null source full
43 43 initializing destination full repository
44 44 scanning source...
45 45 sorting...
46 46 converting...
47 47 5 0: add a b f
48 48 4 1: add c, move f to d
49 49 3 2: copy e from a, change b
50 50 2 3: change a
51 51 1 4: merge 2 and 3
52 52 0 5: change a
53 53
54 54 $ glog full
55 55 o 5 "5: change a" files: a
56 56 |
57 57 o 4 "4: merge 2 and 3" files: e f
58 58 |\
59 59 | o 3 "3: change a" files: a
60 60 | |
61 61 o | 2 "2: copy e from a, change b" files: b e
62 62 | |
63 63 o | 1 "1: add c, move f to d" files: c d f
64 64 |/
65 65 o 0 "0: add a b f" files: a b f
66 66
67 67 $ rm -Rf full
68 68
69 69 Convert from zero revision
70 70
71 71 $ hg convert --config convert.hg.startrev=0 source full
72 72 initializing destination full repository
73 73 scanning source...
74 74 sorting...
75 75 converting...
76 76 5 0: add a b f
77 77 4 1: add c, move f to d
78 78 3 2: copy e from a, change b
79 79 2 3: change a
80 80 1 4: merge 2 and 3
81 81 0 5: change a
82 82
83 83 $ glog full
84 84 o 5 "5: change a" files: a
85 85 |
86 86 o 4 "4: merge 2 and 3" files: e f
87 87 |\
88 88 | o 3 "3: change a" files: a
89 89 | |
90 90 o | 2 "2: copy e from a, change b" files: b e
91 91 | |
92 92 o | 1 "1: add c, move f to d" files: c d f
93 93 |/
94 94 o 0 "0: add a b f" files: a b f
95 95
96 96 Convert from merge parent
97 97
98 98 $ hg convert --config convert.hg.startrev=1 source conv1
99 99 initializing destination conv1 repository
100 100 scanning source...
101 101 sorting...
102 102 converting...
103 103 3 1: add c, move f to d
104 104 2 2: copy e from a, change b
105 105 1 4: merge 2 and 3
106 106 0 5: change a
107 107
108 108 $ glog conv1
109 109 o 3 "5: change a" files: a
110 110 |
111 111 o 2 "4: merge 2 and 3" files: a e
112 112 |
113 113 o 1 "2: copy e from a, change b" files: b e
114 114 |
115 115 o 0 "1: add c, move f to d" files: a b c d
116 116
117 117 $ cd conv1
118 118 $ hg up -q
119 119
120 120 Check copy preservation
121 121
122 122 $ hg st -C --change 2 e
123 123 M e
124 124 $ hg st -C --change 1 e
125 125 A e
126 126 a
127 127 $ hg st -C --change 0 a
128 128 A a
129 129
130 130 (It seems like a bug in log that the following doesn't show rev 1.)
131 131
132 132 $ hg log --follow --copies e
133 133 changeset: 2:82bbac3d2cf4
134 134 user: test
135 135 date: Thu Jan 01 00:00:04 1970 +0000
136 136 summary: 4: merge 2 and 3
137 137
138 138 changeset: 0:23c3be426dce
139 139 user: test
140 140 date: Thu Jan 01 00:00:01 1970 +0000
141 141 summary: 1: add c, move f to d
142 142
143 143 Check copy removal on missing parent
144 144
145 145 $ hg log --follow --copies d
146 146 changeset: 0:23c3be426dce
147 147 user: test
148 148 date: Thu Jan 01 00:00:01 1970 +0000
149 149 summary: 1: add c, move f to d
150 150
151 151 $ hg cat -r tip a b
152 152 a
153 153 a
154 154 a
155 155 b
156 156 b
157 157 $ hg -q verify
158 158 $ cd ..
159 159
160 160 Convert from merge
161 161
162 162 $ hg convert --config convert.hg.startrev=4 source conv4
163 163 initializing destination conv4 repository
164 164 scanning source...
165 165 sorting...
166 166 converting...
167 167 1 4: merge 2 and 3
168 168 0 5: change a
169 169 $ glog conv4
170 170 o 1 "5: change a" files: a
171 171 |
172 172 o 0 "4: merge 2 and 3" files: a b c d e
173 173
174 174 $ cd conv4
175 175 $ hg up -C
176 176 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
177 177 $ hg cat -r tip a b
178 178 a
179 179 a
180 180 a
181 181 b
182 182 b
183 183 $ hg -q verify
184 184 $ cd ..
185 185
186 186 Convert from revset in convert.hg.revs
187 187
188 188 $ hg convert --config convert.hg.revs='3:4+0' source revsetrepo
189 189 initializing destination revsetrepo repository
190 190 scanning source...
191 191 sorting...
192 192 converting...
193 193 2 0: add a b f
194 194 1 3: change a
195 195 0 4: merge 2 and 3
196 196
197 197 $ glog revsetrepo
198 198 o 2 "4: merge 2 and 3" files: b c d e f
199 199 |
200 200 o 1 "3: change a" files: a
201 201 |
202 202 o 0 "0: add a b f" files: a b f
203 203
204 204 Convert from specified revs
205 205
206 206 $ hg convert --rev 3 --rev 2 source multiplerevs
207 207 initializing destination multiplerevs repository
208 208 scanning source...
209 209 sorting...
210 210 converting...
211 211 3 0: add a b f
212 212 2 1: add c, move f to d
213 213 1 2: copy e from a, change b
214 214 0 3: change a
215 215 $ glog multiplerevs
216 216 o 3 "3: change a" files: a
217 217 |
218 218 | o 2 "2: copy e from a, change b" files: b e
219 219 | |
220 220 | o 1 "1: add c, move f to d" files: c d f
221 221 |/
222 222 o 0 "0: add a b f" files: a b f
223 223
224 224 Convert in multiple steps that doesn't overlap - the link to the parent is
225 currently missing
225 preserved anyway
226 226
227 227 $ hg convert --config convert.hg.revs=::1 source multistep
228 228 initializing destination multistep repository
229 229 scanning source...
230 230 sorting...
231 231 converting...
232 232 1 0: add a b f
233 233 0 1: add c, move f to d
234 234 $ hg convert --config convert.hg.revs=2 source multistep
235 235 scanning source...
236 236 sorting...
237 237 converting...
238 238 0 2: copy e from a, change b
239 239 $ glog multistep
240 o 2 "2: copy e from a, change b" files: a b c d e
241
240 o 2 "2: copy e from a, change b" files: b e
241 |
242 242 o 1 "1: add c, move f to d" files: c d f
243 243 |
244 244 o 0 "0: add a b f" files: a b f
245 245
General Comments 0
You need to be logged in to leave comments. Login now