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