##// END OF EJS Templates
cvsps: pull function definition out of loop
Martin Geisler -
r15790:52f816b4 default
parent child Browse files
Show More
@@ -1,854 +1,852 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 366 cpeek = peek
367 367 if cpeek.endswith('\n'):
368 368 cpeek = cpeek[:-1]
369 369 if re_50.match(cpeek):
370 370 state = 5
371 371 store = True
372 372 else:
373 373 e.comment.append(line)
374 374 elif re_32.match(line):
375 375 state = 0
376 376 store = True
377 377 else:
378 378 e.comment.append(line)
379 379
380 380 # When a file is added on a branch B1, CVS creates a synthetic
381 381 # dead trunk revision 1.1 so that the branch has a root.
382 382 # Likewise, if you merge such a file to a later branch B2 (one
383 383 # that already existed when the file was added on B1), CVS
384 384 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
385 385 # these revisions now, but mark them synthetic so
386 386 # createchangeset() can take care of them.
387 387 if (store and
388 388 e.dead and
389 389 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
390 390 len(e.comment) == 1 and
391 391 file_added_re.match(e.comment[0])):
392 392 ui.debug('found synthetic revision in %s: %r\n'
393 393 % (e.rcs, e.comment[0]))
394 394 e.synthetic = True
395 395
396 396 if store:
397 397 # clean up the results and save in the log.
398 398 store = False
399 399 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
400 400 e.comment = scache('\n'.join(e.comment))
401 401
402 402 revn = len(e.revision)
403 403 if revn > 3 and (revn % 2) == 0:
404 404 e.branch = tags.get(e.revision[:-1], [None])[0]
405 405 else:
406 406 e.branch = None
407 407
408 408 # find the branches starting from this revision
409 409 branchpoints = set()
410 410 for branch, revision in branchmap.iteritems():
411 411 revparts = tuple([int(i) for i in revision.split('.')])
412 412 if len(revparts) < 2: # bad tags
413 413 continue
414 414 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
415 415 # normal branch
416 416 if revparts[:-2] == e.revision:
417 417 branchpoints.add(branch)
418 418 elif revparts == (1, 1, 1): # vendor branch
419 419 if revparts in e.branches:
420 420 branchpoints.add(branch)
421 421 e.branchpoints = branchpoints
422 422
423 423 log.append(e)
424 424
425 425 if len(log) % 100 == 0:
426 426 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
427 427
428 428 log.sort(key=lambda x: (x.rcs, x.revision))
429 429
430 430 # find parent revisions of individual files
431 431 versions = {}
432 432 for e in log:
433 433 branch = e.revision[:-1]
434 434 p = versions.get((e.rcs, branch), None)
435 435 if p is None:
436 436 p = e.revision[:-2]
437 437 e.parent = p
438 438 versions[(e.rcs, branch)] = e.revision
439 439
440 440 # update the log cache
441 441 if cache:
442 442 if log:
443 443 # join up the old and new logs
444 444 log.sort(key=lambda x: x.date)
445 445
446 446 if oldlog and oldlog[-1].date >= log[0].date:
447 447 raise logerror(_('log cache overlaps with new log entries,'
448 448 ' re-run without cache.'))
449 449
450 450 log = oldlog + log
451 451
452 452 # write the new cachefile
453 453 ui.note(_('writing cvs log cache %s\n') % cachefile)
454 454 pickle.dump(log, open(cachefile, 'w'))
455 455 else:
456 456 log = oldlog
457 457
458 458 ui.status(_('%d log entries\n') % len(log))
459 459
460 460 hook.hook(ui, None, "cvslog", True, log=log)
461 461
462 462 return log
463 463
464 464
465 465 class changeset(object):
466 466 '''Class changeset has the following attributes:
467 467 .id - integer identifying this changeset (list index)
468 468 .author - author name as CVS knows it
469 469 .branch - name of branch this changeset is on, or None
470 470 .comment - commit message
471 471 .date - the commit date as a (time,tz) tuple
472 472 .entries - list of logentry objects in this changeset
473 473 .parents - list of one or two parent changesets
474 474 .tags - list of tags on this changeset
475 475 .synthetic - from synthetic revision "file ... added on branch ..."
476 476 .mergepoint- the branch that has been merged from
477 477 (if present in rlog output)
478 478 .branchpoints- the branches that start at the current entry
479 479 '''
480 480 def __init__(self, **entries):
481 481 self.synthetic = False
482 482 self.__dict__.update(entries)
483 483
484 484 def __repr__(self):
485 485 return "<%s at 0x%x: %s>" % (self.__class__.__name__,
486 486 id(self),
487 487 getattr(self, 'id', "(no id)"))
488 488
489 489 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
490 490 '''Convert log into changesets.'''
491 491
492 492 ui.status(_('creating changesets\n'))
493 493
494 494 # Merge changesets
495 495
496 496 log.sort(key=lambda x: (x.comment, x.author, x.branch, x.date))
497 497
498 498 changesets = []
499 499 files = set()
500 500 c = None
501 501 for i, e in enumerate(log):
502 502
503 503 # Check if log entry belongs to the current changeset or not.
504 504
505 505 # Since CVS is file centric, two different file revisions with
506 506 # different branchpoints should be treated as belonging to two
507 507 # different changesets (and the ordering is important and not
508 508 # honoured by cvsps at this point).
509 509 #
510 510 # Consider the following case:
511 511 # foo 1.1 branchpoints: [MYBRANCH]
512 512 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
513 513 #
514 514 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
515 515 # later version of foo may be in MYBRANCH2, so foo should be the
516 516 # first changeset and bar the next and MYBRANCH and MYBRANCH2
517 517 # should both start off of the bar changeset. No provisions are
518 518 # made to ensure that this is, in fact, what happens.
519 519 if not (c and
520 520 e.comment == c.comment and
521 521 e.author == c.author and
522 522 e.branch == c.branch and
523 523 (not util.safehasattr(e, 'branchpoints') or
524 524 not util.safehasattr (c, 'branchpoints') or
525 525 e.branchpoints == c.branchpoints) and
526 526 ((c.date[0] + c.date[1]) <=
527 527 (e.date[0] + e.date[1]) <=
528 528 (c.date[0] + c.date[1]) + fuzz) and
529 529 e.file not in files):
530 530 c = changeset(comment=e.comment, author=e.author,
531 531 branch=e.branch, date=e.date, entries=[],
532 532 mergepoint=getattr(e, 'mergepoint', None),
533 533 branchpoints=getattr(e, 'branchpoints', set()))
534 534 changesets.append(c)
535 535 files = set()
536 536 if len(changesets) % 100 == 0:
537 537 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
538 538 ui.status(util.ellipsis(t, 80) + '\n')
539 539
540 540 c.entries.append(e)
541 541 files.add(e.file)
542 542 c.date = e.date # changeset date is date of latest commit in it
543 543
544 544 # Mark synthetic changesets
545 545
546 546 for c in changesets:
547 547 # Synthetic revisions always get their own changeset, because
548 548 # the log message includes the filename. E.g. if you add file3
549 549 # and file4 on a branch, you get four log entries and three
550 550 # changesets:
551 551 # "File file3 was added on branch ..." (synthetic, 1 entry)
552 552 # "File file4 was added on branch ..." (synthetic, 1 entry)
553 553 # "Add file3 and file4 to fix ..." (real, 2 entries)
554 554 # Hence the check for 1 entry here.
555 555 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
556 556
557 557 # Sort files in each changeset
558 558
559 def entitycompare(l, r):
560 'Mimic cvsps sorting order'
561 l = l.file.split('/')
562 r = r.file.split('/')
563 nl = len(l)
564 nr = len(r)
565 n = min(nl, nr)
566 for i in range(n):
567 if i + 1 == nl and nl < nr:
568 return -1
569 elif i + 1 == nr and nl > nr:
570 return +1
571 elif l[i] < r[i]:
572 return -1
573 elif l[i] > r[i]:
574 return +1
575 return 0
576
559 577 for c in changesets:
560 def pathcompare(l, r):
561 'Mimic cvsps sorting order'
562 l = l.split('/')
563 r = r.split('/')
564 nl = len(l)
565 nr = len(r)
566 n = min(nl, nr)
567 for i in range(n):
568 if i + 1 == nl and nl < nr:
569 return -1
570 elif i + 1 == nr and nl > nr:
571 return +1
572 elif l[i] < r[i]:
573 return -1
574 elif l[i] > r[i]:
575 return +1
576 return 0
577 def entitycompare(l, r):
578 return pathcompare(l.file, r.file)
579
580 578 c.entries.sort(entitycompare)
581 579
582 580 # Sort changesets by date
583 581
584 582 def cscmp(l, r):
585 583 d = sum(l.date) - sum(r.date)
586 584 if d:
587 585 return d
588 586
589 587 # detect vendor branches and initial commits on a branch
590 588 le = {}
591 589 for e in l.entries:
592 590 le[e.rcs] = e.revision
593 591 re = {}
594 592 for e in r.entries:
595 593 re[e.rcs] = e.revision
596 594
597 595 d = 0
598 596 for e in l.entries:
599 597 if re.get(e.rcs, None) == e.parent:
600 598 assert not d
601 599 d = 1
602 600 break
603 601
604 602 for e in r.entries:
605 603 if le.get(e.rcs, None) == e.parent:
606 604 assert not d
607 605 d = -1
608 606 break
609 607
610 608 return d
611 609
612 610 changesets.sort(cscmp)
613 611
614 612 # Collect tags
615 613
616 614 globaltags = {}
617 615 for c in changesets:
618 616 for e in c.entries:
619 617 for tag in e.tags:
620 618 # remember which is the latest changeset to have this tag
621 619 globaltags[tag] = c
622 620
623 621 for c in changesets:
624 622 tags = set()
625 623 for e in c.entries:
626 624 tags.update(e.tags)
627 625 # remember tags only if this is the latest changeset to have it
628 626 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
629 627
630 628 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
631 629 # by inserting dummy changesets with two parents, and handle
632 630 # {{mergefrombranch BRANCHNAME}} by setting two parents.
633 631
634 632 if mergeto is None:
635 633 mergeto = r'{{mergetobranch ([-\w]+)}}'
636 634 if mergeto:
637 635 mergeto = re.compile(mergeto)
638 636
639 637 if mergefrom is None:
640 638 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
641 639 if mergefrom:
642 640 mergefrom = re.compile(mergefrom)
643 641
644 642 versions = {} # changeset index where we saw any particular file version
645 643 branches = {} # changeset index where we saw a branch
646 644 n = len(changesets)
647 645 i = 0
648 646 while i < n:
649 647 c = changesets[i]
650 648
651 649 for f in c.entries:
652 650 versions[(f.rcs, f.revision)] = i
653 651
654 652 p = None
655 653 if c.branch in branches:
656 654 p = branches[c.branch]
657 655 else:
658 656 # first changeset on a new branch
659 657 # the parent is a changeset with the branch in its
660 658 # branchpoints such that it is the latest possible
661 659 # commit without any intervening, unrelated commits.
662 660
663 661 for candidate in xrange(i):
664 662 if c.branch not in changesets[candidate].branchpoints:
665 663 if p is not None:
666 664 break
667 665 continue
668 666 p = candidate
669 667
670 668 c.parents = []
671 669 if p is not None:
672 670 p = changesets[p]
673 671
674 672 # Ensure no changeset has a synthetic changeset as a parent.
675 673 while p.synthetic:
676 674 assert len(p.parents) <= 1, \
677 675 _('synthetic changeset cannot have multiple parents')
678 676 if p.parents:
679 677 p = p.parents[0]
680 678 else:
681 679 p = None
682 680 break
683 681
684 682 if p is not None:
685 683 c.parents.append(p)
686 684
687 685 if c.mergepoint:
688 686 if c.mergepoint == 'HEAD':
689 687 c.mergepoint = None
690 688 c.parents.append(changesets[branches[c.mergepoint]])
691 689
692 690 if mergefrom:
693 691 m = mergefrom.search(c.comment)
694 692 if m:
695 693 m = m.group(1)
696 694 if m == 'HEAD':
697 695 m = None
698 696 try:
699 697 candidate = changesets[branches[m]]
700 698 except KeyError:
701 699 ui.warn(_("warning: CVS commit message references "
702 700 "non-existent branch %r:\n%s\n")
703 701 % (m, c.comment))
704 702 if m in branches and c.branch != m and not candidate.synthetic:
705 703 c.parents.append(candidate)
706 704
707 705 if mergeto:
708 706 m = mergeto.search(c.comment)
709 707 if m:
710 708 try:
711 709 m = m.group(1)
712 710 if m == 'HEAD':
713 711 m = None
714 712 except:
715 713 m = None # if no group found then merge to HEAD
716 714 if m in branches and c.branch != m:
717 715 # insert empty changeset for merge
718 716 cc = changeset(
719 717 author=c.author, branch=m, date=c.date,
720 718 comment='convert-repo: CVS merge from branch %s'
721 719 % c.branch,
722 720 entries=[], tags=[],
723 721 parents=[changesets[branches[m]], c])
724 722 changesets.insert(i + 1, cc)
725 723 branches[m] = i + 1
726 724
727 725 # adjust our loop counters now we have inserted a new entry
728 726 n += 1
729 727 i += 2
730 728 continue
731 729
732 730 branches[c.branch] = i
733 731 i += 1
734 732
735 733 # Drop synthetic changesets (safe now that we have ensured no other
736 734 # changesets can have them as parents).
737 735 i = 0
738 736 while i < len(changesets):
739 737 if changesets[i].synthetic:
740 738 del changesets[i]
741 739 else:
742 740 i += 1
743 741
744 742 # Number changesets
745 743
746 744 for i, c in enumerate(changesets):
747 745 c.id = i + 1
748 746
749 747 ui.status(_('%d changeset entries\n') % len(changesets))
750 748
751 749 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
752 750
753 751 return changesets
754 752
755 753
756 754 def debugcvsps(ui, *args, **opts):
757 755 '''Read CVS rlog for current directory or named path in
758 756 repository, and convert the log to changesets based on matching
759 757 commit log entries and dates.
760 758 '''
761 759 if opts["new_cache"]:
762 760 cache = "write"
763 761 elif opts["update_cache"]:
764 762 cache = "update"
765 763 else:
766 764 cache = None
767 765
768 766 revisions = opts["revisions"]
769 767
770 768 try:
771 769 if args:
772 770 log = []
773 771 for d in args:
774 772 log += createlog(ui, d, root=opts["root"], cache=cache)
775 773 else:
776 774 log = createlog(ui, root=opts["root"], cache=cache)
777 775 except logerror, e:
778 776 ui.write("%r\n"%e)
779 777 return
780 778
781 779 changesets = createchangeset(ui, log, opts["fuzz"])
782 780 del log
783 781
784 782 # Print changesets (optionally filtered)
785 783
786 784 off = len(revisions)
787 785 branches = {} # latest version number in each branch
788 786 ancestors = {} # parent branch
789 787 for cs in changesets:
790 788
791 789 if opts["ancestors"]:
792 790 if cs.branch not in branches and cs.parents and cs.parents[0].id:
793 791 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
794 792 cs.parents[0].id)
795 793 branches[cs.branch] = cs.id
796 794
797 795 # limit by branches
798 796 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
799 797 continue
800 798
801 799 if not off:
802 800 # Note: trailing spaces on several lines here are needed to have
803 801 # bug-for-bug compatibility with cvsps.
804 802 ui.write('---------------------\n')
805 803 ui.write('PatchSet %d \n' % cs.id)
806 804 ui.write('Date: %s\n' % util.datestr(cs.date,
807 805 '%Y/%m/%d %H:%M:%S %1%2'))
808 806 ui.write('Author: %s\n' % cs.author)
809 807 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
810 808 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
811 809 ','.join(cs.tags) or '(none)'))
812 810 branchpoints = getattr(cs, 'branchpoints', None)
813 811 if branchpoints:
814 812 ui.write('Branchpoints: %s \n' % ', '.join(branchpoints))
815 813 if opts["parents"] and cs.parents:
816 814 if len(cs.parents) > 1:
817 815 ui.write('Parents: %s\n' %
818 816 (','.join([str(p.id) for p in cs.parents])))
819 817 else:
820 818 ui.write('Parent: %d\n' % cs.parents[0].id)
821 819
822 820 if opts["ancestors"]:
823 821 b = cs.branch
824 822 r = []
825 823 while b:
826 824 b, c = ancestors[b]
827 825 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
828 826 if r:
829 827 ui.write('Ancestors: %s\n' % (','.join(r)))
830 828
831 829 ui.write('Log:\n')
832 830 ui.write('%s\n\n' % cs.comment)
833 831 ui.write('Members: \n')
834 832 for f in cs.entries:
835 833 fn = f.file
836 834 if fn.startswith(opts["prefix"]):
837 835 fn = fn[len(opts["prefix"]):]
838 836 ui.write('\t%s:%s->%s%s \n' % (
839 837 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
840 838 '.'.join([str(x) for x in f.revision]),
841 839 ['', '(DEAD)'][f.dead]))
842 840 ui.write('\n')
843 841
844 842 # have we seen the start tag?
845 843 if revisions and off:
846 844 if revisions[0] == str(cs.id) or \
847 845 revisions[0] in cs.tags:
848 846 off = False
849 847
850 848 # see if we reached the end tag
851 849 if len(revisions) > 1 and not off:
852 850 if revisions[1] == str(cs.id) or \
853 851 revisions[1] in cs.tags:
854 852 break
General Comments 0
You need to be logged in to leave comments. Login now