PageRenderTime 66ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/hgext/convert/cvsps.py

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