##// END OF EJS Templates
convert: stabilize cvsps commitid sort order
Matt Mackall -
r18718:c8c3887a default
parent child Browse files
Show More
@@ -1,871 +1,877 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 'c:/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 'c:/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 'c:/foo/bar'
66 66 """
67 67 # According to CVS manual, CVS paths are expressed like:
68 68 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
69 69 #
70 70 # Unfortunately, Windows absolute paths start with a drive letter
71 71 # like 'c:' making it harder to parse. Here we assume that drive
72 72 # letters are only one character long and any CVS component before
73 73 # the repository path is at least 2 characters long, and use this
74 74 # to disambiguate.
75 75 parts = cvspath.split(':')
76 76 if len(parts) == 1:
77 77 return parts[0]
78 78 # Here there is an ambiguous case if we have a port number
79 79 # immediately followed by a Windows driver letter. We assume this
80 80 # never happens and decide it must be CVS path component,
81 81 # therefore ignoring it.
82 82 if len(parts[-2]) > 1:
83 83 return parts[-1].lstrip('0123456789')
84 84 return parts[-2] + ':' + parts[-1]
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 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 # try to order commitids by date
512 mindate = {}
513 for e in log:
514 if e.commitid:
515 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
516
511 517 # Merge changesets
512 log.sort(key=lambda x: (x.commitid, x.comment, x.author, x.branch, x.date,
513 x.branchpoints))
518 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
519 x.author, x.branch, x.date, x.branchpoints))
514 520
515 521 changesets = []
516 522 files = set()
517 523 c = None
518 524 for i, e in enumerate(log):
519 525
520 526 # Check if log entry belongs to the current changeset or not.
521 527
522 528 # Since CVS is file-centric, two different file revisions with
523 529 # different branchpoints should be treated as belonging to two
524 530 # different changesets (and the ordering is important and not
525 531 # honoured by cvsps at this point).
526 532 #
527 533 # Consider the following case:
528 534 # foo 1.1 branchpoints: [MYBRANCH]
529 535 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
530 536 #
531 537 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
532 538 # later version of foo may be in MYBRANCH2, so foo should be the
533 539 # first changeset and bar the next and MYBRANCH and MYBRANCH2
534 540 # should both start off of the bar changeset. No provisions are
535 541 # made to ensure that this is, in fact, what happens.
536 542 if not (c and e.branchpoints == c.branchpoints and
537 543 (# cvs commitids
538 544 (e.commitid is not None and e.commitid == c.commitid) or
539 545 (# no commitids, use fuzzy commit detection
540 546 (e.commitid is None or c.commitid is None) and
541 547 e.comment == c.comment and
542 548 e.author == c.author and
543 549 e.branch == c.branch and
544 550 ((c.date[0] + c.date[1]) <=
545 551 (e.date[0] + e.date[1]) <=
546 552 (c.date[0] + c.date[1]) + fuzz) and
547 553 e.file not in files))):
548 554 c = changeset(comment=e.comment, author=e.author,
549 555 branch=e.branch, date=e.date,
550 556 entries=[], mergepoint=e.mergepoint,
551 557 branchpoints=e.branchpoints, commitid=e.commitid)
552 558 changesets.append(c)
553 559
554 560 files = set()
555 561 if len(changesets) % 100 == 0:
556 562 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
557 563 ui.status(util.ellipsis(t, 80) + '\n')
558 564
559 565 c.entries.append(e)
560 566 files.add(e.file)
561 567 c.date = e.date # changeset date is date of latest commit in it
562 568
563 569 # Mark synthetic changesets
564 570
565 571 for c in changesets:
566 572 # Synthetic revisions always get their own changeset, because
567 573 # the log message includes the filename. E.g. if you add file3
568 574 # and file4 on a branch, you get four log entries and three
569 575 # changesets:
570 576 # "File file3 was added on branch ..." (synthetic, 1 entry)
571 577 # "File file4 was added on branch ..." (synthetic, 1 entry)
572 578 # "Add file3 and file4 to fix ..." (real, 2 entries)
573 579 # Hence the check for 1 entry here.
574 580 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
575 581
576 582 # Sort files in each changeset
577 583
578 584 def entitycompare(l, r):
579 585 'Mimic cvsps sorting order'
580 586 l = l.file.split('/')
581 587 r = r.file.split('/')
582 588 nl = len(l)
583 589 nr = len(r)
584 590 n = min(nl, nr)
585 591 for i in range(n):
586 592 if i + 1 == nl and nl < nr:
587 593 return -1
588 594 elif i + 1 == nr and nl > nr:
589 595 return +1
590 596 elif l[i] < r[i]:
591 597 return -1
592 598 elif l[i] > r[i]:
593 599 return +1
594 600 return 0
595 601
596 602 for c in changesets:
597 603 c.entries.sort(entitycompare)
598 604
599 605 # Sort changesets by date
600 606
601 607 def cscmp(l, r):
602 608 d = sum(l.date) - sum(r.date)
603 609 if d:
604 610 return d
605 611
606 612 # detect vendor branches and initial commits on a branch
607 613 le = {}
608 614 for e in l.entries:
609 615 le[e.rcs] = e.revision
610 616 re = {}
611 617 for e in r.entries:
612 618 re[e.rcs] = e.revision
613 619
614 620 d = 0
615 621 for e in l.entries:
616 622 if re.get(e.rcs, None) == e.parent:
617 623 assert not d
618 624 d = 1
619 625 break
620 626
621 627 for e in r.entries:
622 628 if le.get(e.rcs, None) == e.parent:
623 629 assert not d
624 630 d = -1
625 631 break
626 632
627 633 return d
628 634
629 635 changesets.sort(cscmp)
630 636
631 637 # Collect tags
632 638
633 639 globaltags = {}
634 640 for c in changesets:
635 641 for e in c.entries:
636 642 for tag in e.tags:
637 643 # remember which is the latest changeset to have this tag
638 644 globaltags[tag] = c
639 645
640 646 for c in changesets:
641 647 tags = set()
642 648 for e in c.entries:
643 649 tags.update(e.tags)
644 650 # remember tags only if this is the latest changeset to have it
645 651 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
646 652
647 653 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
648 654 # by inserting dummy changesets with two parents, and handle
649 655 # {{mergefrombranch BRANCHNAME}} by setting two parents.
650 656
651 657 if mergeto is None:
652 658 mergeto = r'{{mergetobranch ([-\w]+)}}'
653 659 if mergeto:
654 660 mergeto = re.compile(mergeto)
655 661
656 662 if mergefrom is None:
657 663 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
658 664 if mergefrom:
659 665 mergefrom = re.compile(mergefrom)
660 666
661 667 versions = {} # changeset index where we saw any particular file version
662 668 branches = {} # changeset index where we saw a branch
663 669 n = len(changesets)
664 670 i = 0
665 671 while i < n:
666 672 c = changesets[i]
667 673
668 674 for f in c.entries:
669 675 versions[(f.rcs, f.revision)] = i
670 676
671 677 p = None
672 678 if c.branch in branches:
673 679 p = branches[c.branch]
674 680 else:
675 681 # first changeset on a new branch
676 682 # the parent is a changeset with the branch in its
677 683 # branchpoints such that it is the latest possible
678 684 # commit without any intervening, unrelated commits.
679 685
680 686 for candidate in xrange(i):
681 687 if c.branch not in changesets[candidate].branchpoints:
682 688 if p is not None:
683 689 break
684 690 continue
685 691 p = candidate
686 692
687 693 c.parents = []
688 694 if p is not None:
689 695 p = changesets[p]
690 696
691 697 # Ensure no changeset has a synthetic changeset as a parent.
692 698 while p.synthetic:
693 699 assert len(p.parents) <= 1, \
694 700 _('synthetic changeset cannot have multiple parents')
695 701 if p.parents:
696 702 p = p.parents[0]
697 703 else:
698 704 p = None
699 705 break
700 706
701 707 if p is not None:
702 708 c.parents.append(p)
703 709
704 710 if c.mergepoint:
705 711 if c.mergepoint == 'HEAD':
706 712 c.mergepoint = None
707 713 c.parents.append(changesets[branches[c.mergepoint]])
708 714
709 715 if mergefrom:
710 716 m = mergefrom.search(c.comment)
711 717 if m:
712 718 m = m.group(1)
713 719 if m == 'HEAD':
714 720 m = None
715 721 try:
716 722 candidate = changesets[branches[m]]
717 723 except KeyError:
718 724 ui.warn(_("warning: CVS commit message references "
719 725 "non-existent branch %r:\n%s\n")
720 726 % (m, c.comment))
721 727 if m in branches and c.branch != m and not candidate.synthetic:
722 728 c.parents.append(candidate)
723 729
724 730 if mergeto:
725 731 m = mergeto.search(c.comment)
726 732 if m:
727 733 if m.groups():
728 734 m = m.group(1)
729 735 if m == 'HEAD':
730 736 m = None
731 737 else:
732 738 m = None # if no group found then merge to HEAD
733 739 if m in branches and c.branch != m:
734 740 # insert empty changeset for merge
735 741 cc = changeset(
736 742 author=c.author, branch=m, date=c.date,
737 743 comment='convert-repo: CVS merge from branch %s'
738 744 % c.branch,
739 745 entries=[], tags=[],
740 746 parents=[changesets[branches[m]], c])
741 747 changesets.insert(i + 1, cc)
742 748 branches[m] = i + 1
743 749
744 750 # adjust our loop counters now we have inserted a new entry
745 751 n += 1
746 752 i += 2
747 753 continue
748 754
749 755 branches[c.branch] = i
750 756 i += 1
751 757
752 758 # Drop synthetic changesets (safe now that we have ensured no other
753 759 # changesets can have them as parents).
754 760 i = 0
755 761 while i < len(changesets):
756 762 if changesets[i].synthetic:
757 763 del changesets[i]
758 764 else:
759 765 i += 1
760 766
761 767 # Number changesets
762 768
763 769 for i, c in enumerate(changesets):
764 770 c.id = i + 1
765 771
766 772 ui.status(_('%d changeset entries\n') % len(changesets))
767 773
768 774 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
769 775
770 776 return changesets
771 777
772 778
773 779 def debugcvsps(ui, *args, **opts):
774 780 '''Read CVS rlog for current directory or named path in
775 781 repository, and convert the log to changesets based on matching
776 782 commit log entries and dates.
777 783 '''
778 784 if opts["new_cache"]:
779 785 cache = "write"
780 786 elif opts["update_cache"]:
781 787 cache = "update"
782 788 else:
783 789 cache = None
784 790
785 791 revisions = opts["revisions"]
786 792
787 793 try:
788 794 if args:
789 795 log = []
790 796 for d in args:
791 797 log += createlog(ui, d, root=opts["root"], cache=cache)
792 798 else:
793 799 log = createlog(ui, root=opts["root"], cache=cache)
794 800 except logerror, e:
795 801 ui.write("%r\n"%e)
796 802 return
797 803
798 804 changesets = createchangeset(ui, log, opts["fuzz"])
799 805 del log
800 806
801 807 # Print changesets (optionally filtered)
802 808
803 809 off = len(revisions)
804 810 branches = {} # latest version number in each branch
805 811 ancestors = {} # parent branch
806 812 for cs in changesets:
807 813
808 814 if opts["ancestors"]:
809 815 if cs.branch not in branches and cs.parents and cs.parents[0].id:
810 816 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
811 817 cs.parents[0].id)
812 818 branches[cs.branch] = cs.id
813 819
814 820 # limit by branches
815 821 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
816 822 continue
817 823
818 824 if not off:
819 825 # Note: trailing spaces on several lines here are needed to have
820 826 # bug-for-bug compatibility with cvsps.
821 827 ui.write('---------------------\n')
822 828 ui.write(('PatchSet %d \n' % cs.id))
823 829 ui.write(('Date: %s\n' % util.datestr(cs.date,
824 830 '%Y/%m/%d %H:%M:%S %1%2')))
825 831 ui.write(('Author: %s\n' % cs.author))
826 832 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
827 833 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
828 834 ','.join(cs.tags) or '(none)')))
829 835 if cs.branchpoints:
830 836 ui.write(('Branchpoints: %s \n') %
831 837 ', '.join(sorted(cs.branchpoints)))
832 838 if opts["parents"] and cs.parents:
833 839 if len(cs.parents) > 1:
834 840 ui.write(('Parents: %s\n' %
835 841 (','.join([str(p.id) for p in cs.parents]))))
836 842 else:
837 843 ui.write(('Parent: %d\n' % cs.parents[0].id))
838 844
839 845 if opts["ancestors"]:
840 846 b = cs.branch
841 847 r = []
842 848 while b:
843 849 b, c = ancestors[b]
844 850 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
845 851 if r:
846 852 ui.write(('Ancestors: %s\n' % (','.join(r))))
847 853
848 854 ui.write(('Log:\n'))
849 855 ui.write('%s\n\n' % cs.comment)
850 856 ui.write(('Members: \n'))
851 857 for f in cs.entries:
852 858 fn = f.file
853 859 if fn.startswith(opts["prefix"]):
854 860 fn = fn[len(opts["prefix"]):]
855 861 ui.write('\t%s:%s->%s%s \n' % (
856 862 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
857 863 '.'.join([str(x) for x in f.revision]),
858 864 ['', '(DEAD)'][f.dead]))
859 865 ui.write('\n')
860 866
861 867 # have we seen the start tag?
862 868 if revisions and off:
863 869 if revisions[0] == str(cs.id) or \
864 870 revisions[0] in cs.tags:
865 871 off = False
866 872
867 873 # see if we reached the end tag
868 874 if len(revisions) > 1 and not off:
869 875 if revisions[1] == str(cs.id) or \
870 876 revisions[1] in cs.tags:
871 877 break
General Comments 0
You need to be logged in to leave comments. Login now