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