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