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