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