##// END OF EJS Templates
convert: look up copies in getchanges instead of getcommit...
Brendan Cully -
r5121:ef338e34 default
parent child Browse files
Show More
@@ -1,441 +1,442
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from common import NoRepo, converter_source, converter_sink
8 from common import NoRepo, converter_source, converter_sink
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from git import convert_git
10 from git import convert_git
11 from hg import mercurial_source, mercurial_sink
11 from hg import mercurial_source, mercurial_sink
12 from subversion import convert_svn
12 from subversion import convert_svn
13
13
14 import os, shlex, shutil
14 import os, shlex, shutil
15 from mercurial import hg, ui, util, commands
15 from mercurial import hg, ui, util, commands
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17
17
18 commands.norepo += " convert"
18 commands.norepo += " convert"
19
19
20 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
20 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
21 mercurial_sink]
21 mercurial_sink]
22
22
23 def convertsource(ui, path, **opts):
23 def convertsource(ui, path, **opts):
24 for c in converters:
24 for c in converters:
25 try:
25 try:
26 return c.getcommit and c(ui, path, **opts)
26 return c.getcommit and c(ui, path, **opts)
27 except (AttributeError, NoRepo):
27 except (AttributeError, NoRepo):
28 pass
28 pass
29 raise util.Abort('%s: unknown repository type' % path)
29 raise util.Abort('%s: unknown repository type' % path)
30
30
31 def convertsink(ui, path):
31 def convertsink(ui, path):
32 if not os.path.isdir(path):
32 if not os.path.isdir(path):
33 raise util.Abort("%s: not a directory" % path)
33 raise util.Abort("%s: not a directory" % path)
34 for c in converters:
34 for c in converters:
35 try:
35 try:
36 return c.putcommit and c(ui, path)
36 return c.putcommit and c(ui, path)
37 except (AttributeError, NoRepo):
37 except (AttributeError, NoRepo):
38 pass
38 pass
39 raise util.Abort('%s: unknown repository type' % path)
39 raise util.Abort('%s: unknown repository type' % path)
40
40
41 class convert(object):
41 class convert(object):
42 def __init__(self, ui, source, dest, revmapfile, filemapper, opts):
42 def __init__(self, ui, source, dest, revmapfile, filemapper, opts):
43
43
44 self.source = source
44 self.source = source
45 self.dest = dest
45 self.dest = dest
46 self.ui = ui
46 self.ui = ui
47 self.opts = opts
47 self.opts = opts
48 self.commitcache = {}
48 self.commitcache = {}
49 self.revmapfile = revmapfile
49 self.revmapfile = revmapfile
50 self.revmapfilefd = None
50 self.revmapfilefd = None
51 self.authors = {}
51 self.authors = {}
52 self.authorfile = None
52 self.authorfile = None
53 self.mapfile = filemapper
53 self.mapfile = filemapper
54
54
55 self.map = {}
55 self.map = {}
56 try:
56 try:
57 origrevmapfile = open(self.revmapfile, 'r')
57 origrevmapfile = open(self.revmapfile, 'r')
58 for l in origrevmapfile:
58 for l in origrevmapfile:
59 sv, dv = l[:-1].split()
59 sv, dv = l[:-1].split()
60 self.map[sv] = dv
60 self.map[sv] = dv
61 origrevmapfile.close()
61 origrevmapfile.close()
62 except IOError:
62 except IOError:
63 pass
63 pass
64
64
65 # Read first the dst author map if any
65 # Read first the dst author map if any
66 authorfile = self.dest.authorfile()
66 authorfile = self.dest.authorfile()
67 if authorfile and os.path.exists(authorfile):
67 if authorfile and os.path.exists(authorfile):
68 self.readauthormap(authorfile)
68 self.readauthormap(authorfile)
69 # Extend/Override with new author map if necessary
69 # Extend/Override with new author map if necessary
70 if opts.get('authors'):
70 if opts.get('authors'):
71 self.readauthormap(opts.get('authors'))
71 self.readauthormap(opts.get('authors'))
72 self.authorfile = self.dest.authorfile()
72 self.authorfile = self.dest.authorfile()
73
73
74 def walktree(self, heads):
74 def walktree(self, heads):
75 '''Return a mapping that identifies the uncommitted parents of every
75 '''Return a mapping that identifies the uncommitted parents of every
76 uncommitted changeset.'''
76 uncommitted changeset.'''
77 visit = heads
77 visit = heads
78 known = {}
78 known = {}
79 parents = {}
79 parents = {}
80 while visit:
80 while visit:
81 n = visit.pop(0)
81 n = visit.pop(0)
82 if n in known or n in self.map: continue
82 if n in known or n in self.map: continue
83 known[n] = 1
83 known[n] = 1
84 self.commitcache[n] = self.source.getcommit(n)
84 self.commitcache[n] = self.source.getcommit(n)
85 cp = self.commitcache[n].parents
85 cp = self.commitcache[n].parents
86 parents[n] = []
86 parents[n] = []
87 for p in cp:
87 for p in cp:
88 parents[n].append(p)
88 parents[n].append(p)
89 visit.append(p)
89 visit.append(p)
90
90
91 return parents
91 return parents
92
92
93 def toposort(self, parents):
93 def toposort(self, parents):
94 '''Return an ordering such that every uncommitted changeset is
94 '''Return an ordering such that every uncommitted changeset is
95 preceeded by all its uncommitted ancestors.'''
95 preceeded by all its uncommitted ancestors.'''
96 visit = parents.keys()
96 visit = parents.keys()
97 seen = {}
97 seen = {}
98 children = {}
98 children = {}
99
99
100 while visit:
100 while visit:
101 n = visit.pop(0)
101 n = visit.pop(0)
102 if n in seen: continue
102 if n in seen: continue
103 seen[n] = 1
103 seen[n] = 1
104 # Ensure that nodes without parents are present in the 'children'
104 # Ensure that nodes without parents are present in the 'children'
105 # mapping.
105 # mapping.
106 children.setdefault(n, [])
106 children.setdefault(n, [])
107 for p in parents[n]:
107 for p in parents[n]:
108 if not p in self.map:
108 if not p in self.map:
109 visit.append(p)
109 visit.append(p)
110 children.setdefault(p, []).append(n)
110 children.setdefault(p, []).append(n)
111
111
112 s = []
112 s = []
113 removed = {}
113 removed = {}
114 visit = children.keys()
114 visit = children.keys()
115 while visit:
115 while visit:
116 n = visit.pop(0)
116 n = visit.pop(0)
117 if n in removed: continue
117 if n in removed: continue
118 dep = 0
118 dep = 0
119 if n in parents:
119 if n in parents:
120 for p in parents[n]:
120 for p in parents[n]:
121 if p in self.map: continue
121 if p in self.map: continue
122 if p not in removed:
122 if p not in removed:
123 # we're still dependent
123 # we're still dependent
124 visit.append(n)
124 visit.append(n)
125 dep = 1
125 dep = 1
126 break
126 break
127
127
128 if not dep:
128 if not dep:
129 # all n's parents are in the list
129 # all n's parents are in the list
130 removed[n] = 1
130 removed[n] = 1
131 if n not in self.map:
131 if n not in self.map:
132 s.append(n)
132 s.append(n)
133 if n in children:
133 if n in children:
134 for c in children[n]:
134 for c in children[n]:
135 visit.insert(0, c)
135 visit.insert(0, c)
136
136
137 if self.opts.get('datesort'):
137 if self.opts.get('datesort'):
138 depth = {}
138 depth = {}
139 for n in s:
139 for n in s:
140 depth[n] = 0
140 depth[n] = 0
141 pl = [p for p in self.commitcache[n].parents
141 pl = [p for p in self.commitcache[n].parents
142 if p not in self.map]
142 if p not in self.map]
143 if pl:
143 if pl:
144 depth[n] = max([depth[p] for p in pl]) + 1
144 depth[n] = max([depth[p] for p in pl]) + 1
145
145
146 s = [(depth[n], self.commitcache[n].date, n) for n in s]
146 s = [(depth[n], self.commitcache[n].date, n) for n in s]
147 s.sort()
147 s.sort()
148 s = [e[2] for e in s]
148 s = [e[2] for e in s]
149
149
150 return s
150 return s
151
151
152 def mapentry(self, src, dst):
152 def mapentry(self, src, dst):
153 if self.revmapfilefd is None:
153 if self.revmapfilefd is None:
154 try:
154 try:
155 self.revmapfilefd = open(self.revmapfile, "a")
155 self.revmapfilefd = open(self.revmapfile, "a")
156 except IOError, (errno, strerror):
156 except IOError, (errno, strerror):
157 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
157 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
158 self.map[src] = dst
158 self.map[src] = dst
159 self.revmapfilefd.write("%s %s\n" % (src, dst))
159 self.revmapfilefd.write("%s %s\n" % (src, dst))
160 self.revmapfilefd.flush()
160 self.revmapfilefd.flush()
161
161
162 def writeauthormap(self):
162 def writeauthormap(self):
163 authorfile = self.authorfile
163 authorfile = self.authorfile
164 if authorfile:
164 if authorfile:
165 self.ui.status('Writing author map file %s\n' % authorfile)
165 self.ui.status('Writing author map file %s\n' % authorfile)
166 ofile = open(authorfile, 'w+')
166 ofile = open(authorfile, 'w+')
167 for author in self.authors:
167 for author in self.authors:
168 ofile.write("%s=%s\n" % (author, self.authors[author]))
168 ofile.write("%s=%s\n" % (author, self.authors[author]))
169 ofile.close()
169 ofile.close()
170
170
171 def readauthormap(self, authorfile):
171 def readauthormap(self, authorfile):
172 afile = open(authorfile, 'r')
172 afile = open(authorfile, 'r')
173 for line in afile:
173 for line in afile:
174 try:
174 try:
175 srcauthor = line.split('=')[0].strip()
175 srcauthor = line.split('=')[0].strip()
176 dstauthor = line.split('=')[1].strip()
176 dstauthor = line.split('=')[1].strip()
177 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
177 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
178 self.ui.status(
178 self.ui.status(
179 'Overriding mapping for author %s, was %s, will be %s\n'
179 'Overriding mapping for author %s, was %s, will be %s\n'
180 % (srcauthor, self.authors[srcauthor], dstauthor))
180 % (srcauthor, self.authors[srcauthor], dstauthor))
181 else:
181 else:
182 self.ui.debug('Mapping author %s to %s\n'
182 self.ui.debug('Mapping author %s to %s\n'
183 % (srcauthor, dstauthor))
183 % (srcauthor, dstauthor))
184 self.authors[srcauthor] = dstauthor
184 self.authors[srcauthor] = dstauthor
185 except IndexError:
185 except IndexError:
186 self.ui.warn(
186 self.ui.warn(
187 'Ignoring bad line in author file map %s: %s\n'
187 'Ignoring bad line in author file map %s: %s\n'
188 % (authorfile, line))
188 % (authorfile, line))
189 afile.close()
189 afile.close()
190
190
191 def copy(self, rev):
191 def copy(self, rev):
192 commit = self.commitcache[rev]
192 commit = self.commitcache[rev]
193 do_copies = hasattr(self.dest, 'copyfile')
193 do_copies = hasattr(self.dest, 'copyfile')
194 filenames = []
194 filenames = []
195
195
196 for f, v in self.source.getchanges(rev):
196 files, copies = self.source.getchanges(rev)
197 for f, v in files:
197 newf = self.mapfile(f)
198 newf = self.mapfile(f)
198 if not newf:
199 if not newf:
199 continue
200 continue
200 filenames.append(newf)
201 filenames.append(newf)
201 try:
202 try:
202 data = self.source.getfile(f, v)
203 data = self.source.getfile(f, v)
203 except IOError, inst:
204 except IOError, inst:
204 self.dest.delfile(newf)
205 self.dest.delfile(newf)
205 else:
206 else:
206 e = self.source.getmode(f, v)
207 e = self.source.getmode(f, v)
207 self.dest.putfile(newf, e, data)
208 self.dest.putfile(newf, e, data)
208 if do_copies:
209 if do_copies:
209 if f in commit.copies:
210 if f in copies:
210 copyf = self.mapfile(commit.copies[f])
211 copyf = self.mapfile(copies[f])
211 if copyf:
212 if copyf:
212 # Merely marks that a copy happened.
213 # Merely marks that a copy happened.
213 self.dest.copyfile(copyf, newf)
214 self.dest.copyfile(copyf, newf)
214
215
215 parents = [self.map[r] for r in commit.parents]
216 parents = [self.map[r] for r in commit.parents]
216 newnode = self.dest.putcommit(filenames, parents, commit)
217 newnode = self.dest.putcommit(filenames, parents, commit)
217 self.mapentry(rev, newnode)
218 self.mapentry(rev, newnode)
218
219
219 def convert(self):
220 def convert(self):
220 try:
221 try:
221 self.dest.before()
222 self.dest.before()
222 self.source.setrevmap(self.map)
223 self.source.setrevmap(self.map)
223 self.ui.status("scanning source...\n")
224 self.ui.status("scanning source...\n")
224 heads = self.source.getheads()
225 heads = self.source.getheads()
225 parents = self.walktree(heads)
226 parents = self.walktree(heads)
226 self.ui.status("sorting...\n")
227 self.ui.status("sorting...\n")
227 t = self.toposort(parents)
228 t = self.toposort(parents)
228 num = len(t)
229 num = len(t)
229 c = None
230 c = None
230
231
231 self.ui.status("converting...\n")
232 self.ui.status("converting...\n")
232 for c in t:
233 for c in t:
233 num -= 1
234 num -= 1
234 desc = self.commitcache[c].desc
235 desc = self.commitcache[c].desc
235 if "\n" in desc:
236 if "\n" in desc:
236 desc = desc.splitlines()[0]
237 desc = desc.splitlines()[0]
237 author = self.commitcache[c].author
238 author = self.commitcache[c].author
238 author = self.authors.get(author, author)
239 author = self.authors.get(author, author)
239 self.commitcache[c].author = author
240 self.commitcache[c].author = author
240 self.ui.status("%d %s\n" % (num, desc))
241 self.ui.status("%d %s\n" % (num, desc))
241 self.copy(c)
242 self.copy(c)
242
243
243 tags = self.source.gettags()
244 tags = self.source.gettags()
244 ctags = {}
245 ctags = {}
245 for k in tags:
246 for k in tags:
246 v = tags[k]
247 v = tags[k]
247 if v in self.map:
248 if v in self.map:
248 ctags[k] = self.map[v]
249 ctags[k] = self.map[v]
249
250
250 if c and ctags:
251 if c and ctags:
251 nrev = self.dest.puttags(ctags)
252 nrev = self.dest.puttags(ctags)
252 # write another hash correspondence to override the previous
253 # write another hash correspondence to override the previous
253 # one so we don't end up with extra tag heads
254 # one so we don't end up with extra tag heads
254 if nrev:
255 if nrev:
255 self.mapentry(c, nrev)
256 self.mapentry(c, nrev)
256
257
257 self.writeauthormap()
258 self.writeauthormap()
258 finally:
259 finally:
259 self.cleanup()
260 self.cleanup()
260
261
261 def cleanup(self):
262 def cleanup(self):
262 self.dest.after()
263 self.dest.after()
263 if self.revmapfilefd:
264 if self.revmapfilefd:
264 self.revmapfilefd.close()
265 self.revmapfilefd.close()
265
266
266 def rpairs(name):
267 def rpairs(name):
267 e = len(name)
268 e = len(name)
268 while e != -1:
269 while e != -1:
269 yield name[:e], name[e+1:]
270 yield name[:e], name[e+1:]
270 e = name.rfind('/', 0, e)
271 e = name.rfind('/', 0, e)
271
272
272 class filemapper(object):
273 class filemapper(object):
273 '''Map and filter filenames when importing.
274 '''Map and filter filenames when importing.
274 A name can be mapped to itself, a new name, or None (omit from new
275 A name can be mapped to itself, a new name, or None (omit from new
275 repository).'''
276 repository).'''
276
277
277 def __init__(self, ui, path=None):
278 def __init__(self, ui, path=None):
278 self.ui = ui
279 self.ui = ui
279 self.include = {}
280 self.include = {}
280 self.exclude = {}
281 self.exclude = {}
281 self.rename = {}
282 self.rename = {}
282 if path:
283 if path:
283 if self.parse(path):
284 if self.parse(path):
284 raise util.Abort(_('errors in filemap'))
285 raise util.Abort(_('errors in filemap'))
285
286
286 def parse(self, path):
287 def parse(self, path):
287 errs = 0
288 errs = 0
288 def check(name, mapping, listname):
289 def check(name, mapping, listname):
289 if name in mapping:
290 if name in mapping:
290 self.ui.warn(_('%s:%d: %r already in %s list\n') %
291 self.ui.warn(_('%s:%d: %r already in %s list\n') %
291 (lex.infile, lex.lineno, name, listname))
292 (lex.infile, lex.lineno, name, listname))
292 return 1
293 return 1
293 return 0
294 return 0
294 lex = shlex.shlex(open(path), path, True)
295 lex = shlex.shlex(open(path), path, True)
295 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
296 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
296 cmd = lex.get_token()
297 cmd = lex.get_token()
297 while cmd:
298 while cmd:
298 if cmd == 'include':
299 if cmd == 'include':
299 name = lex.get_token()
300 name = lex.get_token()
300 errs += check(name, self.exclude, 'exclude')
301 errs += check(name, self.exclude, 'exclude')
301 self.include[name] = name
302 self.include[name] = name
302 elif cmd == 'exclude':
303 elif cmd == 'exclude':
303 name = lex.get_token()
304 name = lex.get_token()
304 errs += check(name, self.include, 'include')
305 errs += check(name, self.include, 'include')
305 errs += check(name, self.rename, 'rename')
306 errs += check(name, self.rename, 'rename')
306 self.exclude[name] = name
307 self.exclude[name] = name
307 elif cmd == 'rename':
308 elif cmd == 'rename':
308 src = lex.get_token()
309 src = lex.get_token()
309 dest = lex.get_token()
310 dest = lex.get_token()
310 errs += check(src, self.exclude, 'exclude')
311 errs += check(src, self.exclude, 'exclude')
311 self.rename[src] = dest
312 self.rename[src] = dest
312 elif cmd == 'source':
313 elif cmd == 'source':
313 errs += self.parse(lex.get_token())
314 errs += self.parse(lex.get_token())
314 else:
315 else:
315 self.ui.warn(_('%s:%d: unknown directive %r\n') %
316 self.ui.warn(_('%s:%d: unknown directive %r\n') %
316 (lex.infile, lex.lineno, cmd))
317 (lex.infile, lex.lineno, cmd))
317 errs += 1
318 errs += 1
318 cmd = lex.get_token()
319 cmd = lex.get_token()
319 return errs
320 return errs
320
321
321 def lookup(self, name, mapping):
322 def lookup(self, name, mapping):
322 for pre, suf in rpairs(name):
323 for pre, suf in rpairs(name):
323 try:
324 try:
324 return mapping[pre], pre, suf
325 return mapping[pre], pre, suf
325 except KeyError, err:
326 except KeyError, err:
326 pass
327 pass
327 return '', name, ''
328 return '', name, ''
328
329
329 def __call__(self, name):
330 def __call__(self, name):
330 if self.include:
331 if self.include:
331 inc = self.lookup(name, self.include)[0]
332 inc = self.lookup(name, self.include)[0]
332 else:
333 else:
333 inc = name
334 inc = name
334 if self.exclude:
335 if self.exclude:
335 exc = self.lookup(name, self.exclude)[0]
336 exc = self.lookup(name, self.exclude)[0]
336 else:
337 else:
337 exc = ''
338 exc = ''
338 if not inc or exc:
339 if not inc or exc:
339 return None
340 return None
340 newpre, pre, suf = self.lookup(name, self.rename)
341 newpre, pre, suf = self.lookup(name, self.rename)
341 if newpre:
342 if newpre:
342 if suf:
343 if suf:
343 return newpre + '/' + suf
344 return newpre + '/' + suf
344 return newpre
345 return newpre
345 return name
346 return name
346
347
347 def _convert(ui, src, dest=None, revmapfile=None, **opts):
348 def _convert(ui, src, dest=None, revmapfile=None, **opts):
348 """Convert a foreign SCM repository to a Mercurial one.
349 """Convert a foreign SCM repository to a Mercurial one.
349
350
350 Accepted source formats:
351 Accepted source formats:
351 - GIT
352 - GIT
352 - CVS
353 - CVS
353 - SVN
354 - SVN
354
355
355 Accepted destination formats:
356 Accepted destination formats:
356 - Mercurial
357 - Mercurial
357
358
358 If no revision is given, all revisions will be converted. Otherwise,
359 If no revision is given, all revisions will be converted. Otherwise,
359 convert will only import up to the named revision (given in a format
360 convert will only import up to the named revision (given in a format
360 understood by the source).
361 understood by the source).
361
362
362 If no destination directory name is specified, it defaults to the
363 If no destination directory name is specified, it defaults to the
363 basename of the source with '-hg' appended. If the destination
364 basename of the source with '-hg' appended. If the destination
364 repository doesn't exist, it will be created.
365 repository doesn't exist, it will be created.
365
366
366 If <revmapfile> isn't given, it will be put in a default location
367 If <revmapfile> isn't given, it will be put in a default location
367 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
368 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
368 file that maps each source commit ID to the destination ID for
369 file that maps each source commit ID to the destination ID for
369 that revision, like so:
370 that revision, like so:
370 <source ID> <destination ID>
371 <source ID> <destination ID>
371
372
372 If the file doesn't exist, it's automatically created. It's updated
373 If the file doesn't exist, it's automatically created. It's updated
373 on each commit copied, so convert-repo can be interrupted and can
374 on each commit copied, so convert-repo can be interrupted and can
374 be run repeatedly to copy new commits.
375 be run repeatedly to copy new commits.
375
376
376 The [username mapping] file is a simple text file that maps each source
377 The [username mapping] file is a simple text file that maps each source
377 commit author to a destination commit author. It is handy for source SCMs
378 commit author to a destination commit author. It is handy for source SCMs
378 that use unix logins to identify authors (eg: CVS). One line per author
379 that use unix logins to identify authors (eg: CVS). One line per author
379 mapping and the line format is:
380 mapping and the line format is:
380 srcauthor=whatever string you want
381 srcauthor=whatever string you want
381 """
382 """
382
383
383 util._encoding = 'UTF-8'
384 util._encoding = 'UTF-8'
384
385
385 if not dest:
386 if not dest:
386 dest = hg.defaultdest(src) + "-hg"
387 dest = hg.defaultdest(src) + "-hg"
387 ui.status("assuming destination %s\n" % dest)
388 ui.status("assuming destination %s\n" % dest)
388
389
389 # Try to be smart and initalize things when required
390 # Try to be smart and initalize things when required
390 created = False
391 created = False
391 if os.path.isdir(dest):
392 if os.path.isdir(dest):
392 if len(os.listdir(dest)) > 0:
393 if len(os.listdir(dest)) > 0:
393 try:
394 try:
394 hg.repository(ui, dest)
395 hg.repository(ui, dest)
395 ui.status("destination %s is a Mercurial repository\n" % dest)
396 ui.status("destination %s is a Mercurial repository\n" % dest)
396 except hg.RepoError:
397 except hg.RepoError:
397 raise util.Abort(
398 raise util.Abort(
398 "destination directory %s is not empty.\n"
399 "destination directory %s is not empty.\n"
399 "Please specify an empty directory to be initialized\n"
400 "Please specify an empty directory to be initialized\n"
400 "or an already initialized mercurial repository"
401 "or an already initialized mercurial repository"
401 % dest)
402 % dest)
402 else:
403 else:
403 ui.status("initializing destination %s repository\n" % dest)
404 ui.status("initializing destination %s repository\n" % dest)
404 hg.repository(ui, dest, create=True)
405 hg.repository(ui, dest, create=True)
405 created = True
406 created = True
406 elif os.path.exists(dest):
407 elif os.path.exists(dest):
407 raise util.Abort("destination %s exists and is not a directory" % dest)
408 raise util.Abort("destination %s exists and is not a directory" % dest)
408 else:
409 else:
409 ui.status("initializing destination %s repository\n" % dest)
410 ui.status("initializing destination %s repository\n" % dest)
410 hg.repository(ui, dest, create=True)
411 hg.repository(ui, dest, create=True)
411 created = True
412 created = True
412
413
413 destc = convertsink(ui, dest)
414 destc = convertsink(ui, dest)
414
415
415 try:
416 try:
416 srcc = convertsource(ui, src, rev=opts.get('rev'))
417 srcc = convertsource(ui, src, rev=opts.get('rev'))
417 except Exception:
418 except Exception:
418 if created:
419 if created:
419 shutil.rmtree(dest, True)
420 shutil.rmtree(dest, True)
420 raise
421 raise
421
422
422 if not revmapfile:
423 if not revmapfile:
423 try:
424 try:
424 revmapfile = destc.revmapfile()
425 revmapfile = destc.revmapfile()
425 except:
426 except:
426 revmapfile = os.path.join(destc, "map")
427 revmapfile = os.path.join(destc, "map")
427
428
428
429
429 c = convert(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']),
430 c = convert(ui, srcc, destc, revmapfile, filemapper(ui, opts['filemap']),
430 opts)
431 opts)
431 c.convert()
432 c.convert()
432
433
433 cmdtable = {
434 cmdtable = {
434 "convert":
435 "convert":
435 (_convert,
436 (_convert,
436 [('A', 'authors', '', 'username mapping filename'),
437 [('A', 'authors', '', 'username mapping filename'),
437 ('', 'filemap', '', 'remap file names using contents of file'),
438 ('', 'filemap', '', 'remap file names using contents of file'),
438 ('r', 'rev', '', 'import up to target revision REV'),
439 ('r', 'rev', '', 'import up to target revision REV'),
439 ('', 'datesort', None, 'try to sort changesets by date')],
440 ('', 'datesort', None, 'try to sort changesets by date')],
440 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
441 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
441 }
442 }
@@ -1,120 +1,120
1 # common code for the convert extension
1 # common code for the convert extension
2
2
3 class NoRepo(Exception): pass
3 class NoRepo(Exception): pass
4
4
5 class commit(object):
5 class commit(object):
6 def __init__(self, author, date, desc, parents, branch=None, rev=None,
6 def __init__(self, author, date, desc, parents, branch=None, rev=None):
7 copies={}):
8 self.author = author
7 self.author = author
9 self.date = date
8 self.date = date
10 self.desc = desc
9 self.desc = desc
11 self.parents = parents
10 self.parents = parents
12 self.branch = branch
11 self.branch = branch
13 self.rev = rev
12 self.rev = rev
14 self.copies = copies
15
13
16 class converter_source(object):
14 class converter_source(object):
17 """Conversion source interface"""
15 """Conversion source interface"""
18
16
19 def __init__(self, ui, path, rev=None):
17 def __init__(self, ui, path, rev=None):
20 """Initialize conversion source (or raise NoRepo("message")
18 """Initialize conversion source (or raise NoRepo("message")
21 exception if path is not a valid repository)"""
19 exception if path is not a valid repository)"""
22 self.ui = ui
20 self.ui = ui
23 self.path = path
21 self.path = path
24 self.rev = rev
22 self.rev = rev
25
23
26 self.encoding = 'utf-8'
24 self.encoding = 'utf-8'
27
25
28 def setrevmap(self, revmap):
26 def setrevmap(self, revmap):
29 """set the map of already-converted revisions"""
27 """set the map of already-converted revisions"""
30 pass
28 pass
31
29
32 def getheads(self):
30 def getheads(self):
33 """Return a list of this repository's heads"""
31 """Return a list of this repository's heads"""
34 raise NotImplementedError()
32 raise NotImplementedError()
35
33
36 def getfile(self, name, rev):
34 def getfile(self, name, rev):
37 """Return file contents as a string"""
35 """Return file contents as a string"""
38 raise NotImplementedError()
36 raise NotImplementedError()
39
37
40 def getmode(self, name, rev):
38 def getmode(self, name, rev):
41 """Return file mode, eg. '', 'x', or 'l'"""
39 """Return file mode, eg. '', 'x', or 'l'"""
42 raise NotImplementedError()
40 raise NotImplementedError()
43
41
44 def getchanges(self, version):
42 def getchanges(self, version):
45 """Return sorted list of (filename, id) tuples for all files changed in rev.
43 """Returns a tuple of (files, copies)
44 Files is a sorted list of (filename, id) tuples for all files changed
45 in version, where id is the source revision id of the file.
46
46
47 id just tells us which revision to return in getfile(), e.g. in
47 copies is a dictionary of dest: source
48 git it's an object hash."""
48 """
49 raise NotImplementedError()
49 raise NotImplementedError()
50
50
51 def getcommit(self, version):
51 def getcommit(self, version):
52 """Return the commit object for version"""
52 """Return the commit object for version"""
53 raise NotImplementedError()
53 raise NotImplementedError()
54
54
55 def gettags(self):
55 def gettags(self):
56 """Return the tags as a dictionary of name: revision"""
56 """Return the tags as a dictionary of name: revision"""
57 raise NotImplementedError()
57 raise NotImplementedError()
58
58
59 def recode(self, s, encoding=None):
59 def recode(self, s, encoding=None):
60 if not encoding:
60 if not encoding:
61 encoding = self.encoding or 'utf-8'
61 encoding = self.encoding or 'utf-8'
62
62
63 try:
63 try:
64 return s.decode(encoding).encode("utf-8")
64 return s.decode(encoding).encode("utf-8")
65 except:
65 except:
66 try:
66 try:
67 return s.decode("latin-1").encode("utf-8")
67 return s.decode("latin-1").encode("utf-8")
68 except:
68 except:
69 return s.decode(encoding, "replace").encode("utf-8")
69 return s.decode(encoding, "replace").encode("utf-8")
70
70
71 class converter_sink(object):
71 class converter_sink(object):
72 """Conversion sink (target) interface"""
72 """Conversion sink (target) interface"""
73
73
74 def __init__(self, ui, path):
74 def __init__(self, ui, path):
75 """Initialize conversion sink (or raise NoRepo("message")
75 """Initialize conversion sink (or raise NoRepo("message")
76 exception if path is not a valid repository)"""
76 exception if path is not a valid repository)"""
77 raise NotImplementedError()
77 raise NotImplementedError()
78
78
79 def getheads(self):
79 def getheads(self):
80 """Return a list of this repository's heads"""
80 """Return a list of this repository's heads"""
81 raise NotImplementedError()
81 raise NotImplementedError()
82
82
83 def revmapfile(self):
83 def revmapfile(self):
84 """Path to a file that will contain lines
84 """Path to a file that will contain lines
85 source_rev_id sink_rev_id
85 source_rev_id sink_rev_id
86 mapping equivalent revision identifiers for each system."""
86 mapping equivalent revision identifiers for each system."""
87 raise NotImplementedError()
87 raise NotImplementedError()
88
88
89 def authorfile(self):
89 def authorfile(self):
90 """Path to a file that will contain lines
90 """Path to a file that will contain lines
91 srcauthor=dstauthor
91 srcauthor=dstauthor
92 mapping equivalent authors identifiers for each system."""
92 mapping equivalent authors identifiers for each system."""
93 return None
93 return None
94
94
95 def putfile(self, f, e, data):
95 def putfile(self, f, e, data):
96 """Put file for next putcommit().
96 """Put file for next putcommit().
97 f: path to file
97 f: path to file
98 e: '', 'x', or 'l' (regular file, executable, or symlink)
98 e: '', 'x', or 'l' (regular file, executable, or symlink)
99 data: file contents"""
99 data: file contents"""
100 raise NotImplementedError()
100 raise NotImplementedError()
101
101
102 def delfile(self, f):
102 def delfile(self, f):
103 """Delete file for next putcommit().
103 """Delete file for next putcommit().
104 f: path to file"""
104 f: path to file"""
105 raise NotImplementedError()
105 raise NotImplementedError()
106
106
107 def putcommit(self, files, parents, commit):
107 def putcommit(self, files, parents, commit):
108 """Create a revision with all changed files listed in 'files'
108 """Create a revision with all changed files listed in 'files'
109 and having listed parents. 'commit' is a commit object containing
109 and having listed parents. 'commit' is a commit object containing
110 at a minimum the author, date, and message for this changeset.
110 at a minimum the author, date, and message for this changeset.
111 Called after putfile() and delfile() calls. Note that the sink
111 Called after putfile() and delfile() calls. Note that the sink
112 repository is not told to update itself to a particular revision
112 repository is not told to update itself to a particular revision
113 (or even what that revision would be) before it receives the
113 (or even what that revision would be) before it receives the
114 file data."""
114 file data."""
115 raise NotImplementedError()
115 raise NotImplementedError()
116
116
117 def puttags(self, tags):
117 def puttags(self, tags):
118 """Put tags into sink.
118 """Put tags into sink.
119 tags: {tagname: sink_rev_id, ...}"""
119 tags: {tagname: sink_rev_id, ...}"""
120 raise NotImplementedError()
120 raise NotImplementedError()
@@ -1,259 +1,259
1 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
1 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
2
2
3 import os, locale, re, socket
3 import os, locale, re, socket
4 from mercurial import util
4 from mercurial import util
5
5
6 from common import NoRepo, commit, converter_source
6 from common import NoRepo, commit, converter_source
7
7
8 class convert_cvs(converter_source):
8 class convert_cvs(converter_source):
9 def __init__(self, ui, path, rev=None):
9 def __init__(self, ui, path, rev=None):
10 super(convert_cvs, self).__init__(ui, path, rev=rev)
10 super(convert_cvs, self).__init__(ui, path, rev=rev)
11
11
12 cvs = os.path.join(path, "CVS")
12 cvs = os.path.join(path, "CVS")
13 if not os.path.exists(cvs):
13 if not os.path.exists(cvs):
14 raise NoRepo("couldn't open CVS repo %s" % path)
14 raise NoRepo("couldn't open CVS repo %s" % path)
15
15
16 self.changeset = {}
16 self.changeset = {}
17 self.files = {}
17 self.files = {}
18 self.tags = {}
18 self.tags = {}
19 self.lastbranch = {}
19 self.lastbranch = {}
20 self.parent = {}
20 self.parent = {}
21 self.socket = None
21 self.socket = None
22 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
22 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
23 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
23 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
24 self.encoding = locale.getpreferredencoding()
24 self.encoding = locale.getpreferredencoding()
25 self._parse()
25 self._parse()
26 self._connect()
26 self._connect()
27
27
28 def _parse(self):
28 def _parse(self):
29 if self.changeset:
29 if self.changeset:
30 return
30 return
31
31
32 maxrev = 0
32 maxrev = 0
33 cmd = 'cvsps -A -u --cvs-direct -q'
33 cmd = 'cvsps -A -u --cvs-direct -q'
34 if self.rev:
34 if self.rev:
35 # TODO: handle tags
35 # TODO: handle tags
36 try:
36 try:
37 # patchset number?
37 # patchset number?
38 maxrev = int(self.rev)
38 maxrev = int(self.rev)
39 except ValueError:
39 except ValueError:
40 try:
40 try:
41 # date
41 # date
42 util.parsedate(self.rev, ['%Y/%m/%d %H:%M:%S'])
42 util.parsedate(self.rev, ['%Y/%m/%d %H:%M:%S'])
43 cmd = "%s -d '1970/01/01 00:00:01' -d '%s'" % (cmd, self.rev)
43 cmd = "%s -d '1970/01/01 00:00:01' -d '%s'" % (cmd, self.rev)
44 except util.Abort:
44 except util.Abort:
45 raise util.Abort('revision %s is not a patchset number or date' % self.rev)
45 raise util.Abort('revision %s is not a patchset number or date' % self.rev)
46
46
47 d = os.getcwd()
47 d = os.getcwd()
48 try:
48 try:
49 os.chdir(self.path)
49 os.chdir(self.path)
50 id = None
50 id = None
51 state = 0
51 state = 0
52 for l in os.popen(cmd):
52 for l in os.popen(cmd):
53 if state == 0: # header
53 if state == 0: # header
54 if l.startswith("PatchSet"):
54 if l.startswith("PatchSet"):
55 id = l[9:-2]
55 id = l[9:-2]
56 if maxrev and int(id) > maxrev:
56 if maxrev and int(id) > maxrev:
57 state = 3
57 state = 3
58 elif l.startswith("Date"):
58 elif l.startswith("Date"):
59 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
59 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
60 date = util.datestr(date)
60 date = util.datestr(date)
61 elif l.startswith("Branch"):
61 elif l.startswith("Branch"):
62 branch = l[8:-1]
62 branch = l[8:-1]
63 self.parent[id] = self.lastbranch.get(branch, 'bad')
63 self.parent[id] = self.lastbranch.get(branch, 'bad')
64 self.lastbranch[branch] = id
64 self.lastbranch[branch] = id
65 elif l.startswith("Ancestor branch"):
65 elif l.startswith("Ancestor branch"):
66 ancestor = l[17:-1]
66 ancestor = l[17:-1]
67 self.parent[id] = self.lastbranch[ancestor]
67 self.parent[id] = self.lastbranch[ancestor]
68 elif l.startswith("Author"):
68 elif l.startswith("Author"):
69 author = self.recode(l[8:-1])
69 author = self.recode(l[8:-1])
70 elif l.startswith("Tag:") or l.startswith("Tags:"):
70 elif l.startswith("Tag:") or l.startswith("Tags:"):
71 t = l[l.index(':')+1:]
71 t = l[l.index(':')+1:]
72 t = [ut.strip() for ut in t.split(',')]
72 t = [ut.strip() for ut in t.split(',')]
73 if (len(t) > 1) or (t[0] and (t[0] != "(none)")):
73 if (len(t) > 1) or (t[0] and (t[0] != "(none)")):
74 self.tags.update(dict.fromkeys(t, id))
74 self.tags.update(dict.fromkeys(t, id))
75 elif l.startswith("Log:"):
75 elif l.startswith("Log:"):
76 state = 1
76 state = 1
77 log = ""
77 log = ""
78 elif state == 1: # log
78 elif state == 1: # log
79 if l == "Members: \n":
79 if l == "Members: \n":
80 files = {}
80 files = {}
81 log = self.recode(log[:-1])
81 log = self.recode(log[:-1])
82 state = 2
82 state = 2
83 else:
83 else:
84 log += l
84 log += l
85 elif state == 2:
85 elif state == 2:
86 if l == "\n": #
86 if l == "\n": #
87 state = 0
87 state = 0
88 p = [self.parent[id]]
88 p = [self.parent[id]]
89 if id == "1":
89 if id == "1":
90 p = []
90 p = []
91 if branch == "HEAD":
91 if branch == "HEAD":
92 branch = ""
92 branch = ""
93 c = commit(author=author, date=date, parents=p,
93 c = commit(author=author, date=date, parents=p,
94 desc=log, branch=branch)
94 desc=log, branch=branch)
95 self.changeset[id] = c
95 self.changeset[id] = c
96 self.files[id] = files
96 self.files[id] = files
97 else:
97 else:
98 colon = l.rfind(':')
98 colon = l.rfind(':')
99 file = l[1:colon]
99 file = l[1:colon]
100 rev = l[colon+1:-2]
100 rev = l[colon+1:-2]
101 rev = rev.split("->")[1]
101 rev = rev.split("->")[1]
102 files[file] = rev
102 files[file] = rev
103 elif state == 3:
103 elif state == 3:
104 continue
104 continue
105
105
106 self.heads = self.lastbranch.values()
106 self.heads = self.lastbranch.values()
107 finally:
107 finally:
108 os.chdir(d)
108 os.chdir(d)
109
109
110 def _connect(self):
110 def _connect(self):
111 root = self.cvsroot
111 root = self.cvsroot
112 conntype = None
112 conntype = None
113 user, host = None, None
113 user, host = None, None
114 cmd = ['cvs', 'server']
114 cmd = ['cvs', 'server']
115
115
116 self.ui.status("connecting to %s\n" % root)
116 self.ui.status("connecting to %s\n" % root)
117
117
118 if root.startswith(":pserver:"):
118 if root.startswith(":pserver:"):
119 root = root[9:]
119 root = root[9:]
120 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
120 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
121 root)
121 root)
122 if m:
122 if m:
123 conntype = "pserver"
123 conntype = "pserver"
124 user, passw, serv, port, root = m.groups()
124 user, passw, serv, port, root = m.groups()
125 if not user:
125 if not user:
126 user = "anonymous"
126 user = "anonymous"
127 rr = ":pserver:" + user + "@" + serv + ":" + root
127 rr = ":pserver:" + user + "@" + serv + ":" + root
128 if port:
128 if port:
129 rr2, port = "-", int(port)
129 rr2, port = "-", int(port)
130 else:
130 else:
131 rr2, port = rr, 2401
131 rr2, port = rr, 2401
132 rr += str(port)
132 rr += str(port)
133
133
134 if not passw:
134 if not passw:
135 passw = "A"
135 passw = "A"
136 pf = open(os.path.join(os.environ["HOME"], ".cvspass"))
136 pf = open(os.path.join(os.environ["HOME"], ".cvspass"))
137 for l in pf:
137 for l in pf:
138 # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
138 # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
139 m = re.match(r'(/\d+\s+/)?(.*)', l)
139 m = re.match(r'(/\d+\s+/)?(.*)', l)
140 l = m.group(2)
140 l = m.group(2)
141 w, p = l.split(' ', 1)
141 w, p = l.split(' ', 1)
142 if w in [rr, rr2]:
142 if w in [rr, rr2]:
143 passw = p
143 passw = p
144 break
144 break
145 pf.close()
145 pf.close()
146
146
147 sck = socket.socket()
147 sck = socket.socket()
148 sck.connect((serv, port))
148 sck.connect((serv, port))
149 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
149 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
150 "END AUTH REQUEST", ""]))
150 "END AUTH REQUEST", ""]))
151 if sck.recv(128) != "I LOVE YOU\n":
151 if sck.recv(128) != "I LOVE YOU\n":
152 raise NoRepo("CVS pserver authentication failed")
152 raise NoRepo("CVS pserver authentication failed")
153
153
154 self.writep = self.readp = sck.makefile('r+')
154 self.writep = self.readp = sck.makefile('r+')
155
155
156 if not conntype and root.startswith(":local:"):
156 if not conntype and root.startswith(":local:"):
157 conntype = "local"
157 conntype = "local"
158 root = root[7:]
158 root = root[7:]
159
159
160 if not conntype:
160 if not conntype:
161 # :ext:user@host/home/user/path/to/cvsroot
161 # :ext:user@host/home/user/path/to/cvsroot
162 if root.startswith(":ext:"):
162 if root.startswith(":ext:"):
163 root = root[5:]
163 root = root[5:]
164 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
164 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
165 if not m:
165 if not m:
166 conntype = "local"
166 conntype = "local"
167 else:
167 else:
168 conntype = "rsh"
168 conntype = "rsh"
169 user, host, root = m.group(1), m.group(2), m.group(3)
169 user, host, root = m.group(1), m.group(2), m.group(3)
170
170
171 if conntype != "pserver":
171 if conntype != "pserver":
172 if conntype == "rsh":
172 if conntype == "rsh":
173 rsh = os.environ.get("CVS_RSH" or "rsh")
173 rsh = os.environ.get("CVS_RSH" or "rsh")
174 if user:
174 if user:
175 cmd = [rsh, '-l', user, host] + cmd
175 cmd = [rsh, '-l', user, host] + cmd
176 else:
176 else:
177 cmd = [rsh, host] + cmd
177 cmd = [rsh, host] + cmd
178
178
179 self.writep, self.readp = os.popen2(cmd)
179 self.writep, self.readp = os.popen2(cmd)
180
180
181 self.realroot = root
181 self.realroot = root
182
182
183 self.writep.write("Root %s\n" % root)
183 self.writep.write("Root %s\n" % root)
184 self.writep.write("Valid-responses ok error Valid-requests Mode"
184 self.writep.write("Valid-responses ok error Valid-requests Mode"
185 " M Mbinary E Checked-in Created Updated"
185 " M Mbinary E Checked-in Created Updated"
186 " Merged Removed\n")
186 " Merged Removed\n")
187 self.writep.write("valid-requests\n")
187 self.writep.write("valid-requests\n")
188 self.writep.flush()
188 self.writep.flush()
189 r = self.readp.readline()
189 r = self.readp.readline()
190 if not r.startswith("Valid-requests"):
190 if not r.startswith("Valid-requests"):
191 raise util.Abort("server sucks")
191 raise util.Abort("server sucks")
192 if "UseUnchanged" in r:
192 if "UseUnchanged" in r:
193 self.writep.write("UseUnchanged\n")
193 self.writep.write("UseUnchanged\n")
194 self.writep.flush()
194 self.writep.flush()
195 r = self.readp.readline()
195 r = self.readp.readline()
196
196
197 def getheads(self):
197 def getheads(self):
198 return self.heads
198 return self.heads
199
199
200 def _getfile(self, name, rev):
200 def _getfile(self, name, rev):
201 if rev.endswith("(DEAD)"):
201 if rev.endswith("(DEAD)"):
202 raise IOError
202 raise IOError
203
203
204 args = ("-N -P -kk -r %s --" % rev).split()
204 args = ("-N -P -kk -r %s --" % rev).split()
205 args.append(os.path.join(self.cvsrepo, name))
205 args.append(os.path.join(self.cvsrepo, name))
206 for x in args:
206 for x in args:
207 self.writep.write("Argument %s\n" % x)
207 self.writep.write("Argument %s\n" % x)
208 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
208 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
209 self.writep.flush()
209 self.writep.flush()
210
210
211 data = ""
211 data = ""
212 while 1:
212 while 1:
213 line = self.readp.readline()
213 line = self.readp.readline()
214 if line.startswith("Created ") or line.startswith("Updated "):
214 if line.startswith("Created ") or line.startswith("Updated "):
215 self.readp.readline() # path
215 self.readp.readline() # path
216 self.readp.readline() # entries
216 self.readp.readline() # entries
217 mode = self.readp.readline()[:-1]
217 mode = self.readp.readline()[:-1]
218 count = int(self.readp.readline()[:-1])
218 count = int(self.readp.readline()[:-1])
219 data = self.readp.read(count)
219 data = self.readp.read(count)
220 elif line.startswith(" "):
220 elif line.startswith(" "):
221 data += line[1:]
221 data += line[1:]
222 elif line.startswith("M "):
222 elif line.startswith("M "):
223 pass
223 pass
224 elif line.startswith("Mbinary "):
224 elif line.startswith("Mbinary "):
225 count = int(self.readp.readline()[:-1])
225 count = int(self.readp.readline()[:-1])
226 data = self.readp.read(count)
226 data = self.readp.read(count)
227 else:
227 else:
228 if line == "ok\n":
228 if line == "ok\n":
229 return (data, "x" in mode and "x" or "")
229 return (data, "x" in mode and "x" or "")
230 elif line.startswith("E "):
230 elif line.startswith("E "):
231 self.ui.warn("cvs server: %s\n" % line[2:])
231 self.ui.warn("cvs server: %s\n" % line[2:])
232 elif line.startswith("Remove"):
232 elif line.startswith("Remove"):
233 l = self.readp.readline()
233 l = self.readp.readline()
234 l = self.readp.readline()
234 l = self.readp.readline()
235 if l != "ok\n":
235 if l != "ok\n":
236 raise util.Abort("unknown CVS response: %s" % l)
236 raise util.Abort("unknown CVS response: %s" % l)
237 else:
237 else:
238 raise util.Abort("unknown CVS response: %s" % line)
238 raise util.Abort("unknown CVS response: %s" % line)
239
239
240 def getfile(self, file, rev):
240 def getfile(self, file, rev):
241 data, mode = self._getfile(file, rev)
241 data, mode = self._getfile(file, rev)
242 self.modecache[(file, rev)] = mode
242 self.modecache[(file, rev)] = mode
243 return data
243 return data
244
244
245 def getmode(self, file, rev):
245 def getmode(self, file, rev):
246 return self.modecache[(file, rev)]
246 return self.modecache[(file, rev)]
247
247
248 def getchanges(self, rev):
248 def getchanges(self, rev):
249 self.modecache = {}
249 self.modecache = {}
250 files = self.files[rev]
250 files = self.files[rev]
251 cl = files.items()
251 cl = files.items()
252 cl.sort()
252 cl.sort()
253 return cl
253 return (cl, {})
254
254
255 def getcommit(self, rev):
255 def getcommit(self, rev):
256 return self.changeset[rev]
256 return self.changeset[rev]
257
257
258 def gettags(self):
258 def gettags(self):
259 return self.tags
259 return self.tags
@@ -1,101 +1,101
1 # git support for the convert extension
1 # git support for the convert extension
2
2
3 import os
3 import os
4
4
5 from common import NoRepo, commit, converter_source
5 from common import NoRepo, commit, converter_source
6
6
7 class convert_git(converter_source):
7 class convert_git(converter_source):
8 def gitcmd(self, s):
8 def gitcmd(self, s):
9 return os.popen('GIT_DIR=%s %s' % (self.path, s))
9 return os.popen('GIT_DIR=%s %s' % (self.path, s))
10
10
11 def __init__(self, ui, path, rev=None):
11 def __init__(self, ui, path, rev=None):
12 super(convert_git, self).__init__(ui, path, rev=rev)
12 super(convert_git, self).__init__(ui, path, rev=rev)
13
13
14 if os.path.isdir(path + "/.git"):
14 if os.path.isdir(path + "/.git"):
15 path += "/.git"
15 path += "/.git"
16 if not os.path.exists(path + "/objects"):
16 if not os.path.exists(path + "/objects"):
17 raise NoRepo("couldn't open GIT repo %s" % path)
17 raise NoRepo("couldn't open GIT repo %s" % path)
18 self.path = path
18 self.path = path
19
19
20 def getheads(self):
20 def getheads(self):
21 if not self.rev:
21 if not self.rev:
22 return self.gitcmd('git-rev-parse --branches').read().splitlines()
22 return self.gitcmd('git-rev-parse --branches').read().splitlines()
23 else:
23 else:
24 fh = self.gitcmd("git-rev-parse --verify %s" % self.rev)
24 fh = self.gitcmd("git-rev-parse --verify %s" % self.rev)
25 return [fh.read()[:-1]]
25 return [fh.read()[:-1]]
26
26
27 def catfile(self, rev, type):
27 def catfile(self, rev, type):
28 if rev == "0" * 40: raise IOError()
28 if rev == "0" * 40: raise IOError()
29 fh = self.gitcmd("git-cat-file %s %s 2>/dev/null" % (type, rev))
29 fh = self.gitcmd("git-cat-file %s %s 2>/dev/null" % (type, rev))
30 return fh.read()
30 return fh.read()
31
31
32 def getfile(self, name, rev):
32 def getfile(self, name, rev):
33 return self.catfile(rev, "blob")
33 return self.catfile(rev, "blob")
34
34
35 def getmode(self, name, rev):
35 def getmode(self, name, rev):
36 return self.modecache[(name, rev)]
36 return self.modecache[(name, rev)]
37
37
38 def getchanges(self, version):
38 def getchanges(self, version):
39 self.modecache = {}
39 self.modecache = {}
40 fh = self.gitcmd("git-diff-tree --root -m -r %s" % version)
40 fh = self.gitcmd("git-diff-tree --root -m -r %s" % version)
41 changes = []
41 changes = []
42 for l in fh:
42 for l in fh:
43 if "\t" not in l: continue
43 if "\t" not in l: continue
44 m, f = l[:-1].split("\t")
44 m, f = l[:-1].split("\t")
45 m = m.split()
45 m = m.split()
46 h = m[3]
46 h = m[3]
47 p = (m[1] == "100755")
47 p = (m[1] == "100755")
48 s = (m[1] == "120000")
48 s = (m[1] == "120000")
49 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
49 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
50 changes.append((f, h))
50 changes.append((f, h))
51 return changes
51 return (changes, {})
52
52
53 def getcommit(self, version):
53 def getcommit(self, version):
54 c = self.catfile(version, "commit") # read the commit hash
54 c = self.catfile(version, "commit") # read the commit hash
55 end = c.find("\n\n")
55 end = c.find("\n\n")
56 message = c[end+2:]
56 message = c[end+2:]
57 message = self.recode(message)
57 message = self.recode(message)
58 l = c[:end].splitlines()
58 l = c[:end].splitlines()
59 manifest = l[0].split()[1]
59 manifest = l[0].split()[1]
60 parents = []
60 parents = []
61 for e in l[1:]:
61 for e in l[1:]:
62 n, v = e.split(" ", 1)
62 n, v = e.split(" ", 1)
63 if n == "author":
63 if n == "author":
64 p = v.split()
64 p = v.split()
65 tm, tz = p[-2:]
65 tm, tz = p[-2:]
66 author = " ".join(p[:-2])
66 author = " ".join(p[:-2])
67 if author[0] == "<": author = author[1:-1]
67 if author[0] == "<": author = author[1:-1]
68 author = self.recode(author)
68 author = self.recode(author)
69 if n == "committer":
69 if n == "committer":
70 p = v.split()
70 p = v.split()
71 tm, tz = p[-2:]
71 tm, tz = p[-2:]
72 committer = " ".join(p[:-2])
72 committer = " ".join(p[:-2])
73 if committer[0] == "<": committer = committer[1:-1]
73 if committer[0] == "<": committer = committer[1:-1]
74 committer = self.recode(committer)
74 committer = self.recode(committer)
75 message += "\ncommitter: %s\n" % committer
75 message += "\ncommitter: %s\n" % committer
76 if n == "parent": parents.append(v)
76 if n == "parent": parents.append(v)
77
77
78 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
78 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
79 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
79 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
80 date = tm + " " + str(tz)
80 date = tm + " " + str(tz)
81 author = author or "unknown"
81 author = author or "unknown"
82
82
83 c = commit(parents=parents, date=date, author=author, desc=message,
83 c = commit(parents=parents, date=date, author=author, desc=message,
84 rev=version)
84 rev=version)
85 return c
85 return c
86
86
87 def gettags(self):
87 def gettags(self):
88 tags = {}
88 tags = {}
89 fh = self.gitcmd('git-ls-remote --tags "%s" 2>/dev/null' % self.path)
89 fh = self.gitcmd('git-ls-remote --tags "%s" 2>/dev/null' % self.path)
90 prefix = 'refs/tags/'
90 prefix = 'refs/tags/'
91 for line in fh:
91 for line in fh:
92 line = line.strip()
92 line = line.strip()
93 if not line.endswith("^{}"):
93 if not line.endswith("^{}"):
94 continue
94 continue
95 node, tag = line.split(None, 1)
95 node, tag = line.split(None, 1)
96 if not tag.startswith(prefix):
96 if not tag.startswith(prefix):
97 continue
97 continue
98 tag = tag[len(prefix):-3]
98 tag = tag[len(prefix):-3]
99 tags[tag] = node
99 tags[tag] = node
100
100
101 return tags
101 return tags
@@ -1,175 +1,175
1 # hg backend for convert extension
1 # hg backend for convert extension
2
2
3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
4 # the whitespace from the ends of commit messages, but new versions
4 # the whitespace from the ends of commit messages, but new versions
5 # do. Changesets created by those older versions, then converted, may
5 # do. Changesets created by those older versions, then converted, may
6 # thus have different hashes for changesets that are otherwise
6 # thus have different hashes for changesets that are otherwise
7 # identical.
7 # identical.
8
8
9
9
10 import os, time
10 import os, time
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import *
12 from mercurial.node import *
13 from mercurial import hg, lock, revlog, util
13 from mercurial import hg, lock, revlog, util
14
14
15 from common import NoRepo, commit, converter_source, converter_sink
15 from common import NoRepo, commit, converter_source, converter_sink
16
16
17 class mercurial_sink(converter_sink):
17 class mercurial_sink(converter_sink):
18 def __init__(self, ui, path):
18 def __init__(self, ui, path):
19 self.path = path
19 self.path = path
20 self.ui = ui
20 self.ui = ui
21 try:
21 try:
22 self.repo = hg.repository(self.ui, path)
22 self.repo = hg.repository(self.ui, path)
23 except:
23 except:
24 raise NoRepo("could not open hg repo %s as sink" % path)
24 raise NoRepo("could not open hg repo %s as sink" % path)
25 self.lock = None
25 self.lock = None
26 self.wlock = None
26 self.wlock = None
27 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
27 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
28
28
29 def before(self):
29 def before(self):
30 self.wlock = self.repo.wlock()
30 self.wlock = self.repo.wlock()
31 self.lock = self.repo.lock()
31 self.lock = self.repo.lock()
32
32
33 def after(self):
33 def after(self):
34 self.lock = None
34 self.lock = None
35 self.wlock = None
35 self.wlock = None
36
36
37 def revmapfile(self):
37 def revmapfile(self):
38 return os.path.join(self.path, ".hg", "shamap")
38 return os.path.join(self.path, ".hg", "shamap")
39
39
40 def authorfile(self):
40 def authorfile(self):
41 return os.path.join(self.path, ".hg", "authormap")
41 return os.path.join(self.path, ".hg", "authormap")
42
42
43 def getheads(self):
43 def getheads(self):
44 h = self.repo.changelog.heads()
44 h = self.repo.changelog.heads()
45 return [ hex(x) for x in h ]
45 return [ hex(x) for x in h ]
46
46
47 def putfile(self, f, e, data):
47 def putfile(self, f, e, data):
48 self.repo.wwrite(f, data, e)
48 self.repo.wwrite(f, data, e)
49 if f not in self.repo.dirstate:
49 if f not in self.repo.dirstate:
50 self.repo.dirstate.add(f)
50 self.repo.dirstate.add(f)
51
51
52 def copyfile(self, source, dest):
52 def copyfile(self, source, dest):
53 self.repo.copy(source, dest)
53 self.repo.copy(source, dest)
54
54
55 def delfile(self, f):
55 def delfile(self, f):
56 try:
56 try:
57 os.unlink(self.repo.wjoin(f))
57 os.unlink(self.repo.wjoin(f))
58 #self.repo.remove([f])
58 #self.repo.remove([f])
59 except:
59 except:
60 pass
60 pass
61
61
62 def putcommit(self, files, parents, commit):
62 def putcommit(self, files, parents, commit):
63 if not files:
63 if not files:
64 return hex(self.repo.changelog.tip())
64 return hex(self.repo.changelog.tip())
65
65
66 seen = {hex(nullid): 1}
66 seen = {hex(nullid): 1}
67 pl = []
67 pl = []
68 for p in parents:
68 for p in parents:
69 if p not in seen:
69 if p not in seen:
70 pl.append(p)
70 pl.append(p)
71 seen[p] = 1
71 seen[p] = 1
72 parents = pl
72 parents = pl
73
73
74 if len(parents) < 2: parents.append("0" * 40)
74 if len(parents) < 2: parents.append("0" * 40)
75 if len(parents) < 2: parents.append("0" * 40)
75 if len(parents) < 2: parents.append("0" * 40)
76 p2 = parents.pop(0)
76 p2 = parents.pop(0)
77
77
78 text = commit.desc
78 text = commit.desc
79 extra = {}
79 extra = {}
80 if self.branchnames and commit.branch:
80 if self.branchnames and commit.branch:
81 extra['branch'] = commit.branch
81 extra['branch'] = commit.branch
82 if commit.rev:
82 if commit.rev:
83 extra['convert_revision'] = commit.rev
83 extra['convert_revision'] = commit.rev
84
84
85 while parents:
85 while parents:
86 p1 = p2
86 p1 = p2
87 p2 = parents.pop(0)
87 p2 = parents.pop(0)
88 a = self.repo.rawcommit(files, text, commit.author, commit.date,
88 a = self.repo.rawcommit(files, text, commit.author, commit.date,
89 bin(p1), bin(p2), extra=extra)
89 bin(p1), bin(p2), extra=extra)
90 self.repo.dirstate.invalidate()
90 self.repo.dirstate.invalidate()
91 text = "(octopus merge fixup)\n"
91 text = "(octopus merge fixup)\n"
92 p2 = hg.hex(self.repo.changelog.tip())
92 p2 = hg.hex(self.repo.changelog.tip())
93
93
94 return p2
94 return p2
95
95
96 def puttags(self, tags):
96 def puttags(self, tags):
97 try:
97 try:
98 old = self.repo.wfile(".hgtags").read()
98 old = self.repo.wfile(".hgtags").read()
99 oldlines = old.splitlines(1)
99 oldlines = old.splitlines(1)
100 oldlines.sort()
100 oldlines.sort()
101 except:
101 except:
102 oldlines = []
102 oldlines = []
103
103
104 k = tags.keys()
104 k = tags.keys()
105 k.sort()
105 k.sort()
106 newlines = []
106 newlines = []
107 for tag in k:
107 for tag in k:
108 newlines.append("%s %s\n" % (tags[tag], tag))
108 newlines.append("%s %s\n" % (tags[tag], tag))
109
109
110 newlines.sort()
110 newlines.sort()
111
111
112 if newlines != oldlines:
112 if newlines != oldlines:
113 self.ui.status("updating tags\n")
113 self.ui.status("updating tags\n")
114 f = self.repo.wfile(".hgtags", "w")
114 f = self.repo.wfile(".hgtags", "w")
115 f.write("".join(newlines))
115 f.write("".join(newlines))
116 f.close()
116 f.close()
117 if not oldlines: self.repo.add([".hgtags"])
117 if not oldlines: self.repo.add([".hgtags"])
118 date = "%s 0" % int(time.mktime(time.gmtime()))
118 date = "%s 0" % int(time.mktime(time.gmtime()))
119 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
119 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
120 date, self.repo.changelog.tip(), nullid)
120 date, self.repo.changelog.tip(), nullid)
121 return hex(self.repo.changelog.tip())
121 return hex(self.repo.changelog.tip())
122
122
123 class mercurial_source(converter_source):
123 class mercurial_source(converter_source):
124 def __init__(self, ui, path, rev=None):
124 def __init__(self, ui, path, rev=None):
125 converter_source.__init__(self, ui, path, rev)
125 converter_source.__init__(self, ui, path, rev)
126 self.repo = hg.repository(self.ui, path)
126 self.repo = hg.repository(self.ui, path)
127 self.lastrev = None
127 self.lastrev = None
128 self.lastctx = None
128 self.lastctx = None
129
129
130 def changectx(self, rev):
130 def changectx(self, rev):
131 if self.lastrev != rev:
131 if self.lastrev != rev:
132 self.lastctx = self.repo.changectx(rev)
132 self.lastctx = self.repo.changectx(rev)
133 self.lastrev = rev
133 self.lastrev = rev
134 return self.lastctx
134 return self.lastctx
135
135
136 def getheads(self):
136 def getheads(self):
137 return [hex(node) for node in self.repo.heads()]
137 return [hex(node) for node in self.repo.heads()]
138
138
139 def getfile(self, name, rev):
139 def getfile(self, name, rev):
140 try:
140 try:
141 return self.changectx(rev).filectx(name).data()
141 return self.changectx(rev).filectx(name).data()
142 except revlog.LookupError, err:
142 except revlog.LookupError, err:
143 raise IOError(err)
143 raise IOError(err)
144
144
145 def getmode(self, name, rev):
145 def getmode(self, name, rev):
146 m = self.changectx(rev).manifest()
146 m = self.changectx(rev).manifest()
147 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
147 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
148
148
149 def getchanges(self, rev):
149 def getchanges(self, rev):
150 ctx = self.changectx(rev)
150 ctx = self.changectx(rev)
151 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
151 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
152 changes = [(name, rev) for name in m + a + r]
152 changes = [(name, rev) for name in m + a + r]
153 changes.sort()
153 changes.sort()
154 return changes
154 return (changes, self.getcopies(ctx))
155
155
156 def getcopies(self, ctx):
156 def getcopies(self, ctx):
157 added = self.repo.status(ctx.parents()[0].node(), ctx.node())[1]
157 added = self.repo.status(ctx.parents()[0].node(), ctx.node())[1]
158 copies = {}
158 copies = {}
159 for name in added:
159 for name in added:
160 try:
160 try:
161 copies[name] = ctx.filectx(name).renamed()[0]
161 copies[name] = ctx.filectx(name).renamed()[0]
162 except TypeError:
162 except TypeError:
163 pass
163 pass
164 return copies
164 return copies
165
165
166 def getcommit(self, rev):
166 def getcommit(self, rev):
167 ctx = self.changectx(rev)
167 ctx = self.changectx(rev)
168 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
168 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
169 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
169 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
170 desc=ctx.description(), parents=parents,
170 desc=ctx.description(), parents=parents,
171 branch=ctx.branch(), copies=self.getcopies(ctx))
171 branch=ctx.branch())
172
172
173 def gettags(self):
173 def gettags(self):
174 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
174 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
175 return dict([(name, hex(node)) for name, node in tags])
175 return dict([(name, hex(node)) for name, node in tags])
@@ -1,641 +1,643
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 #
4 #
5 # Configuration options:
5 # Configuration options:
6 #
6 #
7 # convert.svn.trunk
7 # convert.svn.trunk
8 # Relative path to the trunk (default: "trunk")
8 # Relative path to the trunk (default: "trunk")
9 # convert.svn.branches
9 # convert.svn.branches
10 # Relative path to tree of branches (default: "branches")
10 # Relative path to tree of branches (default: "branches")
11 #
11 #
12 # Set these in a hgrc, or on the command line as follows:
12 # Set these in a hgrc, or on the command line as follows:
13 #
13 #
14 # hg convert --config convert.svn.trunk=wackoname [...]
14 # hg convert --config convert.svn.trunk=wackoname [...]
15
15
16 import locale
16 import locale
17 import os
17 import os
18 import cPickle as pickle
18 import cPickle as pickle
19 from mercurial import util
19 from mercurial import util
20
20
21 # Subversion stuff. Works best with very recent Python SVN bindings
21 # Subversion stuff. Works best with very recent Python SVN bindings
22 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
22 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
23 # these bindings.
23 # these bindings.
24
24
25 from cStringIO import StringIO
25 from cStringIO import StringIO
26
26
27 from common import NoRepo, commit, converter_source
27 from common import NoRepo, commit, converter_source
28
28
29 try:
29 try:
30 from svn.core import SubversionException, Pool
30 from svn.core import SubversionException, Pool
31 import svn
31 import svn
32 import svn.client
32 import svn.client
33 import svn.core
33 import svn.core
34 import svn.ra
34 import svn.ra
35 import svn.delta
35 import svn.delta
36 import transport
36 import transport
37 except ImportError:
37 except ImportError:
38 pass
38 pass
39
39
40 def geturl(path):
40 def geturl(path):
41 try:
41 try:
42 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
42 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
43 except SubversionException:
43 except SubversionException:
44 pass
44 pass
45 if os.path.isdir(path):
45 if os.path.isdir(path):
46 return 'file://%s' % os.path.normpath(os.path.abspath(path))
46 return 'file://%s' % os.path.normpath(os.path.abspath(path))
47 return path
47 return path
48
48
49 def optrev(number):
49 def optrev(number):
50 optrev = svn.core.svn_opt_revision_t()
50 optrev = svn.core.svn_opt_revision_t()
51 optrev.kind = svn.core.svn_opt_revision_number
51 optrev.kind = svn.core.svn_opt_revision_number
52 optrev.value.number = number
52 optrev.value.number = number
53 return optrev
53 return optrev
54
54
55 class changedpath(object):
55 class changedpath(object):
56 def __init__(self, p):
56 def __init__(self, p):
57 self.copyfrom_path = p.copyfrom_path
57 self.copyfrom_path = p.copyfrom_path
58 self.copyfrom_rev = p.copyfrom_rev
58 self.copyfrom_rev = p.copyfrom_rev
59 self.action = p.action
59 self.action = p.action
60
60
61 # SVN conversion code stolen from bzr-svn and tailor
61 # SVN conversion code stolen from bzr-svn and tailor
62 class convert_svn(converter_source):
62 class convert_svn(converter_source):
63 def __init__(self, ui, url, rev=None):
63 def __init__(self, ui, url, rev=None):
64 super(convert_svn, self).__init__(ui, url, rev=rev)
64 super(convert_svn, self).__init__(ui, url, rev=rev)
65
65
66 try:
66 try:
67 SubversionException
67 SubversionException
68 except NameError:
68 except NameError:
69 msg = 'subversion python bindings could not be loaded\n'
69 msg = 'subversion python bindings could not be loaded\n'
70 ui.warn(msg)
70 ui.warn(msg)
71 raise NoRepo(msg)
71 raise NoRepo(msg)
72
72
73 self.encoding = locale.getpreferredencoding()
73 self.encoding = locale.getpreferredencoding()
74 self.lastrevs = {}
74 self.lastrevs = {}
75
75
76 latest = None
76 latest = None
77 if rev:
77 if rev:
78 try:
78 try:
79 latest = int(rev)
79 latest = int(rev)
80 except ValueError:
80 except ValueError:
81 raise util.Abort('svn: revision %s is not an integer' % rev)
81 raise util.Abort('svn: revision %s is not an integer' % rev)
82 try:
82 try:
83 # Support file://path@rev syntax. Useful e.g. to convert
83 # Support file://path@rev syntax. Useful e.g. to convert
84 # deleted branches.
84 # deleted branches.
85 at = url.rfind('@')
85 at = url.rfind('@')
86 if at >= 0:
86 if at >= 0:
87 latest = int(url[at+1:])
87 latest = int(url[at+1:])
88 url = url[:at]
88 url = url[:at]
89 except ValueError, e:
89 except ValueError, e:
90 pass
90 pass
91 self.url = geturl(url)
91 self.url = geturl(url)
92 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
92 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
93 try:
93 try:
94 self.transport = transport.SvnRaTransport(url=self.url)
94 self.transport = transport.SvnRaTransport(url=self.url)
95 self.ra = self.transport.ra
95 self.ra = self.transport.ra
96 self.ctx = self.transport.client
96 self.ctx = self.transport.client
97 self.base = svn.ra.get_repos_root(self.ra)
97 self.base = svn.ra.get_repos_root(self.ra)
98 self.module = self.url[len(self.base):]
98 self.module = self.url[len(self.base):]
99 self.modulemap = {} # revision, module
99 self.modulemap = {} # revision, module
100 self.commits = {}
100 self.commits = {}
101 self.files = {}
101 self.paths = {}
102 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
102 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
103 except SubversionException, e:
103 except SubversionException, e:
104 raise NoRepo("couldn't open SVN repo %s" % self.url)
104 raise NoRepo("couldn't open SVN repo %s" % self.url)
105
105
106 try:
106 try:
107 self.get_blacklist()
107 self.get_blacklist()
108 except IOError, e:
108 except IOError, e:
109 pass
109 pass
110
110
111 self.last_changed = self.latest(self.module, latest)
111 self.last_changed = self.latest(self.module, latest)
112
112
113 self.head = self.revid(self.last_changed)
113 self.head = self.revid(self.last_changed)
114
114
115 def setrevmap(self, revmap):
115 def setrevmap(self, revmap):
116 lastrevs = {}
116 lastrevs = {}
117 for revid in revmap.keys():
117 for revid in revmap.keys():
118 uuid, module, revnum = self.revsplit(revid)
118 uuid, module, revnum = self.revsplit(revid)
119 lastrevnum = lastrevs.setdefault(module, revnum)
119 lastrevnum = lastrevs.setdefault(module, revnum)
120 if revnum > lastrevnum:
120 if revnum > lastrevnum:
121 lastrevs[module] = revnum
121 lastrevs[module] = revnum
122 self.lastrevs = lastrevs
122 self.lastrevs = lastrevs
123
123
124 def exists(self, path, optrev):
124 def exists(self, path, optrev):
125 try:
125 try:
126 return svn.client.ls(self.url.rstrip('/') + '/' + path,
126 return svn.client.ls(self.url.rstrip('/') + '/' + path,
127 optrev, False, self.ctx)
127 optrev, False, self.ctx)
128 except SubversionException, err:
128 except SubversionException, err:
129 return []
129 return []
130
130
131 def getheads(self):
131 def getheads(self):
132 # detect standard /branches, /tags, /trunk layout
132 # detect standard /branches, /tags, /trunk layout
133 rev = optrev(self.last_changed)
133 rev = optrev(self.last_changed)
134 rpath = self.url.strip('/')
134 rpath = self.url.strip('/')
135 cfgtrunk = self.ui.config('convert', 'svn.trunk')
135 cfgtrunk = self.ui.config('convert', 'svn.trunk')
136 cfgbranches = self.ui.config('convert', 'svn.branches')
136 cfgbranches = self.ui.config('convert', 'svn.branches')
137 trunk = (cfgtrunk or 'trunk').strip('/')
137 trunk = (cfgtrunk or 'trunk').strip('/')
138 branches = (cfgbranches or 'branches').strip('/')
138 branches = (cfgbranches or 'branches').strip('/')
139 if self.exists(trunk, rev) and self.exists(branches, rev):
139 if self.exists(trunk, rev) and self.exists(branches, rev):
140 self.ui.note('found trunk at %r and branches at %r\n' %
140 self.ui.note('found trunk at %r and branches at %r\n' %
141 (trunk, branches))
141 (trunk, branches))
142 oldmodule = self.module
142 oldmodule = self.module
143 self.module += '/' + trunk
143 self.module += '/' + trunk
144 lt = self.latest(self.module, self.last_changed)
144 lt = self.latest(self.module, self.last_changed)
145 self.head = self.revid(lt)
145 self.head = self.revid(lt)
146 self.heads = [self.head]
146 self.heads = [self.head]
147 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
147 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
148 self.ctx)
148 self.ctx)
149 for branch in branchnames.keys():
149 for branch in branchnames.keys():
150 if oldmodule:
150 if oldmodule:
151 module = '/' + oldmodule + '/' + branches + '/' + branch
151 module = '/' + oldmodule + '/' + branches + '/' + branch
152 else:
152 else:
153 module = '/' + branches + '/' + branch
153 module = '/' + branches + '/' + branch
154 brevnum = self.latest(module, self.last_changed)
154 brevnum = self.latest(module, self.last_changed)
155 brev = self.revid(brevnum, module)
155 brev = self.revid(brevnum, module)
156 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
156 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
157 self.heads.append(brev)
157 self.heads.append(brev)
158 elif cfgtrunk or cfgbranches:
158 elif cfgtrunk or cfgbranches:
159 raise util.Abort(_('trunk/branch layout expected, '
159 raise util.Abort(_('trunk/branch layout expected, '
160 'but not found'))
160 'but not found'))
161 else:
161 else:
162 self.ui.note('working with one branch\n')
162 self.ui.note('working with one branch\n')
163 self.heads = [self.head]
163 self.heads = [self.head]
164 return self.heads
164 return self.heads
165
165
166 def getfile(self, file, rev):
166 def getfile(self, file, rev):
167 data, mode = self._getfile(file, rev)
167 data, mode = self._getfile(file, rev)
168 self.modecache[(file, rev)] = mode
168 self.modecache[(file, rev)] = mode
169 return data
169 return data
170
170
171 def getmode(self, file, rev):
171 def getmode(self, file, rev):
172 return self.modecache[(file, rev)]
172 return self.modecache[(file, rev)]
173
173
174 def getchanges(self, rev):
174 def getchanges(self, rev):
175 self.modecache = {}
175 self.modecache = {}
176 files = self.files[rev]
176 (paths, parents) = self.paths[rev]
177 cl = files
177 files, copies = self.expandpaths(rev, paths, parents)
178 cl.sort()
178 files.sort()
179 files = zip(files, [rev] * len(files))
180
179 # caller caches the result, so free it here to release memory
181 # caller caches the result, so free it here to release memory
180 del self.files[rev]
182 del self.paths[rev]
181 return cl
183 return (files, copies)
182
184
183 def getcommit(self, rev):
185 def getcommit(self, rev):
184 if rev not in self.commits:
186 if rev not in self.commits:
185 uuid, module, revnum = self.revsplit(rev)
187 uuid, module, revnum = self.revsplit(rev)
186 self.module = module
188 self.module = module
187 self.reparent(module)
189 self.reparent(module)
188 stop = self.lastrevs.get(module, 0)
190 stop = self.lastrevs.get(module, 0)
189 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
191 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
190 commit = self.commits[rev]
192 commit = self.commits[rev]
191 # caller caches the result, so free it here to release memory
193 # caller caches the result, so free it here to release memory
192 del self.commits[rev]
194 del self.commits[rev]
193 return commit
195 return commit
194
196
195 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
197 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
196 strict_node_history=False):
198 strict_node_history=False):
197 '''wrapper for svn.ra.get_log.
199 '''wrapper for svn.ra.get_log.
198 on a large repository, svn.ra.get_log pins huge amounts of
200 on a large repository, svn.ra.get_log pins huge amounts of
199 memory that cannot be recovered. work around it by forking
201 memory that cannot be recovered. work around it by forking
200 and writing results over a pipe.'''
202 and writing results over a pipe.'''
201
203
202 def child(fp):
204 def child(fp):
203 protocol = -1
205 protocol = -1
204 def receiver(orig_paths, revnum, author, date, message, pool):
206 def receiver(orig_paths, revnum, author, date, message, pool):
205 if orig_paths is not None:
207 if orig_paths is not None:
206 for k, v in orig_paths.iteritems():
208 for k, v in orig_paths.iteritems():
207 orig_paths[k] = changedpath(v)
209 orig_paths[k] = changedpath(v)
208 pickle.dump((orig_paths, revnum, author, date, message),
210 pickle.dump((orig_paths, revnum, author, date, message),
209 fp, protocol)
211 fp, protocol)
210
212
211 try:
213 try:
212 # Use an ra of our own so that our parent can consume
214 # Use an ra of our own so that our parent can consume
213 # our results without confusing the server.
215 # our results without confusing the server.
214 t = transport.SvnRaTransport(url=self.url)
216 t = transport.SvnRaTransport(url=self.url)
215 svn.ra.get_log(t.ra, paths, start, end, limit,
217 svn.ra.get_log(t.ra, paths, start, end, limit,
216 discover_changed_paths,
218 discover_changed_paths,
217 strict_node_history,
219 strict_node_history,
218 receiver)
220 receiver)
219 except SubversionException, (_, num):
221 except SubversionException, (_, num):
220 self.ui.print_exc()
222 self.ui.print_exc()
221 pickle.dump(num, fp, protocol)
223 pickle.dump(num, fp, protocol)
222 else:
224 else:
223 pickle.dump(None, fp, protocol)
225 pickle.dump(None, fp, protocol)
224 fp.close()
226 fp.close()
225
227
226 def parent(fp):
228 def parent(fp):
227 while True:
229 while True:
228 entry = pickle.load(fp)
230 entry = pickle.load(fp)
229 try:
231 try:
230 orig_paths, revnum, author, date, message = entry
232 orig_paths, revnum, author, date, message = entry
231 except:
233 except:
232 if entry is None:
234 if entry is None:
233 break
235 break
234 raise SubversionException("child raised exception", entry)
236 raise SubversionException("child raised exception", entry)
235 yield entry
237 yield entry
236
238
237 rfd, wfd = os.pipe()
239 rfd, wfd = os.pipe()
238 pid = os.fork()
240 pid = os.fork()
239 if pid:
241 if pid:
240 os.close(wfd)
242 os.close(wfd)
241 for p in parent(os.fdopen(rfd, 'rb')):
243 for p in parent(os.fdopen(rfd, 'rb')):
242 yield p
244 yield p
243 ret = os.waitpid(pid, 0)[1]
245 ret = os.waitpid(pid, 0)[1]
244 if ret:
246 if ret:
245 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
247 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
246 else:
248 else:
247 os.close(rfd)
249 os.close(rfd)
248 child(os.fdopen(wfd, 'wb'))
250 child(os.fdopen(wfd, 'wb'))
249 os._exit(0)
251 os._exit(0)
250
252
251 def gettags(self):
253 def gettags(self):
252 tags = {}
254 tags = {}
253 start = self.revnum(self.head)
255 start = self.revnum(self.head)
254 try:
256 try:
255 for entry in self.get_log(['/tags'], 0, start):
257 for entry in self.get_log(['/tags'], 0, start):
256 orig_paths, revnum, author, date, message = entry
258 orig_paths, revnum, author, date, message = entry
257 for path in orig_paths:
259 for path in orig_paths:
258 if not path.startswith('/tags/'):
260 if not path.startswith('/tags/'):
259 continue
261 continue
260 ent = orig_paths[path]
262 ent = orig_paths[path]
261 source = ent.copyfrom_path
263 source = ent.copyfrom_path
262 rev = ent.copyfrom_rev
264 rev = ent.copyfrom_rev
263 tag = path.split('/', 2)[2]
265 tag = path.split('/', 2)[2]
264 tags[tag] = self.revid(rev, module=source)
266 tags[tag] = self.revid(rev, module=source)
265 except SubversionException, (_, num):
267 except SubversionException, (_, num):
266 self.ui.note('no tags found at revision %d\n' % start)
268 self.ui.note('no tags found at revision %d\n' % start)
267 return tags
269 return tags
268
270
269 # -- helper functions --
271 # -- helper functions --
270
272
271 def revid(self, revnum, module=None):
273 def revid(self, revnum, module=None):
272 if not module:
274 if not module:
273 module = self.module
275 module = self.module
274 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
276 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
275
277
276 def revnum(self, rev):
278 def revnum(self, rev):
277 return int(rev.split('@')[-1])
279 return int(rev.split('@')[-1])
278
280
279 def revsplit(self, rev):
281 def revsplit(self, rev):
280 url, revnum = rev.encode(self.encoding).split('@', 1)
282 url, revnum = rev.encode(self.encoding).split('@', 1)
281 revnum = int(revnum)
283 revnum = int(revnum)
282 parts = url.split('/', 1)
284 parts = url.split('/', 1)
283 uuid = parts.pop(0)[4:]
285 uuid = parts.pop(0)[4:]
284 mod = ''
286 mod = ''
285 if parts:
287 if parts:
286 mod = '/' + parts[0]
288 mod = '/' + parts[0]
287 return uuid, mod, revnum
289 return uuid, mod, revnum
288
290
289 def latest(self, path, stop=0):
291 def latest(self, path, stop=0):
290 'find the latest revision affecting path, up to stop'
292 'find the latest revision affecting path, up to stop'
291 if not stop:
293 if not stop:
292 stop = svn.ra.get_latest_revnum(self.ra)
294 stop = svn.ra.get_latest_revnum(self.ra)
293 try:
295 try:
294 self.reparent('')
296 self.reparent('')
295 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
297 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
296 self.reparent(self.module)
298 self.reparent(self.module)
297 except SubversionException:
299 except SubversionException:
298 dirent = None
300 dirent = None
299 if not dirent:
301 if not dirent:
300 print self.base, path
302 print self.base, path
301 raise util.Abort('%s not found up to revision %d' % (path, stop))
303 raise util.Abort('%s not found up to revision %d' % (path, stop))
302
304
303 return dirent.created_rev
305 return dirent.created_rev
304
306
305 def get_blacklist(self):
307 def get_blacklist(self):
306 """Avoid certain revision numbers.
308 """Avoid certain revision numbers.
307 It is not uncommon for two nearby revisions to cancel each other
309 It is not uncommon for two nearby revisions to cancel each other
308 out, e.g. 'I copied trunk into a subdirectory of itself instead
310 out, e.g. 'I copied trunk into a subdirectory of itself instead
309 of making a branch'. The converted repository is significantly
311 of making a branch'. The converted repository is significantly
310 smaller if we ignore such revisions."""
312 smaller if we ignore such revisions."""
311 self.blacklist = set()
313 self.blacklist = set()
312 blacklist = self.blacklist
314 blacklist = self.blacklist
313 for line in file("blacklist.txt", "r"):
315 for line in file("blacklist.txt", "r"):
314 if not line.startswith("#"):
316 if not line.startswith("#"):
315 try:
317 try:
316 svn_rev = int(line.strip())
318 svn_rev = int(line.strip())
317 blacklist.add(svn_rev)
319 blacklist.add(svn_rev)
318 except ValueError, e:
320 except ValueError, e:
319 pass # not an integer or a comment
321 pass # not an integer or a comment
320
322
321 def is_blacklisted(self, svn_rev):
323 def is_blacklisted(self, svn_rev):
322 return svn_rev in self.blacklist
324 return svn_rev in self.blacklist
323
325
324 def reparent(self, module):
326 def reparent(self, module):
325 svn_url = self.base + module
327 svn_url = self.base + module
326 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
328 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
327 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
329 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
328
330
329 def expandpaths(self, rev, paths, parents):
331 def expandpaths(self, rev, paths, parents):
330 def get_entry_from_path(path, module=self.module):
332 def get_entry_from_path(path, module=self.module):
331 # Given the repository url of this wc, say
333 # Given the repository url of this wc, say
332 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
334 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
333 # extract the "entry" portion (a relative path) from what
335 # extract the "entry" portion (a relative path) from what
334 # svn log --xml says, ie
336 # svn log --xml says, ie
335 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
337 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
336 # that is to say "tests/PloneTestCase.py"
338 # that is to say "tests/PloneTestCase.py"
337 if path.startswith(module):
339 if path.startswith(module):
338 relative = path[len(module):]
340 relative = path[len(module):]
339 if relative.startswith('/'):
341 if relative.startswith('/'):
340 return relative[1:]
342 return relative[1:]
341 else:
343 else:
342 return relative
344 return relative
343
345
344 # The path is outside our tracked tree...
346 # The path is outside our tracked tree...
345 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
347 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
346 return None
348 return None
347
349
348 entries = []
350 entries = []
349 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
351 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
350 copies = {}
352 copies = {}
351 revnum = self.revnum(rev)
353 revnum = self.revnum(rev)
352
354
355 if revnum in self.modulemap:
356 new_module = self.modulemap[revnum]
357 if new_module != self.module:
358 self.module = new_module
359 self.reparent(self.module)
360
353 for path, ent in paths:
361 for path, ent in paths:
354 # self.ui.write("path %s\n" % path)
362 self.ui.write("path %s\n" % path)
355 entrypath = get_entry_from_path(path, module=self.module)
363 entrypath = get_entry_from_path(path, module=self.module)
356 entry = entrypath.decode(self.encoding)
364 entry = entrypath.decode(self.encoding)
357
365
358 kind = svn.ra.check_path(self.ra, entrypath, revnum)
366 kind = svn.ra.check_path(self.ra, entrypath, revnum)
359 if kind == svn.core.svn_node_file:
367 if kind == svn.core.svn_node_file:
360 if ent.copyfrom_path:
368 if ent.copyfrom_path:
361 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
369 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
362 if copyfrom_path:
370 if copyfrom_path:
363 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
371 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
364 # It's probably important for hg that the source
372 # It's probably important for hg that the source
365 # exists in the revision's parent, not just the
373 # exists in the revision's parent, not just the
366 # ent.copyfrom_rev
374 # ent.copyfrom_rev
367 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
375 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
368 if fromkind != 0:
376 if fromkind != 0:
369 copies[self.recode(entry)] = self.recode(copyfrom_path)
377 copies[self.recode(entry)] = self.recode(copyfrom_path)
370 entries.append(self.recode(entry))
378 entries.append(self.recode(entry))
371 elif kind == 0: # gone, but had better be a deleted *file*
379 elif kind == 0: # gone, but had better be a deleted *file*
372 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
380 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
373
381
374 # if a branch is created but entries are removed in the same
382 # if a branch is created but entries are removed in the same
375 # changeset, get the right fromrev
383 # changeset, get the right fromrev
376 if parents:
384 if parents:
377 uuid, old_module, fromrev = self.revsplit(parents[0])
385 uuid, old_module, fromrev = self.revsplit(parents[0])
378 else:
386 else:
379 fromrev = revnum - 1
387 fromrev = revnum - 1
380 # might always need to be revnum - 1 in these 3 lines?
388 # might always need to be revnum - 1 in these 3 lines?
381 old_module = self.modulemap.get(fromrev, self.module)
389 old_module = self.modulemap.get(fromrev, self.module)
382
390
383 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
391 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
384 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
392 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
385
393
386 def lookup_parts(p):
394 def lookup_parts(p):
387 rc = None
395 rc = None
388 parts = p.split("/")
396 parts = p.split("/")
389 for i in range(len(parts)):
397 for i in range(len(parts)):
390 part = "/".join(parts[:i])
398 part = "/".join(parts[:i])
391 info = part, copyfrom.get(part, None)
399 info = part, copyfrom.get(part, None)
392 if info[1] is not None:
400 if info[1] is not None:
393 self.ui.debug("Found parent directory %s\n" % info[1])
401 self.ui.debug("Found parent directory %s\n" % info[1])
394 rc = info
402 rc = info
395 return rc
403 return rc
396
404
397 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
405 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
398
406
399 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
407 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
400
408
401 # need to remove fragment from lookup_parts and replace with copyfrom_path
409 # need to remove fragment from lookup_parts and replace with copyfrom_path
402 if frompath is not None:
410 if frompath is not None:
403 self.ui.debug("munge-o-matic\n")
411 self.ui.debug("munge-o-matic\n")
404 self.ui.debug(entrypath + '\n')
412 self.ui.debug(entrypath + '\n')
405 self.ui.debug(entrypath[len(frompath):] + '\n')
413 self.ui.debug(entrypath[len(frompath):] + '\n')
406 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
414 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
407 fromrev = froment.copyfrom_rev
415 fromrev = froment.copyfrom_rev
408 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
416 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
409
417
410 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
418 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
411 if fromkind == svn.core.svn_node_file: # a deleted file
419 if fromkind == svn.core.svn_node_file: # a deleted file
412 entries.append(self.recode(entry))
420 entries.append(self.recode(entry))
413 elif fromkind == svn.core.svn_node_dir:
421 elif fromkind == svn.core.svn_node_dir:
414 # print "Deleted/moved non-file:", revnum, path, ent
422 # print "Deleted/moved non-file:", revnum, path, ent
415 # children = self._find_children(path, revnum - 1)
423 # children = self._find_children(path, revnum - 1)
416 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
424 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
417 # Sometimes this is tricky. For example: in
425 # Sometimes this is tricky. For example: in
418 # The Subversion Repository revision 6940 a dir
426 # The Subversion Repository revision 6940 a dir
419 # was copied and one of its files was deleted
427 # was copied and one of its files was deleted
420 # from the new location in the same commit. This
428 # from the new location in the same commit. This
421 # code can't deal with that yet.
429 # code can't deal with that yet.
422 if ent.action == 'C':
430 if ent.action == 'C':
423 children = self._find_children(path, fromrev)
431 children = self._find_children(path, fromrev)
424 else:
432 else:
425 oroot = entrypath.strip('/')
433 oroot = entrypath.strip('/')
426 nroot = path.strip('/')
434 nroot = path.strip('/')
427 children = self._find_children(oroot, fromrev)
435 children = self._find_children(oroot, fromrev)
428 children = [s.replace(oroot,nroot) for s in children]
436 children = [s.replace(oroot,nroot) for s in children]
429 # Mark all [files, not directories] as deleted.
437 # Mark all [files, not directories] as deleted.
430 for child in children:
438 for child in children:
431 # Can we move a child directory and its
439 # Can we move a child directory and its
432 # parent in the same commit? (probably can). Could
440 # parent in the same commit? (probably can). Could
433 # cause problems if instead of revnum -1,
441 # cause problems if instead of revnum -1,
434 # we have to look in (copyfrom_path, revnum - 1)
442 # we have to look in (copyfrom_path, revnum - 1)
435 entrypath = get_entry_from_path("/" + child, module=old_module)
443 entrypath = get_entry_from_path("/" + child, module=old_module)
436 if entrypath:
444 if entrypath:
437 entry = self.recode(entrypath.decode(self.encoding))
445 entry = self.recode(entrypath.decode(self.encoding))
438 if entry in copies:
446 if entry in copies:
439 # deleted file within a copy
447 # deleted file within a copy
440 del copies[entry]
448 del copies[entry]
441 else:
449 else:
442 entries.append(entry)
450 entries.append(entry)
443 else:
451 else:
444 self.ui.debug('unknown path in revision %d: %s\n' % \
452 self.ui.debug('unknown path in revision %d: %s\n' % \
445 (revnum, path))
453 (revnum, path))
446 elif kind == svn.core.svn_node_dir:
454 elif kind == svn.core.svn_node_dir:
447 # Should probably synthesize normal file entries
455 # Should probably synthesize normal file entries
448 # and handle as above to clean up copy/rename handling.
456 # and handle as above to clean up copy/rename handling.
449
457
450 # If the directory just had a prop change,
458 # If the directory just had a prop change,
451 # then we shouldn't need to look for its children.
459 # then we shouldn't need to look for its children.
452 # Also this could create duplicate entries. Not sure
460 # Also this could create duplicate entries. Not sure
453 # whether this will matter. Maybe should make entries a set.
461 # whether this will matter. Maybe should make entries a set.
454 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
462 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
455 # This will fail if a directory was copied
463 # This will fail if a directory was copied
456 # from another branch and then some of its files
464 # from another branch and then some of its files
457 # were deleted in the same transaction.
465 # were deleted in the same transaction.
458 children = self._find_children(path, revnum)
466 children = self._find_children(path, revnum)
459 children.sort()
467 children.sort()
460 for child in children:
468 for child in children:
461 # Can we move a child directory and its
469 # Can we move a child directory and its
462 # parent in the same commit? (probably can). Could
470 # parent in the same commit? (probably can). Could
463 # cause problems if instead of revnum -1,
471 # cause problems if instead of revnum -1,
464 # we have to look in (copyfrom_path, revnum - 1)
472 # we have to look in (copyfrom_path, revnum - 1)
465 entrypath = get_entry_from_path("/" + child, module=self.module)
473 entrypath = get_entry_from_path("/" + child, module=self.module)
466 # print child, self.module, entrypath
474 # print child, self.module, entrypath
467 if entrypath:
475 if entrypath:
468 # Need to filter out directories here...
476 # Need to filter out directories here...
469 kind = svn.ra.check_path(self.ra, entrypath, revnum)
477 kind = svn.ra.check_path(self.ra, entrypath, revnum)
470 if kind != svn.core.svn_node_dir:
478 if kind != svn.core.svn_node_dir:
471 entries.append(self.recode(entrypath))
479 entries.append(self.recode(entrypath))
472
480
473 # Copies here (must copy all from source)
481 # Copies here (must copy all from source)
474 # Probably not a real problem for us if
482 # Probably not a real problem for us if
475 # source does not exist
483 # source does not exist
476
484
477 # Can do this with the copy command "hg copy"
485 # Can do this with the copy command "hg copy"
478 # if ent.copyfrom_path:
486 # if ent.copyfrom_path:
479 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
487 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
480 # module=self.module)
488 # module=self.module)
481 # copyto_entry = entrypath
489 # copyto_entry = entrypath
482 #
490 #
483 # print "copy directory", copyfrom_entry, 'to', copyto_entry
491 # print "copy directory", copyfrom_entry, 'to', copyto_entry
484 #
492 #
485 # copies.append((copyfrom_entry, copyto_entry))
493 # copies.append((copyfrom_entry, copyto_entry))
486
494
487 if ent.copyfrom_path:
495 if ent.copyfrom_path:
488 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
496 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
489 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
497 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
490 if copyfrom_entry:
498 if copyfrom_entry:
491 copyfrom[path] = ent
499 copyfrom[path] = ent
492 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
500 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
493
501
494 # Good, /probably/ a regular copy. Really should check
502 # Good, /probably/ a regular copy. Really should check
495 # to see whether the parent revision actually contains
503 # to see whether the parent revision actually contains
496 # the directory in question.
504 # the directory in question.
497 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
505 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
498 children.sort()
506 children.sort()
499 for child in children:
507 for child in children:
500 entrypath = get_entry_from_path("/" + child, module=self.module)
508 entrypath = get_entry_from_path("/" + child, module=self.module)
501 if entrypath:
509 if entrypath:
502 entry = entrypath.decode(self.encoding)
510 entry = entrypath.decode(self.encoding)
503 # print "COPY COPY From", copyfrom_entry, entry
511 # print "COPY COPY From", copyfrom_entry, entry
504 copyto_path = path + entry[len(copyfrom_entry):]
512 copyto_path = path + entry[len(copyfrom_entry):]
505 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
513 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
506 # print "COPY", entry, "COPY To", copyto_entry
514 # print "COPY", entry, "COPY To", copyto_entry
507 copies[self.recode(copyto_entry)] = self.recode(entry)
515 copies[self.recode(copyto_entry)] = self.recode(entry)
508 # copy from quux splort/quuxfile
516 # copy from quux splort/quuxfile
509
517
510 return (entries, copies)
518 return (entries, copies)
511
519
512 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
520 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
513 self.child_cset = None
521 self.child_cset = None
514 def parselogentry(orig_paths, revnum, author, date, message):
522 def parselogentry(orig_paths, revnum, author, date, message):
515 self.ui.debug("parsing revision %d (%d changes)\n" %
523 self.ui.debug("parsing revision %d (%d changes)\n" %
516 (revnum, len(orig_paths)))
524 (revnum, len(orig_paths)))
517
525
518 if revnum in self.modulemap:
526 if revnum in self.modulemap:
519 new_module = self.modulemap[revnum]
527 new_module = self.modulemap[revnum]
520 if new_module != self.module:
528 if new_module != self.module:
521 self.module = new_module
529 self.module = new_module
522 self.reparent(self.module)
530 self.reparent(self.module)
523
531
524 rev = self.revid(revnum)
532 rev = self.revid(revnum)
525 # branch log might return entries for a parent we already have
533 # branch log might return entries for a parent we already have
526 if (rev in self.commits or
534 if (rev in self.commits or
527 (revnum < self.lastrevs.get(self.module, 0))):
535 (revnum < self.lastrevs.get(self.module, 0))):
528 return
536 return
529
537
530 parents = []
538 parents = []
531 orig_paths = orig_paths.items()
539 orig_paths = orig_paths.items()
532 orig_paths.sort()
540 orig_paths.sort()
533
541
534 # check whether this revision is the start of a branch
542 # check whether this revision is the start of a branch
535 path, ent = orig_paths and orig_paths[0] or (None, None)
543 path, ent = orig_paths and orig_paths[0] or (None, None)
536 if ent and path == self.module:
544 if ent and path == self.module:
537 if ent.copyfrom_path:
545 if ent.copyfrom_path:
538 # ent.copyfrom_rev may not be the actual last revision
546 # ent.copyfrom_rev may not be the actual last revision
539 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
547 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
540 self.modulemap[prev] = ent.copyfrom_path
548 self.modulemap[prev] = ent.copyfrom_path
541 parents = [self.revid(prev, ent.copyfrom_path)]
549 parents = [self.revid(prev, ent.copyfrom_path)]
542 self.ui.note('found parent of branch %s at %d: %s\n' % \
550 self.ui.note('found parent of branch %s at %d: %s\n' % \
543 (self.module, prev, ent.copyfrom_path))
551 (self.module, prev, ent.copyfrom_path))
544 else:
552 else:
545 self.ui.debug("No copyfrom path, don't know what to do.\n")
553 self.ui.debug("No copyfrom path, don't know what to do.\n")
546
554
547 self.modulemap[revnum] = self.module # track backwards in time
555 self.modulemap[revnum] = self.module # track backwards in time
548
556
549 paths = []
557 paths = []
550 # filter out unrelated paths
558 # filter out unrelated paths
551 for path, ent in orig_paths:
559 for path, ent in orig_paths:
552 if not path.startswith(self.module):
560 if not path.startswith(self.module):
553 self.ui.debug("boring@%s: %s\n" % (revnum, path))
561 self.ui.debug("boring@%s: %s\n" % (revnum, path))
554 continue
562 continue
555 paths.append((path, ent))
563 paths.append((path, ent))
556
564
557 entries, copies = self.expandpaths(rev, paths, parents)
565 self.paths[rev] = (paths, parents)
558 # a list of (filename, id) where id lets us retrieve the file.
559 # eg in git, id is the object hash. for svn it'll be the
560 self.files[rev] = zip(entries, [rev] * len(entries))
561 if not entries:
562 return
563
566
564 # Example SVN datetime. Includes microseconds.
567 # Example SVN datetime. Includes microseconds.
565 # ISO-8601 conformant
568 # ISO-8601 conformant
566 # '2007-01-04T17:35:00.902377Z'
569 # '2007-01-04T17:35:00.902377Z'
567 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
570 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
568
571
569 log = message and self.recode(message)
572 log = message and self.recode(message)
570 author = author and self.recode(author) or ''
573 author = author and self.recode(author) or ''
571 try:
574 try:
572 branch = self.module.split("/")[-1]
575 branch = self.module.split("/")[-1]
573 if branch == 'trunk':
576 if branch == 'trunk':
574 branch = ''
577 branch = ''
575 except IndexError:
578 except IndexError:
576 branch = None
579 branch = None
577
580
578 cset = commit(author=author,
581 cset = commit(author=author,
579 date=util.datestr(date),
582 date=util.datestr(date),
580 desc=log,
583 desc=log,
581 parents=parents,
584 parents=parents,
582 copies=copies,
583 branch=branch,
585 branch=branch,
584 rev=rev.encode('utf-8'))
586 rev=rev.encode('utf-8'))
585
587
586 self.commits[rev] = cset
588 self.commits[rev] = cset
587 if self.child_cset and not self.child_cset.parents:
589 if self.child_cset and not self.child_cset.parents:
588 self.child_cset.parents = [rev]
590 self.child_cset.parents = [rev]
589 self.child_cset = cset
591 self.child_cset = cset
590
592
591 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
593 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
592 (self.module, from_revnum, to_revnum))
594 (self.module, from_revnum, to_revnum))
593
595
594 try:
596 try:
595 for entry in self.get_log([self.module], from_revnum, to_revnum):
597 for entry in self.get_log([self.module], from_revnum, to_revnum):
596 orig_paths, revnum, author, date, message = entry
598 orig_paths, revnum, author, date, message = entry
597 if self.is_blacklisted(revnum):
599 if self.is_blacklisted(revnum):
598 self.ui.note('skipping blacklisted revision %d\n' % revnum)
600 self.ui.note('skipping blacklisted revision %d\n' % revnum)
599 continue
601 continue
600 if orig_paths is None:
602 if orig_paths is None:
601 self.ui.debug('revision %d has no entries\n' % revnum)
603 self.ui.debug('revision %d has no entries\n' % revnum)
602 continue
604 continue
603 parselogentry(orig_paths, revnum, author, date, message)
605 parselogentry(orig_paths, revnum, author, date, message)
604 except SubversionException, (_, num):
606 except SubversionException, (_, num):
605 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
607 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
606 raise NoSuchRevision(branch=self,
608 raise NoSuchRevision(branch=self,
607 revision="Revision number %d" % to_revnum)
609 revision="Revision number %d" % to_revnum)
608 raise
610 raise
609
611
610 def _getfile(self, file, rev):
612 def _getfile(self, file, rev):
611 io = StringIO()
613 io = StringIO()
612 # TODO: ra.get_file transmits the whole file instead of diffs.
614 # TODO: ra.get_file transmits the whole file instead of diffs.
613 mode = ''
615 mode = ''
614 try:
616 try:
615 revnum = self.revnum(rev)
617 revnum = self.revnum(rev)
616 if self.module != self.modulemap[revnum]:
618 if self.module != self.modulemap[revnum]:
617 self.module = self.modulemap[revnum]
619 self.module = self.modulemap[revnum]
618 self.reparent(self.module)
620 self.reparent(self.module)
619 info = svn.ra.get_file(self.ra, file, revnum, io)
621 info = svn.ra.get_file(self.ra, file, revnum, io)
620 if isinstance(info, list):
622 if isinstance(info, list):
621 info = info[-1]
623 info = info[-1]
622 mode = ("svn:executable" in info) and 'x' or ''
624 mode = ("svn:executable" in info) and 'x' or ''
623 mode = ("svn:special" in info) and 'l' or mode
625 mode = ("svn:special" in info) and 'l' or mode
624 except SubversionException, e:
626 except SubversionException, e:
625 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
627 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
626 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
628 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
627 if e.apr_err in notfound: # File not found
629 if e.apr_err in notfound: # File not found
628 raise IOError()
630 raise IOError()
629 raise
631 raise
630 data = io.getvalue()
632 data = io.getvalue()
631 if mode == 'l':
633 if mode == 'l':
632 link_prefix = "link "
634 link_prefix = "link "
633 if data.startswith(link_prefix):
635 if data.startswith(link_prefix):
634 data = data[len(link_prefix):]
636 data = data[len(link_prefix):]
635 return data, mode
637 return data, mode
636
638
637 def _find_children(self, path, revnum):
639 def _find_children(self, path, revnum):
638 path = path.strip('/')
640 path = path.strip('/')
639 pool = Pool()
641 pool = Pool()
640 rpath = '/'.join([self.base, path]).strip('/')
642 rpath = '/'.join([self.base, path]).strip('/')
641 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
643 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
General Comments 0
You need to be logged in to leave comments. Login now