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