##// END OF EJS Templates
cvsps: wrap cmp methods (deprecated) in functools.cmp_to_key...
Augie Fackler -
r37903:fe148d75 default
parent child Browse files
Show More
@@ -1,958 +1,959 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 import functools
9 10 import os
10 11 import re
11 12
12 13 from mercurial.i18n import _
13 14 from mercurial import (
14 15 encoding,
15 16 error,
16 17 hook,
17 18 pycompat,
18 19 util,
19 20 )
20 21 from mercurial.utils import (
21 22 dateutil,
22 23 procutil,
23 24 stringutil,
24 25 )
25 26
26 27 pickle = util.pickle
27 28
28 29 class logentry(object):
29 30 '''Class logentry has the following attributes:
30 31 .author - author name as CVS knows it
31 32 .branch - name of branch this revision is on
32 33 .branches - revision tuple of branches starting at this revision
33 34 .comment - commit message
34 35 .commitid - CVS commitid or None
35 36 .date - the commit date as a (time, tz) tuple
36 37 .dead - true if file revision is dead
37 38 .file - Name of file
38 39 .lines - a tuple (+lines, -lines) or None
39 40 .parent - Previous revision of this entry
40 41 .rcs - name of file as returned from CVS
41 42 .revision - revision number as tuple
42 43 .tags - list of tags on the file
43 44 .synthetic - is this a synthetic "file ... added on ..." revision?
44 45 .mergepoint - the branch that has been merged from (if present in
45 46 rlog output) or None
46 47 .branchpoints - the branches that start at the current entry or empty
47 48 '''
48 49 def __init__(self, **entries):
49 50 self.synthetic = False
50 51 self.__dict__.update(entries)
51 52
52 53 def __repr__(self):
53 54 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
54 55 return "%s(%s)"%(type(self).__name__, ", ".join(items))
55 56
56 57 class logerror(Exception):
57 58 pass
58 59
59 60 def getrepopath(cvspath):
60 61 """Return the repository path from a CVS path.
61 62
62 63 >>> getrepopath(b'/foo/bar')
63 64 '/foo/bar'
64 65 >>> getrepopath(b'c:/foo/bar')
65 66 '/foo/bar'
66 67 >>> getrepopath(b':pserver:10/foo/bar')
67 68 '/foo/bar'
68 69 >>> getrepopath(b':pserver:10c:/foo/bar')
69 70 '/foo/bar'
70 71 >>> getrepopath(b':pserver:/foo/bar')
71 72 '/foo/bar'
72 73 >>> getrepopath(b':pserver:c:/foo/bar')
73 74 '/foo/bar'
74 75 >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar')
75 76 '/foo/bar'
76 77 >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar')
77 78 '/foo/bar'
78 79 >>> getrepopath(b'user@server/path/to/repository')
79 80 '/path/to/repository'
80 81 """
81 82 # According to CVS manual, CVS paths are expressed like:
82 83 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
83 84 #
84 85 # CVSpath is splitted into parts and then position of the first occurrence
85 86 # of the '/' char after the '@' is located. The solution is the rest of the
86 87 # string after that '/' sign including it
87 88
88 89 parts = cvspath.split(':')
89 90 atposition = parts[-1].find('@')
90 91 start = 0
91 92
92 93 if atposition != -1:
93 94 start = atposition
94 95
95 96 repopath = parts[-1][parts[-1].find('/', start):]
96 97 return repopath
97 98
98 99 def createlog(ui, directory=None, root="", rlog=True, cache=None):
99 100 '''Collect the CVS rlog'''
100 101
101 102 # Because we store many duplicate commit log messages, reusing strings
102 103 # saves a lot of memory and pickle storage space.
103 104 _scache = {}
104 105 def scache(s):
105 106 "return a shared version of a string"
106 107 return _scache.setdefault(s, s)
107 108
108 109 ui.status(_('collecting CVS rlog\n'))
109 110
110 111 log = [] # list of logentry objects containing the CVS state
111 112
112 113 # patterns to match in CVS (r)log output, by state of use
113 114 re_00 = re.compile(b'RCS file: (.+)$')
114 115 re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$')
115 116 re_02 = re.compile(b'cvs (r?log|server): (.+)\n$')
116 117 re_03 = re.compile(b"(Cannot access.+CVSROOT)|"
117 118 b"(can't create temporary directory.+)$")
118 119 re_10 = re.compile(b'Working file: (.+)$')
119 120 re_20 = re.compile(b'symbolic names:')
120 121 re_30 = re.compile(b'\t(.+): ([\\d.]+)$')
121 122 re_31 = re.compile(b'----------------------------$')
122 123 re_32 = re.compile(b'======================================='
123 124 b'======================================$')
124 125 re_50 = re.compile(b'revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
125 126 re_60 = re.compile(br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
126 127 br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
127 128 br'(\s+commitid:\s+([^;]+);)?'
128 129 br'(.*mergepoint:\s+([^;]+);)?')
129 130 re_70 = re.compile(b'branches: (.+);$')
130 131
131 132 file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch')
132 133
133 134 prefix = '' # leading path to strip of what we get from CVS
134 135
135 136 if directory is None:
136 137 # Current working directory
137 138
138 139 # Get the real directory in the repository
139 140 try:
140 141 prefix = open(os.path.join('CVS','Repository'), 'rb').read().strip()
141 142 directory = prefix
142 143 if prefix == ".":
143 144 prefix = ""
144 145 except IOError:
145 146 raise logerror(_('not a CVS sandbox'))
146 147
147 148 if prefix and not prefix.endswith(pycompat.ossep):
148 149 prefix += pycompat.ossep
149 150
150 151 # Use the Root file in the sandbox, if it exists
151 152 try:
152 153 root = open(os.path.join('CVS','Root'), 'rb').read().strip()
153 154 except IOError:
154 155 pass
155 156
156 157 if not root:
157 158 root = encoding.environ.get('CVSROOT', '')
158 159
159 160 # read log cache if one exists
160 161 oldlog = []
161 162 date = None
162 163
163 164 if cache:
164 165 cachedir = os.path.expanduser('~/.hg.cvsps')
165 166 if not os.path.exists(cachedir):
166 167 os.mkdir(cachedir)
167 168
168 169 # The cvsps cache pickle needs a uniquified name, based on the
169 170 # repository location. The address may have all sort of nasties
170 171 # in it, slashes, colons and such. So here we take just the
171 172 # alphanumeric characters, concatenated in a way that does not
172 173 # mix up the various components, so that
173 174 # :pserver:user@server:/path
174 175 # and
175 176 # /pserver/user/server/path
176 177 # are mapped to different cache file names.
177 178 cachefile = root.split(":") + [directory, "cache"]
178 179 cachefile = ['-'.join(re.findall(br'\w+', s)) for s in cachefile if s]
179 180 cachefile = os.path.join(cachedir,
180 181 '.'.join([s for s in cachefile if s]))
181 182
182 183 if cache == 'update':
183 184 try:
184 185 ui.note(_('reading cvs log cache %s\n') % cachefile)
185 186 oldlog = pickle.load(open(cachefile, 'rb'))
186 187 for e in oldlog:
187 188 if not (util.safehasattr(e, 'branchpoints') and
188 189 util.safehasattr(e, 'commitid') and
189 190 util.safehasattr(e, 'mergepoint')):
190 191 ui.status(_('ignoring old cache\n'))
191 192 oldlog = []
192 193 break
193 194
194 195 ui.note(_('cache has %d log entries\n') % len(oldlog))
195 196 except Exception as e:
196 197 ui.note(_('error reading cache: %r\n') % e)
197 198
198 199 if oldlog:
199 200 date = oldlog[-1].date # last commit date as a (time,tz) tuple
200 201 date = dateutil.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
201 202
202 203 # build the CVS commandline
203 204 cmd = ['cvs', '-q']
204 205 if root:
205 206 cmd.append('-d%s' % root)
206 207 p = util.normpath(getrepopath(root))
207 208 if not p.endswith('/'):
208 209 p += '/'
209 210 if prefix:
210 211 # looks like normpath replaces "" by "."
211 212 prefix = p + util.normpath(prefix)
212 213 else:
213 214 prefix = p
214 215 cmd.append(['log', 'rlog'][rlog])
215 216 if date:
216 217 # no space between option and date string
217 218 cmd.append('-d>%s' % date)
218 219 cmd.append(directory)
219 220
220 221 # state machine begins here
221 222 tags = {} # dictionary of revisions on current file with their tags
222 223 branchmap = {} # mapping between branch names and revision numbers
223 224 rcsmap = {}
224 225 state = 0
225 226 store = False # set when a new record can be appended
226 227
227 228 cmd = [procutil.shellquote(arg) for arg in cmd]
228 229 ui.note(_("running %s\n") % (' '.join(cmd)))
229 230 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
230 231
231 232 pfp = procutil.popen(' '.join(cmd), 'rb')
232 233 peek = util.fromnativeeol(pfp.readline())
233 234 while True:
234 235 line = peek
235 236 if line == '':
236 237 break
237 238 peek = util.fromnativeeol(pfp.readline())
238 239 if line.endswith('\n'):
239 240 line = line[:-1]
240 241 #ui.debug('state=%d line=%r\n' % (state, line))
241 242
242 243 if state == 0:
243 244 # initial state, consume input until we see 'RCS file'
244 245 match = re_00.match(line)
245 246 if match:
246 247 rcs = match.group(1)
247 248 tags = {}
248 249 if rlog:
249 250 filename = util.normpath(rcs[:-2])
250 251 if filename.startswith(prefix):
251 252 filename = filename[len(prefix):]
252 253 if filename.startswith('/'):
253 254 filename = filename[1:]
254 255 if filename.startswith('Attic/'):
255 256 filename = filename[6:]
256 257 else:
257 258 filename = filename.replace('/Attic/', '/')
258 259 state = 2
259 260 continue
260 261 state = 1
261 262 continue
262 263 match = re_01.match(line)
263 264 if match:
264 265 raise logerror(match.group(1))
265 266 match = re_02.match(line)
266 267 if match:
267 268 raise logerror(match.group(2))
268 269 if re_03.match(line):
269 270 raise logerror(line)
270 271
271 272 elif state == 1:
272 273 # expect 'Working file' (only when using log instead of rlog)
273 274 match = re_10.match(line)
274 275 assert match, _('RCS file must be followed by working file')
275 276 filename = util.normpath(match.group(1))
276 277 state = 2
277 278
278 279 elif state == 2:
279 280 # expect 'symbolic names'
280 281 if re_20.match(line):
281 282 branchmap = {}
282 283 state = 3
283 284
284 285 elif state == 3:
285 286 # read the symbolic names and store as tags
286 287 match = re_30.match(line)
287 288 if match:
288 289 rev = [int(x) for x in match.group(2).split('.')]
289 290
290 291 # Convert magic branch number to an odd-numbered one
291 292 revn = len(rev)
292 293 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
293 294 rev = rev[:-2] + rev[-1:]
294 295 rev = tuple(rev)
295 296
296 297 if rev not in tags:
297 298 tags[rev] = []
298 299 tags[rev].append(match.group(1))
299 300 branchmap[match.group(1)] = match.group(2)
300 301
301 302 elif re_31.match(line):
302 303 state = 5
303 304 elif re_32.match(line):
304 305 state = 0
305 306
306 307 elif state == 4:
307 308 # expecting '------' separator before first revision
308 309 if re_31.match(line):
309 310 state = 5
310 311 else:
311 312 assert not re_32.match(line), _('must have at least '
312 313 'some revisions')
313 314
314 315 elif state == 5:
315 316 # expecting revision number and possibly (ignored) lock indication
316 317 # we create the logentry here from values stored in states 0 to 4,
317 318 # as this state is re-entered for subsequent revisions of a file.
318 319 match = re_50.match(line)
319 320 assert match, _('expected revision number')
320 321 e = logentry(rcs=scache(rcs),
321 322 file=scache(filename),
322 323 revision=tuple([int(x) for x in
323 324 match.group(1).split('.')]),
324 325 branches=[],
325 326 parent=None,
326 327 commitid=None,
327 328 mergepoint=None,
328 329 branchpoints=set())
329 330
330 331 state = 6
331 332
332 333 elif state == 6:
333 334 # expecting date, author, state, lines changed
334 335 match = re_60.match(line)
335 336 assert match, _('revision must be followed by date line')
336 337 d = match.group(1)
337 338 if d[2] == '/':
338 339 # Y2K
339 340 d = '19' + d
340 341
341 342 if len(d.split()) != 3:
342 343 # cvs log dates always in GMT
343 344 d = d + ' UTC'
344 345 e.date = dateutil.parsedate(d, ['%y/%m/%d %H:%M:%S',
345 346 '%Y/%m/%d %H:%M:%S',
346 347 '%Y-%m-%d %H:%M:%S'])
347 348 e.author = scache(match.group(2))
348 349 e.dead = match.group(3).lower() == 'dead'
349 350
350 351 if match.group(5):
351 352 if match.group(6):
352 353 e.lines = (int(match.group(5)), int(match.group(6)))
353 354 else:
354 355 e.lines = (int(match.group(5)), 0)
355 356 elif match.group(6):
356 357 e.lines = (0, int(match.group(6)))
357 358 else:
358 359 e.lines = None
359 360
360 361 if match.group(7): # cvs 1.12 commitid
361 362 e.commitid = match.group(8)
362 363
363 364 if match.group(9): # cvsnt mergepoint
364 365 myrev = match.group(10).split('.')
365 366 if len(myrev) == 2: # head
366 367 e.mergepoint = 'HEAD'
367 368 else:
368 369 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
369 370 branches = [b for b in branchmap if branchmap[b] == myrev]
370 371 assert len(branches) == 1, ('unknown branch: %s'
371 372 % e.mergepoint)
372 373 e.mergepoint = branches[0]
373 374
374 375 e.comment = []
375 376 state = 7
376 377
377 378 elif state == 7:
378 379 # read the revision numbers of branches that start at this revision
379 380 # or store the commit log message otherwise
380 381 m = re_70.match(line)
381 382 if m:
382 383 e.branches = [tuple([int(y) for y in x.strip().split('.')])
383 384 for x in m.group(1).split(';')]
384 385 state = 8
385 386 elif re_31.match(line) and re_50.match(peek):
386 387 state = 5
387 388 store = True
388 389 elif re_32.match(line):
389 390 state = 0
390 391 store = True
391 392 else:
392 393 e.comment.append(line)
393 394
394 395 elif state == 8:
395 396 # store commit log message
396 397 if re_31.match(line):
397 398 cpeek = peek
398 399 if cpeek.endswith('\n'):
399 400 cpeek = cpeek[:-1]
400 401 if re_50.match(cpeek):
401 402 state = 5
402 403 store = True
403 404 else:
404 405 e.comment.append(line)
405 406 elif re_32.match(line):
406 407 state = 0
407 408 store = True
408 409 else:
409 410 e.comment.append(line)
410 411
411 412 # When a file is added on a branch B1, CVS creates a synthetic
412 413 # dead trunk revision 1.1 so that the branch has a root.
413 414 # Likewise, if you merge such a file to a later branch B2 (one
414 415 # that already existed when the file was added on B1), CVS
415 416 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
416 417 # these revisions now, but mark them synthetic so
417 418 # createchangeset() can take care of them.
418 419 if (store and
419 420 e.dead and
420 421 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
421 422 len(e.comment) == 1 and
422 423 file_added_re.match(e.comment[0])):
423 424 ui.debug('found synthetic revision in %s: %r\n'
424 425 % (e.rcs, e.comment[0]))
425 426 e.synthetic = True
426 427
427 428 if store:
428 429 # clean up the results and save in the log.
429 430 store = False
430 431 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
431 432 e.comment = scache('\n'.join(e.comment))
432 433
433 434 revn = len(e.revision)
434 435 if revn > 3 and (revn % 2) == 0:
435 436 e.branch = tags.get(e.revision[:-1], [None])[0]
436 437 else:
437 438 e.branch = None
438 439
439 440 # find the branches starting from this revision
440 441 branchpoints = set()
441 442 for branch, revision in branchmap.iteritems():
442 443 revparts = tuple([int(i) for i in revision.split('.')])
443 444 if len(revparts) < 2: # bad tags
444 445 continue
445 446 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
446 447 # normal branch
447 448 if revparts[:-2] == e.revision:
448 449 branchpoints.add(branch)
449 450 elif revparts == (1, 1, 1): # vendor branch
450 451 if revparts in e.branches:
451 452 branchpoints.add(branch)
452 453 e.branchpoints = branchpoints
453 454
454 455 log.append(e)
455 456
456 457 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
457 458
458 459 if len(log) % 100 == 0:
459 460 ui.status(stringutil.ellipsis('%d %s' % (len(log), e.file), 80)
460 461 + '\n')
461 462
462 463 log.sort(key=lambda x: (x.rcs, x.revision))
463 464
464 465 # find parent revisions of individual files
465 466 versions = {}
466 467 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
467 468 rcs = e.rcs.replace('/Attic/', '/')
468 469 if rcs in rcsmap:
469 470 e.rcs = rcsmap[rcs]
470 471 branch = e.revision[:-1]
471 472 versions[(e.rcs, branch)] = e.revision
472 473
473 474 for e in log:
474 475 branch = e.revision[:-1]
475 476 p = versions.get((e.rcs, branch), None)
476 477 if p is None:
477 478 p = e.revision[:-2]
478 479 e.parent = p
479 480 versions[(e.rcs, branch)] = e.revision
480 481
481 482 # update the log cache
482 483 if cache:
483 484 if log:
484 485 # join up the old and new logs
485 486 log.sort(key=lambda x: x.date)
486 487
487 488 if oldlog and oldlog[-1].date >= log[0].date:
488 489 raise logerror(_('log cache overlaps with new log entries,'
489 490 ' re-run without cache.'))
490 491
491 492 log = oldlog + log
492 493
493 494 # write the new cachefile
494 495 ui.note(_('writing cvs log cache %s\n') % cachefile)
495 496 pickle.dump(log, open(cachefile, 'wb'))
496 497 else:
497 498 log = oldlog
498 499
499 500 ui.status(_('%d log entries\n') % len(log))
500 501
501 502 encodings = ui.configlist('convert', 'cvsps.logencoding')
502 503 if encodings:
503 504 def revstr(r):
504 505 # this is needed, because logentry.revision is a tuple of "int"
505 506 # (e.g. (1, 2) for "1.2")
506 507 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
507 508
508 509 for entry in log:
509 510 comment = entry.comment
510 511 for e in encodings:
511 512 try:
512 513 entry.comment = comment.decode(e).encode('utf-8')
513 514 if ui.debugflag:
514 515 ui.debug("transcoding by %s: %s of %s\n" %
515 516 (e, revstr(entry.revision), entry.file))
516 517 break
517 518 except UnicodeDecodeError:
518 519 pass # try next encoding
519 520 except LookupError as inst: # unknown encoding, maybe
520 521 raise error.Abort(inst,
521 522 hint=_('check convert.cvsps.logencoding'
522 523 ' configuration'))
523 524 else:
524 525 raise error.Abort(_("no encoding can transcode"
525 526 " CVS log message for %s of %s")
526 527 % (revstr(entry.revision), entry.file),
527 528 hint=_('check convert.cvsps.logencoding'
528 529 ' configuration'))
529 530
530 531 hook.hook(ui, None, "cvslog", True, log=log)
531 532
532 533 return log
533 534
534 535
535 536 class changeset(object):
536 537 '''Class changeset has the following attributes:
537 538 .id - integer identifying this changeset (list index)
538 539 .author - author name as CVS knows it
539 540 .branch - name of branch this changeset is on, or None
540 541 .comment - commit message
541 542 .commitid - CVS commitid or None
542 543 .date - the commit date as a (time,tz) tuple
543 544 .entries - list of logentry objects in this changeset
544 545 .parents - list of one or two parent changesets
545 546 .tags - list of tags on this changeset
546 547 .synthetic - from synthetic revision "file ... added on branch ..."
547 548 .mergepoint- the branch that has been merged from or None
548 549 .branchpoints- the branches that start at the current entry or empty
549 550 '''
550 551 def __init__(self, **entries):
551 552 self.id = None
552 553 self.synthetic = False
553 554 self.__dict__.update(entries)
554 555
555 556 def __repr__(self):
556 557 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
557 558 return "%s(%s)"%(type(self).__name__, ", ".join(items))
558 559
559 560 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
560 561 '''Convert log into changesets.'''
561 562
562 563 ui.status(_('creating changesets\n'))
563 564
564 565 # try to order commitids by date
565 566 mindate = {}
566 567 for e in log:
567 568 if e.commitid:
568 569 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
569 570
570 571 # Merge changesets
571 572 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
572 573 x.author, x.branch, x.date, x.branchpoints))
573 574
574 575 changesets = []
575 576 files = set()
576 577 c = None
577 578 for i, e in enumerate(log):
578 579
579 580 # Check if log entry belongs to the current changeset or not.
580 581
581 582 # Since CVS is file-centric, two different file revisions with
582 583 # different branchpoints should be treated as belonging to two
583 584 # different changesets (and the ordering is important and not
584 585 # honoured by cvsps at this point).
585 586 #
586 587 # Consider the following case:
587 588 # foo 1.1 branchpoints: [MYBRANCH]
588 589 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
589 590 #
590 591 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
591 592 # later version of foo may be in MYBRANCH2, so foo should be the
592 593 # first changeset and bar the next and MYBRANCH and MYBRANCH2
593 594 # should both start off of the bar changeset. No provisions are
594 595 # made to ensure that this is, in fact, what happens.
595 596 if not (c and e.branchpoints == c.branchpoints and
596 597 (# cvs commitids
597 598 (e.commitid is not None and e.commitid == c.commitid) or
598 599 (# no commitids, use fuzzy commit detection
599 600 (e.commitid is None or c.commitid is None) and
600 601 e.comment == c.comment and
601 602 e.author == c.author and
602 603 e.branch == c.branch and
603 604 ((c.date[0] + c.date[1]) <=
604 605 (e.date[0] + e.date[1]) <=
605 606 (c.date[0] + c.date[1]) + fuzz) and
606 607 e.file not in files))):
607 608 c = changeset(comment=e.comment, author=e.author,
608 609 branch=e.branch, date=e.date,
609 610 entries=[], mergepoint=e.mergepoint,
610 611 branchpoints=e.branchpoints, commitid=e.commitid)
611 612 changesets.append(c)
612 613
613 614 files = set()
614 615 if len(changesets) % 100 == 0:
615 616 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
616 617 ui.status(stringutil.ellipsis(t, 80) + '\n')
617 618
618 619 c.entries.append(e)
619 620 files.add(e.file)
620 621 c.date = e.date # changeset date is date of latest commit in it
621 622
622 623 # Mark synthetic changesets
623 624
624 625 for c in changesets:
625 626 # Synthetic revisions always get their own changeset, because
626 627 # the log message includes the filename. E.g. if you add file3
627 628 # and file4 on a branch, you get four log entries and three
628 629 # changesets:
629 630 # "File file3 was added on branch ..." (synthetic, 1 entry)
630 631 # "File file4 was added on branch ..." (synthetic, 1 entry)
631 632 # "Add file3 and file4 to fix ..." (real, 2 entries)
632 633 # Hence the check for 1 entry here.
633 634 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
634 635
635 636 # Sort files in each changeset
636 637
637 638 def entitycompare(l, r):
638 639 'Mimic cvsps sorting order'
639 640 l = l.file.split('/')
640 641 r = r.file.split('/')
641 642 nl = len(l)
642 643 nr = len(r)
643 644 n = min(nl, nr)
644 645 for i in range(n):
645 646 if i + 1 == nl and nl < nr:
646 647 return -1
647 648 elif i + 1 == nr and nl > nr:
648 649 return +1
649 650 elif l[i] < r[i]:
650 651 return -1
651 652 elif l[i] > r[i]:
652 653 return +1
653 654 return 0
654 655
655 656 for c in changesets:
656 c.entries.sort(entitycompare)
657 c.entries.sort(key=functools.cmp_to_key(entitycompare))
657 658
658 659 # Sort changesets by date
659 660
660 661 odd = set()
661 662 def cscmp(l, r):
662 663 d = sum(l.date) - sum(r.date)
663 664 if d:
664 665 return d
665 666
666 667 # detect vendor branches and initial commits on a branch
667 668 le = {}
668 669 for e in l.entries:
669 670 le[e.rcs] = e.revision
670 671 re = {}
671 672 for e in r.entries:
672 673 re[e.rcs] = e.revision
673 674
674 675 d = 0
675 676 for e in l.entries:
676 677 if re.get(e.rcs, None) == e.parent:
677 678 assert not d
678 679 d = 1
679 680 break
680 681
681 682 for e in r.entries:
682 683 if le.get(e.rcs, None) == e.parent:
683 684 if d:
684 685 odd.add((l, r))
685 686 d = -1
686 687 break
687 688 # By this point, the changesets are sufficiently compared that
688 689 # we don't really care about ordering. However, this leaves
689 690 # some race conditions in the tests, so we compare on the
690 691 # number of files modified, the files contained in each
691 692 # changeset, and the branchpoints in the change to ensure test
692 693 # output remains stable.
693 694
694 695 # recommended replacement for cmp from
695 696 # https://docs.python.org/3.0/whatsnew/3.0.html
696 697 c = lambda x, y: (x > y) - (x < y)
697 698 # Sort bigger changes first.
698 699 if not d:
699 700 d = c(len(l.entries), len(r.entries))
700 701 # Try sorting by filename in the change.
701 702 if not d:
702 703 d = c([e.file for e in l.entries], [e.file for e in r.entries])
703 704 # Try and put changes without a branch point before ones with
704 705 # a branch point.
705 706 if not d:
706 707 d = c(len(l.branchpoints), len(r.branchpoints))
707 708 return d
708 709
709 changesets.sort(cscmp)
710 changesets.sort(key=functools.cmp_to_key(cscmp))
710 711
711 712 # Collect tags
712 713
713 714 globaltags = {}
714 715 for c in changesets:
715 716 for e in c.entries:
716 717 for tag in e.tags:
717 718 # remember which is the latest changeset to have this tag
718 719 globaltags[tag] = c
719 720
720 721 for c in changesets:
721 722 tags = set()
722 723 for e in c.entries:
723 724 tags.update(e.tags)
724 725 # remember tags only if this is the latest changeset to have it
725 726 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
726 727
727 728 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
728 729 # by inserting dummy changesets with two parents, and handle
729 730 # {{mergefrombranch BRANCHNAME}} by setting two parents.
730 731
731 732 if mergeto is None:
732 733 mergeto = br'{{mergetobranch ([-\w]+)}}'
733 734 if mergeto:
734 735 mergeto = re.compile(mergeto)
735 736
736 737 if mergefrom is None:
737 738 mergefrom = br'{{mergefrombranch ([-\w]+)}}'
738 739 if mergefrom:
739 740 mergefrom = re.compile(mergefrom)
740 741
741 742 versions = {} # changeset index where we saw any particular file version
742 743 branches = {} # changeset index where we saw a branch
743 744 n = len(changesets)
744 745 i = 0
745 746 while i < n:
746 747 c = changesets[i]
747 748
748 749 for f in c.entries:
749 750 versions[(f.rcs, f.revision)] = i
750 751
751 752 p = None
752 753 if c.branch in branches:
753 754 p = branches[c.branch]
754 755 else:
755 756 # first changeset on a new branch
756 757 # the parent is a changeset with the branch in its
757 758 # branchpoints such that it is the latest possible
758 759 # commit without any intervening, unrelated commits.
759 760
760 761 for candidate in xrange(i):
761 762 if c.branch not in changesets[candidate].branchpoints:
762 763 if p is not None:
763 764 break
764 765 continue
765 766 p = candidate
766 767
767 768 c.parents = []
768 769 if p is not None:
769 770 p = changesets[p]
770 771
771 772 # Ensure no changeset has a synthetic changeset as a parent.
772 773 while p.synthetic:
773 774 assert len(p.parents) <= 1, \
774 775 _('synthetic changeset cannot have multiple parents')
775 776 if p.parents:
776 777 p = p.parents[0]
777 778 else:
778 779 p = None
779 780 break
780 781
781 782 if p is not None:
782 783 c.parents.append(p)
783 784
784 785 if c.mergepoint:
785 786 if c.mergepoint == 'HEAD':
786 787 c.mergepoint = None
787 788 c.parents.append(changesets[branches[c.mergepoint]])
788 789
789 790 if mergefrom:
790 791 m = mergefrom.search(c.comment)
791 792 if m:
792 793 m = m.group(1)
793 794 if m == 'HEAD':
794 795 m = None
795 796 try:
796 797 candidate = changesets[branches[m]]
797 798 except KeyError:
798 799 ui.warn(_("warning: CVS commit message references "
799 800 "non-existent branch %r:\n%s\n")
800 801 % (m, c.comment))
801 802 if m in branches and c.branch != m and not candidate.synthetic:
802 803 c.parents.append(candidate)
803 804
804 805 if mergeto:
805 806 m = mergeto.search(c.comment)
806 807 if m:
807 808 if m.groups():
808 809 m = m.group(1)
809 810 if m == 'HEAD':
810 811 m = None
811 812 else:
812 813 m = None # if no group found then merge to HEAD
813 814 if m in branches and c.branch != m:
814 815 # insert empty changeset for merge
815 816 cc = changeset(
816 817 author=c.author, branch=m, date=c.date,
817 818 comment='convert-repo: CVS merge from branch %s'
818 819 % c.branch,
819 820 entries=[], tags=[],
820 821 parents=[changesets[branches[m]], c])
821 822 changesets.insert(i + 1, cc)
822 823 branches[m] = i + 1
823 824
824 825 # adjust our loop counters now we have inserted a new entry
825 826 n += 1
826 827 i += 2
827 828 continue
828 829
829 830 branches[c.branch] = i
830 831 i += 1
831 832
832 833 # Drop synthetic changesets (safe now that we have ensured no other
833 834 # changesets can have them as parents).
834 835 i = 0
835 836 while i < len(changesets):
836 837 if changesets[i].synthetic:
837 838 del changesets[i]
838 839 else:
839 840 i += 1
840 841
841 842 # Number changesets
842 843
843 844 for i, c in enumerate(changesets):
844 845 c.id = i + 1
845 846
846 847 if odd:
847 848 for l, r in odd:
848 849 if l.id is not None and r.id is not None:
849 850 ui.warn(_('changeset %d is both before and after %d\n')
850 851 % (l.id, r.id))
851 852
852 853 ui.status(_('%d changeset entries\n') % len(changesets))
853 854
854 855 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
855 856
856 857 return changesets
857 858
858 859
859 860 def debugcvsps(ui, *args, **opts):
860 861 '''Read CVS rlog for current directory or named path in
861 862 repository, and convert the log to changesets based on matching
862 863 commit log entries and dates.
863 864 '''
864 865 opts = pycompat.byteskwargs(opts)
865 866 if opts["new_cache"]:
866 867 cache = "write"
867 868 elif opts["update_cache"]:
868 869 cache = "update"
869 870 else:
870 871 cache = None
871 872
872 873 revisions = opts["revisions"]
873 874
874 875 try:
875 876 if args:
876 877 log = []
877 878 for d in args:
878 879 log += createlog(ui, d, root=opts["root"], cache=cache)
879 880 else:
880 881 log = createlog(ui, root=opts["root"], cache=cache)
881 882 except logerror as e:
882 883 ui.write("%r\n"%e)
883 884 return
884 885
885 886 changesets = createchangeset(ui, log, opts["fuzz"])
886 887 del log
887 888
888 889 # Print changesets (optionally filtered)
889 890
890 891 off = len(revisions)
891 892 branches = {} # latest version number in each branch
892 893 ancestors = {} # parent branch
893 894 for cs in changesets:
894 895
895 896 if opts["ancestors"]:
896 897 if cs.branch not in branches and cs.parents and cs.parents[0].id:
897 898 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
898 899 cs.parents[0].id)
899 900 branches[cs.branch] = cs.id
900 901
901 902 # limit by branches
902 903 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
903 904 continue
904 905
905 906 if not off:
906 907 # Note: trailing spaces on several lines here are needed to have
907 908 # bug-for-bug compatibility with cvsps.
908 909 ui.write('---------------------\n')
909 910 ui.write(('PatchSet %d \n' % cs.id))
910 911 ui.write(('Date: %s\n' % dateutil.datestr(cs.date,
911 912 '%Y/%m/%d %H:%M:%S %1%2')))
912 913 ui.write(('Author: %s\n' % cs.author))
913 914 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
914 915 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
915 916 ','.join(cs.tags) or '(none)')))
916 917 if cs.branchpoints:
917 918 ui.write(('Branchpoints: %s \n') %
918 919 ', '.join(sorted(cs.branchpoints)))
919 920 if opts["parents"] and cs.parents:
920 921 if len(cs.parents) > 1:
921 922 ui.write(('Parents: %s\n' %
922 923 (','.join([(b"%d" % p.id) for p in cs.parents]))))
923 924 else:
924 925 ui.write(('Parent: %d\n' % cs.parents[0].id))
925 926
926 927 if opts["ancestors"]:
927 928 b = cs.branch
928 929 r = []
929 930 while b:
930 931 b, c = ancestors[b]
931 932 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
932 933 if r:
933 934 ui.write(('Ancestors: %s\n' % (','.join(r))))
934 935
935 936 ui.write(('Log:\n'))
936 937 ui.write('%s\n\n' % cs.comment)
937 938 ui.write(('Members: \n'))
938 939 for f in cs.entries:
939 940 fn = f.file
940 941 if fn.startswith(opts["prefix"]):
941 942 fn = fn[len(opts["prefix"]):]
942 943 ui.write('\t%s:%s->%s%s \n' % (
943 944 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
944 945 '.'.join([(b"%d" % x) for x in f.revision]),
945 946 ['', '(DEAD)'][f.dead]))
946 947 ui.write('\n')
947 948
948 949 # have we seen the start tag?
949 950 if revisions and off:
950 951 if revisions[0] == (b"%d" % cs.id) or \
951 952 revisions[0] in cs.tags:
952 953 off = False
953 954
954 955 # see if we reached the end tag
955 956 if len(revisions) > 1 and not off:
956 957 if revisions[1] == (b"%d" % cs.id) or \
957 958 revisions[1] in cs.tags:
958 959 break
General Comments 0
You need to be logged in to leave comments. Login now