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