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