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