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