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