##// END OF EJS Templates
py3: use pycompat.byteskwargs in hgext/convert/...
Pulkit Goyal -
r36347:93943eef default
parent child Browse files
Show More
@@ -1,493 +1,495 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 datetime
11 11 import errno
12 12 import os
13 13 import re
14 14 import subprocess
15 15
16 16 from mercurial.i18n import _
17 17 from mercurial import (
18 18 encoding,
19 19 error,
20 20 phases,
21 pycompat,
21 22 util,
22 23 )
23 24
24 25 pickle = util.pickle
25 26 propertycache = util.propertycache
26 27
27 28 def encodeargs(args):
28 29 def encodearg(s):
29 30 lines = base64.encodestring(s)
30 31 lines = [l.splitlines()[0] for l in lines]
31 32 return ''.join(lines)
32 33
33 34 s = pickle.dumps(args)
34 35 return encodearg(s)
35 36
36 37 def decodeargs(s):
37 38 s = base64.decodestring(s)
38 39 return pickle.loads(s)
39 40
40 41 class MissingTool(Exception):
41 42 pass
42 43
43 44 def checktool(exe, name=None, abort=True):
44 45 name = name or exe
45 46 if not util.findexe(exe):
46 47 if abort:
47 48 exc = error.Abort
48 49 else:
49 50 exc = MissingTool
50 51 raise exc(_('cannot find required "%s" tool') % name)
51 52
52 53 class NoRepo(Exception):
53 54 pass
54 55
55 56 SKIPREV = 'SKIP'
56 57
57 58 class commit(object):
58 59 def __init__(self, author, date, desc, parents, branch=None, rev=None,
59 60 extra=None, sortkey=None, saverev=True, phase=phases.draft,
60 61 optparents=None):
61 62 self.author = author or 'unknown'
62 63 self.date = date or '0 0'
63 64 self.desc = desc
64 65 self.parents = parents # will be converted and used as parents
65 66 self.optparents = optparents or [] # will be used if already converted
66 67 self.branch = branch
67 68 self.rev = rev
68 69 self.extra = extra or {}
69 70 self.sortkey = sortkey
70 71 self.saverev = saverev
71 72 self.phase = phase
72 73
73 74 class converter_source(object):
74 75 """Conversion source interface"""
75 76
76 77 def __init__(self, ui, repotype, path=None, revs=None):
77 78 """Initialize conversion source (or raise NoRepo("message")
78 79 exception if path is not a valid repository)"""
79 80 self.ui = ui
80 81 self.path = path
81 82 self.revs = revs
82 83 self.repotype = repotype
83 84
84 85 self.encoding = 'utf-8'
85 86
86 87 def checkhexformat(self, revstr, mapname='splicemap'):
87 88 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
88 89 such format for their revision numbering
89 90 """
90 91 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
91 92 raise error.Abort(_('%s entry %s is not a valid revision'
92 93 ' identifier') % (mapname, revstr))
93 94
94 95 def before(self):
95 96 pass
96 97
97 98 def after(self):
98 99 pass
99 100
100 101 def targetfilebelongstosource(self, targetfilename):
101 102 """Returns true if the given targetfile belongs to the source repo. This
102 103 is useful when only a subdirectory of the target belongs to the source
103 104 repo."""
104 105 # For normal full repo converts, this is always True.
105 106 return True
106 107
107 108 def setrevmap(self, revmap):
108 109 """set the map of already-converted revisions"""
109 110
110 111 def getheads(self):
111 112 """Return a list of this repository's heads"""
112 113 raise NotImplementedError
113 114
114 115 def getfile(self, name, rev):
115 116 """Return a pair (data, mode) where data is the file content
116 117 as a string and mode one of '', 'x' or 'l'. rev is the
117 118 identifier returned by a previous call to getchanges().
118 119 Data is None if file is missing/deleted in rev.
119 120 """
120 121 raise NotImplementedError
121 122
122 123 def getchanges(self, version, full):
123 124 """Returns a tuple of (files, copies, cleanp2).
124 125
125 126 files is a sorted list of (filename, id) tuples for all files
126 127 changed between version and its first parent returned by
127 128 getcommit(). If full, all files in that revision is returned.
128 129 id is the source revision id of the file.
129 130
130 131 copies is a dictionary of dest: source
131 132
132 133 cleanp2 is the set of files filenames that are clean against p2.
133 134 (Files that are clean against p1 are already not in files (unless
134 135 full). This makes it possible to handle p2 clean files similarly.)
135 136 """
136 137 raise NotImplementedError
137 138
138 139 def getcommit(self, version):
139 140 """Return the commit object for version"""
140 141 raise NotImplementedError
141 142
142 143 def numcommits(self):
143 144 """Return the number of commits in this source.
144 145
145 146 If unknown, return None.
146 147 """
147 148 return None
148 149
149 150 def gettags(self):
150 151 """Return the tags as a dictionary of name: revision
151 152
152 153 Tag names must be UTF-8 strings.
153 154 """
154 155 raise NotImplementedError
155 156
156 157 def recode(self, s, encoding=None):
157 158 if not encoding:
158 159 encoding = self.encoding or 'utf-8'
159 160
160 161 if isinstance(s, unicode):
161 162 return s.encode("utf-8")
162 163 try:
163 164 return s.decode(encoding).encode("utf-8")
164 165 except UnicodeError:
165 166 try:
166 167 return s.decode("latin-1").encode("utf-8")
167 168 except UnicodeError:
168 169 return s.decode(encoding, "replace").encode("utf-8")
169 170
170 171 def getchangedfiles(self, rev, i):
171 172 """Return the files changed by rev compared to parent[i].
172 173
173 174 i is an index selecting one of the parents of rev. The return
174 175 value should be the list of files that are different in rev and
175 176 this parent.
176 177
177 178 If rev has no parents, i is None.
178 179
179 180 This function is only needed to support --filemap
180 181 """
181 182 raise NotImplementedError
182 183
183 184 def converted(self, rev, sinkrev):
184 185 '''Notify the source that a revision has been converted.'''
185 186
186 187 def hasnativeorder(self):
187 188 """Return true if this source has a meaningful, native revision
188 189 order. For instance, Mercurial revisions are store sequentially
189 190 while there is no such global ordering with Darcs.
190 191 """
191 192 return False
192 193
193 194 def hasnativeclose(self):
194 195 """Return true if this source has ability to close branch.
195 196 """
196 197 return False
197 198
198 199 def lookuprev(self, rev):
199 200 """If rev is a meaningful revision reference in source, return
200 201 the referenced identifier in the same format used by getcommit().
201 202 return None otherwise.
202 203 """
203 204 return None
204 205
205 206 def getbookmarks(self):
206 207 """Return the bookmarks as a dictionary of name: revision
207 208
208 209 Bookmark names are to be UTF-8 strings.
209 210 """
210 211 return {}
211 212
212 213 def checkrevformat(self, revstr, mapname='splicemap'):
213 214 """revstr is a string that describes a revision in the given
214 215 source control system. Return true if revstr has correct
215 216 format.
216 217 """
217 218 return True
218 219
219 220 class converter_sink(object):
220 221 """Conversion sink (target) interface"""
221 222
222 223 def __init__(self, ui, repotype, path):
223 224 """Initialize conversion sink (or raise NoRepo("message")
224 225 exception if path is not a valid repository)
225 226
226 227 created is a list of paths to remove if a fatal error occurs
227 228 later"""
228 229 self.ui = ui
229 230 self.path = path
230 231 self.created = []
231 232 self.repotype = repotype
232 233
233 234 def revmapfile(self):
234 235 """Path to a file that will contain lines
235 236 source_rev_id sink_rev_id
236 237 mapping equivalent revision identifiers for each system."""
237 238 raise NotImplementedError
238 239
239 240 def authorfile(self):
240 241 """Path to a file that will contain lines
241 242 srcauthor=dstauthor
242 243 mapping equivalent authors identifiers for each system."""
243 244 return None
244 245
245 246 def putcommit(self, files, copies, parents, commit, source, revmap, full,
246 247 cleanp2):
247 248 """Create a revision with all changed files listed in 'files'
248 249 and having listed parents. 'commit' is a commit object
249 250 containing at a minimum the author, date, and message for this
250 251 changeset. 'files' is a list of (path, version) tuples,
251 252 'copies' is a dictionary mapping destinations to sources,
252 253 'source' is the source repository, and 'revmap' is a mapfile
253 254 of source revisions to converted revisions. Only getfile() and
254 255 lookuprev() should be called on 'source'. 'full' means that 'files'
255 256 is complete and all other files should be removed.
256 257 'cleanp2' is a set of the filenames that are unchanged from p2
257 258 (only in the common merge case where there two parents).
258 259
259 260 Note that the sink repository is not told to update itself to
260 261 a particular revision (or even what that revision would be)
261 262 before it receives the file data.
262 263 """
263 264 raise NotImplementedError
264 265
265 266 def puttags(self, tags):
266 267 """Put tags into sink.
267 268
268 269 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
269 270 Return a pair (tag_revision, tag_parent_revision), or (None, None)
270 271 if nothing was changed.
271 272 """
272 273 raise NotImplementedError
273 274
274 275 def setbranch(self, branch, pbranches):
275 276 """Set the current branch name. Called before the first putcommit
276 277 on the branch.
277 278 branch: branch name for subsequent commits
278 279 pbranches: (converted parent revision, parent branch) tuples"""
279 280
280 281 def setfilemapmode(self, active):
281 282 """Tell the destination that we're using a filemap
282 283
283 284 Some converter_sources (svn in particular) can claim that a file
284 285 was changed in a revision, even if there was no change. This method
285 286 tells the destination that we're using a filemap and that it should
286 287 filter empty revisions.
287 288 """
288 289
289 290 def before(self):
290 291 pass
291 292
292 293 def after(self):
293 294 pass
294 295
295 296 def putbookmarks(self, bookmarks):
296 297 """Put bookmarks into sink.
297 298
298 299 bookmarks: {bookmarkname: sink_rev_id, ...}
299 300 where bookmarkname is an UTF-8 string.
300 301 """
301 302
302 303 def hascommitfrommap(self, rev):
303 304 """Return False if a rev mentioned in a filemap is known to not be
304 305 present."""
305 306 raise NotImplementedError
306 307
307 308 def hascommitforsplicemap(self, rev):
308 309 """This method is for the special needs for splicemap handling and not
309 310 for general use. Returns True if the sink contains rev, aborts on some
310 311 special cases."""
311 312 raise NotImplementedError
312 313
313 314 class commandline(object):
314 315 def __init__(self, ui, command):
315 316 self.ui = ui
316 317 self.command = command
317 318
318 319 def prerun(self):
319 320 pass
320 321
321 322 def postrun(self):
322 323 pass
323 324
324 325 def _cmdline(self, cmd, *args, **kwargs):
326 kwargs = pycompat.byteskwargs(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, 'rb')
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(util.iterfile(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, 'ab')
475 477 except IOError as err:
476 478 raise error.Abort(
477 479 _('could not open map file %r: %s') %
478 480 (self.path, encoding.strtolocal(err.strerror)))
479 481 self.fp.write(util.tonativeeol('%s %s\n' % (key, value)))
480 482 self.fp.flush()
481 483 super(mapfile, self).__setitem__(key, value)
482 484
483 485 def close(self):
484 486 if self.fp:
485 487 self.fp.close()
486 488 self.fp = None
487 489
488 490 def makedatetimestamp(t):
489 491 """Like util.makedate() but for time t instead of current time"""
490 492 delta = (datetime.datetime.utcfromtimestamp(t) -
491 493 datetime.datetime.fromtimestamp(t))
492 494 tz = delta.days * 86400 + delta.seconds
493 495 return t, tz
@@ -1,617 +1,618 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 collections
10 10 import os
11 11 import shlex
12 12 import shutil
13 13
14 14 from mercurial.i18n import _
15 15 from mercurial import (
16 16 encoding,
17 17 error,
18 18 hg,
19 19 pycompat,
20 20 scmutil,
21 21 util,
22 22 )
23 23
24 24 from . import (
25 25 bzr,
26 26 common,
27 27 cvs,
28 28 darcs,
29 29 filemap,
30 30 git,
31 31 gnuarch,
32 32 hg as hgconvert,
33 33 monotone,
34 34 p4,
35 35 subversion,
36 36 )
37 37
38 38 mapfile = common.mapfile
39 39 MissingTool = common.MissingTool
40 40 NoRepo = common.NoRepo
41 41 SKIPREV = common.SKIPREV
42 42
43 43 bzr_source = bzr.bzr_source
44 44 convert_cvs = cvs.convert_cvs
45 45 convert_git = git.convert_git
46 46 darcs_source = darcs.darcs_source
47 47 gnuarch_source = gnuarch.gnuarch_source
48 48 mercurial_sink = hgconvert.mercurial_sink
49 49 mercurial_source = hgconvert.mercurial_source
50 50 monotone_source = monotone.monotone_source
51 51 p4_source = p4.p4_source
52 52 svn_sink = subversion.svn_sink
53 53 svn_source = subversion.svn_source
54 54
55 55 orig_encoding = 'ascii'
56 56
57 57 def recode(s):
58 58 if isinstance(s, unicode):
59 59 return s.encode(pycompat.sysstr(orig_encoding), 'replace')
60 60 else:
61 61 return s.decode('utf-8').encode(
62 62 pycompat.sysstr(orig_encoding), 'replace')
63 63
64 64 def mapbranch(branch, branchmap):
65 65 '''
66 66 >>> bmap = {b'default': b'branch1'}
67 67 >>> for i in [b'', None]:
68 68 ... mapbranch(i, bmap)
69 69 'branch1'
70 70 'branch1'
71 71 >>> bmap = {b'None': b'branch2'}
72 72 >>> for i in [b'', None]:
73 73 ... mapbranch(i, bmap)
74 74 'branch2'
75 75 'branch2'
76 76 >>> bmap = {b'None': b'branch3', b'default': b'branch4'}
77 77 >>> for i in [b'None', b'', None, b'default', b'branch5']:
78 78 ... mapbranch(i, bmap)
79 79 'branch3'
80 80 'branch4'
81 81 'branch4'
82 82 'branch4'
83 83 'branch5'
84 84 '''
85 85 # If branch is None or empty, this commit is coming from the source
86 86 # repository's default branch and destined for the default branch in the
87 87 # destination repository. For such commits, using a literal "default"
88 88 # in branchmap below allows the user to map "default" to an alternate
89 89 # default branch in the destination repository.
90 90 branch = branchmap.get(branch or 'default', branch)
91 91 # At some point we used "None" literal to denote the default branch,
92 92 # attempt to use that for backward compatibility.
93 93 if (not branch):
94 94 branch = branchmap.get('None', branch)
95 95 return branch
96 96
97 97 source_converters = [
98 98 ('cvs', convert_cvs, 'branchsort'),
99 99 ('git', convert_git, 'branchsort'),
100 100 ('svn', svn_source, 'branchsort'),
101 101 ('hg', mercurial_source, 'sourcesort'),
102 102 ('darcs', darcs_source, 'branchsort'),
103 103 ('mtn', monotone_source, 'branchsort'),
104 104 ('gnuarch', gnuarch_source, 'branchsort'),
105 105 ('bzr', bzr_source, 'branchsort'),
106 106 ('p4', p4_source, 'branchsort'),
107 107 ]
108 108
109 109 sink_converters = [
110 110 ('hg', mercurial_sink),
111 111 ('svn', svn_sink),
112 112 ]
113 113
114 114 def convertsource(ui, path, type, revs):
115 115 exceptions = []
116 116 if type and type not in [s[0] for s in source_converters]:
117 117 raise error.Abort(_('%s: invalid source repository type') % type)
118 118 for name, source, sortmode in source_converters:
119 119 try:
120 120 if not type or name == type:
121 121 return source(ui, name, path, revs), sortmode
122 122 except (NoRepo, MissingTool) as inst:
123 123 exceptions.append(inst)
124 124 if not ui.quiet:
125 125 for inst in exceptions:
126 126 ui.write("%s\n" % inst)
127 127 raise error.Abort(_('%s: missing or unsupported repository') % path)
128 128
129 129 def convertsink(ui, path, type):
130 130 if type and type not in [s[0] for s in sink_converters]:
131 131 raise error.Abort(_('%s: invalid destination repository type') % type)
132 132 for name, sink in sink_converters:
133 133 try:
134 134 if not type or name == type:
135 135 return sink(ui, name, path)
136 136 except NoRepo as inst:
137 137 ui.note(_("convert: %s\n") % inst)
138 138 except MissingTool as inst:
139 139 raise error.Abort('%s\n' % inst)
140 140 raise error.Abort(_('%s: unknown repository type') % path)
141 141
142 142 class progresssource(object):
143 143 def __init__(self, ui, source, filecount):
144 144 self.ui = ui
145 145 self.source = source
146 146 self.filecount = filecount
147 147 self.retrieved = 0
148 148
149 149 def getfile(self, file, rev):
150 150 self.retrieved += 1
151 151 self.ui.progress(_('getting files'), self.retrieved,
152 152 item=file, total=self.filecount, unit=_('files'))
153 153 return self.source.getfile(file, rev)
154 154
155 155 def targetfilebelongstosource(self, targetfilename):
156 156 return self.source.targetfilebelongstosource(targetfilename)
157 157
158 158 def lookuprev(self, rev):
159 159 return self.source.lookuprev(rev)
160 160
161 161 def close(self):
162 162 self.ui.progress(_('getting files'), None)
163 163
164 164 class converter(object):
165 165 def __init__(self, ui, source, dest, revmapfile, opts):
166 166
167 167 self.source = source
168 168 self.dest = dest
169 169 self.ui = ui
170 170 self.opts = opts
171 171 self.commitcache = {}
172 172 self.authors = {}
173 173 self.authorfile = None
174 174
175 175 # Record converted revisions persistently: maps source revision
176 176 # ID to target revision ID (both strings). (This is how
177 177 # incremental conversions work.)
178 178 self.map = mapfile(ui, revmapfile)
179 179
180 180 # Read first the dst author map if any
181 181 authorfile = self.dest.authorfile()
182 182 if authorfile and os.path.exists(authorfile):
183 183 self.readauthormap(authorfile)
184 184 # Extend/Override with new author map if necessary
185 185 if opts.get('authormap'):
186 186 self.readauthormap(opts.get('authormap'))
187 187 self.authorfile = self.dest.authorfile()
188 188
189 189 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
190 190 self.branchmap = mapfile(ui, opts.get('branchmap'))
191 191
192 192 def parsesplicemap(self, path):
193 193 """ check and validate the splicemap format and
194 194 return a child/parents dictionary.
195 195 Format checking has two parts.
196 196 1. generic format which is same across all source types
197 197 2. specific format checking which may be different for
198 198 different source type. This logic is implemented in
199 199 checkrevformat function in source files like
200 200 hg.py, subversion.py etc.
201 201 """
202 202
203 203 if not path:
204 204 return {}
205 205 m = {}
206 206 try:
207 207 fp = open(path, 'rb')
208 208 for i, line in enumerate(util.iterfile(fp)):
209 209 line = line.splitlines()[0].rstrip()
210 210 if not line:
211 211 # Ignore blank lines
212 212 continue
213 213 # split line
214 214 lex = shlex.shlex(line, posix=True)
215 215 lex.whitespace_split = True
216 216 lex.whitespace += ','
217 217 line = list(lex)
218 218 # check number of parents
219 219 if not (2 <= len(line) <= 3):
220 220 raise error.Abort(_('syntax error in %s(%d): child parent1'
221 221 '[,parent2] expected') % (path, i + 1))
222 222 for part in line:
223 223 self.source.checkrevformat(part)
224 224 child, p1, p2 = line[0], line[1:2], line[2:]
225 225 if p1 == p2:
226 226 m[child] = p1
227 227 else:
228 228 m[child] = p1 + p2
229 229 # if file does not exist or error reading, exit
230 230 except IOError:
231 231 raise error.Abort(_('splicemap file not found or error reading %s:')
232 232 % path)
233 233 return m
234 234
235 235
236 236 def walktree(self, heads):
237 237 '''Return a mapping that identifies the uncommitted parents of every
238 238 uncommitted changeset.'''
239 239 visit = heads
240 240 known = set()
241 241 parents = {}
242 242 numcommits = self.source.numcommits()
243 243 while visit:
244 244 n = visit.pop(0)
245 245 if n in known:
246 246 continue
247 247 if n in self.map:
248 248 m = self.map[n]
249 249 if m == SKIPREV or self.dest.hascommitfrommap(m):
250 250 continue
251 251 known.add(n)
252 252 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
253 253 total=numcommits)
254 254 commit = self.cachecommit(n)
255 255 parents[n] = []
256 256 for p in commit.parents:
257 257 parents[n].append(p)
258 258 visit.append(p)
259 259 self.ui.progress(_('scanning'), None)
260 260
261 261 return parents
262 262
263 263 def mergesplicemap(self, parents, splicemap):
264 264 """A splicemap redefines child/parent relationships. Check the
265 265 map contains valid revision identifiers and merge the new
266 266 links in the source graph.
267 267 """
268 268 for c in sorted(splicemap):
269 269 if c not in parents:
270 270 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
271 271 # Could be in source but not converted during this run
272 272 self.ui.warn(_('splice map revision %s is not being '
273 273 'converted, ignoring\n') % c)
274 274 continue
275 275 pc = []
276 276 for p in splicemap[c]:
277 277 # We do not have to wait for nodes already in dest.
278 278 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
279 279 continue
280 280 # Parent is not in dest and not being converted, not good
281 281 if p not in parents:
282 282 raise error.Abort(_('unknown splice map parent: %s') % p)
283 283 pc.append(p)
284 284 parents[c] = pc
285 285
286 286 def toposort(self, parents, sortmode):
287 287 '''Return an ordering such that every uncommitted changeset is
288 288 preceded by all its uncommitted ancestors.'''
289 289
290 290 def mapchildren(parents):
291 291 """Return a (children, roots) tuple where 'children' maps parent
292 292 revision identifiers to children ones, and 'roots' is the list of
293 293 revisions without parents. 'parents' must be a mapping of revision
294 294 identifier to its parents ones.
295 295 """
296 296 visit = collections.deque(sorted(parents))
297 297 seen = set()
298 298 children = {}
299 299 roots = []
300 300
301 301 while visit:
302 302 n = visit.popleft()
303 303 if n in seen:
304 304 continue
305 305 seen.add(n)
306 306 # Ensure that nodes without parents are present in the
307 307 # 'children' mapping.
308 308 children.setdefault(n, [])
309 309 hasparent = False
310 310 for p in parents[n]:
311 311 if p not in self.map:
312 312 visit.append(p)
313 313 hasparent = True
314 314 children.setdefault(p, []).append(n)
315 315 if not hasparent:
316 316 roots.append(n)
317 317
318 318 return children, roots
319 319
320 320 # Sort functions are supposed to take a list of revisions which
321 321 # can be converted immediately and pick one
322 322
323 323 def makebranchsorter():
324 324 """If the previously converted revision has a child in the
325 325 eligible revisions list, pick it. Return the list head
326 326 otherwise. Branch sort attempts to minimize branch
327 327 switching, which is harmful for Mercurial backend
328 328 compression.
329 329 """
330 330 prev = [None]
331 331 def picknext(nodes):
332 332 next = nodes[0]
333 333 for n in nodes:
334 334 if prev[0] in parents[n]:
335 335 next = n
336 336 break
337 337 prev[0] = next
338 338 return next
339 339 return picknext
340 340
341 341 def makesourcesorter():
342 342 """Source specific sort."""
343 343 keyfn = lambda n: self.commitcache[n].sortkey
344 344 def picknext(nodes):
345 345 return sorted(nodes, key=keyfn)[0]
346 346 return picknext
347 347
348 348 def makeclosesorter():
349 349 """Close order sort."""
350 350 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
351 351 self.commitcache[n].sortkey)
352 352 def picknext(nodes):
353 353 return sorted(nodes, key=keyfn)[0]
354 354 return picknext
355 355
356 356 def makedatesorter():
357 357 """Sort revisions by date."""
358 358 dates = {}
359 359 def getdate(n):
360 360 if n not in dates:
361 361 dates[n] = util.parsedate(self.commitcache[n].date)
362 362 return dates[n]
363 363
364 364 def picknext(nodes):
365 365 return min([(getdate(n), n) for n in nodes])[1]
366 366
367 367 return picknext
368 368
369 369 if sortmode == 'branchsort':
370 370 picknext = makebranchsorter()
371 371 elif sortmode == 'datesort':
372 372 picknext = makedatesorter()
373 373 elif sortmode == 'sourcesort':
374 374 picknext = makesourcesorter()
375 375 elif sortmode == 'closesort':
376 376 picknext = makeclosesorter()
377 377 else:
378 378 raise error.Abort(_('unknown sort mode: %s') % sortmode)
379 379
380 380 children, actives = mapchildren(parents)
381 381
382 382 s = []
383 383 pendings = {}
384 384 while actives:
385 385 n = picknext(actives)
386 386 actives.remove(n)
387 387 s.append(n)
388 388
389 389 # Update dependents list
390 390 for c in children.get(n, []):
391 391 if c not in pendings:
392 392 pendings[c] = [p for p in parents[c] if p not in self.map]
393 393 try:
394 394 pendings[c].remove(n)
395 395 except ValueError:
396 396 raise error.Abort(_('cycle detected between %s and %s')
397 397 % (recode(c), recode(n)))
398 398 if not pendings[c]:
399 399 # Parents are converted, node is eligible
400 400 actives.insert(0, c)
401 401 pendings[c] = None
402 402
403 403 if len(s) != len(parents):
404 404 raise error.Abort(_("not all revisions were sorted"))
405 405
406 406 return s
407 407
408 408 def writeauthormap(self):
409 409 authorfile = self.authorfile
410 410 if authorfile:
411 411 self.ui.status(_('writing author map file %s\n') % authorfile)
412 412 ofile = open(authorfile, 'wb+')
413 413 for author in self.authors:
414 414 ofile.write(util.tonativeeol("%s=%s\n"
415 415 % (author, self.authors[author])))
416 416 ofile.close()
417 417
418 418 def readauthormap(self, authorfile):
419 419 afile = open(authorfile, 'rb')
420 420 for line in afile:
421 421
422 422 line = line.strip()
423 423 if not line or line.startswith('#'):
424 424 continue
425 425
426 426 try:
427 427 srcauthor, dstauthor = line.split('=', 1)
428 428 except ValueError:
429 429 msg = _('ignoring bad line in author map file %s: %s\n')
430 430 self.ui.warn(msg % (authorfile, line.rstrip()))
431 431 continue
432 432
433 433 srcauthor = srcauthor.strip()
434 434 dstauthor = dstauthor.strip()
435 435 if self.authors.get(srcauthor) in (None, dstauthor):
436 436 msg = _('mapping author %s to %s\n')
437 437 self.ui.debug(msg % (srcauthor, dstauthor))
438 438 self.authors[srcauthor] = dstauthor
439 439 continue
440 440
441 441 m = _('overriding mapping for author %s, was %s, will be %s\n')
442 442 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
443 443
444 444 afile.close()
445 445
446 446 def cachecommit(self, rev):
447 447 commit = self.source.getcommit(rev)
448 448 commit.author = self.authors.get(commit.author, commit.author)
449 449 commit.branch = mapbranch(commit.branch, self.branchmap)
450 450 self.commitcache[rev] = commit
451 451 return commit
452 452
453 453 def copy(self, rev):
454 454 commit = self.commitcache[rev]
455 455 full = self.opts.get('full')
456 456 changes = self.source.getchanges(rev, full)
457 457 if isinstance(changes, bytes):
458 458 if changes == SKIPREV:
459 459 dest = SKIPREV
460 460 else:
461 461 dest = self.map[changes]
462 462 self.map[rev] = dest
463 463 return
464 464 files, copies, cleanp2 = changes
465 465 pbranches = []
466 466 if commit.parents:
467 467 for prev in commit.parents:
468 468 if prev not in self.commitcache:
469 469 self.cachecommit(prev)
470 470 pbranches.append((self.map[prev],
471 471 self.commitcache[prev].branch))
472 472 self.dest.setbranch(commit.branch, pbranches)
473 473 try:
474 474 parents = self.splicemap[rev]
475 475 self.ui.status(_('spliced in %s as parents of %s\n') %
476 476 (_(' and ').join(parents), rev))
477 477 parents = [self.map.get(p, p) for p in parents]
478 478 except KeyError:
479 479 parents = [b[0] for b in pbranches]
480 480 parents.extend(self.map[x]
481 481 for x in commit.optparents
482 482 if x in self.map)
483 483 if len(pbranches) != 2:
484 484 cleanp2 = set()
485 485 if len(parents) < 3:
486 486 source = progresssource(self.ui, self.source, len(files))
487 487 else:
488 488 # For an octopus merge, we end up traversing the list of
489 489 # changed files N-1 times. This tweak to the number of
490 490 # files makes it so the progress bar doesn't overflow
491 491 # itself.
492 492 source = progresssource(self.ui, self.source,
493 493 len(files) * (len(parents) - 1))
494 494 newnode = self.dest.putcommit(files, copies, parents, commit,
495 495 source, self.map, full, cleanp2)
496 496 source.close()
497 497 self.source.converted(rev, newnode)
498 498 self.map[rev] = newnode
499 499
500 500 def convert(self, sortmode):
501 501 try:
502 502 self.source.before()
503 503 self.dest.before()
504 504 self.source.setrevmap(self.map)
505 505 self.ui.status(_("scanning source...\n"))
506 506 heads = self.source.getheads()
507 507 parents = self.walktree(heads)
508 508 self.mergesplicemap(parents, self.splicemap)
509 509 self.ui.status(_("sorting...\n"))
510 510 t = self.toposort(parents, sortmode)
511 511 num = len(t)
512 512 c = None
513 513
514 514 self.ui.status(_("converting...\n"))
515 515 for i, c in enumerate(t):
516 516 num -= 1
517 517 desc = self.commitcache[c].desc
518 518 if "\n" in desc:
519 519 desc = desc.splitlines()[0]
520 520 # convert log message to local encoding without using
521 521 # tolocal() because the encoding.encoding convert()
522 522 # uses is 'utf-8'
523 523 self.ui.status("%d %s\n" % (num, recode(desc)))
524 524 self.ui.note(_("source: %s\n") % recode(c))
525 525 self.ui.progress(_('converting'), i, unit=_('revisions'),
526 526 total=len(t))
527 527 self.copy(c)
528 528 self.ui.progress(_('converting'), None)
529 529
530 530 if not self.ui.configbool('convert', 'skiptags'):
531 531 tags = self.source.gettags()
532 532 ctags = {}
533 533 for k in tags:
534 534 v = tags[k]
535 535 if self.map.get(v, SKIPREV) != SKIPREV:
536 536 ctags[k] = self.map[v]
537 537
538 538 if c and ctags:
539 539 nrev, tagsparent = self.dest.puttags(ctags)
540 540 if nrev and tagsparent:
541 541 # write another hash correspondence to override the
542 542 # previous one so we don't end up with extra tag heads
543 543 tagsparents = [e for e in self.map.iteritems()
544 544 if e[1] == tagsparent]
545 545 if tagsparents:
546 546 self.map[tagsparents[0][0]] = nrev
547 547
548 548 bookmarks = self.source.getbookmarks()
549 549 cbookmarks = {}
550 550 for k in bookmarks:
551 551 v = bookmarks[k]
552 552 if self.map.get(v, SKIPREV) != SKIPREV:
553 553 cbookmarks[k] = self.map[v]
554 554
555 555 if c and cbookmarks:
556 556 self.dest.putbookmarks(cbookmarks)
557 557
558 558 self.writeauthormap()
559 559 finally:
560 560 self.cleanup()
561 561
562 562 def cleanup(self):
563 563 try:
564 564 self.dest.after()
565 565 finally:
566 566 self.source.after()
567 567 self.map.close()
568 568
569 569 def convert(ui, src, dest=None, revmapfile=None, **opts):
570 opts = pycompat.byteskwargs(opts)
570 571 global orig_encoding
571 572 orig_encoding = encoding.encoding
572 573 encoding.encoding = 'UTF-8'
573 574
574 575 # support --authors as an alias for --authormap
575 576 if not opts.get('authormap'):
576 577 opts['authormap'] = opts.get('authors')
577 578
578 579 if not dest:
579 580 dest = hg.defaultdest(src) + "-hg"
580 581 ui.status(_("assuming destination %s\n") % dest)
581 582
582 583 destc = convertsink(ui, dest, opts.get('dest_type'))
583 584 destc = scmutil.wrapconvertsink(destc)
584 585
585 586 try:
586 587 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
587 588 opts.get('rev'))
588 589 except Exception:
589 590 for path in destc.created:
590 591 shutil.rmtree(path, True)
591 592 raise
592 593
593 594 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
594 595 sortmode = [m for m in sortmodes if opts.get(m)]
595 596 if len(sortmode) > 1:
596 597 raise error.Abort(_('more than one sort mode specified'))
597 598 if sortmode:
598 599 sortmode = sortmode[0]
599 600 else:
600 601 sortmode = defaultsort
601 602
602 603 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
603 604 raise error.Abort(_('--sourcesort is not supported by this data source')
604 605 )
605 606 if sortmode == 'closesort' and not srcc.hasnativeclose():
606 607 raise error.Abort(_('--closesort is not supported by this data source'))
607 608
608 609 fmap = opts.get('filemap')
609 610 if fmap:
610 611 srcc = filemap.filemap_source(ui, srcc, fmap)
611 612 destc.setfilemapmode(True)
612 613
613 614 if not revmapfile:
614 615 revmapfile = destc.revmapfile()
615 616
616 617 c = converter(ui, srcc, destc, revmapfile, opts)
617 618 c.convert(sortmode)
@@ -1,951 +1,952 b''
1 1 # Mercurial built-in replacement for cvsps.
2 2 #
3 3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
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 re
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial import (
14 14 encoding,
15 15 error,
16 16 hook,
17 17 pycompat,
18 18 util,
19 19 )
20 20
21 21 pickle = util.pickle
22 22
23 23 class logentry(object):
24 24 '''Class logentry has the following attributes:
25 25 .author - author name as CVS knows it
26 26 .branch - name of branch this revision is on
27 27 .branches - revision tuple of branches starting at this revision
28 28 .comment - commit message
29 29 .commitid - CVS commitid or None
30 30 .date - the commit date as a (time, tz) tuple
31 31 .dead - true if file revision is dead
32 32 .file - Name of file
33 33 .lines - a tuple (+lines, -lines) or None
34 34 .parent - Previous revision of this entry
35 35 .rcs - name of file as returned from CVS
36 36 .revision - revision number as tuple
37 37 .tags - list of tags on the file
38 38 .synthetic - is this a synthetic "file ... added on ..." revision?
39 39 .mergepoint - the branch that has been merged from (if present in
40 40 rlog output) or None
41 41 .branchpoints - the branches that start at the current entry or empty
42 42 '''
43 43 def __init__(self, **entries):
44 44 self.synthetic = False
45 45 self.__dict__.update(entries)
46 46
47 47 def __repr__(self):
48 48 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
49 49 return "%s(%s)"%(type(self).__name__, ", ".join(items))
50 50
51 51 class logerror(Exception):
52 52 pass
53 53
54 54 def getrepopath(cvspath):
55 55 """Return the repository path from a CVS path.
56 56
57 57 >>> getrepopath(b'/foo/bar')
58 58 '/foo/bar'
59 59 >>> getrepopath(b'c:/foo/bar')
60 60 '/foo/bar'
61 61 >>> getrepopath(b':pserver:10/foo/bar')
62 62 '/foo/bar'
63 63 >>> getrepopath(b':pserver:10c:/foo/bar')
64 64 '/foo/bar'
65 65 >>> getrepopath(b':pserver:/foo/bar')
66 66 '/foo/bar'
67 67 >>> getrepopath(b':pserver:c:/foo/bar')
68 68 '/foo/bar'
69 69 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
70 70 '/foo/bar'
71 71 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
72 72 '/foo/bar'
73 73 >>> getrepopath(b'user@server/path/to/repository')
74 74 '/path/to/repository'
75 75 """
76 76 # According to CVS manual, CVS paths are expressed like:
77 77 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
78 78 #
79 79 # CVSpath is splitted into parts and then position of the first occurrence
80 80 # of the '/' char after the '@' is located. The solution is the rest of the
81 81 # string after that '/' sign including it
82 82
83 83 parts = cvspath.split(':')
84 84 atposition = parts[-1].find('@')
85 85 start = 0
86 86
87 87 if atposition != -1:
88 88 start = atposition
89 89
90 90 repopath = parts[-1][parts[-1].find('/', start):]
91 91 return repopath
92 92
93 93 def createlog(ui, directory=None, root="", rlog=True, cache=None):
94 94 '''Collect the CVS rlog'''
95 95
96 96 # Because we store many duplicate commit log messages, reusing strings
97 97 # saves a lot of memory and pickle storage space.
98 98 _scache = {}
99 99 def scache(s):
100 100 "return a shared version of a string"
101 101 return _scache.setdefault(s, s)
102 102
103 103 ui.status(_('collecting CVS rlog\n'))
104 104
105 105 log = [] # list of logentry objects containing the CVS state
106 106
107 107 # patterns to match in CVS (r)log output, by state of use
108 108 re_00 = re.compile('RCS file: (.+)$')
109 109 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
110 110 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
111 111 re_03 = re.compile("(Cannot access.+CVSROOT)|"
112 112 "(can't create temporary directory.+)$")
113 113 re_10 = re.compile('Working file: (.+)$')
114 114 re_20 = re.compile('symbolic names:')
115 115 re_30 = re.compile('\t(.+): ([\\d.]+)$')
116 116 re_31 = re.compile('----------------------------$')
117 117 re_32 = re.compile('======================================='
118 118 '======================================$')
119 119 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
120 120 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
121 121 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
122 122 r'(\s+commitid:\s+([^;]+);)?'
123 123 r'(.*mergepoint:\s+([^;]+);)?')
124 124 re_70 = re.compile('branches: (.+);$')
125 125
126 126 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
127 127
128 128 prefix = '' # leading path to strip of what we get from CVS
129 129
130 130 if directory is None:
131 131 # Current working directory
132 132
133 133 # Get the real directory in the repository
134 134 try:
135 135 prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip()
136 136 directory = prefix
137 137 if prefix == ".":
138 138 prefix = ""
139 139 except IOError:
140 140 raise logerror(_('not a CVS sandbox'))
141 141
142 142 if prefix and not prefix.endswith(pycompat.ossep):
143 143 prefix += pycompat.ossep
144 144
145 145 # Use the Root file in the sandbox, if it exists
146 146 try:
147 147 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
148 148 except IOError:
149 149 pass
150 150
151 151 if not root:
152 152 root = encoding.environ.get('CVSROOT', '')
153 153
154 154 # read log cache if one exists
155 155 oldlog = []
156 156 date = None
157 157
158 158 if cache:
159 159 cachedir = os.path.expanduser('~/.hg.cvsps')
160 160 if not os.path.exists(cachedir):
161 161 os.mkdir(cachedir)
162 162
163 163 # The cvsps cache pickle needs a uniquified name, based on the
164 164 # repository location. The address may have all sort of nasties
165 165 # in it, slashes, colons and such. So here we take just the
166 166 # alphanumeric characters, concatenated in a way that does not
167 167 # mix up the various components, so that
168 168 # :pserver:user@server:/path
169 169 # and
170 170 # /pserver/user/server/path
171 171 # are mapped to different cache file names.
172 172 cachefile = root.split(":") + [directory, "cache"]
173 173 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
174 174 cachefile = os.path.join(cachedir,
175 175 '.'.join([s for s in cachefile if s]))
176 176
177 177 if cache == 'update':
178 178 try:
179 179 ui.note(_('reading cvs log cache %s\n') % cachefile)
180 180 oldlog = pickle.load(open(cachefile, 'rb'))
181 181 for e in oldlog:
182 182 if not (util.safehasattr(e, 'branchpoints') and
183 183 util.safehasattr(e, 'commitid') and
184 184 util.safehasattr(e, 'mergepoint')):
185 185 ui.status(_('ignoring old cache\n'))
186 186 oldlog = []
187 187 break
188 188
189 189 ui.note(_('cache has %d log entries\n') % len(oldlog))
190 190 except Exception as e:
191 191 ui.note(_('error reading cache: %r\n') % e)
192 192
193 193 if oldlog:
194 194 date = oldlog[-1].date # last commit date as a (time,tz) tuple
195 195 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
196 196
197 197 # build the CVS commandline
198 198 cmd = ['cvs', '-q']
199 199 if root:
200 200 cmd.append('-d%s' % root)
201 201 p = util.normpath(getrepopath(root))
202 202 if not p.endswith('/'):
203 203 p += '/'
204 204 if prefix:
205 205 # looks like normpath replaces "" by "."
206 206 prefix = p + util.normpath(prefix)
207 207 else:
208 208 prefix = p
209 209 cmd.append(['log', 'rlog'][rlog])
210 210 if date:
211 211 # no space between option and date string
212 212 cmd.append('-d>%s' % date)
213 213 cmd.append(directory)
214 214
215 215 # state machine begins here
216 216 tags = {} # dictionary of revisions on current file with their tags
217 217 branchmap = {} # mapping between branch names and revision numbers
218 218 rcsmap = {}
219 219 state = 0
220 220 store = False # set when a new record can be appended
221 221
222 222 cmd = [util.shellquote(arg) for arg in cmd]
223 223 ui.note(_("running %s\n") % (' '.join(cmd)))
224 224 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
225 225
226 226 pfp = util.popen(' '.join(cmd))
227 227 peek = pfp.readline()
228 228 while True:
229 229 line = peek
230 230 if line == '':
231 231 break
232 232 peek = pfp.readline()
233 233 if line.endswith('\n'):
234 234 line = line[:-1]
235 235 #ui.debug('state=%d line=%r\n' % (state, line))
236 236
237 237 if state == 0:
238 238 # initial state, consume input until we see 'RCS file'
239 239 match = re_00.match(line)
240 240 if match:
241 241 rcs = match.group(1)
242 242 tags = {}
243 243 if rlog:
244 244 filename = util.normpath(rcs[:-2])
245 245 if filename.startswith(prefix):
246 246 filename = filename[len(prefix):]
247 247 if filename.startswith('/'):
248 248 filename = filename[1:]
249 249 if filename.startswith('Attic/'):
250 250 filename = filename[6:]
251 251 else:
252 252 filename = filename.replace('/Attic/', '/')
253 253 state = 2
254 254 continue
255 255 state = 1
256 256 continue
257 257 match = re_01.match(line)
258 258 if match:
259 259 raise logerror(match.group(1))
260 260 match = re_02.match(line)
261 261 if match:
262 262 raise logerror(match.group(2))
263 263 if re_03.match(line):
264 264 raise logerror(line)
265 265
266 266 elif state == 1:
267 267 # expect 'Working file' (only when using log instead of rlog)
268 268 match = re_10.match(line)
269 269 assert match, _('RCS file must be followed by working file')
270 270 filename = util.normpath(match.group(1))
271 271 state = 2
272 272
273 273 elif state == 2:
274 274 # expect 'symbolic names'
275 275 if re_20.match(line):
276 276 branchmap = {}
277 277 state = 3
278 278
279 279 elif state == 3:
280 280 # read the symbolic names and store as tags
281 281 match = re_30.match(line)
282 282 if match:
283 283 rev = [int(x) for x in match.group(2).split('.')]
284 284
285 285 # Convert magic branch number to an odd-numbered one
286 286 revn = len(rev)
287 287 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
288 288 rev = rev[:-2] + rev[-1:]
289 289 rev = tuple(rev)
290 290
291 291 if rev not in tags:
292 292 tags[rev] = []
293 293 tags[rev].append(match.group(1))
294 294 branchmap[match.group(1)] = match.group(2)
295 295
296 296 elif re_31.match(line):
297 297 state = 5
298 298 elif re_32.match(line):
299 299 state = 0
300 300
301 301 elif state == 4:
302 302 # expecting '------' separator before first revision
303 303 if re_31.match(line):
304 304 state = 5
305 305 else:
306 306 assert not re_32.match(line), _('must have at least '
307 307 'some revisions')
308 308
309 309 elif state == 5:
310 310 # expecting revision number and possibly (ignored) lock indication
311 311 # we create the logentry here from values stored in states 0 to 4,
312 312 # as this state is re-entered for subsequent revisions of a file.
313 313 match = re_50.match(line)
314 314 assert match, _('expected revision number')
315 315 e = logentry(rcs=scache(rcs),
316 316 file=scache(filename),
317 317 revision=tuple([int(x) for x in
318 318 match.group(1).split('.')]),
319 319 branches=[],
320 320 parent=None,
321 321 commitid=None,
322 322 mergepoint=None,
323 323 branchpoints=set())
324 324
325 325 state = 6
326 326
327 327 elif state == 6:
328 328 # expecting date, author, state, lines changed
329 329 match = re_60.match(line)
330 330 assert match, _('revision must be followed by date line')
331 331 d = match.group(1)
332 332 if d[2] == '/':
333 333 # Y2K
334 334 d = '19' + d
335 335
336 336 if len(d.split()) != 3:
337 337 # cvs log dates always in GMT
338 338 d = d + ' UTC'
339 339 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
340 340 '%Y/%m/%d %H:%M:%S',
341 341 '%Y-%m-%d %H:%M:%S'])
342 342 e.author = scache(match.group(2))
343 343 e.dead = match.group(3).lower() == 'dead'
344 344
345 345 if match.group(5):
346 346 if match.group(6):
347 347 e.lines = (int(match.group(5)), int(match.group(6)))
348 348 else:
349 349 e.lines = (int(match.group(5)), 0)
350 350 elif match.group(6):
351 351 e.lines = (0, int(match.group(6)))
352 352 else:
353 353 e.lines = None
354 354
355 355 if match.group(7): # cvs 1.12 commitid
356 356 e.commitid = match.group(8)
357 357
358 358 if match.group(9): # cvsnt mergepoint
359 359 myrev = match.group(10).split('.')
360 360 if len(myrev) == 2: # head
361 361 e.mergepoint = 'HEAD'
362 362 else:
363 363 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
364 364 branches = [b for b in branchmap if branchmap[b] == myrev]
365 365 assert len(branches) == 1, ('unknown branch: %s'
366 366 % e.mergepoint)
367 367 e.mergepoint = branches[0]
368 368
369 369 e.comment = []
370 370 state = 7
371 371
372 372 elif state == 7:
373 373 # read the revision numbers of branches that start at this revision
374 374 # or store the commit log message otherwise
375 375 m = re_70.match(line)
376 376 if m:
377 377 e.branches = [tuple([int(y) for y in x.strip().split('.')])
378 378 for x in m.group(1).split(';')]
379 379 state = 8
380 380 elif re_31.match(line) and re_50.match(peek):
381 381 state = 5
382 382 store = True
383 383 elif re_32.match(line):
384 384 state = 0
385 385 store = True
386 386 else:
387 387 e.comment.append(line)
388 388
389 389 elif state == 8:
390 390 # store commit log message
391 391 if re_31.match(line):
392 392 cpeek = peek
393 393 if cpeek.endswith('\n'):
394 394 cpeek = cpeek[:-1]
395 395 if re_50.match(cpeek):
396 396 state = 5
397 397 store = True
398 398 else:
399 399 e.comment.append(line)
400 400 elif re_32.match(line):
401 401 state = 0
402 402 store = True
403 403 else:
404 404 e.comment.append(line)
405 405
406 406 # When a file is added on a branch B1, CVS creates a synthetic
407 407 # dead trunk revision 1.1 so that the branch has a root.
408 408 # Likewise, if you merge such a file to a later branch B2 (one
409 409 # that already existed when the file was added on B1), CVS
410 410 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
411 411 # these revisions now, but mark them synthetic so
412 412 # createchangeset() can take care of them.
413 413 if (store and
414 414 e.dead and
415 415 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
416 416 len(e.comment) == 1 and
417 417 file_added_re.match(e.comment[0])):
418 418 ui.debug('found synthetic revision in %s: %r\n'
419 419 % (e.rcs, e.comment[0]))
420 420 e.synthetic = True
421 421
422 422 if store:
423 423 # clean up the results and save in the log.
424 424 store = False
425 425 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
426 426 e.comment = scache('\n'.join(e.comment))
427 427
428 428 revn = len(e.revision)
429 429 if revn > 3 and (revn % 2) == 0:
430 430 e.branch = tags.get(e.revision[:-1], [None])[0]
431 431 else:
432 432 e.branch = None
433 433
434 434 # find the branches starting from this revision
435 435 branchpoints = set()
436 436 for branch, revision in branchmap.iteritems():
437 437 revparts = tuple([int(i) for i in revision.split('.')])
438 438 if len(revparts) < 2: # bad tags
439 439 continue
440 440 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
441 441 # normal branch
442 442 if revparts[:-2] == e.revision:
443 443 branchpoints.add(branch)
444 444 elif revparts == (1, 1, 1): # vendor branch
445 445 if revparts in e.branches:
446 446 branchpoints.add(branch)
447 447 e.branchpoints = branchpoints
448 448
449 449 log.append(e)
450 450
451 451 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
452 452
453 453 if len(log) % 100 == 0:
454 454 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
455 455
456 456 log.sort(key=lambda x: (x.rcs, x.revision))
457 457
458 458 # find parent revisions of individual files
459 459 versions = {}
460 460 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
461 461 rcs = e.rcs.replace('/Attic/', '/')
462 462 if rcs in rcsmap:
463 463 e.rcs = rcsmap[rcs]
464 464 branch = e.revision[:-1]
465 465 versions[(e.rcs, branch)] = e.revision
466 466
467 467 for e in log:
468 468 branch = e.revision[:-1]
469 469 p = versions.get((e.rcs, branch), None)
470 470 if p is None:
471 471 p = e.revision[:-2]
472 472 e.parent = p
473 473 versions[(e.rcs, branch)] = e.revision
474 474
475 475 # update the log cache
476 476 if cache:
477 477 if log:
478 478 # join up the old and new logs
479 479 log.sort(key=lambda x: x.date)
480 480
481 481 if oldlog and oldlog[-1].date >= log[0].date:
482 482 raise logerror(_('log cache overlaps with new log entries,'
483 483 ' re-run without cache.'))
484 484
485 485 log = oldlog + log
486 486
487 487 # write the new cachefile
488 488 ui.note(_('writing cvs log cache %s\n') % cachefile)
489 489 pickle.dump(log, open(cachefile, 'wb'))
490 490 else:
491 491 log = oldlog
492 492
493 493 ui.status(_('%d log entries\n') % len(log))
494 494
495 495 encodings = ui.configlist('convert', 'cvsps.logencoding')
496 496 if encodings:
497 497 def revstr(r):
498 498 # this is needed, because logentry.revision is a tuple of "int"
499 499 # (e.g. (1, 2) for "1.2")
500 500 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
501 501
502 502 for entry in log:
503 503 comment = entry.comment
504 504 for e in encodings:
505 505 try:
506 506 entry.comment = comment.decode(e).encode('utf-8')
507 507 if ui.debugflag:
508 508 ui.debug("transcoding by %s: %s of %s\n" %
509 509 (e, revstr(entry.revision), entry.file))
510 510 break
511 511 except UnicodeDecodeError:
512 512 pass # try next encoding
513 513 except LookupError as inst: # unknown encoding, maybe
514 514 raise error.Abort(inst,
515 515 hint=_('check convert.cvsps.logencoding'
516 516 ' configuration'))
517 517 else:
518 518 raise error.Abort(_("no encoding can transcode"
519 519 " CVS log message for %s of %s")
520 520 % (revstr(entry.revision), entry.file),
521 521 hint=_('check convert.cvsps.logencoding'
522 522 ' configuration'))
523 523
524 524 hook.hook(ui, None, "cvslog", True, log=log)
525 525
526 526 return log
527 527
528 528
529 529 class changeset(object):
530 530 '''Class changeset has the following attributes:
531 531 .id - integer identifying this changeset (list index)
532 532 .author - author name as CVS knows it
533 533 .branch - name of branch this changeset is on, or None
534 534 .comment - commit message
535 535 .commitid - CVS commitid or None
536 536 .date - the commit date as a (time,tz) tuple
537 537 .entries - list of logentry objects in this changeset
538 538 .parents - list of one or two parent changesets
539 539 .tags - list of tags on this changeset
540 540 .synthetic - from synthetic revision "file ... added on branch ..."
541 541 .mergepoint- the branch that has been merged from or None
542 542 .branchpoints- the branches that start at the current entry or empty
543 543 '''
544 544 def __init__(self, **entries):
545 545 self.id = None
546 546 self.synthetic = False
547 547 self.__dict__.update(entries)
548 548
549 549 def __repr__(self):
550 550 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
551 551 return "%s(%s)"%(type(self).__name__, ", ".join(items))
552 552
553 553 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
554 554 '''Convert log into changesets.'''
555 555
556 556 ui.status(_('creating changesets\n'))
557 557
558 558 # try to order commitids by date
559 559 mindate = {}
560 560 for e in log:
561 561 if e.commitid:
562 562 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
563 563
564 564 # Merge changesets
565 565 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
566 566 x.author, x.branch, x.date, x.branchpoints))
567 567
568 568 changesets = []
569 569 files = set()
570 570 c = None
571 571 for i, e in enumerate(log):
572 572
573 573 # Check if log entry belongs to the current changeset or not.
574 574
575 575 # Since CVS is file-centric, two different file revisions with
576 576 # different branchpoints should be treated as belonging to two
577 577 # different changesets (and the ordering is important and not
578 578 # honoured by cvsps at this point).
579 579 #
580 580 # Consider the following case:
581 581 # foo 1.1 branchpoints: [MYBRANCH]
582 582 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
583 583 #
584 584 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
585 585 # later version of foo may be in MYBRANCH2, so foo should be the
586 586 # first changeset and bar the next and MYBRANCH and MYBRANCH2
587 587 # should both start off of the bar changeset. No provisions are
588 588 # made to ensure that this is, in fact, what happens.
589 589 if not (c and e.branchpoints == c.branchpoints and
590 590 (# cvs commitids
591 591 (e.commitid is not None and e.commitid == c.commitid) or
592 592 (# no commitids, use fuzzy commit detection
593 593 (e.commitid is None or c.commitid is None) and
594 594 e.comment == c.comment and
595 595 e.author == c.author and
596 596 e.branch == c.branch and
597 597 ((c.date[0] + c.date[1]) <=
598 598 (e.date[0] + e.date[1]) <=
599 599 (c.date[0] + c.date[1]) + fuzz) and
600 600 e.file not in files))):
601 601 c = changeset(comment=e.comment, author=e.author,
602 602 branch=e.branch, date=e.date,
603 603 entries=[], mergepoint=e.mergepoint,
604 604 branchpoints=e.branchpoints, commitid=e.commitid)
605 605 changesets.append(c)
606 606
607 607 files = set()
608 608 if len(changesets) % 100 == 0:
609 609 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
610 610 ui.status(util.ellipsis(t, 80) + '\n')
611 611
612 612 c.entries.append(e)
613 613 files.add(e.file)
614 614 c.date = e.date # changeset date is date of latest commit in it
615 615
616 616 # Mark synthetic changesets
617 617
618 618 for c in changesets:
619 619 # Synthetic revisions always get their own changeset, because
620 620 # the log message includes the filename. E.g. if you add file3
621 621 # and file4 on a branch, you get four log entries and three
622 622 # changesets:
623 623 # "File file3 was added on branch ..." (synthetic, 1 entry)
624 624 # "File file4 was added on branch ..." (synthetic, 1 entry)
625 625 # "Add file3 and file4 to fix ..." (real, 2 entries)
626 626 # Hence the check for 1 entry here.
627 627 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
628 628
629 629 # Sort files in each changeset
630 630
631 631 def entitycompare(l, r):
632 632 'Mimic cvsps sorting order'
633 633 l = l.file.split('/')
634 634 r = r.file.split('/')
635 635 nl = len(l)
636 636 nr = len(r)
637 637 n = min(nl, nr)
638 638 for i in range(n):
639 639 if i + 1 == nl and nl < nr:
640 640 return -1
641 641 elif i + 1 == nr and nl > nr:
642 642 return +1
643 643 elif l[i] < r[i]:
644 644 return -1
645 645 elif l[i] > r[i]:
646 646 return +1
647 647 return 0
648 648
649 649 for c in changesets:
650 650 c.entries.sort(entitycompare)
651 651
652 652 # Sort changesets by date
653 653
654 654 odd = set()
655 655 def cscmp(l, r):
656 656 d = sum(l.date) - sum(r.date)
657 657 if d:
658 658 return d
659 659
660 660 # detect vendor branches and initial commits on a branch
661 661 le = {}
662 662 for e in l.entries:
663 663 le[e.rcs] = e.revision
664 664 re = {}
665 665 for e in r.entries:
666 666 re[e.rcs] = e.revision
667 667
668 668 d = 0
669 669 for e in l.entries:
670 670 if re.get(e.rcs, None) == e.parent:
671 671 assert not d
672 672 d = 1
673 673 break
674 674
675 675 for e in r.entries:
676 676 if le.get(e.rcs, None) == e.parent:
677 677 if d:
678 678 odd.add((l, r))
679 679 d = -1
680 680 break
681 681 # By this point, the changesets are sufficiently compared that
682 682 # we don't really care about ordering. However, this leaves
683 683 # some race conditions in the tests, so we compare on the
684 684 # number of files modified, the files contained in each
685 685 # changeset, and the branchpoints in the change to ensure test
686 686 # output remains stable.
687 687
688 688 # recommended replacement for cmp from
689 689 # https://docs.python.org/3.0/whatsnew/3.0.html
690 690 c = lambda x, y: (x > y) - (x < y)
691 691 # Sort bigger changes first.
692 692 if not d:
693 693 d = c(len(l.entries), len(r.entries))
694 694 # Try sorting by filename in the change.
695 695 if not d:
696 696 d = c([e.file for e in l.entries], [e.file for e in r.entries])
697 697 # Try and put changes without a branch point before ones with
698 698 # a branch point.
699 699 if not d:
700 700 d = c(len(l.branchpoints), len(r.branchpoints))
701 701 return d
702 702
703 703 changesets.sort(cscmp)
704 704
705 705 # Collect tags
706 706
707 707 globaltags = {}
708 708 for c in changesets:
709 709 for e in c.entries:
710 710 for tag in e.tags:
711 711 # remember which is the latest changeset to have this tag
712 712 globaltags[tag] = c
713 713
714 714 for c in changesets:
715 715 tags = set()
716 716 for e in c.entries:
717 717 tags.update(e.tags)
718 718 # remember tags only if this is the latest changeset to have it
719 719 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
720 720
721 721 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
722 722 # by inserting dummy changesets with two parents, and handle
723 723 # {{mergefrombranch BRANCHNAME}} by setting two parents.
724 724
725 725 if mergeto is None:
726 726 mergeto = r'{{mergetobranch ([-\w]+)}}'
727 727 if mergeto:
728 728 mergeto = re.compile(mergeto)
729 729
730 730 if mergefrom is None:
731 731 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
732 732 if mergefrom:
733 733 mergefrom = re.compile(mergefrom)
734 734
735 735 versions = {} # changeset index where we saw any particular file version
736 736 branches = {} # changeset index where we saw a branch
737 737 n = len(changesets)
738 738 i = 0
739 739 while i < n:
740 740 c = changesets[i]
741 741
742 742 for f in c.entries:
743 743 versions[(f.rcs, f.revision)] = i
744 744
745 745 p = None
746 746 if c.branch in branches:
747 747 p = branches[c.branch]
748 748 else:
749 749 # first changeset on a new branch
750 750 # the parent is a changeset with the branch in its
751 751 # branchpoints such that it is the latest possible
752 752 # commit without any intervening, unrelated commits.
753 753
754 754 for candidate in xrange(i):
755 755 if c.branch not in changesets[candidate].branchpoints:
756 756 if p is not None:
757 757 break
758 758 continue
759 759 p = candidate
760 760
761 761 c.parents = []
762 762 if p is not None:
763 763 p = changesets[p]
764 764
765 765 # Ensure no changeset has a synthetic changeset as a parent.
766 766 while p.synthetic:
767 767 assert len(p.parents) <= 1, \
768 768 _('synthetic changeset cannot have multiple parents')
769 769 if p.parents:
770 770 p = p.parents[0]
771 771 else:
772 772 p = None
773 773 break
774 774
775 775 if p is not None:
776 776 c.parents.append(p)
777 777
778 778 if c.mergepoint:
779 779 if c.mergepoint == 'HEAD':
780 780 c.mergepoint = None
781 781 c.parents.append(changesets[branches[c.mergepoint]])
782 782
783 783 if mergefrom:
784 784 m = mergefrom.search(c.comment)
785 785 if m:
786 786 m = m.group(1)
787 787 if m == 'HEAD':
788 788 m = None
789 789 try:
790 790 candidate = changesets[branches[m]]
791 791 except KeyError:
792 792 ui.warn(_("warning: CVS commit message references "
793 793 "non-existent branch %r:\n%s\n")
794 794 % (m, c.comment))
795 795 if m in branches and c.branch != m and not candidate.synthetic:
796 796 c.parents.append(candidate)
797 797
798 798 if mergeto:
799 799 m = mergeto.search(c.comment)
800 800 if m:
801 801 if m.groups():
802 802 m = m.group(1)
803 803 if m == 'HEAD':
804 804 m = None
805 805 else:
806 806 m = None # if no group found then merge to HEAD
807 807 if m in branches and c.branch != m:
808 808 # insert empty changeset for merge
809 809 cc = changeset(
810 810 author=c.author, branch=m, date=c.date,
811 811 comment='convert-repo: CVS merge from branch %s'
812 812 % c.branch,
813 813 entries=[], tags=[],
814 814 parents=[changesets[branches[m]], c])
815 815 changesets.insert(i + 1, cc)
816 816 branches[m] = i + 1
817 817
818 818 # adjust our loop counters now we have inserted a new entry
819 819 n += 1
820 820 i += 2
821 821 continue
822 822
823 823 branches[c.branch] = i
824 824 i += 1
825 825
826 826 # Drop synthetic changesets (safe now that we have ensured no other
827 827 # changesets can have them as parents).
828 828 i = 0
829 829 while i < len(changesets):
830 830 if changesets[i].synthetic:
831 831 del changesets[i]
832 832 else:
833 833 i += 1
834 834
835 835 # Number changesets
836 836
837 837 for i, c in enumerate(changesets):
838 838 c.id = i + 1
839 839
840 840 if odd:
841 841 for l, r in odd:
842 842 if l.id is not None and r.id is not None:
843 843 ui.warn(_('changeset %d is both before and after %d\n')
844 844 % (l.id, r.id))
845 845
846 846 ui.status(_('%d changeset entries\n') % len(changesets))
847 847
848 848 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
849 849
850 850 return changesets
851 851
852 852
853 853 def debugcvsps(ui, *args, **opts):
854 854 '''Read CVS rlog for current directory or named path in
855 855 repository, and convert the log to changesets based on matching
856 856 commit log entries and dates.
857 857 '''
858 opts = pycompat.byteskwargs(opts)
858 859 if opts["new_cache"]:
859 860 cache = "write"
860 861 elif opts["update_cache"]:
861 862 cache = "update"
862 863 else:
863 864 cache = None
864 865
865 866 revisions = opts["revisions"]
866 867
867 868 try:
868 869 if args:
869 870 log = []
870 871 for d in args:
871 872 log += createlog(ui, d, root=opts["root"], cache=cache)
872 873 else:
873 874 log = createlog(ui, root=opts["root"], cache=cache)
874 875 except logerror as e:
875 876 ui.write("%r\n"%e)
876 877 return
877 878
878 879 changesets = createchangeset(ui, log, opts["fuzz"])
879 880 del log
880 881
881 882 # Print changesets (optionally filtered)
882 883
883 884 off = len(revisions)
884 885 branches = {} # latest version number in each branch
885 886 ancestors = {} # parent branch
886 887 for cs in changesets:
887 888
888 889 if opts["ancestors"]:
889 890 if cs.branch not in branches and cs.parents and cs.parents[0].id:
890 891 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
891 892 cs.parents[0].id)
892 893 branches[cs.branch] = cs.id
893 894
894 895 # limit by branches
895 896 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
896 897 continue
897 898
898 899 if not off:
899 900 # Note: trailing spaces on several lines here are needed to have
900 901 # bug-for-bug compatibility with cvsps.
901 902 ui.write('---------------------\n')
902 903 ui.write(('PatchSet %d \n' % cs.id))
903 904 ui.write(('Date: %s\n' % util.datestr(cs.date,
904 905 '%Y/%m/%d %H:%M:%S %1%2')))
905 906 ui.write(('Author: %s\n' % cs.author))
906 907 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
907 908 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
908 909 ','.join(cs.tags) or '(none)')))
909 910 if cs.branchpoints:
910 911 ui.write(('Branchpoints: %s \n') %
911 912 ', '.join(sorted(cs.branchpoints)))
912 913 if opts["parents"] and cs.parents:
913 914 if len(cs.parents) > 1:
914 915 ui.write(('Parents: %s\n' %
915 916 (','.join([str(p.id) for p in cs.parents]))))
916 917 else:
917 918 ui.write(('Parent: %d\n' % cs.parents[0].id))
918 919
919 920 if opts["ancestors"]:
920 921 b = cs.branch
921 922 r = []
922 923 while b:
923 924 b, c = ancestors[b]
924 925 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
925 926 if r:
926 927 ui.write(('Ancestors: %s\n' % (','.join(r))))
927 928
928 929 ui.write(('Log:\n'))
929 930 ui.write('%s\n\n' % cs.comment)
930 931 ui.write(('Members: \n'))
931 932 for f in cs.entries:
932 933 fn = f.file
933 934 if fn.startswith(opts["prefix"]):
934 935 fn = fn[len(opts["prefix"]):]
935 936 ui.write('\t%s:%s->%s%s \n' % (
936 937 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
937 938 '.'.join([str(x) for x in f.revision]),
938 939 ['', '(DEAD)'][f.dead]))
939 940 ui.write('\n')
940 941
941 942 # have we seen the start tag?
942 943 if revisions and off:
943 944 if revisions[0] == str(cs.id) or \
944 945 revisions[0] in cs.tags:
945 946 off = False
946 947
947 948 # see if we reached the end tag
948 949 if len(revisions) > 1 and not off:
949 950 if revisions[1] == str(cs.id) or \
950 951 revisions[1] in cs.tags:
951 952 break
@@ -1,371 +1,373 b''
1 1 # monotone.py - monotone support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 4 # others
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import re
12 12
13 13 from mercurial.i18n import _
14 14 from mercurial import (
15 15 error,
16 pycompat,
16 17 util,
17 18 )
18 19
19 20 from . import common
20 21
21 22 class monotone_source(common.converter_source, common.commandline):
22 23 def __init__(self, ui, repotype, path=None, revs=None):
23 24 common.converter_source.__init__(self, ui, repotype, path, revs)
24 25 if revs and len(revs) > 1:
25 26 raise error.Abort(_('monotone source does not support specifying '
26 27 'multiple revs'))
27 28 common.commandline.__init__(self, ui, 'mtn')
28 29
29 30 self.ui = ui
30 31 self.path = path
31 32 self.automatestdio = False
32 33 self.revs = revs
33 34
34 35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
35 36 % path)
36 37 if not os.path.exists(os.path.join(path, '_MTN')):
37 38 # Could be a monotone repository (SQLite db file)
38 39 try:
39 40 f = file(path, 'rb')
40 41 header = f.read(16)
41 42 f.close()
42 43 except IOError:
43 44 header = ''
44 45 if header != 'SQLite format 3\x00':
45 46 raise norepo
46 47
47 48 # regular expressions for parsing monotone output
48 49 space = r'\s*'
49 50 name = r'\s+"((?:\\"|[^"])*)"\s*'
50 51 value = name
51 52 revision = r'\s+\[(\w+)\]\s*'
52 53 lines = r'(?:.|\n)+'
53 54
54 55 self.dir_re = re.compile(space + "dir" + name)
55 56 self.file_re = re.compile(space + "file" + name +
56 57 "content" + revision)
57 58 self.add_file_re = re.compile(space + "add_file" + name +
58 59 "content" + revision)
59 60 self.patch_re = re.compile(space + "patch" + name +
60 61 "from" + revision + "to" + revision)
61 62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
62 63 self.delete_re = re.compile(space + "delete" + name)
63 64 self.tag_re = re.compile(space + "tag" + name + "revision" +
64 65 revision)
65 66 self.cert_re = re.compile(lines + space + "name" + name +
66 67 "value" + value)
67 68
68 69 attr = space + "file" + lines + space + "attr" + space
69 70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
70 71 space + '"true"')
71 72
72 73 # cached data
73 74 self.manifest_rev = None
74 75 self.manifest = None
75 76 self.files = None
76 77 self.dirs = None
77 78
78 79 common.checktool('mtn', abort=False)
79 80
80 81 def mtnrun(self, *args, **kwargs):
81 82 if self.automatestdio:
82 83 return self.mtnrunstdio(*args, **kwargs)
83 84 else:
84 85 return self.mtnrunsingle(*args, **kwargs)
85 86
86 87 def mtnrunsingle(self, *args, **kwargs):
87 88 kwargs['d'] = self.path
88 89 return self.run0('automate', *args, **kwargs)
89 90
90 91 def mtnrunstdio(self, *args, **kwargs):
91 92 # Prepare the command in automate stdio format
93 kwargs = pycompat.byteskwargs(kwargs)
92 94 command = []
93 95 for k, v in kwargs.iteritems():
94 96 command.append("%s:%s" % (len(k), k))
95 97 if v:
96 98 command.append("%s:%s" % (len(v), v))
97 99 if command:
98 100 command.insert(0, 'o')
99 101 command.append('e')
100 102
101 103 command.append('l')
102 104 for arg in args:
103 105 command += "%s:%s" % (len(arg), arg)
104 106 command.append('e')
105 107 command = ''.join(command)
106 108
107 109 self.ui.debug("mtn: sending '%s'\n" % command)
108 110 self.mtnwritefp.write(command)
109 111 self.mtnwritefp.flush()
110 112
111 113 return self.mtnstdioreadcommandoutput(command)
112 114
113 115 def mtnstdioreadpacket(self):
114 116 read = None
115 117 commandnbr = ''
116 118 while read != ':':
117 119 read = self.mtnreadfp.read(1)
118 120 if not read:
119 121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
120 122 commandnbr += read
121 123 commandnbr = commandnbr[:-1]
122 124
123 125 stream = self.mtnreadfp.read(1)
124 126 if stream not in 'mewptl':
125 127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
126 128
127 129 read = self.mtnreadfp.read(1)
128 130 if read != ':':
129 131 raise error.Abort(_('bad mtn packet - no divider before size'))
130 132
131 133 read = None
132 134 lengthstr = ''
133 135 while read != ':':
134 136 read = self.mtnreadfp.read(1)
135 137 if not read:
136 138 raise error.Abort(_('bad mtn packet - no end of packet size'))
137 139 lengthstr += read
138 140 try:
139 141 length = long(lengthstr[:-1])
140 142 except TypeError:
141 143 raise error.Abort(_('bad mtn packet - bad packet size %s')
142 144 % lengthstr)
143 145
144 146 read = self.mtnreadfp.read(length)
145 147 if len(read) != length:
146 148 raise error.Abort(_("bad mtn packet - unable to read full packet "
147 149 "read %s of %s") % (len(read), length))
148 150
149 151 return (commandnbr, stream, length, read)
150 152
151 153 def mtnstdioreadcommandoutput(self, command):
152 154 retval = []
153 155 while True:
154 156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
155 157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
156 158 (commandnbr, stream, length))
157 159
158 160 if stream == 'l':
159 161 # End of command
160 162 if output != '0':
161 163 raise error.Abort(_("mtn command '%s' returned %s") %
162 164 (command, output))
163 165 break
164 166 elif stream in 'ew':
165 167 # Error, warning output
166 168 self.ui.warn(_('%s error:\n') % self.command)
167 169 self.ui.warn(output)
168 170 elif stream == 'p':
169 171 # Progress messages
170 172 self.ui.debug('mtn: ' + output)
171 173 elif stream == 'm':
172 174 # Main stream - command output
173 175 retval.append(output)
174 176
175 177 return ''.join(retval)
176 178
177 179 def mtnloadmanifest(self, rev):
178 180 if self.manifest_rev == rev:
179 181 return
180 182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
181 183 self.manifest_rev = rev
182 184 self.files = {}
183 185 self.dirs = {}
184 186
185 187 for e in self.manifest:
186 188 m = self.file_re.match(e)
187 189 if m:
188 190 attr = ""
189 191 name = m.group(1)
190 192 node = m.group(2)
191 193 if self.attr_execute_re.match(e):
192 194 attr += "x"
193 195 self.files[name] = (node, attr)
194 196 m = self.dir_re.match(e)
195 197 if m:
196 198 self.dirs[m.group(1)] = True
197 199
198 200 def mtnisfile(self, name, rev):
199 201 # a non-file could be a directory or a deleted or renamed file
200 202 self.mtnloadmanifest(rev)
201 203 return name in self.files
202 204
203 205 def mtnisdir(self, name, rev):
204 206 self.mtnloadmanifest(rev)
205 207 return name in self.dirs
206 208
207 209 def mtngetcerts(self, rev):
208 210 certs = {"author":"<missing>", "date":"<missing>",
209 211 "changelog":"<missing>", "branch":"<missing>"}
210 212 certlist = self.mtnrun("certs", rev)
211 213 # mtn < 0.45:
212 214 # key "test@selenic.com"
213 215 # mtn >= 0.45:
214 216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
215 217 certlist = re.split('\n\n key ["\[]', certlist)
216 218 for e in certlist:
217 219 m = self.cert_re.match(e)
218 220 if m:
219 221 name, value = m.groups()
220 222 value = value.replace(r'\"', '"')
221 223 value = value.replace(r'\\', '\\')
222 224 certs[name] = value
223 225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
224 226 # and all times are stored in UTC
225 227 certs["date"] = certs["date"].split('.')[0] + " UTC"
226 228 return certs
227 229
228 230 # implement the converter_source interface:
229 231
230 232 def getheads(self):
231 233 if not self.revs:
232 234 return self.mtnrun("leaves").splitlines()
233 235 else:
234 236 return self.revs
235 237
236 238 def getchanges(self, rev, full):
237 239 if full:
238 240 raise error.Abort(_("convert from monotone does not support "
239 241 "--full"))
240 242 revision = self.mtnrun("get_revision", rev).split("\n\n")
241 243 files = {}
242 244 ignoremove = {}
243 245 renameddirs = []
244 246 copies = {}
245 247 for e in revision:
246 248 m = self.add_file_re.match(e)
247 249 if m:
248 250 files[m.group(1)] = rev
249 251 ignoremove[m.group(1)] = rev
250 252 m = self.patch_re.match(e)
251 253 if m:
252 254 files[m.group(1)] = rev
253 255 # Delete/rename is handled later when the convert engine
254 256 # discovers an IOError exception from getfile,
255 257 # but only if we add the "from" file to the list of changes.
256 258 m = self.delete_re.match(e)
257 259 if m:
258 260 files[m.group(1)] = rev
259 261 m = self.rename_re.match(e)
260 262 if m:
261 263 toname = m.group(2)
262 264 fromname = m.group(1)
263 265 if self.mtnisfile(toname, rev):
264 266 ignoremove[toname] = 1
265 267 copies[toname] = fromname
266 268 files[toname] = rev
267 269 files[fromname] = rev
268 270 elif self.mtnisdir(toname, rev):
269 271 renameddirs.append((fromname, toname))
270 272
271 273 # Directory renames can be handled only once we have recorded
272 274 # all new files
273 275 for fromdir, todir in renameddirs:
274 276 renamed = {}
275 277 for tofile in self.files:
276 278 if tofile in ignoremove:
277 279 continue
278 280 if tofile.startswith(todir + '/'):
279 281 renamed[tofile] = fromdir + tofile[len(todir):]
280 282 # Avoid chained moves like:
281 283 # d1(/a) => d3/d1(/a)
282 284 # d2 => d3
283 285 ignoremove[tofile] = 1
284 286 for tofile, fromfile in renamed.items():
285 287 self.ui.debug (_("copying file in renamed directory "
286 288 "from '%s' to '%s'")
287 289 % (fromfile, tofile), '\n')
288 290 files[tofile] = rev
289 291 copies[tofile] = fromfile
290 292 for fromfile in renamed.values():
291 293 files[fromfile] = rev
292 294
293 295 return (files.items(), copies, set())
294 296
295 297 def getfile(self, name, rev):
296 298 if not self.mtnisfile(name, rev):
297 299 return None, None
298 300 try:
299 301 data = self.mtnrun("get_file_of", name, r=rev)
300 302 except Exception:
301 303 return None, None
302 304 self.mtnloadmanifest(rev)
303 305 node, attr = self.files.get(name, (None, ""))
304 306 return data, attr
305 307
306 308 def getcommit(self, rev):
307 309 extra = {}
308 310 certs = self.mtngetcerts(rev)
309 311 if certs.get('suspend') == certs["branch"]:
310 312 extra['close'] = 1
311 313 return common.commit(
312 314 author=certs["author"],
313 315 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
314 316 desc=certs["changelog"],
315 317 rev=rev,
316 318 parents=self.mtnrun("parents", rev).splitlines(),
317 319 branch=certs["branch"],
318 320 extra=extra)
319 321
320 322 def gettags(self):
321 323 tags = {}
322 324 for e in self.mtnrun("tags").split("\n\n"):
323 325 m = self.tag_re.match(e)
324 326 if m:
325 327 tags[m.group(1)] = m.group(2)
326 328 return tags
327 329
328 330 def getchangedfiles(self, rev, i):
329 331 # This function is only needed to support --filemap
330 332 # ... and we don't support that
331 333 raise NotImplementedError
332 334
333 335 def before(self):
334 336 # Check if we have a new enough version to use automate stdio
335 337 version = 0.0
336 338 try:
337 339 versionstr = self.mtnrunsingle("interface_version")
338 340 version = float(versionstr)
339 341 except Exception:
340 342 raise error.Abort(_("unable to determine mtn automate interface "
341 343 "version"))
342 344
343 345 if version >= 12.0:
344 346 self.automatestdio = True
345 347 self.ui.debug("mtn automate version %s - using automate stdio\n" %
346 348 version)
347 349
348 350 # launch the long-running automate stdio process
349 351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
350 352 '-d', self.path)
351 353 # read the headers
352 354 read = self.mtnreadfp.readline()
353 355 if read != 'format-version: 2\n':
354 356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
355 357 % read)
356 358 while read != '\n':
357 359 read = self.mtnreadfp.readline()
358 360 if not read:
359 361 raise error.Abort(_("failed to reach end of mtn automate "
360 362 "stdio headers"))
361 363 else:
362 364 self.ui.debug("mtn automate version %s - not using automate stdio "
363 365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
364 366
365 367 def after(self):
366 368 if self.automatestdio:
367 369 self.mtnwritefp.close()
368 370 self.mtnwritefp = None
369 371 self.mtnreadfp.close()
370 372 self.mtnreadfp = None
371 373
General Comments 0
You need to be logged in to leave comments. Login now