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