comparison hgext/convert/cvsps.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents 3b8a4587a456
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
23 procutil, 23 procutil,
24 stringutil, 24 stringutil,
25 ) 25 )
26 26
27 pickle = util.pickle 27 pickle = util.pickle
28
28 29
29 class logentry(object): 30 class logentry(object):
30 '''Class logentry has the following attributes: 31 '''Class logentry has the following attributes:
31 .author - author name as CVS knows it 32 .author - author name as CVS knows it
32 .branch - name of branch this revision is on 33 .branch - name of branch this revision is on
44 .synthetic - is this a synthetic "file ... added on ..." revision? 45 .synthetic - is this a synthetic "file ... added on ..." revision?
45 .mergepoint - the branch that has been merged from (if present in 46 .mergepoint - the branch that has been merged from (if present in
46 rlog output) or None 47 rlog output) or None
47 .branchpoints - the branches that start at the current entry or empty 48 .branchpoints - the branches that start at the current entry or empty
48 ''' 49 '''
50
49 def __init__(self, **entries): 51 def __init__(self, **entries):
50 self.synthetic = False 52 self.synthetic = False
51 self.__dict__.update(entries) 53 self.__dict__.update(entries)
52 54
53 def __repr__(self): 55 def __repr__(self):
54 items = (r"%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__)) 56 items = (
55 return r"%s(%s)"%(type(self).__name__, r", ".join(items)) 57 r"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__)
58 )
59 return r"%s(%s)" % (type(self).__name__, r", ".join(items))
60
56 61
57 class logerror(Exception): 62 class logerror(Exception):
58 pass 63 pass
64
59 65
60 def getrepopath(cvspath): 66 def getrepopath(cvspath):
61 """Return the repository path from a CVS path. 67 """Return the repository path from a CVS path.
62 68
63 >>> getrepopath(b'/foo/bar') 69 >>> getrepopath(b'/foo/bar')
91 start = 0 97 start = 0
92 98
93 if atposition != -1: 99 if atposition != -1:
94 start = atposition 100 start = atposition
95 101
96 repopath = parts[-1][parts[-1].find('/', start):] 102 repopath = parts[-1][parts[-1].find('/', start) :]
97 return repopath 103 return repopath
104
98 105
99 def createlog(ui, directory=None, root="", rlog=True, cache=None): 106 def createlog(ui, directory=None, root="", rlog=True, cache=None):
100 '''Collect the CVS rlog''' 107 '''Collect the CVS rlog'''
101 108
102 # Because we store many duplicate commit log messages, reusing strings 109 # Because we store many duplicate commit log messages, reusing strings
103 # saves a lot of memory and pickle storage space. 110 # saves a lot of memory and pickle storage space.
104 _scache = {} 111 _scache = {}
112
105 def scache(s): 113 def scache(s):
106 "return a shared version of a string" 114 "return a shared version of a string"
107 return _scache.setdefault(s, s) 115 return _scache.setdefault(s, s)
108 116
109 ui.status(_('collecting CVS rlog\n')) 117 ui.status(_('collecting CVS rlog\n'))
110 118
111 log = [] # list of logentry objects containing the CVS state 119 log = [] # list of logentry objects containing the CVS state
112 120
113 # patterns to match in CVS (r)log output, by state of use 121 # patterns to match in CVS (r)log output, by state of use
114 re_00 = re.compile(b'RCS file: (.+)$') 122 re_00 = re.compile(b'RCS file: (.+)$')
115 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$') 123 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
116 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$') 124 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
117 re_03 = re.compile(b"(Cannot access.+CVSROOT)|" 125 re_03 = re.compile(
118 b"(can't create temporary directory.+)$") 126 b"(Cannot access.+CVSROOT)|" b"(can't create temporary directory.+)$"
127 )
119 re_10 = re.compile(b'Working file: (.+)$') 128 re_10 = re.compile(b'Working file: (.+)$')
120 re_20 = re.compile(b'symbolic names:') 129 re_20 = re.compile(b'symbolic names:')
121 re_30 = re.compile(b'\t(.+): ([\\d.]+)$') 130 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
122 re_31 = re.compile(b'----------------------------$') 131 re_31 = re.compile(b'----------------------------$')
123 re_32 = re.compile(b'=======================================' 132 re_32 = re.compile(
124 b'======================================$') 133 b'======================================='
134 b'======================================$'
135 )
125 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$') 136 re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$')
126 re_60 = re.compile(br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);' 137 re_60 = re.compile(
127 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?' 138 br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
128 br'(\s+commitid:\s+([^;]+);)?' 139 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
129 br'(.*mergepoint:\s+([^;]+);)?') 140 br'(\s+commitid:\s+([^;]+);)?'
141 br'(.*mergepoint:\s+([^;]+);)?'
142 )
130 re_70 = re.compile(b'branches: (.+);$') 143 re_70 = re.compile(b'branches: (.+);$')
131 144
132 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch') 145 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
133 146
134 prefix = '' # leading path to strip of what we get from CVS 147 prefix = '' # leading path to strip of what we get from CVS
135 148
136 if directory is None: 149 if directory is None:
137 # Current working directory 150 # Current working directory
138 151
139 # Get the real directory in the repository 152 # Get the real directory in the repository
149 if prefix and not prefix.endswith(pycompat.ossep): 162 if prefix and not prefix.endswith(pycompat.ossep):
150 prefix += pycompat.ossep 163 prefix += pycompat.ossep
151 164
152 # Use the Root file in the sandbox, if it exists 165 # Use the Root file in the sandbox, if it exists
153 try: 166 try:
154 root = open(os.path.join('CVS','Root'), 'rb').read().strip() 167 root = open(os.path.join('CVS', 'Root'), 'rb').read().strip()
155 except IOError: 168 except IOError:
156 pass 169 pass
157 170
158 if not root: 171 if not root:
159 root = encoding.environ.get('CVSROOT', '') 172 root = encoding.environ.get('CVSROOT', '')
176 # and 189 # and
177 # /pserver/user/server/path 190 # /pserver/user/server/path
178 # are mapped to different cache file names. 191 # are mapped to different cache file names.
179 cachefile = root.split(":") + [directory, "cache"] 192 cachefile = root.split(":") + [directory, "cache"]
180 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s] 193 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
181 cachefile = os.path.join(cachedir, 194 cachefile = os.path.join(
182 '.'.join([s for s in cachefile if s])) 195 cachedir, '.'.join([s for s in cachefile if s])
196 )
183 197
184 if cache == 'update': 198 if cache == 'update':
185 try: 199 try:
186 ui.note(_('reading cvs log cache %s\n') % cachefile) 200 ui.note(_('reading cvs log cache %s\n') % cachefile)
187 oldlog = pickle.load(open(cachefile, 'rb')) 201 oldlog = pickle.load(open(cachefile, 'rb'))
188 for e in oldlog: 202 for e in oldlog:
189 if not (util.safehasattr(e, 'branchpoints') and 203 if not (
190 util.safehasattr(e, 'commitid') and 204 util.safehasattr(e, 'branchpoints')
191 util.safehasattr(e, 'mergepoint')): 205 and util.safehasattr(e, 'commitid')
206 and util.safehasattr(e, 'mergepoint')
207 ):
192 ui.status(_('ignoring old cache\n')) 208 ui.status(_('ignoring old cache\n'))
193 oldlog = [] 209 oldlog = []
194 break 210 break
195 211
196 ui.note(_('cache has %d log entries\n') % len(oldlog)) 212 ui.note(_('cache has %d log entries\n') % len(oldlog))
197 except Exception as e: 213 except Exception as e:
198 ui.note(_('error reading cache: %r\n') % e) 214 ui.note(_('error reading cache: %r\n') % e)
199 215
200 if oldlog: 216 if oldlog:
201 date = oldlog[-1].date # last commit date as a (time,tz) tuple 217 date = oldlog[-1].date # last commit date as a (time,tz) tuple
202 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2') 218 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
203 219
204 # build the CVS commandline 220 # build the CVS commandline
205 cmd = ['cvs', '-q'] 221 cmd = ['cvs', '-q']
206 if root: 222 if root:
218 # no space between option and date string 234 # no space between option and date string
219 cmd.append('-d>%s' % date) 235 cmd.append('-d>%s' % date)
220 cmd.append(directory) 236 cmd.append(directory)
221 237
222 # state machine begins here 238 # state machine begins here
223 tags = {} # dictionary of revisions on current file with their tags 239 tags = {} # dictionary of revisions on current file with their tags
224 branchmap = {} # mapping between branch names and revision numbers 240 branchmap = {} # mapping between branch names and revision numbers
225 rcsmap = {} 241 rcsmap = {}
226 state = 0 242 state = 0
227 store = False # set when a new record can be appended 243 store = False # set when a new record can be appended
228 244
229 cmd = [procutil.shellquote(arg) for arg in cmd] 245 cmd = [procutil.shellquote(arg) for arg in cmd]
230 ui.note(_("running %s\n") % (' '.join(cmd))) 246 ui.note(_("running %s\n") % (' '.join(cmd)))
231 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root)) 247 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
232 248
237 if line == '': 253 if line == '':
238 break 254 break
239 peek = util.fromnativeeol(pfp.readline()) 255 peek = util.fromnativeeol(pfp.readline())
240 if line.endswith('\n'): 256 if line.endswith('\n'):
241 line = line[:-1] 257 line = line[:-1]
242 #ui.debug('state=%d line=%r\n' % (state, line)) 258 # ui.debug('state=%d line=%r\n' % (state, line))
243 259
244 if state == 0: 260 if state == 0:
245 # initial state, consume input until we see 'RCS file' 261 # initial state, consume input until we see 'RCS file'
246 match = re_00.match(line) 262 match = re_00.match(line)
247 if match: 263 if match:
248 rcs = match.group(1) 264 rcs = match.group(1)
249 tags = {} 265 tags = {}
250 if rlog: 266 if rlog:
251 filename = util.normpath(rcs[:-2]) 267 filename = util.normpath(rcs[:-2])
252 if filename.startswith(prefix): 268 if filename.startswith(prefix):
253 filename = filename[len(prefix):] 269 filename = filename[len(prefix) :]
254 if filename.startswith('/'): 270 if filename.startswith('/'):
255 filename = filename[1:] 271 filename = filename[1:]
256 if filename.startswith('Attic/'): 272 if filename.startswith('Attic/'):
257 filename = filename[6:] 273 filename = filename[6:]
258 else: 274 else:
308 elif state == 4: 324 elif state == 4:
309 # expecting '------' separator before first revision 325 # expecting '------' separator before first revision
310 if re_31.match(line): 326 if re_31.match(line):
311 state = 5 327 state = 5
312 else: 328 else:
313 assert not re_32.match(line), _('must have at least ' 329 assert not re_32.match(line), _(
314 'some revisions') 330 'must have at least ' 'some revisions'
331 )
315 332
316 elif state == 5: 333 elif state == 5:
317 # expecting revision number and possibly (ignored) lock indication 334 # expecting revision number and possibly (ignored) lock indication
318 # we create the logentry here from values stored in states 0 to 4, 335 # we create the logentry here from values stored in states 0 to 4,
319 # as this state is re-entered for subsequent revisions of a file. 336 # as this state is re-entered for subsequent revisions of a file.
320 match = re_50.match(line) 337 match = re_50.match(line)
321 assert match, _('expected revision number') 338 assert match, _('expected revision number')
322 e = logentry(rcs=scache(rcs), 339 e = logentry(
323 file=scache(filename), 340 rcs=scache(rcs),
324 revision=tuple([int(x) for x in 341 file=scache(filename),
325 match.group(1).split('.')]), 342 revision=tuple([int(x) for x in match.group(1).split('.')]),
326 branches=[], 343 branches=[],
327 parent=None, 344 parent=None,
328 commitid=None, 345 commitid=None,
329 mergepoint=None, 346 mergepoint=None,
330 branchpoints=set()) 347 branchpoints=set(),
348 )
331 349
332 state = 6 350 state = 6
333 351
334 elif state == 6: 352 elif state == 6:
335 # expecting date, author, state, lines changed 353 # expecting date, author, state, lines changed
341 d = '19' + d 359 d = '19' + d
342 360
343 if len(d.split()) != 3: 361 if len(d.split()) != 3:
344 # cvs log dates always in GMT 362 # cvs log dates always in GMT
345 d = d + ' UTC' 363 d = d + ' UTC'
346 e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S', 364 e.date = dateutil.parsedate(
347 '%Y/%m/%d %H:%M:%S', 365 d,
348 '%Y-%m-%d %H:%M:%S']) 366 ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'],
367 )
349 e.author = scache(match.group(2)) 368 e.author = scache(match.group(2))
350 e.dead = match.group(3).lower() == 'dead' 369 e.dead = match.group(3).lower() == 'dead'
351 370
352 if match.group(5): 371 if match.group(5):
353 if match.group(6): 372 if match.group(6):
357 elif match.group(6): 376 elif match.group(6):
358 e.lines = (0, int(match.group(6))) 377 e.lines = (0, int(match.group(6)))
359 else: 378 else:
360 e.lines = None 379 e.lines = None
361 380
362 if match.group(7): # cvs 1.12 commitid 381 if match.group(7): # cvs 1.12 commitid
363 e.commitid = match.group(8) 382 e.commitid = match.group(8)
364 383
365 if match.group(9): # cvsnt mergepoint 384 if match.group(9): # cvsnt mergepoint
366 myrev = match.group(10).split('.') 385 myrev = match.group(10).split('.')
367 if len(myrev) == 2: # head 386 if len(myrev) == 2: # head
368 e.mergepoint = 'HEAD' 387 e.mergepoint = 'HEAD'
369 else: 388 else:
370 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]]) 389 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
371 branches = [b for b in branchmap if branchmap[b] == myrev] 390 branches = [b for b in branchmap if branchmap[b] == myrev]
372 assert len(branches) == 1, ('unknown branch: %s' 391 assert len(branches) == 1, (
373 % e.mergepoint) 392 'unknown branch: %s' % e.mergepoint
393 )
374 e.mergepoint = branches[0] 394 e.mergepoint = branches[0]
375 395
376 e.comment = [] 396 e.comment = []
377 state = 7 397 state = 7
378 398
379 elif state == 7: 399 elif state == 7:
380 # read the revision numbers of branches that start at this revision 400 # read the revision numbers of branches that start at this revision
381 # or store the commit log message otherwise 401 # or store the commit log message otherwise
382 m = re_70.match(line) 402 m = re_70.match(line)
383 if m: 403 if m:
384 e.branches = [tuple([int(y) for y in x.strip().split('.')]) 404 e.branches = [
385 for x in m.group(1).split(';')] 405 tuple([int(y) for y in x.strip().split('.')])
406 for x in m.group(1).split(';')
407 ]
386 state = 8 408 state = 8
387 elif re_31.match(line) and re_50.match(peek): 409 elif re_31.match(line) and re_50.match(peek):
388 state = 5 410 state = 5
389 store = True 411 store = True
390 elif re_32.match(line): 412 elif re_32.match(line):
415 # Likewise, if you merge such a file to a later branch B2 (one 437 # Likewise, if you merge such a file to a later branch B2 (one
416 # that already existed when the file was added on B1), CVS 438 # that already existed when the file was added on B1), CVS
417 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop 439 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
418 # these revisions now, but mark them synthetic so 440 # these revisions now, but mark them synthetic so
419 # createchangeset() can take care of them. 441 # createchangeset() can take care of them.
420 if (store and 442 if (
421 e.dead and 443 store
422 e.revision[-1] == 1 and # 1.1 or 1.1.x.1 444 and e.dead
423 len(e.comment) == 1 and 445 and e.revision[-1] == 1
424 file_added_re.match(e.comment[0])): 446 and len(e.comment) == 1 # 1.1 or 1.1.x.1
425 ui.debug('found synthetic revision in %s: %r\n' 447 and file_added_re.match(e.comment[0])
426 % (e.rcs, e.comment[0])) 448 ):
449 ui.debug(
450 'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0])
451 )
427 e.synthetic = True 452 e.synthetic = True
428 453
429 if store: 454 if store:
430 # clean up the results and save in the log. 455 # clean up the results and save in the log.
431 store = False 456 store = False
440 465
441 # find the branches starting from this revision 466 # find the branches starting from this revision
442 branchpoints = set() 467 branchpoints = set()
443 for branch, revision in branchmap.iteritems(): 468 for branch, revision in branchmap.iteritems():
444 revparts = tuple([int(i) for i in revision.split('.')]) 469 revparts = tuple([int(i) for i in revision.split('.')])
445 if len(revparts) < 2: # bad tags 470 if len(revparts) < 2: # bad tags
446 continue 471 continue
447 if revparts[-2] == 0 and revparts[-1] % 2 == 0: 472 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
448 # normal branch 473 # normal branch
449 if revparts[:-2] == e.revision: 474 if revparts[:-2] == e.revision:
450 branchpoints.add(branch) 475 branchpoints.add(branch)
451 elif revparts == (1, 1, 1): # vendor branch 476 elif revparts == (1, 1, 1): # vendor branch
452 if revparts in e.branches: 477 if revparts in e.branches:
453 branchpoints.add(branch) 478 branchpoints.add(branch)
454 e.branchpoints = branchpoints 479 e.branchpoints = branchpoints
455 480
456 log.append(e) 481 log.append(e)
457 482
458 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs 483 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
459 484
460 if len(log) % 100 == 0: 485 if len(log) % 100 == 0:
461 ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80) 486 ui.status(
462 + '\n') 487 stringutil.ellipsis('%d %s' % (len(log), e.file), 80) + '\n'
488 )
463 489
464 log.sort(key=lambda x: (x.rcs, x.revision)) 490 log.sort(key=lambda x: (x.rcs, x.revision))
465 491
466 # find parent revisions of individual files 492 # find parent revisions of individual files
467 versions = {} 493 versions = {}
485 if log: 511 if log:
486 # join up the old and new logs 512 # join up the old and new logs
487 log.sort(key=lambda x: x.date) 513 log.sort(key=lambda x: x.date)
488 514
489 if oldlog and oldlog[-1].date >= log[0].date: 515 if oldlog and oldlog[-1].date >= log[0].date:
490 raise logerror(_('log cache overlaps with new log entries,' 516 raise logerror(
491 ' re-run without cache.')) 517 _(
518 'log cache overlaps with new log entries,'
519 ' re-run without cache.'
520 )
521 )
492 522
493 log = oldlog + log 523 log = oldlog + log
494 524
495 # write the new cachefile 525 # write the new cachefile
496 ui.note(_('writing cvs log cache %s\n') % cachefile) 526 ui.note(_('writing cvs log cache %s\n') % cachefile)
500 530
501 ui.status(_('%d log entries\n') % len(log)) 531 ui.status(_('%d log entries\n') % len(log))
502 532
503 encodings = ui.configlist('convert', 'cvsps.logencoding') 533 encodings = ui.configlist('convert', 'cvsps.logencoding')
504 if encodings: 534 if encodings:
535
505 def revstr(r): 536 def revstr(r):
506 # this is needed, because logentry.revision is a tuple of "int" 537 # this is needed, because logentry.revision is a tuple of "int"
507 # (e.g. (1, 2) for "1.2") 538 # (e.g. (1, 2) for "1.2")
508 return '.'.join(pycompat.maplist(pycompat.bytestr, r)) 539 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
509 540
510 for entry in log: 541 for entry in log:
511 comment = entry.comment 542 comment = entry.comment
512 for e in encodings: 543 for e in encodings:
513 try: 544 try:
514 entry.comment = comment.decode( 545 entry.comment = comment.decode(pycompat.sysstr(e)).encode(
515 pycompat.sysstr(e)).encode('utf-8') 546 'utf-8'
547 )
516 if ui.debugflag: 548 if ui.debugflag:
517 ui.debug("transcoding by %s: %s of %s\n" % 549 ui.debug(
518 (e, revstr(entry.revision), entry.file)) 550 "transcoding by %s: %s of %s\n"
551 % (e, revstr(entry.revision), entry.file)
552 )
519 break 553 break
520 except UnicodeDecodeError: 554 except UnicodeDecodeError:
521 pass # try next encoding 555 pass # try next encoding
522 except LookupError as inst: # unknown encoding, maybe 556 except LookupError as inst: # unknown encoding, maybe
523 raise error.Abort(inst, 557 raise error.Abort(
524 hint=_('check convert.cvsps.logencoding' 558 inst,
525 ' configuration')) 559 hint=_(
560 'check convert.cvsps.logencoding' ' configuration'
561 ),
562 )
526 else: 563 else:
527 raise error.Abort(_("no encoding can transcode" 564 raise error.Abort(
528 " CVS log message for %s of %s") 565 _(
529 % (revstr(entry.revision), entry.file), 566 "no encoding can transcode"
530 hint=_('check convert.cvsps.logencoding' 567 " CVS log message for %s of %s"
531 ' configuration')) 568 )
569 % (revstr(entry.revision), entry.file),
570 hint=_('check convert.cvsps.logencoding' ' configuration'),
571 )
532 572
533 hook.hook(ui, None, "cvslog", True, log=log) 573 hook.hook(ui, None, "cvslog", True, log=log)
534 574
535 return log 575 return log
536 576
548 .tags - list of tags on this changeset 588 .tags - list of tags on this changeset
549 .synthetic - from synthetic revision "file ... added on branch ..." 589 .synthetic - from synthetic revision "file ... added on branch ..."
550 .mergepoint- the branch that has been merged from or None 590 .mergepoint- the branch that has been merged from or None
551 .branchpoints- the branches that start at the current entry or empty 591 .branchpoints- the branches that start at the current entry or empty
552 ''' 592 '''
593
553 def __init__(self, **entries): 594 def __init__(self, **entries):
554 self.id = None 595 self.id = None
555 self.synthetic = False 596 self.synthetic = False
556 self.__dict__.update(entries) 597 self.__dict__.update(entries)
557 598
558 def __repr__(self): 599 def __repr__(self):
559 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__)) 600 items = ("%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__))
560 return "%s(%s)"%(type(self).__name__, ", ".join(items)) 601 return "%s(%s)" % (type(self).__name__, ", ".join(items))
602
561 603
562 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None): 604 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
563 '''Convert log into changesets.''' 605 '''Convert log into changesets.'''
564 606
565 ui.status(_('creating changesets\n')) 607 ui.status(_('creating changesets\n'))
572 mindate[e.commitid] = e.date 614 mindate[e.commitid] = e.date
573 else: 615 else:
574 mindate[e.commitid] = min(e.date, mindate[e.commitid]) 616 mindate[e.commitid] = min(e.date, mindate[e.commitid])
575 617
576 # Merge changesets 618 # Merge changesets
577 log.sort(key=lambda x: (mindate.get(x.commitid, (-1, 0)), 619 log.sort(
578 x.commitid or '', x.comment, 620 key=lambda x: (
579 x.author, x.branch or '', x.date, x.branchpoints)) 621 mindate.get(x.commitid, (-1, 0)),
622 x.commitid or '',
623 x.comment,
624 x.author,
625 x.branch or '',
626 x.date,
627 x.branchpoints,
628 )
629 )
580 630
581 changesets = [] 631 changesets = []
582 files = set() 632 files = set()
583 c = None 633 c = None
584 for i, e in enumerate(log): 634 for i, e in enumerate(log):
597 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a 647 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
598 # later version of foo may be in MYBRANCH2, so foo should be the 648 # later version of foo may be in MYBRANCH2, so foo should be the
599 # first changeset and bar the next and MYBRANCH and MYBRANCH2 649 # first changeset and bar the next and MYBRANCH and MYBRANCH2
600 # should both start off of the bar changeset. No provisions are 650 # should both start off of the bar changeset. No provisions are
601 # made to ensure that this is, in fact, what happens. 651 # made to ensure that this is, in fact, what happens.
602 if not (c and e.branchpoints == c.branchpoints and 652 if not (
603 (# cvs commitids 653 c
604 (e.commitid is not None and e.commitid == c.commitid) or 654 and e.branchpoints == c.branchpoints
605 (# no commitids, use fuzzy commit detection 655 and ( # cvs commitids
606 (e.commitid is None or c.commitid is None) and 656 (e.commitid is not None and e.commitid == c.commitid)
607 e.comment == c.comment and 657 or ( # no commitids, use fuzzy commit detection
608 e.author == c.author and 658 (e.commitid is None or c.commitid is None)
609 e.branch == c.branch and 659 and e.comment == c.comment
610 ((c.date[0] + c.date[1]) <= 660 and e.author == c.author
611 (e.date[0] + e.date[1]) <= 661 and e.branch == c.branch
612 (c.date[0] + c.date[1]) + fuzz) and 662 and (
613 e.file not in files))): 663 (c.date[0] + c.date[1])
614 c = changeset(comment=e.comment, author=e.author, 664 <= (e.date[0] + e.date[1])
615 branch=e.branch, date=e.date, 665 <= (c.date[0] + c.date[1]) + fuzz
616 entries=[], mergepoint=e.mergepoint, 666 )
617 branchpoints=e.branchpoints, commitid=e.commitid) 667 and e.file not in files
668 )
669 )
670 ):
671 c = changeset(
672 comment=e.comment,
673 author=e.author,
674 branch=e.branch,
675 date=e.date,
676 entries=[],
677 mergepoint=e.mergepoint,
678 branchpoints=e.branchpoints,
679 commitid=e.commitid,
680 )
618 changesets.append(c) 681 changesets.append(c)
619 682
620 files = set() 683 files = set()
621 if len(changesets) % 100 == 0: 684 if len(changesets) % 100 == 0:
622 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1]) 685 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
623 ui.status(stringutil.ellipsis(t, 80) + '\n') 686 ui.status(stringutil.ellipsis(t, 80) + '\n')
624 687
625 c.entries.append(e) 688 c.entries.append(e)
626 files.add(e.file) 689 files.add(e.file)
627 c.date = e.date # changeset date is date of latest commit in it 690 c.date = e.date # changeset date is date of latest commit in it
628 691
629 # Mark synthetic changesets 692 # Mark synthetic changesets
630 693
631 for c in changesets: 694 for c in changesets:
632 # Synthetic revisions always get their own changeset, because 695 # Synthetic revisions always get their own changeset, because
663 c.entries.sort(key=functools.cmp_to_key(entitycompare)) 726 c.entries.sort(key=functools.cmp_to_key(entitycompare))
664 727
665 # Sort changesets by date 728 # Sort changesets by date
666 729
667 odd = set() 730 odd = set()
731
668 def cscmp(l, r): 732 def cscmp(l, r):
669 d = sum(l.date) - sum(r.date) 733 d = sum(l.date) - sum(r.date)
670 if d: 734 if d:
671 return d 735 return d
672 736
743 if mergefrom is None: 807 if mergefrom is None:
744 mergefrom = br'{{mergefrombranch ([-\w]+)}}' 808 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
745 if mergefrom: 809 if mergefrom:
746 mergefrom = re.compile(mergefrom) 810 mergefrom = re.compile(mergefrom)
747 811
748 versions = {} # changeset index where we saw any particular file version 812 versions = {} # changeset index where we saw any particular file version
749 branches = {} # changeset index where we saw a branch 813 branches = {} # changeset index where we saw a branch
750 n = len(changesets) 814 n = len(changesets)
751 i = 0 815 i = 0
752 while i < n: 816 while i < n:
753 c = changesets[i] 817 c = changesets[i]
754 818
775 if p is not None: 839 if p is not None:
776 p = changesets[p] 840 p = changesets[p]
777 841
778 # Ensure no changeset has a synthetic changeset as a parent. 842 # Ensure no changeset has a synthetic changeset as a parent.
779 while p.synthetic: 843 while p.synthetic:
780 assert len(p.parents) <= 1, ( 844 assert len(p.parents) <= 1, _(
781 _('synthetic changeset cannot have multiple parents')) 845 'synthetic changeset cannot have multiple parents'
846 )
782 if p.parents: 847 if p.parents:
783 p = p.parents[0] 848 p = p.parents[0]
784 else: 849 else:
785 p = None 850 p = None
786 break 851 break
800 if m == 'HEAD': 865 if m == 'HEAD':
801 m = None 866 m = None
802 try: 867 try:
803 candidate = changesets[branches[m]] 868 candidate = changesets[branches[m]]
804 except KeyError: 869 except KeyError:
805 ui.warn(_("warning: CVS commit message references " 870 ui.warn(
806 "non-existent branch %r:\n%s\n") 871 _(
807 % (pycompat.bytestr(m), c.comment)) 872 "warning: CVS commit message references "
873 "non-existent branch %r:\n%s\n"
874 )
875 % (pycompat.bytestr(m), c.comment)
876 )
808 if m in branches and c.branch != m and not candidate.synthetic: 877 if m in branches and c.branch != m and not candidate.synthetic:
809 c.parents.append(candidate) 878 c.parents.append(candidate)
810 879
811 if mergeto: 880 if mergeto:
812 m = mergeto.search(c.comment) 881 m = mergeto.search(c.comment)
814 if m.groups(): 883 if m.groups():
815 m = m.group(1) 884 m = m.group(1)
816 if m == 'HEAD': 885 if m == 'HEAD':
817 m = None 886 m = None
818 else: 887 else:
819 m = None # if no group found then merge to HEAD 888 m = None # if no group found then merge to HEAD
820 if m in branches and c.branch != m: 889 if m in branches and c.branch != m:
821 # insert empty changeset for merge 890 # insert empty changeset for merge
822 cc = changeset( 891 cc = changeset(
823 author=c.author, branch=m, date=c.date, 892 author=c.author,
893 branch=m,
894 date=c.date,
824 comment='convert-repo: CVS merge from branch %s' 895 comment='convert-repo: CVS merge from branch %s'
825 % c.branch, 896 % c.branch,
826 entries=[], tags=[], 897 entries=[],
827 parents=[changesets[branches[m]], c]) 898 tags=[],
899 parents=[changesets[branches[m]], c],
900 )
828 changesets.insert(i + 1, cc) 901 changesets.insert(i + 1, cc)
829 branches[m] = i + 1 902 branches[m] = i + 1
830 903
831 # adjust our loop counters now we have inserted a new entry 904 # adjust our loop counters now we have inserted a new entry
832 n += 1 905 n += 1
851 c.id = i + 1 924 c.id = i + 1
852 925
853 if odd: 926 if odd:
854 for l, r in odd: 927 for l, r in odd:
855 if l.id is not None and r.id is not None: 928 if l.id is not None and r.id is not None:
856 ui.warn(_('changeset %d is both before and after %d\n') 929 ui.warn(
857 % (l.id, r.id)) 930 _('changeset %d is both before and after %d\n')
931 % (l.id, r.id)
932 )
858 933
859 ui.status(_('%d changeset entries\n') % len(changesets)) 934 ui.status(_('%d changeset entries\n') % len(changesets))
860 935
861 hook.hook(ui, None, "cvschangesets", True, changesets=changesets) 936 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
862 937
884 for d in args: 959 for d in args:
885 log += createlog(ui, d, root=opts["root"], cache=cache) 960 log += createlog(ui, d, root=opts["root"], cache=cache)
886 else: 961 else:
887 log = createlog(ui, root=opts["root"], cache=cache) 962 log = createlog(ui, root=opts["root"], cache=cache)
888 except logerror as e: 963 except logerror as e:
889 ui.write("%r\n"%e) 964 ui.write("%r\n" % e)
890 return 965 return
891 966
892 changesets = createchangeset(ui, log, opts["fuzz"]) 967 changesets = createchangeset(ui, log, opts["fuzz"])
893 del log 968 del log
894 969
895 # Print changesets (optionally filtered) 970 # Print changesets (optionally filtered)
896 971
897 off = len(revisions) 972 off = len(revisions)
898 branches = {} # latest version number in each branch 973 branches = {} # latest version number in each branch
899 ancestors = {} # parent branch 974 ancestors = {} # parent branch
900 for cs in changesets: 975 for cs in changesets:
901 976
902 if opts["ancestors"]: 977 if opts["ancestors"]:
903 if cs.branch not in branches and cs.parents and cs.parents[0].id: 978 if cs.branch not in branches and cs.parents and cs.parents[0].id:
904 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch, 979 ancestors[cs.branch] = (
905 cs.parents[0].id) 980 changesets[cs.parents[0].id - 1].branch,
981 cs.parents[0].id,
982 )
906 branches[cs.branch] = cs.id 983 branches[cs.branch] = cs.id
907 984
908 # limit by branches 985 # limit by branches
909 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]: 986 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
910 continue 987 continue
912 if not off: 989 if not off:
913 # Note: trailing spaces on several lines here are needed to have 990 # Note: trailing spaces on several lines here are needed to have
914 # bug-for-bug compatibility with cvsps. 991 # bug-for-bug compatibility with cvsps.
915 ui.write('---------------------\n') 992 ui.write('---------------------\n')
916 ui.write(('PatchSet %d \n' % cs.id)) 993 ui.write(('PatchSet %d \n' % cs.id))
917 ui.write(('Date: %s\n' % dateutil.datestr(cs.date, 994 ui.write(
918 '%Y/%m/%d %H:%M:%S %1%2'))) 995 (
996 'Date: %s\n'
997 % dateutil.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2')
998 )
999 )
919 ui.write(('Author: %s\n' % cs.author)) 1000 ui.write(('Author: %s\n' % cs.author))
920 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD'))) 1001 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
921 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1], 1002 ui.write(
922 ','.join(cs.tags) or '(none)'))) 1003 (
1004 'Tag%s: %s \n'
1005 % (
1006 ['', 's'][len(cs.tags) > 1],
1007 ','.join(cs.tags) or '(none)',
1008 )
1009 )
1010 )
923 if cs.branchpoints: 1011 if cs.branchpoints:
924 ui.write(('Branchpoints: %s \n') % 1012 ui.write(
925 ', '.join(sorted(cs.branchpoints))) 1013 'Branchpoints: %s \n' % ', '.join(sorted(cs.branchpoints))
1014 )
926 if opts["parents"] and cs.parents: 1015 if opts["parents"] and cs.parents:
927 if len(cs.parents) > 1: 1016 if len(cs.parents) > 1:
928 ui.write(('Parents: %s\n' % 1017 ui.write(
929 (','.join([(b"%d" % p.id) for p in cs.parents])))) 1018 (
1019 'Parents: %s\n'
1020 % (','.join([(b"%d" % p.id) for p in cs.parents]))
1021 )
1022 )
930 else: 1023 else:
931 ui.write(('Parent: %d\n' % cs.parents[0].id)) 1024 ui.write(('Parent: %d\n' % cs.parents[0].id))
932 1025
933 if opts["ancestors"]: 1026 if opts["ancestors"]:
934 b = cs.branch 1027 b = cs.branch
937 b, c = ancestors[b] 1030 b, c = ancestors[b]
938 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b])) 1031 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
939 if r: 1032 if r:
940 ui.write(('Ancestors: %s\n' % (','.join(r)))) 1033 ui.write(('Ancestors: %s\n' % (','.join(r))))
941 1034
942 ui.write(('Log:\n')) 1035 ui.write('Log:\n')
943 ui.write('%s\n\n' % cs.comment) 1036 ui.write('%s\n\n' % cs.comment)
944 ui.write(('Members: \n')) 1037 ui.write('Members: \n')
945 for f in cs.entries: 1038 for f in cs.entries:
946 fn = f.file 1039 fn = f.file
947 if fn.startswith(opts["prefix"]): 1040 if fn.startswith(opts["prefix"]):
948 fn = fn[len(opts["prefix"]):] 1041 fn = fn[len(opts["prefix"]) :]
949 ui.write('\t%s:%s->%s%s \n' % ( 1042 ui.write(
1043 '\t%s:%s->%s%s \n'
1044 % (
950 fn, 1045 fn,
951 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL', 1046 '.'.join([b"%d" % x for x in f.parent]) or 'INITIAL',
952 '.'.join([(b"%d" % x) for x in f.revision]), 1047 '.'.join([(b"%d" % x) for x in f.revision]),
953 ['', '(DEAD)'][f.dead])) 1048 ['', '(DEAD)'][f.dead],
1049 )
1050 )
954 ui.write('\n') 1051 ui.write('\n')
955 1052
956 # have we seen the start tag? 1053 # have we seen the start tag?
957 if revisions and off: 1054 if revisions and off:
958 if (revisions[0] == (b"%d" % cs.id) or 1055 if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags:
959 revisions[0] in cs.tags):
960 off = False 1056 off = False
961 1057
962 # see if we reached the end tag 1058 # see if we reached the end tag
963 if len(revisions) > 1 and not off: 1059 if len(revisions) > 1 and not off:
964 if (revisions[1] == (b"%d" % cs.id) or 1060 if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags:
965 revisions[1] in cs.tags):
966 break 1061 break