##// END OF EJS Templates
convert: use set instead of dict
Benoit Boissinot -
r8456:e9e2a2c9 default
parent child Browse files
Show More
@@ -1,351 +1,351
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from darcs import darcs_source
10 from darcs import darcs_source
11 from git import convert_git
11 from git import convert_git
12 from hg import mercurial_source, mercurial_sink
12 from hg import mercurial_source, mercurial_sink
13 from subversion import svn_source, svn_sink
13 from subversion import svn_source, svn_sink
14 from monotone import monotone_source
14 from monotone import monotone_source
15 from gnuarch import gnuarch_source
15 from gnuarch import gnuarch_source
16 from bzr import bzr_source
16 from bzr import bzr_source
17 from p4 import p4_source
17 from p4 import p4_source
18 import filemap
18 import filemap
19
19
20 import os, shutil
20 import os, shutil
21 from mercurial import hg, util, encoding
21 from mercurial import hg, util, encoding
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23
23
24 orig_encoding = 'ascii'
24 orig_encoding = 'ascii'
25
25
26 def recode(s):
26 def recode(s):
27 if isinstance(s, unicode):
27 if isinstance(s, unicode):
28 return s.encode(orig_encoding, 'replace')
28 return s.encode(orig_encoding, 'replace')
29 else:
29 else:
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31
31
32 source_converters = [
32 source_converters = [
33 ('cvs', convert_cvs),
33 ('cvs', convert_cvs),
34 ('git', convert_git),
34 ('git', convert_git),
35 ('svn', svn_source),
35 ('svn', svn_source),
36 ('hg', mercurial_source),
36 ('hg', mercurial_source),
37 ('darcs', darcs_source),
37 ('darcs', darcs_source),
38 ('mtn', monotone_source),
38 ('mtn', monotone_source),
39 ('gnuarch', gnuarch_source),
39 ('gnuarch', gnuarch_source),
40 ('bzr', bzr_source),
40 ('bzr', bzr_source),
41 ('p4', p4_source),
41 ('p4', p4_source),
42 ]
42 ]
43
43
44 sink_converters = [
44 sink_converters = [
45 ('hg', mercurial_sink),
45 ('hg', mercurial_sink),
46 ('svn', svn_sink),
46 ('svn', svn_sink),
47 ]
47 ]
48
48
49 def convertsource(ui, path, type, rev):
49 def convertsource(ui, path, type, rev):
50 exceptions = []
50 exceptions = []
51 for name, source in source_converters:
51 for name, source in source_converters:
52 try:
52 try:
53 if not type or name == type:
53 if not type or name == type:
54 return source(ui, path, rev)
54 return source(ui, path, rev)
55 except (NoRepo, MissingTool), inst:
55 except (NoRepo, MissingTool), inst:
56 exceptions.append(inst)
56 exceptions.append(inst)
57 if not ui.quiet:
57 if not ui.quiet:
58 for inst in exceptions:
58 for inst in exceptions:
59 ui.write("%s\n" % inst)
59 ui.write("%s\n" % inst)
60 raise util.Abort(_('%s: missing or unsupported repository') % path)
60 raise util.Abort(_('%s: missing or unsupported repository') % path)
61
61
62 def convertsink(ui, path, type):
62 def convertsink(ui, path, type):
63 for name, sink in sink_converters:
63 for name, sink in sink_converters:
64 try:
64 try:
65 if not type or name == type:
65 if not type or name == type:
66 return sink(ui, path)
66 return sink(ui, path)
67 except NoRepo, inst:
67 except NoRepo, inst:
68 ui.note(_("convert: %s\n") % inst)
68 ui.note(_("convert: %s\n") % inst)
69 raise util.Abort(_('%s: unknown repository type') % path)
69 raise util.Abort(_('%s: unknown repository type') % path)
70
70
71 class converter(object):
71 class converter(object):
72 def __init__(self, ui, source, dest, revmapfile, opts):
72 def __init__(self, ui, source, dest, revmapfile, opts):
73
73
74 self.source = source
74 self.source = source
75 self.dest = dest
75 self.dest = dest
76 self.ui = ui
76 self.ui = ui
77 self.opts = opts
77 self.opts = opts
78 self.commitcache = {}
78 self.commitcache = {}
79 self.authors = {}
79 self.authors = {}
80 self.authorfile = None
80 self.authorfile = None
81
81
82 # Record converted revisions persistently: maps source revision
82 # Record converted revisions persistently: maps source revision
83 # ID to target revision ID (both strings). (This is how
83 # ID to target revision ID (both strings). (This is how
84 # incremental conversions work.)
84 # incremental conversions work.)
85 self.map = mapfile(ui, revmapfile)
85 self.map = mapfile(ui, revmapfile)
86
86
87 # Read first the dst author map if any
87 # Read first the dst author map if any
88 authorfile = self.dest.authorfile()
88 authorfile = self.dest.authorfile()
89 if authorfile and os.path.exists(authorfile):
89 if authorfile and os.path.exists(authorfile):
90 self.readauthormap(authorfile)
90 self.readauthormap(authorfile)
91 # Extend/Override with new author map if necessary
91 # Extend/Override with new author map if necessary
92 if opts.get('authors'):
92 if opts.get('authors'):
93 self.readauthormap(opts.get('authors'))
93 self.readauthormap(opts.get('authors'))
94 self.authorfile = self.dest.authorfile()
94 self.authorfile = self.dest.authorfile()
95
95
96 self.splicemap = mapfile(ui, opts.get('splicemap'))
96 self.splicemap = mapfile(ui, opts.get('splicemap'))
97 self.branchmap = mapfile(ui, opts.get('branchmap'))
97 self.branchmap = mapfile(ui, opts.get('branchmap'))
98
98
99 def walktree(self, heads):
99 def walktree(self, heads):
100 '''Return a mapping that identifies the uncommitted parents of every
100 '''Return a mapping that identifies the uncommitted parents of every
101 uncommitted changeset.'''
101 uncommitted changeset.'''
102 visit = heads
102 visit = heads
103 known = {}
103 known = set()
104 parents = {}
104 parents = {}
105 while visit:
105 while visit:
106 n = visit.pop(0)
106 n = visit.pop(0)
107 if n in known or n in self.map: continue
107 if n in known or n in self.map: continue
108 known[n] = 1
108 known.add(n)
109 commit = self.cachecommit(n)
109 commit = self.cachecommit(n)
110 parents[n] = []
110 parents[n] = []
111 for p in commit.parents:
111 for p in commit.parents:
112 parents[n].append(p)
112 parents[n].append(p)
113 visit.append(p)
113 visit.append(p)
114
114
115 return parents
115 return parents
116
116
117 def toposort(self, parents):
117 def toposort(self, parents):
118 '''Return an ordering such that every uncommitted changeset is
118 '''Return an ordering such that every uncommitted changeset is
119 preceeded by all its uncommitted ancestors.'''
119 preceeded by all its uncommitted ancestors.'''
120 visit = parents.keys()
120 visit = parents.keys()
121 seen = {}
121 seen = set()
122 children = {}
122 children = {}
123 actives = []
123 actives = []
124
124
125 while visit:
125 while visit:
126 n = visit.pop(0)
126 n = visit.pop(0)
127 if n in seen: continue
127 if n in seen: continue
128 seen[n] = 1
128 seen.add(n)
129 # Ensure that nodes without parents are present in the 'children'
129 # Ensure that nodes without parents are present in the 'children'
130 # mapping.
130 # mapping.
131 children.setdefault(n, [])
131 children.setdefault(n, [])
132 hasparent = False
132 hasparent = False
133 for p in parents[n]:
133 for p in parents[n]:
134 if not p in self.map:
134 if not p in self.map:
135 visit.append(p)
135 visit.append(p)
136 hasparent = True
136 hasparent = True
137 children.setdefault(p, []).append(n)
137 children.setdefault(p, []).append(n)
138 if not hasparent:
138 if not hasparent:
139 actives.append(n)
139 actives.append(n)
140
140
141 del seen
141 del seen
142 del visit
142 del visit
143
143
144 if self.opts.get('datesort'):
144 if self.opts.get('datesort'):
145 dates = {}
145 dates = {}
146 def getdate(n):
146 def getdate(n):
147 if n not in dates:
147 if n not in dates:
148 dates[n] = util.parsedate(self.commitcache[n].date)
148 dates[n] = util.parsedate(self.commitcache[n].date)
149 return dates[n]
149 return dates[n]
150
150
151 def picknext(nodes):
151 def picknext(nodes):
152 return min([(getdate(n), n) for n in nodes])[1]
152 return min([(getdate(n), n) for n in nodes])[1]
153 else:
153 else:
154 prev = [None]
154 prev = [None]
155 def picknext(nodes):
155 def picknext(nodes):
156 # Return the first eligible child of the previously converted
156 # Return the first eligible child of the previously converted
157 # revision, or any of them.
157 # revision, or any of them.
158 next = nodes[0]
158 next = nodes[0]
159 for n in nodes:
159 for n in nodes:
160 if prev[0] in parents[n]:
160 if prev[0] in parents[n]:
161 next = n
161 next = n
162 break
162 break
163 prev[0] = next
163 prev[0] = next
164 return next
164 return next
165
165
166 s = []
166 s = []
167 pendings = {}
167 pendings = {}
168 while actives:
168 while actives:
169 n = picknext(actives)
169 n = picknext(actives)
170 actives.remove(n)
170 actives.remove(n)
171 s.append(n)
171 s.append(n)
172
172
173 # Update dependents list
173 # Update dependents list
174 for c in children.get(n, []):
174 for c in children.get(n, []):
175 if c not in pendings:
175 if c not in pendings:
176 pendings[c] = [p for p in parents[c] if p not in self.map]
176 pendings[c] = [p for p in parents[c] if p not in self.map]
177 try:
177 try:
178 pendings[c].remove(n)
178 pendings[c].remove(n)
179 except ValueError:
179 except ValueError:
180 raise util.Abort(_('cycle detected between %s and %s')
180 raise util.Abort(_('cycle detected between %s and %s')
181 % (recode(c), recode(n)))
181 % (recode(c), recode(n)))
182 if not pendings[c]:
182 if not pendings[c]:
183 # Parents are converted, node is eligible
183 # Parents are converted, node is eligible
184 actives.insert(0, c)
184 actives.insert(0, c)
185 pendings[c] = None
185 pendings[c] = None
186
186
187 if len(s) != len(parents):
187 if len(s) != len(parents):
188 raise util.Abort(_("not all revisions were sorted"))
188 raise util.Abort(_("not all revisions were sorted"))
189
189
190 return s
190 return s
191
191
192 def writeauthormap(self):
192 def writeauthormap(self):
193 authorfile = self.authorfile
193 authorfile = self.authorfile
194 if authorfile:
194 if authorfile:
195 self.ui.status(_('Writing author map file %s\n') % authorfile)
195 self.ui.status(_('Writing author map file %s\n') % authorfile)
196 ofile = open(authorfile, 'w+')
196 ofile = open(authorfile, 'w+')
197 for author in self.authors:
197 for author in self.authors:
198 ofile.write("%s=%s\n" % (author, self.authors[author]))
198 ofile.write("%s=%s\n" % (author, self.authors[author]))
199 ofile.close()
199 ofile.close()
200
200
201 def readauthormap(self, authorfile):
201 def readauthormap(self, authorfile):
202 afile = open(authorfile, 'r')
202 afile = open(authorfile, 'r')
203 for line in afile:
203 for line in afile:
204
204
205 line = line.strip()
205 line = line.strip()
206 if not line or line.startswith('#'):
206 if not line or line.startswith('#'):
207 continue
207 continue
208
208
209 try:
209 try:
210 srcauthor, dstauthor = line.split('=', 1)
210 srcauthor, dstauthor = line.split('=', 1)
211 except ValueError:
211 except ValueError:
212 msg = _('Ignoring bad line in author map file %s: %s\n')
212 msg = _('Ignoring bad line in author map file %s: %s\n')
213 self.ui.warn(msg % (authorfile, line.rstrip()))
213 self.ui.warn(msg % (authorfile, line.rstrip()))
214 continue
214 continue
215
215
216 srcauthor = srcauthor.strip()
216 srcauthor = srcauthor.strip()
217 dstauthor = dstauthor.strip()
217 dstauthor = dstauthor.strip()
218 if self.authors.get(srcauthor) in (None, dstauthor):
218 if self.authors.get(srcauthor) in (None, dstauthor):
219 msg = _('mapping author %s to %s\n')
219 msg = _('mapping author %s to %s\n')
220 self.ui.debug(msg % (srcauthor, dstauthor))
220 self.ui.debug(msg % (srcauthor, dstauthor))
221 self.authors[srcauthor] = dstauthor
221 self.authors[srcauthor] = dstauthor
222 continue
222 continue
223
223
224 m = _('overriding mapping for author %s, was %s, will be %s\n')
224 m = _('overriding mapping for author %s, was %s, will be %s\n')
225 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
225 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
226
226
227 afile.close()
227 afile.close()
228
228
229 def cachecommit(self, rev):
229 def cachecommit(self, rev):
230 commit = self.source.getcommit(rev)
230 commit = self.source.getcommit(rev)
231 commit.author = self.authors.get(commit.author, commit.author)
231 commit.author = self.authors.get(commit.author, commit.author)
232 commit.branch = self.branchmap.get(commit.branch, commit.branch)
232 commit.branch = self.branchmap.get(commit.branch, commit.branch)
233 self.commitcache[rev] = commit
233 self.commitcache[rev] = commit
234 return commit
234 return commit
235
235
236 def copy(self, rev):
236 def copy(self, rev):
237 commit = self.commitcache[rev]
237 commit = self.commitcache[rev]
238
238
239 changes = self.source.getchanges(rev)
239 changes = self.source.getchanges(rev)
240 if isinstance(changes, basestring):
240 if isinstance(changes, basestring):
241 if changes == SKIPREV:
241 if changes == SKIPREV:
242 dest = SKIPREV
242 dest = SKIPREV
243 else:
243 else:
244 dest = self.map[changes]
244 dest = self.map[changes]
245 self.map[rev] = dest
245 self.map[rev] = dest
246 return
246 return
247 files, copies = changes
247 files, copies = changes
248 pbranches = []
248 pbranches = []
249 if commit.parents:
249 if commit.parents:
250 for prev in commit.parents:
250 for prev in commit.parents:
251 if prev not in self.commitcache:
251 if prev not in self.commitcache:
252 self.cachecommit(prev)
252 self.cachecommit(prev)
253 pbranches.append((self.map[prev],
253 pbranches.append((self.map[prev],
254 self.commitcache[prev].branch))
254 self.commitcache[prev].branch))
255 self.dest.setbranch(commit.branch, pbranches)
255 self.dest.setbranch(commit.branch, pbranches)
256 try:
256 try:
257 parents = self.splicemap[rev].replace(',', ' ').split()
257 parents = self.splicemap[rev].replace(',', ' ').split()
258 self.ui.status(_('spliced in %s as parents of %s\n') %
258 self.ui.status(_('spliced in %s as parents of %s\n') %
259 (parents, rev))
259 (parents, rev))
260 parents = [self.map.get(p, p) for p in parents]
260 parents = [self.map.get(p, p) for p in parents]
261 except KeyError:
261 except KeyError:
262 parents = [b[0] for b in pbranches]
262 parents = [b[0] for b in pbranches]
263 newnode = self.dest.putcommit(files, copies, parents, commit, self.source)
263 newnode = self.dest.putcommit(files, copies, parents, commit, self.source)
264 self.source.converted(rev, newnode)
264 self.source.converted(rev, newnode)
265 self.map[rev] = newnode
265 self.map[rev] = newnode
266
266
267 def convert(self):
267 def convert(self):
268
268
269 try:
269 try:
270 self.source.before()
270 self.source.before()
271 self.dest.before()
271 self.dest.before()
272 self.source.setrevmap(self.map)
272 self.source.setrevmap(self.map)
273 self.ui.status(_("scanning source...\n"))
273 self.ui.status(_("scanning source...\n"))
274 heads = self.source.getheads()
274 heads = self.source.getheads()
275 parents = self.walktree(heads)
275 parents = self.walktree(heads)
276 self.ui.status(_("sorting...\n"))
276 self.ui.status(_("sorting...\n"))
277 t = self.toposort(parents)
277 t = self.toposort(parents)
278 num = len(t)
278 num = len(t)
279 c = None
279 c = None
280
280
281 self.ui.status(_("converting...\n"))
281 self.ui.status(_("converting...\n"))
282 for c in t:
282 for c in t:
283 num -= 1
283 num -= 1
284 desc = self.commitcache[c].desc
284 desc = self.commitcache[c].desc
285 if "\n" in desc:
285 if "\n" in desc:
286 desc = desc.splitlines()[0]
286 desc = desc.splitlines()[0]
287 # convert log message to local encoding without using
287 # convert log message to local encoding without using
288 # tolocal() because encoding.encoding conver() use it as
288 # tolocal() because encoding.encoding conver() use it as
289 # 'utf-8'
289 # 'utf-8'
290 self.ui.status("%d %s\n" % (num, recode(desc)))
290 self.ui.status("%d %s\n" % (num, recode(desc)))
291 self.ui.note(_("source: %s\n") % recode(c))
291 self.ui.note(_("source: %s\n") % recode(c))
292 self.copy(c)
292 self.copy(c)
293
293
294 tags = self.source.gettags()
294 tags = self.source.gettags()
295 ctags = {}
295 ctags = {}
296 for k in tags:
296 for k in tags:
297 v = tags[k]
297 v = tags[k]
298 if self.map.get(v, SKIPREV) != SKIPREV:
298 if self.map.get(v, SKIPREV) != SKIPREV:
299 ctags[k] = self.map[v]
299 ctags[k] = self.map[v]
300
300
301 if c and ctags:
301 if c and ctags:
302 nrev = self.dest.puttags(ctags)
302 nrev = self.dest.puttags(ctags)
303 # write another hash correspondence to override the previous
303 # write another hash correspondence to override the previous
304 # one so we don't end up with extra tag heads
304 # one so we don't end up with extra tag heads
305 if nrev:
305 if nrev:
306 self.map[c] = nrev
306 self.map[c] = nrev
307
307
308 self.writeauthormap()
308 self.writeauthormap()
309 finally:
309 finally:
310 self.cleanup()
310 self.cleanup()
311
311
312 def cleanup(self):
312 def cleanup(self):
313 try:
313 try:
314 self.dest.after()
314 self.dest.after()
315 finally:
315 finally:
316 self.source.after()
316 self.source.after()
317 self.map.close()
317 self.map.close()
318
318
319 def convert(ui, src, dest=None, revmapfile=None, **opts):
319 def convert(ui, src, dest=None, revmapfile=None, **opts):
320 global orig_encoding
320 global orig_encoding
321 orig_encoding = encoding.encoding
321 orig_encoding = encoding.encoding
322 encoding.encoding = 'UTF-8'
322 encoding.encoding = 'UTF-8'
323
323
324 if not dest:
324 if not dest:
325 dest = hg.defaultdest(src) + "-hg"
325 dest = hg.defaultdest(src) + "-hg"
326 ui.status(_("assuming destination %s\n") % dest)
326 ui.status(_("assuming destination %s\n") % dest)
327
327
328 destc = convertsink(ui, dest, opts.get('dest_type'))
328 destc = convertsink(ui, dest, opts.get('dest_type'))
329
329
330 try:
330 try:
331 srcc = convertsource(ui, src, opts.get('source_type'),
331 srcc = convertsource(ui, src, opts.get('source_type'),
332 opts.get('rev'))
332 opts.get('rev'))
333 except Exception:
333 except Exception:
334 for path in destc.created:
334 for path in destc.created:
335 shutil.rmtree(path, True)
335 shutil.rmtree(path, True)
336 raise
336 raise
337
337
338 fmap = opts.get('filemap')
338 fmap = opts.get('filemap')
339 if fmap:
339 if fmap:
340 srcc = filemap.filemap_source(ui, srcc, fmap)
340 srcc = filemap.filemap_source(ui, srcc, fmap)
341 destc.setfilemapmode(True)
341 destc.setfilemapmode(True)
342
342
343 if not revmapfile:
343 if not revmapfile:
344 try:
344 try:
345 revmapfile = destc.revmapfile()
345 revmapfile = destc.revmapfile()
346 except:
346 except:
347 revmapfile = os.path.join(destc, "map")
347 revmapfile = os.path.join(destc, "map")
348
348
349 c = converter(ui, srcc, destc, revmapfile, opts)
349 c = converter(ui, srcc, destc, revmapfile, opts)
350 c.convert()
350 c.convert()
351
351
@@ -1,782 +1,781
1 #
1 #
2 # Mercurial built-in replacement for cvsps.
2 # Mercurial built-in replacement for cvsps.
3 #
3 #
4 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 import os
9 import os
10 import re
10 import re
11 import cPickle as pickle
11 import cPickle as pickle
12 from mercurial import util
12 from mercurial import util
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14
14
15 def listsort(list, key):
15 def listsort(list, key):
16 "helper to sort by key in Python 2.3"
16 "helper to sort by key in Python 2.3"
17 try:
17 try:
18 list.sort(key=key)
18 list.sort(key=key)
19 except TypeError:
19 except TypeError:
20 list.sort(lambda l, r: cmp(key(l), key(r)))
20 list.sort(lambda l, r: cmp(key(l), key(r)))
21
21
22 class logentry(object):
22 class logentry(object):
23 '''Class logentry has the following attributes:
23 '''Class logentry has the following attributes:
24 .author - author name as CVS knows it
24 .author - author name as CVS knows it
25 .branch - name of branch this revision is on
25 .branch - name of branch this revision is on
26 .branches - revision tuple of branches starting at this revision
26 .branches - revision tuple of branches starting at this revision
27 .comment - commit message
27 .comment - commit message
28 .date - the commit date as a (time, tz) tuple
28 .date - the commit date as a (time, tz) tuple
29 .dead - true if file revision is dead
29 .dead - true if file revision is dead
30 .file - Name of file
30 .file - Name of file
31 .lines - a tuple (+lines, -lines) or None
31 .lines - a tuple (+lines, -lines) or None
32 .parent - Previous revision of this entry
32 .parent - Previous revision of this entry
33 .rcs - name of file as returned from CVS
33 .rcs - name of file as returned from CVS
34 .revision - revision number as tuple
34 .revision - revision number as tuple
35 .tags - list of tags on the file
35 .tags - list of tags on the file
36 .synthetic - is this a synthetic "file ... added on ..." revision?
36 .synthetic - is this a synthetic "file ... added on ..." revision?
37 .mergepoint- the branch that has been merged from (if present in rlog output)
37 .mergepoint- the branch that has been merged from (if present in rlog output)
38 '''
38 '''
39 def __init__(self, **entries):
39 def __init__(self, **entries):
40 self.__dict__.update(entries)
40 self.__dict__.update(entries)
41
41
42 def __repr__(self):
42 def __repr__(self):
43 return "<%s at 0x%x: %s %s>" % (self.__class__.__name__,
43 return "<%s at 0x%x: %s %s>" % (self.__class__.__name__,
44 id(self),
44 id(self),
45 self.file,
45 self.file,
46 ".".join(map(str, self.revision)))
46 ".".join(map(str, self.revision)))
47
47
48 class logerror(Exception):
48 class logerror(Exception):
49 pass
49 pass
50
50
51 def getrepopath(cvspath):
51 def getrepopath(cvspath):
52 """Return the repository path from a CVS path.
52 """Return the repository path from a CVS path.
53
53
54 >>> getrepopath('/foo/bar')
54 >>> getrepopath('/foo/bar')
55 '/foo/bar'
55 '/foo/bar'
56 >>> getrepopath('c:/foo/bar')
56 >>> getrepopath('c:/foo/bar')
57 'c:/foo/bar'
57 'c:/foo/bar'
58 >>> getrepopath(':pserver:10/foo/bar')
58 >>> getrepopath(':pserver:10/foo/bar')
59 '/foo/bar'
59 '/foo/bar'
60 >>> getrepopath(':pserver:10c:/foo/bar')
60 >>> getrepopath(':pserver:10c:/foo/bar')
61 '/foo/bar'
61 '/foo/bar'
62 >>> getrepopath(':pserver:/foo/bar')
62 >>> getrepopath(':pserver:/foo/bar')
63 '/foo/bar'
63 '/foo/bar'
64 >>> getrepopath(':pserver:c:/foo/bar')
64 >>> getrepopath(':pserver:c:/foo/bar')
65 'c:/foo/bar'
65 'c:/foo/bar'
66 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
66 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
67 '/foo/bar'
67 '/foo/bar'
68 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
68 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
69 'c:/foo/bar'
69 'c:/foo/bar'
70 """
70 """
71 # According to CVS manual, CVS paths are expressed like:
71 # According to CVS manual, CVS paths are expressed like:
72 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
72 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
73 #
73 #
74 # Unfortunately, Windows absolute paths start with a drive letter
74 # Unfortunately, Windows absolute paths start with a drive letter
75 # like 'c:' making it harder to parse. Here we assume that drive
75 # like 'c:' making it harder to parse. Here we assume that drive
76 # letters are only one character long and any CVS component before
76 # letters are only one character long and any CVS component before
77 # the repository path is at least 2 characters long, and use this
77 # the repository path is at least 2 characters long, and use this
78 # to disambiguate.
78 # to disambiguate.
79 parts = cvspath.split(':')
79 parts = cvspath.split(':')
80 if len(parts) == 1:
80 if len(parts) == 1:
81 return parts[0]
81 return parts[0]
82 # Here there is an ambiguous case if we have a port number
82 # Here there is an ambiguous case if we have a port number
83 # immediately followed by a Windows driver letter. We assume this
83 # immediately followed by a Windows driver letter. We assume this
84 # never happens and decide it must be CVS path component,
84 # never happens and decide it must be CVS path component,
85 # therefore ignoring it.
85 # therefore ignoring it.
86 if len(parts[-2]) > 1:
86 if len(parts[-2]) > 1:
87 return parts[-1].lstrip('0123456789')
87 return parts[-1].lstrip('0123456789')
88 return parts[-2] + ':' + parts[-1]
88 return parts[-2] + ':' + parts[-1]
89
89
90 def createlog(ui, directory=None, root="", rlog=True, cache=None):
90 def createlog(ui, directory=None, root="", rlog=True, cache=None):
91 '''Collect the CVS rlog'''
91 '''Collect the CVS rlog'''
92
92
93 # Because we store many duplicate commit log messages, reusing strings
93 # Because we store many duplicate commit log messages, reusing strings
94 # saves a lot of memory and pickle storage space.
94 # saves a lot of memory and pickle storage space.
95 _scache = {}
95 _scache = {}
96 def scache(s):
96 def scache(s):
97 "return a shared version of a string"
97 "return a shared version of a string"
98 return _scache.setdefault(s, s)
98 return _scache.setdefault(s, s)
99
99
100 ui.status(_('collecting CVS rlog\n'))
100 ui.status(_('collecting CVS rlog\n'))
101
101
102 log = [] # list of logentry objects containing the CVS state
102 log = [] # list of logentry objects containing the CVS state
103
103
104 # patterns to match in CVS (r)log output, by state of use
104 # patterns to match in CVS (r)log output, by state of use
105 re_00 = re.compile('RCS file: (.+)$')
105 re_00 = re.compile('RCS file: (.+)$')
106 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
106 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
107 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
107 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
108 re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
108 re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
109 re_10 = re.compile('Working file: (.+)$')
109 re_10 = re.compile('Working file: (.+)$')
110 re_20 = re.compile('symbolic names:')
110 re_20 = re.compile('symbolic names:')
111 re_30 = re.compile('\t(.+): ([\\d.]+)$')
111 re_30 = re.compile('\t(.+): ([\\d.]+)$')
112 re_31 = re.compile('----------------------------$')
112 re_31 = re.compile('----------------------------$')
113 re_32 = re.compile('=============================================================================$')
113 re_32 = re.compile('=============================================================================$')
114 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
114 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
115 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?(.*mergepoint:\s+([^;]+);)?')
115 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?(.*mergepoint:\s+([^;]+);)?')
116 re_70 = re.compile('branches: (.+);$')
116 re_70 = re.compile('branches: (.+);$')
117
117
118 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
118 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
119
119
120 prefix = '' # leading path to strip of what we get from CVS
120 prefix = '' # leading path to strip of what we get from CVS
121
121
122 if directory is None:
122 if directory is None:
123 # Current working directory
123 # Current working directory
124
124
125 # Get the real directory in the repository
125 # Get the real directory in the repository
126 try:
126 try:
127 prefix = file(os.path.join('CVS','Repository')).read().strip()
127 prefix = file(os.path.join('CVS','Repository')).read().strip()
128 if prefix == ".":
128 if prefix == ".":
129 prefix = ""
129 prefix = ""
130 directory = prefix
130 directory = prefix
131 except IOError:
131 except IOError:
132 raise logerror('Not a CVS sandbox')
132 raise logerror('Not a CVS sandbox')
133
133
134 if prefix and not prefix.endswith(os.sep):
134 if prefix and not prefix.endswith(os.sep):
135 prefix += os.sep
135 prefix += os.sep
136
136
137 # Use the Root file in the sandbox, if it exists
137 # Use the Root file in the sandbox, if it exists
138 try:
138 try:
139 root = file(os.path.join('CVS','Root')).read().strip()
139 root = file(os.path.join('CVS','Root')).read().strip()
140 except IOError:
140 except IOError:
141 pass
141 pass
142
142
143 if not root:
143 if not root:
144 root = os.environ.get('CVSROOT', '')
144 root = os.environ.get('CVSROOT', '')
145
145
146 # read log cache if one exists
146 # read log cache if one exists
147 oldlog = []
147 oldlog = []
148 date = None
148 date = None
149
149
150 if cache:
150 if cache:
151 cachedir = os.path.expanduser('~/.hg.cvsps')
151 cachedir = os.path.expanduser('~/.hg.cvsps')
152 if not os.path.exists(cachedir):
152 if not os.path.exists(cachedir):
153 os.mkdir(cachedir)
153 os.mkdir(cachedir)
154
154
155 # The cvsps cache pickle needs a uniquified name, based on the
155 # The cvsps cache pickle needs a uniquified name, based on the
156 # repository location. The address may have all sort of nasties
156 # repository location. The address may have all sort of nasties
157 # in it, slashes, colons and such. So here we take just the
157 # in it, slashes, colons and such. So here we take just the
158 # alphanumerics, concatenated in a way that does not mix up the
158 # alphanumerics, concatenated in a way that does not mix up the
159 # various components, so that
159 # various components, so that
160 # :pserver:user@server:/path
160 # :pserver:user@server:/path
161 # and
161 # and
162 # /pserver/user/server/path
162 # /pserver/user/server/path
163 # are mapped to different cache file names.
163 # are mapped to different cache file names.
164 cachefile = root.split(":") + [directory, "cache"]
164 cachefile = root.split(":") + [directory, "cache"]
165 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
165 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
166 cachefile = os.path.join(cachedir,
166 cachefile = os.path.join(cachedir,
167 '.'.join([s for s in cachefile if s]))
167 '.'.join([s for s in cachefile if s]))
168
168
169 if cache == 'update':
169 if cache == 'update':
170 try:
170 try:
171 ui.note(_('reading cvs log cache %s\n') % cachefile)
171 ui.note(_('reading cvs log cache %s\n') % cachefile)
172 oldlog = pickle.load(file(cachefile))
172 oldlog = pickle.load(file(cachefile))
173 ui.note(_('cache has %d log entries\n') % len(oldlog))
173 ui.note(_('cache has %d log entries\n') % len(oldlog))
174 except Exception, e:
174 except Exception, e:
175 ui.note(_('error reading cache: %r\n') % e)
175 ui.note(_('error reading cache: %r\n') % e)
176
176
177 if oldlog:
177 if oldlog:
178 date = oldlog[-1].date # last commit date as a (time,tz) tuple
178 date = oldlog[-1].date # last commit date as a (time,tz) tuple
179 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
179 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
180
180
181 # build the CVS commandline
181 # build the CVS commandline
182 cmd = ['cvs', '-q']
182 cmd = ['cvs', '-q']
183 if root:
183 if root:
184 cmd.append('-d%s' % root)
184 cmd.append('-d%s' % root)
185 p = util.normpath(getrepopath(root))
185 p = util.normpath(getrepopath(root))
186 if not p.endswith('/'):
186 if not p.endswith('/'):
187 p += '/'
187 p += '/'
188 prefix = p + util.normpath(prefix)
188 prefix = p + util.normpath(prefix)
189 cmd.append(['log', 'rlog'][rlog])
189 cmd.append(['log', 'rlog'][rlog])
190 if date:
190 if date:
191 # no space between option and date string
191 # no space between option and date string
192 cmd.append('-d>%s' % date)
192 cmd.append('-d>%s' % date)
193 cmd.append(directory)
193 cmd.append(directory)
194
194
195 # state machine begins here
195 # state machine begins here
196 tags = {} # dictionary of revisions on current file with their tags
196 tags = {} # dictionary of revisions on current file with their tags
197 branchmap = {} # mapping between branch names and revision numbers
197 branchmap = {} # mapping between branch names and revision numbers
198 state = 0
198 state = 0
199 store = False # set when a new record can be appended
199 store = False # set when a new record can be appended
200
200
201 cmd = [util.shellquote(arg) for arg in cmd]
201 cmd = [util.shellquote(arg) for arg in cmd]
202 ui.note(_("running %s\n") % (' '.join(cmd)))
202 ui.note(_("running %s\n") % (' '.join(cmd)))
203 ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
203 ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
204
204
205 pfp = util.popen(' '.join(cmd))
205 pfp = util.popen(' '.join(cmd))
206 peek = pfp.readline()
206 peek = pfp.readline()
207 while True:
207 while True:
208 line = peek
208 line = peek
209 if line == '':
209 if line == '':
210 break
210 break
211 peek = pfp.readline()
211 peek = pfp.readline()
212 if line.endswith('\n'):
212 if line.endswith('\n'):
213 line = line[:-1]
213 line = line[:-1]
214 #ui.debug('state=%d line=%r\n' % (state, line))
214 #ui.debug('state=%d line=%r\n' % (state, line))
215
215
216 if state == 0:
216 if state == 0:
217 # initial state, consume input until we see 'RCS file'
217 # initial state, consume input until we see 'RCS file'
218 match = re_00.match(line)
218 match = re_00.match(line)
219 if match:
219 if match:
220 rcs = match.group(1)
220 rcs = match.group(1)
221 tags = {}
221 tags = {}
222 if rlog:
222 if rlog:
223 filename = util.normpath(rcs[:-2])
223 filename = util.normpath(rcs[:-2])
224 if filename.startswith(prefix):
224 if filename.startswith(prefix):
225 filename = filename[len(prefix):]
225 filename = filename[len(prefix):]
226 if filename.startswith('/'):
226 if filename.startswith('/'):
227 filename = filename[1:]
227 filename = filename[1:]
228 if filename.startswith('Attic/'):
228 if filename.startswith('Attic/'):
229 filename = filename[6:]
229 filename = filename[6:]
230 else:
230 else:
231 filename = filename.replace('/Attic/', '/')
231 filename = filename.replace('/Attic/', '/')
232 state = 2
232 state = 2
233 continue
233 continue
234 state = 1
234 state = 1
235 continue
235 continue
236 match = re_01.match(line)
236 match = re_01.match(line)
237 if match:
237 if match:
238 raise Exception(match.group(1))
238 raise Exception(match.group(1))
239 match = re_02.match(line)
239 match = re_02.match(line)
240 if match:
240 if match:
241 raise Exception(match.group(2))
241 raise Exception(match.group(2))
242 if re_03.match(line):
242 if re_03.match(line):
243 raise Exception(line)
243 raise Exception(line)
244
244
245 elif state == 1:
245 elif state == 1:
246 # expect 'Working file' (only when using log instead of rlog)
246 # expect 'Working file' (only when using log instead of rlog)
247 match = re_10.match(line)
247 match = re_10.match(line)
248 assert match, _('RCS file must be followed by working file')
248 assert match, _('RCS file must be followed by working file')
249 filename = util.normpath(match.group(1))
249 filename = util.normpath(match.group(1))
250 state = 2
250 state = 2
251
251
252 elif state == 2:
252 elif state == 2:
253 # expect 'symbolic names'
253 # expect 'symbolic names'
254 if re_20.match(line):
254 if re_20.match(line):
255 branchmap = {}
255 branchmap = {}
256 state = 3
256 state = 3
257
257
258 elif state == 3:
258 elif state == 3:
259 # read the symbolic names and store as tags
259 # read the symbolic names and store as tags
260 match = re_30.match(line)
260 match = re_30.match(line)
261 if match:
261 if match:
262 rev = [int(x) for x in match.group(2).split('.')]
262 rev = [int(x) for x in match.group(2).split('.')]
263
263
264 # Convert magic branch number to an odd-numbered one
264 # Convert magic branch number to an odd-numbered one
265 revn = len(rev)
265 revn = len(rev)
266 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
266 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
267 rev = rev[:-2] + rev[-1:]
267 rev = rev[:-2] + rev[-1:]
268 rev = tuple(rev)
268 rev = tuple(rev)
269
269
270 if rev not in tags:
270 if rev not in tags:
271 tags[rev] = []
271 tags[rev] = []
272 tags[rev].append(match.group(1))
272 tags[rev].append(match.group(1))
273 branchmap[match.group(1)] = match.group(2)
273 branchmap[match.group(1)] = match.group(2)
274
274
275 elif re_31.match(line):
275 elif re_31.match(line):
276 state = 5
276 state = 5
277 elif re_32.match(line):
277 elif re_32.match(line):
278 state = 0
278 state = 0
279
279
280 elif state == 4:
280 elif state == 4:
281 # expecting '------' separator before first revision
281 # expecting '------' separator before first revision
282 if re_31.match(line):
282 if re_31.match(line):
283 state = 5
283 state = 5
284 else:
284 else:
285 assert not re_32.match(line), _('must have at least some revisions')
285 assert not re_32.match(line), _('must have at least some revisions')
286
286
287 elif state == 5:
287 elif state == 5:
288 # expecting revision number and possibly (ignored) lock indication
288 # expecting revision number and possibly (ignored) lock indication
289 # we create the logentry here from values stored in states 0 to 4,
289 # we create the logentry here from values stored in states 0 to 4,
290 # as this state is re-entered for subsequent revisions of a file.
290 # as this state is re-entered for subsequent revisions of a file.
291 match = re_50.match(line)
291 match = re_50.match(line)
292 assert match, _('expected revision number')
292 assert match, _('expected revision number')
293 e = logentry(rcs=scache(rcs), file=scache(filename),
293 e = logentry(rcs=scache(rcs), file=scache(filename),
294 revision=tuple([int(x) for x in match.group(1).split('.')]),
294 revision=tuple([int(x) for x in match.group(1).split('.')]),
295 branches=[], parent=None,
295 branches=[], parent=None,
296 synthetic=False)
296 synthetic=False)
297 state = 6
297 state = 6
298
298
299 elif state == 6:
299 elif state == 6:
300 # expecting date, author, state, lines changed
300 # expecting date, author, state, lines changed
301 match = re_60.match(line)
301 match = re_60.match(line)
302 assert match, _('revision must be followed by date line')
302 assert match, _('revision must be followed by date line')
303 d = match.group(1)
303 d = match.group(1)
304 if d[2] == '/':
304 if d[2] == '/':
305 # Y2K
305 # Y2K
306 d = '19' + d
306 d = '19' + d
307
307
308 if len(d.split()) != 3:
308 if len(d.split()) != 3:
309 # cvs log dates always in GMT
309 # cvs log dates always in GMT
310 d = d + ' UTC'
310 d = d + ' UTC'
311 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'])
311 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'])
312 e.author = scache(match.group(2))
312 e.author = scache(match.group(2))
313 e.dead = match.group(3).lower() == 'dead'
313 e.dead = match.group(3).lower() == 'dead'
314
314
315 if match.group(5):
315 if match.group(5):
316 if match.group(6):
316 if match.group(6):
317 e.lines = (int(match.group(5)), int(match.group(6)))
317 e.lines = (int(match.group(5)), int(match.group(6)))
318 else:
318 else:
319 e.lines = (int(match.group(5)), 0)
319 e.lines = (int(match.group(5)), 0)
320 elif match.group(6):
320 elif match.group(6):
321 e.lines = (0, int(match.group(6)))
321 e.lines = (0, int(match.group(6)))
322 else:
322 else:
323 e.lines = None
323 e.lines = None
324
324
325 if match.group(7): # cvsnt mergepoint
325 if match.group(7): # cvsnt mergepoint
326 myrev = match.group(8).split('.')
326 myrev = match.group(8).split('.')
327 if len(myrev) == 2: # head
327 if len(myrev) == 2: # head
328 e.mergepoint = 'HEAD'
328 e.mergepoint = 'HEAD'
329 else:
329 else:
330 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
330 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
331 branches = [b for b in branchmap if branchmap[b] == myrev]
331 branches = [b for b in branchmap if branchmap[b] == myrev]
332 assert len(branches) == 1, 'unknown branch: %s' % e.mergepoint
332 assert len(branches) == 1, 'unknown branch: %s' % e.mergepoint
333 e.mergepoint = branches[0]
333 e.mergepoint = branches[0]
334 else:
334 else:
335 e.mergepoint = None
335 e.mergepoint = None
336 e.comment = []
336 e.comment = []
337 state = 7
337 state = 7
338
338
339 elif state == 7:
339 elif state == 7:
340 # read the revision numbers of branches that start at this revision
340 # read the revision numbers of branches that start at this revision
341 # or store the commit log message otherwise
341 # or store the commit log message otherwise
342 m = re_70.match(line)
342 m = re_70.match(line)
343 if m:
343 if m:
344 e.branches = [tuple([int(y) for y in x.strip().split('.')])
344 e.branches = [tuple([int(y) for y in x.strip().split('.')])
345 for x in m.group(1).split(';')]
345 for x in m.group(1).split(';')]
346 state = 8
346 state = 8
347 elif re_31.match(line) and re_50.match(peek):
347 elif re_31.match(line) and re_50.match(peek):
348 state = 5
348 state = 5
349 store = True
349 store = True
350 elif re_32.match(line):
350 elif re_32.match(line):
351 state = 0
351 state = 0
352 store = True
352 store = True
353 else:
353 else:
354 e.comment.append(line)
354 e.comment.append(line)
355
355
356 elif state == 8:
356 elif state == 8:
357 # store commit log message
357 # store commit log message
358 if re_31.match(line):
358 if re_31.match(line):
359 state = 5
359 state = 5
360 store = True
360 store = True
361 elif re_32.match(line):
361 elif re_32.match(line):
362 state = 0
362 state = 0
363 store = True
363 store = True
364 else:
364 else:
365 e.comment.append(line)
365 e.comment.append(line)
366
366
367 # When a file is added on a branch B1, CVS creates a synthetic
367 # When a file is added on a branch B1, CVS creates a synthetic
368 # dead trunk revision 1.1 so that the branch has a root.
368 # dead trunk revision 1.1 so that the branch has a root.
369 # Likewise, if you merge such a file to a later branch B2 (one
369 # Likewise, if you merge such a file to a later branch B2 (one
370 # that already existed when the file was added on B1), CVS
370 # that already existed when the file was added on B1), CVS
371 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
371 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
372 # these revisions now, but mark them synthetic so
372 # these revisions now, but mark them synthetic so
373 # createchangeset() can take care of them.
373 # createchangeset() can take care of them.
374 if (store and
374 if (store and
375 e.dead and
375 e.dead and
376 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
376 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
377 len(e.comment) == 1 and
377 len(e.comment) == 1 and
378 file_added_re.match(e.comment[0])):
378 file_added_re.match(e.comment[0])):
379 ui.debug(_('found synthetic revision in %s: %r\n')
379 ui.debug(_('found synthetic revision in %s: %r\n')
380 % (e.rcs, e.comment[0]))
380 % (e.rcs, e.comment[0]))
381 e.synthetic = True
381 e.synthetic = True
382
382
383 if store:
383 if store:
384 # clean up the results and save in the log.
384 # clean up the results and save in the log.
385 store = False
385 store = False
386 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
386 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
387 e.comment = scache('\n'.join(e.comment))
387 e.comment = scache('\n'.join(e.comment))
388
388
389 revn = len(e.revision)
389 revn = len(e.revision)
390 if revn > 3 and (revn % 2) == 0:
390 if revn > 3 and (revn % 2) == 0:
391 e.branch = tags.get(e.revision[:-1], [None])[0]
391 e.branch = tags.get(e.revision[:-1], [None])[0]
392 else:
392 else:
393 e.branch = None
393 e.branch = None
394
394
395 log.append(e)
395 log.append(e)
396
396
397 if len(log) % 100 == 0:
397 if len(log) % 100 == 0:
398 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
398 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
399
399
400 listsort(log, key=lambda x:(x.rcs, x.revision))
400 listsort(log, key=lambda x:(x.rcs, x.revision))
401
401
402 # find parent revisions of individual files
402 # find parent revisions of individual files
403 versions = {}
403 versions = {}
404 for e in log:
404 for e in log:
405 branch = e.revision[:-1]
405 branch = e.revision[:-1]
406 p = versions.get((e.rcs, branch), None)
406 p = versions.get((e.rcs, branch), None)
407 if p is None:
407 if p is None:
408 p = e.revision[:-2]
408 p = e.revision[:-2]
409 e.parent = p
409 e.parent = p
410 versions[(e.rcs, branch)] = e.revision
410 versions[(e.rcs, branch)] = e.revision
411
411
412 # update the log cache
412 # update the log cache
413 if cache:
413 if cache:
414 if log:
414 if log:
415 # join up the old and new logs
415 # join up the old and new logs
416 listsort(log, key=lambda x:x.date)
416 listsort(log, key=lambda x:x.date)
417
417
418 if oldlog and oldlog[-1].date >= log[0].date:
418 if oldlog and oldlog[-1].date >= log[0].date:
419 raise logerror('Log cache overlaps with new log entries,'
419 raise logerror('Log cache overlaps with new log entries,'
420 ' re-run without cache.')
420 ' re-run without cache.')
421
421
422 log = oldlog + log
422 log = oldlog + log
423
423
424 # write the new cachefile
424 # write the new cachefile
425 ui.note(_('writing cvs log cache %s\n') % cachefile)
425 ui.note(_('writing cvs log cache %s\n') % cachefile)
426 pickle.dump(log, file(cachefile, 'w'))
426 pickle.dump(log, file(cachefile, 'w'))
427 else:
427 else:
428 log = oldlog
428 log = oldlog
429
429
430 ui.status(_('%d log entries\n') % len(log))
430 ui.status(_('%d log entries\n') % len(log))
431
431
432 return log
432 return log
433
433
434
434
435 class changeset(object):
435 class changeset(object):
436 '''Class changeset has the following attributes:
436 '''Class changeset has the following attributes:
437 .id - integer identifying this changeset (list index)
437 .id - integer identifying this changeset (list index)
438 .author - author name as CVS knows it
438 .author - author name as CVS knows it
439 .branch - name of branch this changeset is on, or None
439 .branch - name of branch this changeset is on, or None
440 .comment - commit message
440 .comment - commit message
441 .date - the commit date as a (time,tz) tuple
441 .date - the commit date as a (time,tz) tuple
442 .entries - list of logentry objects in this changeset
442 .entries - list of logentry objects in this changeset
443 .parents - list of one or two parent changesets
443 .parents - list of one or two parent changesets
444 .tags - list of tags on this changeset
444 .tags - list of tags on this changeset
445 .synthetic - from synthetic revision "file ... added on branch ..."
445 .synthetic - from synthetic revision "file ... added on branch ..."
446 .mergepoint- the branch that has been merged from (if present in rlog output)
446 .mergepoint- the branch that has been merged from (if present in rlog output)
447 '''
447 '''
448 def __init__(self, **entries):
448 def __init__(self, **entries):
449 self.__dict__.update(entries)
449 self.__dict__.update(entries)
450
450
451 def __repr__(self):
451 def __repr__(self):
452 return "<%s at 0x%x: %s>" % (self.__class__.__name__,
452 return "<%s at 0x%x: %s>" % (self.__class__.__name__,
453 id(self),
453 id(self),
454 getattr(self, 'id', "(no id)"))
454 getattr(self, 'id', "(no id)"))
455
455
456 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
456 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
457 '''Convert log into changesets.'''
457 '''Convert log into changesets.'''
458
458
459 ui.status(_('creating changesets\n'))
459 ui.status(_('creating changesets\n'))
460
460
461 # Merge changesets
461 # Merge changesets
462
462
463 listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date))
463 listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date))
464
464
465 changesets = []
465 changesets = []
466 files = {}
466 files = set()
467 c = None
467 c = None
468 for i, e in enumerate(log):
468 for i, e in enumerate(log):
469
469
470 # Check if log entry belongs to the current changeset or not.
470 # Check if log entry belongs to the current changeset or not.
471 if not (c and
471 if not (c and
472 e.comment == c.comment and
472 e.comment == c.comment and
473 e.author == c.author and
473 e.author == c.author and
474 e.branch == c.branch and
474 e.branch == c.branch and
475 ((c.date[0] + c.date[1]) <=
475 ((c.date[0] + c.date[1]) <=
476 (e.date[0] + e.date[1]) <=
476 (e.date[0] + e.date[1]) <=
477 (c.date[0] + c.date[1]) + fuzz) and
477 (c.date[0] + c.date[1]) + fuzz) and
478 e.file not in files):
478 e.file not in files):
479 c = changeset(comment=e.comment, author=e.author,
479 c = changeset(comment=e.comment, author=e.author,
480 branch=e.branch, date=e.date, entries=[],
480 branch=e.branch, date=e.date, entries=[],
481 mergepoint=getattr(e, 'mergepoint', None))
481 mergepoint=getattr(e, 'mergepoint', None))
482 changesets.append(c)
482 changesets.append(c)
483 files = {}
483 files = set()
484 if len(changesets) % 100 == 0:
484 if len(changesets) % 100 == 0:
485 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
485 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
486 ui.status(util.ellipsis(t, 80) + '\n')
486 ui.status(util.ellipsis(t, 80) + '\n')
487
487
488 c.entries.append(e)
488 c.entries.append(e)
489 files[e.file] = True
489 files.add(e.file)
490 c.date = e.date # changeset date is date of latest commit in it
490 c.date = e.date # changeset date is date of latest commit in it
491
491
492 # Mark synthetic changesets
492 # Mark synthetic changesets
493
493
494 for c in changesets:
494 for c in changesets:
495 # Synthetic revisions always get their own changeset, because
495 # Synthetic revisions always get their own changeset, because
496 # the log message includes the filename. E.g. if you add file3
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
497 # and file4 on a branch, you get four log entries and three
498 # changesets:
498 # changesets:
499 # "File file3 was added on branch ..." (synthetic, 1 entry)
499 # "File file3 was added on branch ..." (synthetic, 1 entry)
500 # "File file4 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)
501 # "Add file3 and file4 to fix ..." (real, 2 entries)
502 # Hence the check for 1 entry here.
502 # Hence the check for 1 entry here.
503 synth = getattr(c.entries[0], 'synthetic', None)
503 synth = getattr(c.entries[0], 'synthetic', None)
504 c.synthetic = (len(c.entries) == 1 and synth)
504 c.synthetic = (len(c.entries) == 1 and synth)
505
505
506 # Sort files in each changeset
506 # Sort files in each changeset
507
507
508 for c in changesets:
508 for c in changesets:
509 def pathcompare(l, r):
509 def pathcompare(l, r):
510 'Mimic cvsps sorting order'
510 'Mimic cvsps sorting order'
511 l = l.split('/')
511 l = l.split('/')
512 r = r.split('/')
512 r = r.split('/')
513 nl = len(l)
513 nl = len(l)
514 nr = len(r)
514 nr = len(r)
515 n = min(nl, nr)
515 n = min(nl, nr)
516 for i in range(n):
516 for i in range(n):
517 if i + 1 == nl and nl < nr:
517 if i + 1 == nl and nl < nr:
518 return -1
518 return -1
519 elif i + 1 == nr and nl > nr:
519 elif i + 1 == nr and nl > nr:
520 return +1
520 return +1
521 elif l[i] < r[i]:
521 elif l[i] < r[i]:
522 return -1
522 return -1
523 elif l[i] > r[i]:
523 elif l[i] > r[i]:
524 return +1
524 return +1
525 return 0
525 return 0
526 def entitycompare(l, r):
526 def entitycompare(l, r):
527 return pathcompare(l.file, r.file)
527 return pathcompare(l.file, r.file)
528
528
529 c.entries.sort(entitycompare)
529 c.entries.sort(entitycompare)
530
530
531 # Sort changesets by date
531 # Sort changesets by date
532
532
533 def cscmp(l, r):
533 def cscmp(l, r):
534 d = sum(l.date) - sum(r.date)
534 d = sum(l.date) - sum(r.date)
535 if d:
535 if d:
536 return d
536 return d
537
537
538 # detect vendor branches and initial commits on a branch
538 # detect vendor branches and initial commits on a branch
539 le = {}
539 le = {}
540 for e in l.entries:
540 for e in l.entries:
541 le[e.rcs] = e.revision
541 le[e.rcs] = e.revision
542 re = {}
542 re = {}
543 for e in r.entries:
543 for e in r.entries:
544 re[e.rcs] = e.revision
544 re[e.rcs] = e.revision
545
545
546 d = 0
546 d = 0
547 for e in l.entries:
547 for e in l.entries:
548 if re.get(e.rcs, None) == e.parent:
548 if re.get(e.rcs, None) == e.parent:
549 assert not d
549 assert not d
550 d = 1
550 d = 1
551 break
551 break
552
552
553 for e in r.entries:
553 for e in r.entries:
554 if le.get(e.rcs, None) == e.parent:
554 if le.get(e.rcs, None) == e.parent:
555 assert not d
555 assert not d
556 d = -1
556 d = -1
557 break
557 break
558
558
559 return d
559 return d
560
560
561 changesets.sort(cscmp)
561 changesets.sort(cscmp)
562
562
563 # Collect tags
563 # Collect tags
564
564
565 globaltags = {}
565 globaltags = {}
566 for c in changesets:
566 for c in changesets:
567 tags = {}
568 for e in c.entries:
567 for e in c.entries:
569 for tag in e.tags:
568 for tag in e.tags:
570 # remember which is the latest changeset to have this tag
569 # remember which is the latest changeset to have this tag
571 globaltags[tag] = c
570 globaltags[tag] = c
572
571
573 for c in changesets:
572 for c in changesets:
574 tags = {}
573 tags = set()
575 for e in c.entries:
574 for e in c.entries:
576 for tag in e.tags:
575 for tag in e.tags:
577 tags[tag] = True
576 tags.add(tag)
578 # remember tags only if this is the latest changeset to have it
577 # remember tags only if this is the latest changeset to have it
579 c.tags = sorted([tag for tag in tags if globaltags[tag] is c])
578 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
580
579
581 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
580 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
582 # by inserting dummy changesets with two parents, and handle
581 # by inserting dummy changesets with two parents, and handle
583 # {{mergefrombranch BRANCHNAME}} by setting two parents.
582 # {{mergefrombranch BRANCHNAME}} by setting two parents.
584
583
585 if mergeto is None:
584 if mergeto is None:
586 mergeto = r'{{mergetobranch ([-\w]+)}}'
585 mergeto = r'{{mergetobranch ([-\w]+)}}'
587 if mergeto:
586 if mergeto:
588 mergeto = re.compile(mergeto)
587 mergeto = re.compile(mergeto)
589
588
590 if mergefrom is None:
589 if mergefrom is None:
591 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
590 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
592 if mergefrom:
591 if mergefrom:
593 mergefrom = re.compile(mergefrom)
592 mergefrom = re.compile(mergefrom)
594
593
595 versions = {} # changeset index where we saw any particular file version
594 versions = {} # changeset index where we saw any particular file version
596 branches = {} # changeset index where we saw a branch
595 branches = {} # changeset index where we saw a branch
597 n = len(changesets)
596 n = len(changesets)
598 i = 0
597 i = 0
599 while i<n:
598 while i<n:
600 c = changesets[i]
599 c = changesets[i]
601
600
602 for f in c.entries:
601 for f in c.entries:
603 versions[(f.rcs, f.revision)] = i
602 versions[(f.rcs, f.revision)] = i
604
603
605 p = None
604 p = None
606 if c.branch in branches:
605 if c.branch in branches:
607 p = branches[c.branch]
606 p = branches[c.branch]
608 else:
607 else:
609 for f in c.entries:
608 for f in c.entries:
610 p = max(p, versions.get((f.rcs, f.parent), None))
609 p = max(p, versions.get((f.rcs, f.parent), None))
611
610
612 c.parents = []
611 c.parents = []
613 if p is not None:
612 if p is not None:
614 p = changesets[p]
613 p = changesets[p]
615
614
616 # Ensure no changeset has a synthetic changeset as a parent.
615 # Ensure no changeset has a synthetic changeset as a parent.
617 while p.synthetic:
616 while p.synthetic:
618 assert len(p.parents) <= 1, \
617 assert len(p.parents) <= 1, \
619 _('synthetic changeset cannot have multiple parents')
618 _('synthetic changeset cannot have multiple parents')
620 if p.parents:
619 if p.parents:
621 p = p.parents[0]
620 p = p.parents[0]
622 else:
621 else:
623 p = None
622 p = None
624 break
623 break
625
624
626 if p is not None:
625 if p is not None:
627 c.parents.append(p)
626 c.parents.append(p)
628
627
629 if c.mergepoint:
628 if c.mergepoint:
630 if c.mergepoint == 'HEAD':
629 if c.mergepoint == 'HEAD':
631 c.mergepoint = None
630 c.mergepoint = None
632 c.parents.append(changesets[branches[c.mergepoint]])
631 c.parents.append(changesets[branches[c.mergepoint]])
633
632
634 if mergefrom:
633 if mergefrom:
635 m = mergefrom.search(c.comment)
634 m = mergefrom.search(c.comment)
636 if m:
635 if m:
637 m = m.group(1)
636 m = m.group(1)
638 if m == 'HEAD':
637 if m == 'HEAD':
639 m = None
638 m = None
640 try:
639 try:
641 candidate = changesets[branches[m]]
640 candidate = changesets[branches[m]]
642 except KeyError:
641 except KeyError:
643 ui.warn(_("warning: CVS commit message references "
642 ui.warn(_("warning: CVS commit message references "
644 "non-existent branch %r:\n%s\n")
643 "non-existent branch %r:\n%s\n")
645 % (m, c.comment))
644 % (m, c.comment))
646 if m in branches and c.branch != m and not candidate.synthetic:
645 if m in branches and c.branch != m and not candidate.synthetic:
647 c.parents.append(candidate)
646 c.parents.append(candidate)
648
647
649 if mergeto:
648 if mergeto:
650 m = mergeto.search(c.comment)
649 m = mergeto.search(c.comment)
651 if m:
650 if m:
652 try:
651 try:
653 m = m.group(1)
652 m = m.group(1)
654 if m == 'HEAD':
653 if m == 'HEAD':
655 m = None
654 m = None
656 except:
655 except:
657 m = None # if no group found then merge to HEAD
656 m = None # if no group found then merge to HEAD
658 if m in branches and c.branch != m:
657 if m in branches and c.branch != m:
659 # insert empty changeset for merge
658 # insert empty changeset for merge
660 cc = changeset(author=c.author, branch=m, date=c.date,
659 cc = changeset(author=c.author, branch=m, date=c.date,
661 comment='convert-repo: CVS merge from branch %s' % c.branch,
660 comment='convert-repo: CVS merge from branch %s' % c.branch,
662 entries=[], tags=[], parents=[changesets[branches[m]], c])
661 entries=[], tags=[], parents=[changesets[branches[m]], c])
663 changesets.insert(i + 1, cc)
662 changesets.insert(i + 1, cc)
664 branches[m] = i + 1
663 branches[m] = i + 1
665
664
666 # adjust our loop counters now we have inserted a new entry
665 # adjust our loop counters now we have inserted a new entry
667 n += 1
666 n += 1
668 i += 2
667 i += 2
669 continue
668 continue
670
669
671 branches[c.branch] = i
670 branches[c.branch] = i
672 i += 1
671 i += 1
673
672
674 # Drop synthetic changesets (safe now that we have ensured no other
673 # Drop synthetic changesets (safe now that we have ensured no other
675 # changesets can have them as parents).
674 # changesets can have them as parents).
676 i = 0
675 i = 0
677 while i < len(changesets):
676 while i < len(changesets):
678 if changesets[i].synthetic:
677 if changesets[i].synthetic:
679 del changesets[i]
678 del changesets[i]
680 else:
679 else:
681 i += 1
680 i += 1
682
681
683 # Number changesets
682 # Number changesets
684
683
685 for i, c in enumerate(changesets):
684 for i, c in enumerate(changesets):
686 c.id = i + 1
685 c.id = i + 1
687
686
688 ui.status(_('%d changeset entries\n') % len(changesets))
687 ui.status(_('%d changeset entries\n') % len(changesets))
689
688
690 return changesets
689 return changesets
691
690
692
691
693 def debugcvsps(ui, *args, **opts):
692 def debugcvsps(ui, *args, **opts):
694 '''Read CVS rlog for current directory or named path in repository, and
693 '''Read CVS rlog for current directory or named path in repository, and
695 convert the log to changesets based on matching commit log entries and dates.'''
694 convert the log to changesets based on matching commit log entries and dates.'''
696
695
697 if opts["new_cache"]:
696 if opts["new_cache"]:
698 cache = "write"
697 cache = "write"
699 elif opts["update_cache"]:
698 elif opts["update_cache"]:
700 cache = "update"
699 cache = "update"
701 else:
700 else:
702 cache = None
701 cache = None
703
702
704 revisions = opts["revisions"]
703 revisions = opts["revisions"]
705
704
706 try:
705 try:
707 if args:
706 if args:
708 log = []
707 log = []
709 for d in args:
708 for d in args:
710 log += createlog(ui, d, root=opts["root"], cache=cache)
709 log += createlog(ui, d, root=opts["root"], cache=cache)
711 else:
710 else:
712 log = createlog(ui, root=opts["root"], cache=cache)
711 log = createlog(ui, root=opts["root"], cache=cache)
713 except logerror, e:
712 except logerror, e:
714 ui.write("%r\n"%e)
713 ui.write("%r\n"%e)
715 return
714 return
716
715
717 changesets = createchangeset(ui, log, opts["fuzz"])
716 changesets = createchangeset(ui, log, opts["fuzz"])
718 del log
717 del log
719
718
720 # Print changesets (optionally filtered)
719 # Print changesets (optionally filtered)
721
720
722 off = len(revisions)
721 off = len(revisions)
723 branches = {} # latest version number in each branch
722 branches = {} # latest version number in each branch
724 ancestors = {} # parent branch
723 ancestors = {} # parent branch
725 for cs in changesets:
724 for cs in changesets:
726
725
727 if opts["ancestors"]:
726 if opts["ancestors"]:
728 if cs.branch not in branches and cs.parents and cs.parents[0].id:
727 if cs.branch not in branches and cs.parents and cs.parents[0].id:
729 ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
728 ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
730 branches[cs.branch] = cs.id
729 branches[cs.branch] = cs.id
731
730
732 # limit by branches
731 # limit by branches
733 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
732 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
734 continue
733 continue
735
734
736 if not off:
735 if not off:
737 # Note: trailing spaces on several lines here are needed to have
736 # Note: trailing spaces on several lines here are needed to have
738 # bug-for-bug compatibility with cvsps.
737 # bug-for-bug compatibility with cvsps.
739 ui.write('---------------------\n')
738 ui.write('---------------------\n')
740 ui.write('PatchSet %d \n' % cs.id)
739 ui.write('PatchSet %d \n' % cs.id)
741 ui.write('Date: %s\n' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2'))
740 ui.write('Date: %s\n' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2'))
742 ui.write('Author: %s\n' % cs.author)
741 ui.write('Author: %s\n' % cs.author)
743 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
742 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
744 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
743 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
745 ','.join(cs.tags) or '(none)'))
744 ','.join(cs.tags) or '(none)'))
746 if opts["parents"] and cs.parents:
745 if opts["parents"] and cs.parents:
747 if len(cs.parents)>1:
746 if len(cs.parents)>1:
748 ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
747 ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
749 else:
748 else:
750 ui.write('Parent: %d\n' % cs.parents[0].id)
749 ui.write('Parent: %d\n' % cs.parents[0].id)
751
750
752 if opts["ancestors"]:
751 if opts["ancestors"]:
753 b = cs.branch
752 b = cs.branch
754 r = []
753 r = []
755 while b:
754 while b:
756 b, c = ancestors[b]
755 b, c = ancestors[b]
757 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
756 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
758 if r:
757 if r:
759 ui.write('Ancestors: %s\n' % (','.join(r)))
758 ui.write('Ancestors: %s\n' % (','.join(r)))
760
759
761 ui.write('Log:\n')
760 ui.write('Log:\n')
762 ui.write('%s\n\n' % cs.comment)
761 ui.write('%s\n\n' % cs.comment)
763 ui.write('Members: \n')
762 ui.write('Members: \n')
764 for f in cs.entries:
763 for f in cs.entries:
765 fn = f.file
764 fn = f.file
766 if fn.startswith(opts["prefix"]):
765 if fn.startswith(opts["prefix"]):
767 fn = fn[len(opts["prefix"]):]
766 fn = fn[len(opts["prefix"]):]
768 ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
767 ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
769 '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
768 '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
770 ui.write('\n')
769 ui.write('\n')
771
770
772 # have we seen the start tag?
771 # have we seen the start tag?
773 if revisions and off:
772 if revisions and off:
774 if revisions[0] == str(cs.id) or \
773 if revisions[0] == str(cs.id) or \
775 revisions[0] in cs.tags:
774 revisions[0] in cs.tags:
776 off = False
775 off = False
777
776
778 # see if we reached the end tag
777 # see if we reached the end tag
779 if len(revisions)>1 and not off:
778 if len(revisions)>1 and not off:
780 if revisions[1] == str(cs.id) or \
779 if revisions[1] == str(cs.id) or \
781 revisions[1] in cs.tags:
780 revisions[1] in cs.tags:
782 break
781 break
@@ -1,152 +1,152
1 # git.py - git support for the convert extension
1 # git.py - git support for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 import os
8 import os
9 from mercurial import util
9 from mercurial import util
10
10
11 from common import NoRepo, commit, converter_source, checktool
11 from common import NoRepo, commit, converter_source, checktool
12
12
13 class convert_git(converter_source):
13 class convert_git(converter_source):
14 # Windows does not support GIT_DIR= construct while other systems
14 # Windows does not support GIT_DIR= construct while other systems
15 # cannot remove environment variable. Just assume none have
15 # cannot remove environment variable. Just assume none have
16 # both issues.
16 # both issues.
17 if hasattr(os, 'unsetenv'):
17 if hasattr(os, 'unsetenv'):
18 def gitcmd(self, s):
18 def gitcmd(self, s):
19 prevgitdir = os.environ.get('GIT_DIR')
19 prevgitdir = os.environ.get('GIT_DIR')
20 os.environ['GIT_DIR'] = self.path
20 os.environ['GIT_DIR'] = self.path
21 try:
21 try:
22 return util.popen(s, 'rb')
22 return util.popen(s, 'rb')
23 finally:
23 finally:
24 if prevgitdir is None:
24 if prevgitdir is None:
25 del os.environ['GIT_DIR']
25 del os.environ['GIT_DIR']
26 else:
26 else:
27 os.environ['GIT_DIR'] = prevgitdir
27 os.environ['GIT_DIR'] = prevgitdir
28 else:
28 else:
29 def gitcmd(self, s):
29 def gitcmd(self, s):
30 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
30 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
31
31
32 def __init__(self, ui, path, rev=None):
32 def __init__(self, ui, path, rev=None):
33 super(convert_git, self).__init__(ui, path, rev=rev)
33 super(convert_git, self).__init__(ui, path, rev=rev)
34
34
35 if os.path.isdir(path + "/.git"):
35 if os.path.isdir(path + "/.git"):
36 path += "/.git"
36 path += "/.git"
37 if not os.path.exists(path + "/objects"):
37 if not os.path.exists(path + "/objects"):
38 raise NoRepo("%s does not look like a Git repo" % path)
38 raise NoRepo("%s does not look like a Git repo" % path)
39
39
40 checktool('git', 'git')
40 checktool('git', 'git')
41
41
42 self.path = path
42 self.path = path
43
43
44 def getheads(self):
44 def getheads(self):
45 if not self.rev:
45 if not self.rev:
46 return self.gitcmd('git rev-parse --branches --remotes').read().splitlines()
46 return self.gitcmd('git rev-parse --branches --remotes').read().splitlines()
47 else:
47 else:
48 fh = self.gitcmd("git rev-parse --verify %s" % self.rev)
48 fh = self.gitcmd("git rev-parse --verify %s" % self.rev)
49 return [fh.read()[:-1]]
49 return [fh.read()[:-1]]
50
50
51 def catfile(self, rev, type):
51 def catfile(self, rev, type):
52 if rev == "0" * 40: raise IOError()
52 if rev == "0" * 40: raise IOError()
53 fh = self.gitcmd("git cat-file %s %s" % (type, rev))
53 fh = self.gitcmd("git cat-file %s %s" % (type, rev))
54 return fh.read()
54 return fh.read()
55
55
56 def getfile(self, name, rev):
56 def getfile(self, name, rev):
57 return self.catfile(rev, "blob")
57 return self.catfile(rev, "blob")
58
58
59 def getmode(self, name, rev):
59 def getmode(self, name, rev):
60 return self.modecache[(name, rev)]
60 return self.modecache[(name, rev)]
61
61
62 def getchanges(self, version):
62 def getchanges(self, version):
63 self.modecache = {}
63 self.modecache = {}
64 fh = self.gitcmd("git diff-tree -z --root -m -r %s" % version)
64 fh = self.gitcmd("git diff-tree -z --root -m -r %s" % version)
65 changes = []
65 changes = []
66 seen = {}
66 seen = set()
67 entry = None
67 entry = None
68 for l in fh.read().split('\x00'):
68 for l in fh.read().split('\x00'):
69 if not entry:
69 if not entry:
70 if not l.startswith(':'):
70 if not l.startswith(':'):
71 continue
71 continue
72 entry = l
72 entry = l
73 continue
73 continue
74 f = l
74 f = l
75 if f not in seen:
75 if f not in seen:
76 seen[f] = 1
76 seen.add(f)
77 entry = entry.split()
77 entry = entry.split()
78 h = entry[3]
78 h = entry[3]
79 p = (entry[1] == "100755")
79 p = (entry[1] == "100755")
80 s = (entry[1] == "120000")
80 s = (entry[1] == "120000")
81 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
81 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
82 changes.append((f, h))
82 changes.append((f, h))
83 entry = None
83 entry = None
84 return (changes, {})
84 return (changes, {})
85
85
86 def getcommit(self, version):
86 def getcommit(self, version):
87 c = self.catfile(version, "commit") # read the commit hash
87 c = self.catfile(version, "commit") # read the commit hash
88 end = c.find("\n\n")
88 end = c.find("\n\n")
89 message = c[end+2:]
89 message = c[end+2:]
90 message = self.recode(message)
90 message = self.recode(message)
91 l = c[:end].splitlines()
91 l = c[:end].splitlines()
92 parents = []
92 parents = []
93 author = committer = None
93 author = committer = None
94 for e in l[1:]:
94 for e in l[1:]:
95 n, v = e.split(" ", 1)
95 n, v = e.split(" ", 1)
96 if n == "author":
96 if n == "author":
97 p = v.split()
97 p = v.split()
98 tm, tz = p[-2:]
98 tm, tz = p[-2:]
99 author = " ".join(p[:-2])
99 author = " ".join(p[:-2])
100 if author[0] == "<": author = author[1:-1]
100 if author[0] == "<": author = author[1:-1]
101 author = self.recode(author)
101 author = self.recode(author)
102 if n == "committer":
102 if n == "committer":
103 p = v.split()
103 p = v.split()
104 tm, tz = p[-2:]
104 tm, tz = p[-2:]
105 committer = " ".join(p[:-2])
105 committer = " ".join(p[:-2])
106 if committer[0] == "<": committer = committer[1:-1]
106 if committer[0] == "<": committer = committer[1:-1]
107 committer = self.recode(committer)
107 committer = self.recode(committer)
108 if n == "parent": parents.append(v)
108 if n == "parent": parents.append(v)
109
109
110 if committer and committer != author:
110 if committer and committer != author:
111 message += "\ncommitter: %s\n" % committer
111 message += "\ncommitter: %s\n" % committer
112 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
112 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
113 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
113 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
114 date = tm + " " + str(tz)
114 date = tm + " " + str(tz)
115
115
116 c = commit(parents=parents, date=date, author=author, desc=message,
116 c = commit(parents=parents, date=date, author=author, desc=message,
117 rev=version)
117 rev=version)
118 return c
118 return c
119
119
120 def gettags(self):
120 def gettags(self):
121 tags = {}
121 tags = {}
122 fh = self.gitcmd('git ls-remote --tags "%s"' % self.path)
122 fh = self.gitcmd('git ls-remote --tags "%s"' % self.path)
123 prefix = 'refs/tags/'
123 prefix = 'refs/tags/'
124 for line in fh:
124 for line in fh:
125 line = line.strip()
125 line = line.strip()
126 if not line.endswith("^{}"):
126 if not line.endswith("^{}"):
127 continue
127 continue
128 node, tag = line.split(None, 1)
128 node, tag = line.split(None, 1)
129 if not tag.startswith(prefix):
129 if not tag.startswith(prefix):
130 continue
130 continue
131 tag = tag[len(prefix):-3]
131 tag = tag[len(prefix):-3]
132 tags[tag] = node
132 tags[tag] = node
133
133
134 return tags
134 return tags
135
135
136 def getchangedfiles(self, version, i):
136 def getchangedfiles(self, version, i):
137 changes = []
137 changes = []
138 if i is None:
138 if i is None:
139 fh = self.gitcmd("git diff-tree --root -m -r %s" % version)
139 fh = self.gitcmd("git diff-tree --root -m -r %s" % version)
140 for l in fh:
140 for l in fh:
141 if "\t" not in l:
141 if "\t" not in l:
142 continue
142 continue
143 m, f = l[:-1].split("\t")
143 m, f = l[:-1].split("\t")
144 changes.append(f)
144 changes.append(f)
145 fh.close()
145 fh.close()
146 else:
146 else:
147 fh = self.gitcmd('git diff-tree --name-only --root -r %s "%s^%s" --'
147 fh = self.gitcmd('git diff-tree --name-only --root -r %s "%s^%s" --'
148 % (version, version, i+1))
148 % (version, version, i+1))
149 changes = [f.rstrip('\n') for f in fh]
149 changes = [f.rstrip('\n') for f in fh]
150 fh.close()
150 fh.close()
151
151
152 return changes
152 return changes
@@ -1,338 +1,338
1 # hg.py - hg backend for convert extension
1 # hg.py - hg backend for convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 # Notes for hg->hg conversion:
8 # Notes for hg->hg conversion:
9 #
9 #
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 # of commit messages, but new versions do. Changesets created by
11 # of commit messages, but new versions do. Changesets created by
12 # those older versions, then converted, may thus have different
12 # those older versions, then converted, may thus have different
13 # hashes for changesets that are otherwise identical.
13 # hashes for changesets that are otherwise identical.
14 #
14 #
15 # * By default, the source revision is stored in the converted
15 # * By default, the source revision is stored in the converted
16 # revision. This will cause the converted revision to have a
16 # revision. This will cause the converted revision to have a
17 # different identity than the source. To avoid this, use the
17 # different identity than the source. To avoid this, use the
18 # following option: "--config convert.hg.saverev=false"
18 # following option: "--config convert.hg.saverev=false"
19
19
20
20
21 import os, time
21 import os, time
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23 from mercurial.node import bin, hex, nullid
23 from mercurial.node import bin, hex, nullid
24 from mercurial import hg, util, context, error
24 from mercurial import hg, util, context, error
25
25
26 from common import NoRepo, commit, converter_source, converter_sink
26 from common import NoRepo, commit, converter_source, converter_sink
27
27
28 class mercurial_sink(converter_sink):
28 class mercurial_sink(converter_sink):
29 def __init__(self, ui, path):
29 def __init__(self, ui, path):
30 converter_sink.__init__(self, ui, path)
30 converter_sink.__init__(self, ui, path)
31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
34 self.lastbranch = None
34 self.lastbranch = None
35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
36 try:
36 try:
37 self.repo = hg.repository(self.ui, path)
37 self.repo = hg.repository(self.ui, path)
38 if not self.repo.local():
38 if not self.repo.local():
39 raise NoRepo(_('%s is not a local Mercurial repo') % path)
39 raise NoRepo(_('%s is not a local Mercurial repo') % path)
40 except error.RepoError, err:
40 except error.RepoError, err:
41 ui.traceback()
41 ui.traceback()
42 raise NoRepo(err.args[0])
42 raise NoRepo(err.args[0])
43 else:
43 else:
44 try:
44 try:
45 ui.status(_('initializing destination %s repository\n') % path)
45 ui.status(_('initializing destination %s repository\n') % path)
46 self.repo = hg.repository(self.ui, path, create=True)
46 self.repo = hg.repository(self.ui, path, create=True)
47 if not self.repo.local():
47 if not self.repo.local():
48 raise NoRepo(_('%s is not a local Mercurial repo') % path)
48 raise NoRepo(_('%s is not a local Mercurial repo') % path)
49 self.created.append(path)
49 self.created.append(path)
50 except error.RepoError:
50 except error.RepoError:
51 ui.traceback()
51 ui.traceback()
52 raise NoRepo("could not create hg repo %s as sink" % path)
52 raise NoRepo("could not create hg repo %s as sink" % path)
53 self.lock = None
53 self.lock = None
54 self.wlock = None
54 self.wlock = None
55 self.filemapmode = False
55 self.filemapmode = False
56
56
57 def before(self):
57 def before(self):
58 self.ui.debug(_('run hg sink pre-conversion action\n'))
58 self.ui.debug(_('run hg sink pre-conversion action\n'))
59 self.wlock = self.repo.wlock()
59 self.wlock = self.repo.wlock()
60 self.lock = self.repo.lock()
60 self.lock = self.repo.lock()
61
61
62 def after(self):
62 def after(self):
63 self.ui.debug(_('run hg sink post-conversion action\n'))
63 self.ui.debug(_('run hg sink post-conversion action\n'))
64 self.lock.release()
64 self.lock.release()
65 self.wlock.release()
65 self.wlock.release()
66
66
67 def revmapfile(self):
67 def revmapfile(self):
68 return os.path.join(self.path, ".hg", "shamap")
68 return os.path.join(self.path, ".hg", "shamap")
69
69
70 def authorfile(self):
70 def authorfile(self):
71 return os.path.join(self.path, ".hg", "authormap")
71 return os.path.join(self.path, ".hg", "authormap")
72
72
73 def getheads(self):
73 def getheads(self):
74 h = self.repo.changelog.heads()
74 h = self.repo.changelog.heads()
75 return [ hex(x) for x in h ]
75 return [ hex(x) for x in h ]
76
76
77 def setbranch(self, branch, pbranches):
77 def setbranch(self, branch, pbranches):
78 if not self.clonebranches:
78 if not self.clonebranches:
79 return
79 return
80
80
81 setbranch = (branch != self.lastbranch)
81 setbranch = (branch != self.lastbranch)
82 self.lastbranch = branch
82 self.lastbranch = branch
83 if not branch:
83 if not branch:
84 branch = 'default'
84 branch = 'default'
85 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
85 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
86 pbranch = pbranches and pbranches[0][1] or 'default'
86 pbranch = pbranches and pbranches[0][1] or 'default'
87
87
88 branchpath = os.path.join(self.path, branch)
88 branchpath = os.path.join(self.path, branch)
89 if setbranch:
89 if setbranch:
90 self.after()
90 self.after()
91 try:
91 try:
92 self.repo = hg.repository(self.ui, branchpath)
92 self.repo = hg.repository(self.ui, branchpath)
93 except:
93 except:
94 self.repo = hg.repository(self.ui, branchpath, create=True)
94 self.repo = hg.repository(self.ui, branchpath, create=True)
95 self.before()
95 self.before()
96
96
97 # pbranches may bring revisions from other branches (merge parents)
97 # pbranches may bring revisions from other branches (merge parents)
98 # Make sure we have them, or pull them.
98 # Make sure we have them, or pull them.
99 missings = {}
99 missings = {}
100 for b in pbranches:
100 for b in pbranches:
101 try:
101 try:
102 self.repo.lookup(b[0])
102 self.repo.lookup(b[0])
103 except:
103 except:
104 missings.setdefault(b[1], []).append(b[0])
104 missings.setdefault(b[1], []).append(b[0])
105
105
106 if missings:
106 if missings:
107 self.after()
107 self.after()
108 for pbranch, heads in missings.iteritems():
108 for pbranch, heads in missings.iteritems():
109 pbranchpath = os.path.join(self.path, pbranch)
109 pbranchpath = os.path.join(self.path, pbranch)
110 prepo = hg.repository(self.ui, pbranchpath)
110 prepo = hg.repository(self.ui, pbranchpath)
111 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
111 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
112 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
112 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
113 self.before()
113 self.before()
114
114
115 def putcommit(self, files, copies, parents, commit, source):
115 def putcommit(self, files, copies, parents, commit, source):
116
116
117 files = dict(files)
117 files = dict(files)
118 def getfilectx(repo, memctx, f):
118 def getfilectx(repo, memctx, f):
119 v = files[f]
119 v = files[f]
120 data = source.getfile(f, v)
120 data = source.getfile(f, v)
121 e = source.getmode(f, v)
121 e = source.getmode(f, v)
122 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
122 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
123
123
124 pl = []
124 pl = []
125 for p in parents:
125 for p in parents:
126 if p not in pl:
126 if p not in pl:
127 pl.append(p)
127 pl.append(p)
128 parents = pl
128 parents = pl
129 nparents = len(parents)
129 nparents = len(parents)
130 if self.filemapmode and nparents == 1:
130 if self.filemapmode and nparents == 1:
131 m1node = self.repo.changelog.read(bin(parents[0]))[0]
131 m1node = self.repo.changelog.read(bin(parents[0]))[0]
132 parent = parents[0]
132 parent = parents[0]
133
133
134 if len(parents) < 2: parents.append("0" * 40)
134 if len(parents) < 2: parents.append("0" * 40)
135 if len(parents) < 2: parents.append("0" * 40)
135 if len(parents) < 2: parents.append("0" * 40)
136 p2 = parents.pop(0)
136 p2 = parents.pop(0)
137
137
138 text = commit.desc
138 text = commit.desc
139 extra = commit.extra.copy()
139 extra = commit.extra.copy()
140 if self.branchnames and commit.branch:
140 if self.branchnames and commit.branch:
141 extra['branch'] = commit.branch
141 extra['branch'] = commit.branch
142 if commit.rev:
142 if commit.rev:
143 extra['convert_revision'] = commit.rev
143 extra['convert_revision'] = commit.rev
144
144
145 while parents:
145 while parents:
146 p1 = p2
146 p1 = p2
147 p2 = parents.pop(0)
147 p2 = parents.pop(0)
148 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(), getfilectx,
148 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(), getfilectx,
149 commit.author, commit.date, extra)
149 commit.author, commit.date, extra)
150 self.repo.commitctx(ctx)
150 self.repo.commitctx(ctx)
151 text = "(octopus merge fixup)\n"
151 text = "(octopus merge fixup)\n"
152 p2 = hex(self.repo.changelog.tip())
152 p2 = hex(self.repo.changelog.tip())
153
153
154 if self.filemapmode and nparents == 1:
154 if self.filemapmode and nparents == 1:
155 man = self.repo.manifest
155 man = self.repo.manifest
156 mnode = self.repo.changelog.read(bin(p2))[0]
156 mnode = self.repo.changelog.read(bin(p2))[0]
157 if not man.cmp(m1node, man.revision(mnode)):
157 if not man.cmp(m1node, man.revision(mnode)):
158 self.repo.rollback()
158 self.repo.rollback()
159 return parent
159 return parent
160 return p2
160 return p2
161
161
162 def puttags(self, tags):
162 def puttags(self, tags):
163 try:
163 try:
164 parentctx = self.repo[self.tagsbranch]
164 parentctx = self.repo[self.tagsbranch]
165 tagparent = parentctx.node()
165 tagparent = parentctx.node()
166 except error.RepoError:
166 except error.RepoError:
167 parentctx = None
167 parentctx = None
168 tagparent = nullid
168 tagparent = nullid
169
169
170 try:
170 try:
171 oldlines = sorted(parentctx['.hgtags'].data().splitlines(1))
171 oldlines = sorted(parentctx['.hgtags'].data().splitlines(1))
172 except:
172 except:
173 oldlines = []
173 oldlines = []
174
174
175 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
175 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
176 if newlines == oldlines:
176 if newlines == oldlines:
177 return None
177 return None
178 data = "".join(newlines)
178 data = "".join(newlines)
179 def getfilectx(repo, memctx, f):
179 def getfilectx(repo, memctx, f):
180 return context.memfilectx(f, data, False, False, None)
180 return context.memfilectx(f, data, False, False, None)
181
181
182 self.ui.status(_("updating tags\n"))
182 self.ui.status(_("updating tags\n"))
183 date = "%s 0" % int(time.mktime(time.gmtime()))
183 date = "%s 0" % int(time.mktime(time.gmtime()))
184 extra = {'branch': self.tagsbranch}
184 extra = {'branch': self.tagsbranch}
185 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
185 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
186 [".hgtags"], getfilectx, "convert-repo", date,
186 [".hgtags"], getfilectx, "convert-repo", date,
187 extra)
187 extra)
188 self.repo.commitctx(ctx)
188 self.repo.commitctx(ctx)
189 return hex(self.repo.changelog.tip())
189 return hex(self.repo.changelog.tip())
190
190
191 def setfilemapmode(self, active):
191 def setfilemapmode(self, active):
192 self.filemapmode = active
192 self.filemapmode = active
193
193
194 class mercurial_source(converter_source):
194 class mercurial_source(converter_source):
195 def __init__(self, ui, path, rev=None):
195 def __init__(self, ui, path, rev=None):
196 converter_source.__init__(self, ui, path, rev)
196 converter_source.__init__(self, ui, path, rev)
197 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
197 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
198 self.ignored = {}
198 self.ignored = set()
199 self.saverev = ui.configbool('convert', 'hg.saverev', False)
199 self.saverev = ui.configbool('convert', 'hg.saverev', False)
200 try:
200 try:
201 self.repo = hg.repository(self.ui, path)
201 self.repo = hg.repository(self.ui, path)
202 # try to provoke an exception if this isn't really a hg
202 # try to provoke an exception if this isn't really a hg
203 # repo, but some other bogus compatible-looking url
203 # repo, but some other bogus compatible-looking url
204 if not self.repo.local():
204 if not self.repo.local():
205 raise error.RepoError()
205 raise error.RepoError()
206 except error.RepoError:
206 except error.RepoError:
207 ui.traceback()
207 ui.traceback()
208 raise NoRepo("%s is not a local Mercurial repo" % path)
208 raise NoRepo("%s is not a local Mercurial repo" % path)
209 self.lastrev = None
209 self.lastrev = None
210 self.lastctx = None
210 self.lastctx = None
211 self._changescache = None
211 self._changescache = None
212 self.convertfp = None
212 self.convertfp = None
213 # Restrict converted revisions to startrev descendants
213 # Restrict converted revisions to startrev descendants
214 startnode = ui.config('convert', 'hg.startrev')
214 startnode = ui.config('convert', 'hg.startrev')
215 if startnode is not None:
215 if startnode is not None:
216 try:
216 try:
217 startnode = self.repo.lookup(startnode)
217 startnode = self.repo.lookup(startnode)
218 except error.RepoError:
218 except error.RepoError:
219 raise util.Abort(_('%s is not a valid start revision')
219 raise util.Abort(_('%s is not a valid start revision')
220 % startnode)
220 % startnode)
221 startrev = self.repo.changelog.rev(startnode)
221 startrev = self.repo.changelog.rev(startnode)
222 children = {startnode: 1}
222 children = {startnode: 1}
223 for rev in self.repo.changelog.descendants(startrev):
223 for rev in self.repo.changelog.descendants(startrev):
224 children[self.repo.changelog.node(rev)] = 1
224 children[self.repo.changelog.node(rev)] = 1
225 self.keep = children.__contains__
225 self.keep = children.__contains__
226 else:
226 else:
227 self.keep = util.always
227 self.keep = util.always
228
228
229 def changectx(self, rev):
229 def changectx(self, rev):
230 if self.lastrev != rev:
230 if self.lastrev != rev:
231 self.lastctx = self.repo[rev]
231 self.lastctx = self.repo[rev]
232 self.lastrev = rev
232 self.lastrev = rev
233 return self.lastctx
233 return self.lastctx
234
234
235 def parents(self, ctx):
235 def parents(self, ctx):
236 return [p.node() for p in ctx.parents()
236 return [p.node() for p in ctx.parents()
237 if p and self.keep(p.node())]
237 if p and self.keep(p.node())]
238
238
239 def getheads(self):
239 def getheads(self):
240 if self.rev:
240 if self.rev:
241 heads = [self.repo[self.rev].node()]
241 heads = [self.repo[self.rev].node()]
242 else:
242 else:
243 heads = self.repo.heads()
243 heads = self.repo.heads()
244 return [hex(h) for h in heads if self.keep(h)]
244 return [hex(h) for h in heads if self.keep(h)]
245
245
246 def getfile(self, name, rev):
246 def getfile(self, name, rev):
247 try:
247 try:
248 return self.changectx(rev)[name].data()
248 return self.changectx(rev)[name].data()
249 except error.LookupError, err:
249 except error.LookupError, err:
250 raise IOError(err)
250 raise IOError(err)
251
251
252 def getmode(self, name, rev):
252 def getmode(self, name, rev):
253 return self.changectx(rev).manifest().flags(name)
253 return self.changectx(rev).manifest().flags(name)
254
254
255 def getchanges(self, rev):
255 def getchanges(self, rev):
256 ctx = self.changectx(rev)
256 ctx = self.changectx(rev)
257 parents = self.parents(ctx)
257 parents = self.parents(ctx)
258 if not parents:
258 if not parents:
259 files = sorted(ctx.manifest())
259 files = sorted(ctx.manifest())
260 if self.ignoreerrors:
260 if self.ignoreerrors:
261 # calling getcopies() is a simple way to detect missing
261 # calling getcopies() is a simple way to detect missing
262 # revlogs and populate self.ignored
262 # revlogs and populate self.ignored
263 self.getcopies(ctx, files)
263 self.getcopies(ctx, files)
264 return [(f, rev) for f in files if f not in self.ignored], {}
264 return [(f, rev) for f in files if f not in self.ignored], {}
265 if self._changescache and self._changescache[0] == rev:
265 if self._changescache and self._changescache[0] == rev:
266 m, a, r = self._changescache[1]
266 m, a, r = self._changescache[1]
267 else:
267 else:
268 m, a, r = self.repo.status(parents[0], ctx.node())[:3]
268 m, a, r = self.repo.status(parents[0], ctx.node())[:3]
269 # getcopies() detects missing revlogs early, run it before
269 # getcopies() detects missing revlogs early, run it before
270 # filtering the changes.
270 # filtering the changes.
271 copies = self.getcopies(ctx, m + a)
271 copies = self.getcopies(ctx, m + a)
272 changes = [(name, rev) for name in m + a + r
272 changes = [(name, rev) for name in m + a + r
273 if name not in self.ignored]
273 if name not in self.ignored]
274 return sorted(changes), copies
274 return sorted(changes), copies
275
275
276 def getcopies(self, ctx, files):
276 def getcopies(self, ctx, files):
277 copies = {}
277 copies = {}
278 for name in files:
278 for name in files:
279 if name in self.ignored:
279 if name in self.ignored:
280 continue
280 continue
281 try:
281 try:
282 copysource, copynode = ctx.filectx(name).renamed()
282 copysource, copynode = ctx.filectx(name).renamed()
283 if copysource in self.ignored or not self.keep(copynode):
283 if copysource in self.ignored or not self.keep(copynode):
284 continue
284 continue
285 copies[name] = copysource
285 copies[name] = copysource
286 except TypeError:
286 except TypeError:
287 pass
287 pass
288 except error.LookupError, e:
288 except error.LookupError, e:
289 if not self.ignoreerrors:
289 if not self.ignoreerrors:
290 raise
290 raise
291 self.ignored[name] = 1
291 self.ignored.add(name)
292 self.ui.warn(_('ignoring: %s\n') % e)
292 self.ui.warn(_('ignoring: %s\n') % e)
293 return copies
293 return copies
294
294
295 def getcommit(self, rev):
295 def getcommit(self, rev):
296 ctx = self.changectx(rev)
296 ctx = self.changectx(rev)
297 parents = [hex(p) for p in self.parents(ctx)]
297 parents = [hex(p) for p in self.parents(ctx)]
298 if self.saverev:
298 if self.saverev:
299 crev = rev
299 crev = rev
300 else:
300 else:
301 crev = None
301 crev = None
302 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
302 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
303 desc=ctx.description(), rev=crev, parents=parents,
303 desc=ctx.description(), rev=crev, parents=parents,
304 branch=ctx.branch(), extra=ctx.extra())
304 branch=ctx.branch(), extra=ctx.extra())
305
305
306 def gettags(self):
306 def gettags(self):
307 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
307 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
308 return dict([(name, hex(node)) for name, node in tags
308 return dict([(name, hex(node)) for name, node in tags
309 if self.keep(node)])
309 if self.keep(node)])
310
310
311 def getchangedfiles(self, rev, i):
311 def getchangedfiles(self, rev, i):
312 ctx = self.changectx(rev)
312 ctx = self.changectx(rev)
313 parents = self.parents(ctx)
313 parents = self.parents(ctx)
314 if not parents and i is None:
314 if not parents and i is None:
315 i = 0
315 i = 0
316 changes = [], ctx.manifest().keys(), []
316 changes = [], ctx.manifest().keys(), []
317 else:
317 else:
318 i = i or 0
318 i = i or 0
319 changes = self.repo.status(parents[i], ctx.node())[:3]
319 changes = self.repo.status(parents[i], ctx.node())[:3]
320 changes = [[f for f in l if f not in self.ignored] for l in changes]
320 changes = [[f for f in l if f not in self.ignored] for l in changes]
321
321
322 if i == 0:
322 if i == 0:
323 self._changescache = (rev, changes)
323 self._changescache = (rev, changes)
324
324
325 return changes[0] + changes[1] + changes[2]
325 return changes[0] + changes[1] + changes[2]
326
326
327 def converted(self, rev, destrev):
327 def converted(self, rev, destrev):
328 if self.convertfp is None:
328 if self.convertfp is None:
329 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
329 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
330 'a')
330 'a')
331 self.convertfp.write('%s %s\n' % (destrev, rev))
331 self.convertfp.write('%s %s\n' % (destrev, rev))
332 self.convertfp.flush()
332 self.convertfp.flush()
333
333
334 def before(self):
334 def before(self):
335 self.ui.debug(_('run hg source pre-conversion action\n'))
335 self.ui.debug(_('run hg source pre-conversion action\n'))
336
336
337 def after(self):
337 def after(self):
338 self.ui.debug(_('run hg source post-conversion action\n'))
338 self.ui.debug(_('run hg source post-conversion action\n'))
General Comments 0
You need to be logged in to leave comments. Login now