##// END OF EJS Templates
convert: readd --filemap...
Alexis S. L. Carvalho -
r5377:756a43a3 default
parent child Browse files
Show More
@@ -1,404 +1,409
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, SKIPREV, converter_source, converter_sink
8 from common import NoRepo, SKIPREV, 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 import filemap
14
15
15 import os, shutil
16 import os, shutil
16 from mercurial import hg, ui, util, commands
17 from mercurial import hg, ui, util, commands
17 from mercurial.i18n import _
18 from mercurial.i18n import _
18
19
19 commands.norepo += " convert debugsvnlog"
20 commands.norepo += " convert debugsvnlog"
20
21
21 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
22 converters = [convert_cvs, convert_git, convert_svn, mercurial_source,
22 mercurial_sink, darcs_source]
23 mercurial_sink, darcs_source]
23
24
24 def convertsource(ui, path, **opts):
25 def convertsource(ui, path, **opts):
25 for c in converters:
26 for c in converters:
26 try:
27 try:
27 return c.getcommit and c(ui, path, **opts)
28 return c.getcommit and c(ui, path, **opts)
28 except (AttributeError, NoRepo):
29 except (AttributeError, NoRepo):
29 pass
30 pass
30 raise util.Abort('%s: unknown repository type' % path)
31 raise util.Abort('%s: unknown repository type' % path)
31
32
32 def convertsink(ui, path):
33 def convertsink(ui, path):
33 if not os.path.isdir(path):
34 if not os.path.isdir(path):
34 raise util.Abort("%s: not a directory" % path)
35 raise util.Abort("%s: not a directory" % path)
35 for c in converters:
36 for c in converters:
36 try:
37 try:
37 return c.putcommit and c(ui, path)
38 return c.putcommit and c(ui, path)
38 except (AttributeError, NoRepo):
39 except (AttributeError, NoRepo):
39 pass
40 pass
40 raise util.Abort('%s: unknown repository type' % path)
41 raise util.Abort('%s: unknown repository type' % path)
41
42
42 class converter(object):
43 class converter(object):
43 def __init__(self, ui, source, dest, revmapfile, opts):
44 def __init__(self, ui, source, dest, revmapfile, opts):
44
45
45 self.source = source
46 self.source = source
46 self.dest = dest
47 self.dest = dest
47 self.ui = ui
48 self.ui = ui
48 self.opts = opts
49 self.opts = opts
49 self.commitcache = {}
50 self.commitcache = {}
50 self.revmapfile = revmapfile
51 self.revmapfile = revmapfile
51 self.revmapfilefd = None
52 self.revmapfilefd = None
52 self.authors = {}
53 self.authors = {}
53 self.authorfile = None
54 self.authorfile = None
54
55
55 self.maporder = []
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()
61 if sv not in self.map:
62 if sv not in self.map:
62 self.maporder.append(sv)
63 self.maporder.append(sv)
63 self.map[sv] = dv
64 self.map[sv] = dv
64 origrevmapfile.close()
65 origrevmapfile.close()
65 except IOError:
66 except IOError:
66 pass
67 pass
67
68
68 # Read first the dst author map if any
69 # Read first the dst author map if any
69 authorfile = self.dest.authorfile()
70 authorfile = self.dest.authorfile()
70 if authorfile and os.path.exists(authorfile):
71 if authorfile and os.path.exists(authorfile):
71 self.readauthormap(authorfile)
72 self.readauthormap(authorfile)
72 # Extend/Override with new author map if necessary
73 # Extend/Override with new author map if necessary
73 if opts.get('authors'):
74 if opts.get('authors'):
74 self.readauthormap(opts.get('authors'))
75 self.readauthormap(opts.get('authors'))
75 self.authorfile = self.dest.authorfile()
76 self.authorfile = self.dest.authorfile()
76
77
77 def walktree(self, heads):
78 def walktree(self, heads):
78 '''Return a mapping that identifies the uncommitted parents of every
79 '''Return a mapping that identifies the uncommitted parents of every
79 uncommitted changeset.'''
80 uncommitted changeset.'''
80 visit = heads
81 visit = heads
81 known = {}
82 known = {}
82 parents = {}
83 parents = {}
83 while visit:
84 while visit:
84 n = visit.pop(0)
85 n = visit.pop(0)
85 if n in known or n in self.map: continue
86 if n in known or n in self.map: continue
86 known[n] = 1
87 known[n] = 1
87 commit = self.cachecommit(n)
88 commit = self.cachecommit(n)
88 parents[n] = []
89 parents[n] = []
89 for p in commit.parents:
90 for p in commit.parents:
90 parents[n].append(p)
91 parents[n].append(p)
91 visit.append(p)
92 visit.append(p)
92
93
93 return parents
94 return parents
94
95
95 def toposort(self, parents):
96 def toposort(self, parents):
96 '''Return an ordering such that every uncommitted changeset is
97 '''Return an ordering such that every uncommitted changeset is
97 preceeded by all its uncommitted ancestors.'''
98 preceeded by all its uncommitted ancestors.'''
98 visit = parents.keys()
99 visit = parents.keys()
99 seen = {}
100 seen = {}
100 children = {}
101 children = {}
101
102
102 while visit:
103 while visit:
103 n = visit.pop(0)
104 n = visit.pop(0)
104 if n in seen: continue
105 if n in seen: continue
105 seen[n] = 1
106 seen[n] = 1
106 # Ensure that nodes without parents are present in the 'children'
107 # Ensure that nodes without parents are present in the 'children'
107 # mapping.
108 # mapping.
108 children.setdefault(n, [])
109 children.setdefault(n, [])
109 for p in parents[n]:
110 for p in parents[n]:
110 if not p in self.map:
111 if not p in self.map:
111 visit.append(p)
112 visit.append(p)
112 children.setdefault(p, []).append(n)
113 children.setdefault(p, []).append(n)
113
114
114 s = []
115 s = []
115 removed = {}
116 removed = {}
116 visit = children.keys()
117 visit = children.keys()
117 while visit:
118 while visit:
118 n = visit.pop(0)
119 n = visit.pop(0)
119 if n in removed: continue
120 if n in removed: continue
120 dep = 0
121 dep = 0
121 if n in parents:
122 if n in parents:
122 for p in parents[n]:
123 for p in parents[n]:
123 if p in self.map: continue
124 if p in self.map: continue
124 if p not in removed:
125 if p not in removed:
125 # we're still dependent
126 # we're still dependent
126 visit.append(n)
127 visit.append(n)
127 dep = 1
128 dep = 1
128 break
129 break
129
130
130 if not dep:
131 if not dep:
131 # all n's parents are in the list
132 # all n's parents are in the list
132 removed[n] = 1
133 removed[n] = 1
133 if n not in self.map:
134 if n not in self.map:
134 s.append(n)
135 s.append(n)
135 if n in children:
136 if n in children:
136 for c in children[n]:
137 for c in children[n]:
137 visit.insert(0, c)
138 visit.insert(0, c)
138
139
139 if self.opts.get('datesort'):
140 if self.opts.get('datesort'):
140 depth = {}
141 depth = {}
141 for n in s:
142 for n in s:
142 depth[n] = 0
143 depth[n] = 0
143 pl = [p for p in self.commitcache[n].parents
144 pl = [p for p in self.commitcache[n].parents
144 if p not in self.map]
145 if p not in self.map]
145 if pl:
146 if pl:
146 depth[n] = max([depth[p] for p in pl]) + 1
147 depth[n] = max([depth[p] for p in pl]) + 1
147
148
148 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]
149 s.sort()
150 s.sort()
150 s = [e[2] for e in s]
151 s = [e[2] for e in s]
151
152
152 return s
153 return s
153
154
154 def mapentry(self, src, dst):
155 def mapentry(self, src, dst):
155 if self.revmapfilefd is None:
156 if self.revmapfilefd is None:
156 try:
157 try:
157 self.revmapfilefd = open(self.revmapfile, "a")
158 self.revmapfilefd = open(self.revmapfile, "a")
158 except IOError, (errno, strerror):
159 except IOError, (errno, strerror):
159 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))
160 self.map[src] = dst
161 self.map[src] = dst
161 self.revmapfilefd.write("%s %s\n" % (src, dst))
162 self.revmapfilefd.write("%s %s\n" % (src, dst))
162 self.revmapfilefd.flush()
163 self.revmapfilefd.flush()
163
164
164 def writeauthormap(self):
165 def writeauthormap(self):
165 authorfile = self.authorfile
166 authorfile = self.authorfile
166 if authorfile:
167 if authorfile:
167 self.ui.status('Writing author map file %s\n' % authorfile)
168 self.ui.status('Writing author map file %s\n' % authorfile)
168 ofile = open(authorfile, 'w+')
169 ofile = open(authorfile, 'w+')
169 for author in self.authors:
170 for author in self.authors:
170 ofile.write("%s=%s\n" % (author, self.authors[author]))
171 ofile.write("%s=%s\n" % (author, self.authors[author]))
171 ofile.close()
172 ofile.close()
172
173
173 def readauthormap(self, authorfile):
174 def readauthormap(self, authorfile):
174 afile = open(authorfile, 'r')
175 afile = open(authorfile, 'r')
175 for line in afile:
176 for line in afile:
176 try:
177 try:
177 srcauthor = line.split('=')[0].strip()
178 srcauthor = line.split('=')[0].strip()
178 dstauthor = line.split('=')[1].strip()
179 dstauthor = line.split('=')[1].strip()
179 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
180 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
180 self.ui.status(
181 self.ui.status(
181 'Overriding mapping for author %s, was %s, will be %s\n'
182 'Overriding mapping for author %s, was %s, will be %s\n'
182 % (srcauthor, self.authors[srcauthor], dstauthor))
183 % (srcauthor, self.authors[srcauthor], dstauthor))
183 else:
184 else:
184 self.ui.debug('Mapping author %s to %s\n'
185 self.ui.debug('Mapping author %s to %s\n'
185 % (srcauthor, dstauthor))
186 % (srcauthor, dstauthor))
186 self.authors[srcauthor] = dstauthor
187 self.authors[srcauthor] = dstauthor
187 except IndexError:
188 except IndexError:
188 self.ui.warn(
189 self.ui.warn(
189 'Ignoring bad line in author file map %s: %s\n'
190 'Ignoring bad line in author file map %s: %s\n'
190 % (authorfile, line))
191 % (authorfile, line))
191 afile.close()
192 afile.close()
192
193
193 def cachecommit(self, rev):
194 def cachecommit(self, rev):
194 commit = self.source.getcommit(rev)
195 commit = self.source.getcommit(rev)
195 commit.author = self.authors.get(commit.author, commit.author)
196 commit.author = self.authors.get(commit.author, commit.author)
196 self.commitcache[rev] = commit
197 self.commitcache[rev] = commit
197 return commit
198 return commit
198
199
199 def copy(self, rev):
200 def copy(self, rev):
200 commit = self.commitcache[rev]
201 commit = self.commitcache[rev]
201 do_copies = hasattr(self.dest, 'copyfile')
202 do_copies = hasattr(self.dest, 'copyfile')
202 filenames = []
203 filenames = []
203
204
204 changes = self.source.getchanges(rev)
205 changes = self.source.getchanges(rev)
205 if isinstance(changes, basestring):
206 if isinstance(changes, basestring):
206 if changes == SKIPREV:
207 if changes == SKIPREV:
207 dest = SKIPREV
208 dest = SKIPREV
208 else:
209 else:
209 dest = self.map[changes]
210 dest = self.map[changes]
210 self.mapentry(rev, dest)
211 self.mapentry(rev, dest)
211 return
212 return
212 files, copies = changes
213 files, copies = changes
213 parents = [self.map[r] for r in commit.parents]
214 parents = [self.map[r] for r in commit.parents]
214 if commit.parents:
215 if commit.parents:
215 prev = commit.parents[0]
216 prev = commit.parents[0]
216 if prev not in self.commitcache:
217 if prev not in self.commitcache:
217 self.cachecommit(prev)
218 self.cachecommit(prev)
218 pbranch = self.commitcache[prev].branch
219 pbranch = self.commitcache[prev].branch
219 else:
220 else:
220 pbranch = None
221 pbranch = None
221 self.dest.setbranch(commit.branch, pbranch, parents)
222 self.dest.setbranch(commit.branch, pbranch, parents)
222 for f, v in files:
223 for f, v in files:
223 filenames.append(f)
224 filenames.append(f)
224 try:
225 try:
225 data = self.source.getfile(f, v)
226 data = self.source.getfile(f, v)
226 except IOError, inst:
227 except IOError, inst:
227 self.dest.delfile(f)
228 self.dest.delfile(f)
228 else:
229 else:
229 e = self.source.getmode(f, v)
230 e = self.source.getmode(f, v)
230 self.dest.putfile(f, e, data)
231 self.dest.putfile(f, e, data)
231 if do_copies:
232 if do_copies:
232 if f in copies:
233 if f in copies:
233 copyf = copies[f]
234 copyf = copies[f]
234 # Merely marks that a copy happened.
235 # Merely marks that a copy happened.
235 self.dest.copyfile(copyf, f)
236 self.dest.copyfile(copyf, f)
236
237
237 newnode = self.dest.putcommit(filenames, parents, commit)
238 newnode = self.dest.putcommit(filenames, parents, commit)
238 self.mapentry(rev, newnode)
239 self.mapentry(rev, newnode)
239
240
240 def convert(self):
241 def convert(self):
241 try:
242 try:
242 self.source.before()
243 self.source.before()
243 self.dest.before()
244 self.dest.before()
244 self.source.setrevmap(self.map, self.maporder)
245 self.source.setrevmap(self.map, self.maporder)
245 self.ui.status("scanning source...\n")
246 self.ui.status("scanning source...\n")
246 heads = self.source.getheads()
247 heads = self.source.getheads()
247 parents = self.walktree(heads)
248 parents = self.walktree(heads)
248 self.ui.status("sorting...\n")
249 self.ui.status("sorting...\n")
249 t = self.toposort(parents)
250 t = self.toposort(parents)
250 num = len(t)
251 num = len(t)
251 c = None
252 c = None
252
253
253 self.ui.status("converting...\n")
254 self.ui.status("converting...\n")
254 for c in t:
255 for c in t:
255 num -= 1
256 num -= 1
256 desc = self.commitcache[c].desc
257 desc = self.commitcache[c].desc
257 if "\n" in desc:
258 if "\n" in desc:
258 desc = desc.splitlines()[0]
259 desc = desc.splitlines()[0]
259 self.ui.status("%d %s\n" % (num, desc))
260 self.ui.status("%d %s\n" % (num, desc))
260 self.copy(c)
261 self.copy(c)
261
262
262 tags = self.source.gettags()
263 tags = self.source.gettags()
263 ctags = {}
264 ctags = {}
264 for k in tags:
265 for k in tags:
265 v = tags[k]
266 v = tags[k]
266 if self.map.get(v, SKIPREV) != SKIPREV:
267 if self.map.get(v, SKIPREV) != SKIPREV:
267 ctags[k] = self.map[v]
268 ctags[k] = self.map[v]
268
269
269 if c and ctags:
270 if c and ctags:
270 nrev = self.dest.puttags(ctags)
271 nrev = self.dest.puttags(ctags)
271 # write another hash correspondence to override the previous
272 # write another hash correspondence to override the previous
272 # one so we don't end up with extra tag heads
273 # one so we don't end up with extra tag heads
273 if nrev:
274 if nrev:
274 self.mapentry(c, nrev)
275 self.mapentry(c, nrev)
275
276
276 self.writeauthormap()
277 self.writeauthormap()
277 finally:
278 finally:
278 self.cleanup()
279 self.cleanup()
279
280
280 def cleanup(self):
281 def cleanup(self):
281 try:
282 try:
282 self.dest.after()
283 self.dest.after()
283 finally:
284 finally:
284 self.source.after()
285 self.source.after()
285 if self.revmapfilefd:
286 if self.revmapfilefd:
286 self.revmapfilefd.close()
287 self.revmapfilefd.close()
287
288
288 def convert(ui, src, dest=None, revmapfile=None, **opts):
289 def convert(ui, src, dest=None, revmapfile=None, **opts):
289 """Convert a foreign SCM repository to a Mercurial one.
290 """Convert a foreign SCM repository to a Mercurial one.
290
291
291 Accepted source formats:
292 Accepted source formats:
292 - CVS
293 - CVS
293 - Darcs
294 - Darcs
294 - git
295 - git
295 - Subversion
296 - Subversion
296
297
297 Accepted destination formats:
298 Accepted destination formats:
298 - Mercurial
299 - Mercurial
299
300
300 If no revision is given, all revisions will be converted. Otherwise,
301 If no revision is given, all revisions will be converted. Otherwise,
301 convert will only import up to the named revision (given in a format
302 convert will only import up to the named revision (given in a format
302 understood by the source).
303 understood by the source).
303
304
304 If no destination directory name is specified, it defaults to the
305 If no destination directory name is specified, it defaults to the
305 basename of the source with '-hg' appended. If the destination
306 basename of the source with '-hg' appended. If the destination
306 repository doesn't exist, it will be created.
307 repository doesn't exist, it will be created.
307
308
308 If <revmapfile> isn't given, it will be put in a default location
309 If <revmapfile> isn't given, it will be put in a default location
309 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
310 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
310 file that maps each source commit ID to the destination ID for
311 file that maps each source commit ID to the destination ID for
311 that revision, like so:
312 that revision, like so:
312 <source ID> <destination ID>
313 <source ID> <destination ID>
313
314
314 If the file doesn't exist, it's automatically created. It's updated
315 If the file doesn't exist, it's automatically created. It's updated
315 on each commit copied, so convert-repo can be interrupted and can
316 on each commit copied, so convert-repo can be interrupted and can
316 be run repeatedly to copy new commits.
317 be run repeatedly to copy new commits.
317
318
318 The [username mapping] file is a simple text file that maps each source
319 The [username mapping] file is a simple text file that maps each source
319 commit author to a destination commit author. It is handy for source SCMs
320 commit author to a destination commit author. It is handy for source SCMs
320 that use unix logins to identify authors (eg: CVS). One line per author
321 that use unix logins to identify authors (eg: CVS). One line per author
321 mapping and the line format is:
322 mapping and the line format is:
322 srcauthor=whatever string you want
323 srcauthor=whatever string you want
323
324
324 The filemap is a file that allows filtering and remapping of files
325 The filemap is a file that allows filtering and remapping of files
325 and directories. Comment lines start with '#'. Each line can
326 and directories. Comment lines start with '#'. Each line can
326 contain one of the following directives:
327 contain one of the following directives:
327
328
328 include path/to/file
329 include path/to/file
329
330
330 exclude path/to/file
331 exclude path/to/file
331
332
332 rename from/file to/file
333 rename from/file to/file
333
334
334 The 'include' directive causes a file, or all files under a
335 The 'include' directive causes a file, or all files under a
335 directory, to be included in the destination repository. The
336 directory, to be included in the destination repository. The
336 'exclude' directive causes files or directories to be omitted.
337 'exclude' directive causes files or directories to be omitted.
337 The 'rename' directive renames a file or directory. To rename
338 The 'rename' directive renames a file or directory. To rename
338 from a subdirectory into the root of the repository, use '.' as
339 from a subdirectory into the root of the repository, use '.' as
339 the path to rename to.
340 the path to rename to.
340 """
341 """
341
342
342 util._encoding = 'UTF-8'
343 util._encoding = 'UTF-8'
343
344
344 if not dest:
345 if not dest:
345 dest = hg.defaultdest(src) + "-hg"
346 dest = hg.defaultdest(src) + "-hg"
346 ui.status("assuming destination %s\n" % dest)
347 ui.status("assuming destination %s\n" % dest)
347
348
348 # Try to be smart and initalize things when required
349 # Try to be smart and initalize things when required
349 created = False
350 created = False
350 if os.path.isdir(dest):
351 if os.path.isdir(dest):
351 if len(os.listdir(dest)) > 0:
352 if len(os.listdir(dest)) > 0:
352 try:
353 try:
353 hg.repository(ui, dest)
354 hg.repository(ui, dest)
354 ui.status("destination %s is a Mercurial repository\n" % dest)
355 ui.status("destination %s is a Mercurial repository\n" % dest)
355 except hg.RepoError:
356 except hg.RepoError:
356 raise util.Abort(
357 raise util.Abort(
357 "destination directory %s is not empty.\n"
358 "destination directory %s is not empty.\n"
358 "Please specify an empty directory to be initialized\n"
359 "Please specify an empty directory to be initialized\n"
359 "or an already initialized mercurial repository"
360 "or an already initialized mercurial repository"
360 % dest)
361 % dest)
361 else:
362 else:
362 ui.status("initializing destination %s repository\n" % dest)
363 ui.status("initializing destination %s repository\n" % dest)
363 hg.repository(ui, dest, create=True)
364 hg.repository(ui, dest, create=True)
364 created = True
365 created = True
365 elif os.path.exists(dest):
366 elif os.path.exists(dest):
366 raise util.Abort("destination %s exists and is not a directory" % dest)
367 raise util.Abort("destination %s exists and is not a directory" % dest)
367 else:
368 else:
368 ui.status("initializing destination %s repository\n" % dest)
369 ui.status("initializing destination %s repository\n" % dest)
369 hg.repository(ui, dest, create=True)
370 hg.repository(ui, dest, create=True)
370 created = True
371 created = True
371
372
372 destc = convertsink(ui, dest)
373 destc = convertsink(ui, dest)
373
374
374 try:
375 try:
375 srcc = convertsource(ui, src, rev=opts.get('rev'))
376 srcc = convertsource(ui, src, rev=opts.get('rev'))
376 except Exception:
377 except Exception:
377 if created:
378 if created:
378 shutil.rmtree(dest, True)
379 shutil.rmtree(dest, True)
379 raise
380 raise
380
381
382 fmap = opts.get('filemap')
383 if fmap:
384 srcc = filemap.filemap_source(ui, srcc, fmap)
385
381 if not revmapfile:
386 if not revmapfile:
382 try:
387 try:
383 revmapfile = destc.revmapfile()
388 revmapfile = destc.revmapfile()
384 except:
389 except:
385 revmapfile = os.path.join(destc, "map")
390 revmapfile = os.path.join(destc, "map")
386
391
387 c = converter(ui, srcc, destc, revmapfile, opts)
392 c = converter(ui, srcc, destc, revmapfile, opts)
388 c.convert()
393 c.convert()
389
394
390
395
391 cmdtable = {
396 cmdtable = {
392 "convert":
397 "convert":
393 (convert,
398 (convert,
394 [('A', 'authors', '', 'username mapping filename'),
399 [('A', 'authors', '', 'username mapping filename'),
395 ('', 'filemap', '', 'remap file names using contents of file'),
400 ('', 'filemap', '', 'remap file names using contents of file'),
396 ('r', 'rev', '', 'import up to target revision REV'),
401 ('r', 'rev', '', 'import up to target revision REV'),
397 ('', 'datesort', None, 'try to sort changesets by date')],
402 ('', 'datesort', None, 'try to sort changesets by date')],
398 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
403 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
399 "debugsvnlog":
404 "debugsvnlog":
400 (debugsvnlog,
405 (debugsvnlog,
401 [],
406 [],
402 'hg debugsvnlog'),
407 'hg debugsvnlog'),
403 }
408 }
404
409
@@ -1,156 +1,169
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 SKIPREV = 'hg-convert-skipped-revision'
20 SKIPREV = 'hg-convert-skipped-revision'
21
21
22 class commit(object):
22 class commit(object):
23 def __init__(self, author, date, desc, parents, branch=None, rev=None):
23 def __init__(self, author, date, desc, parents, branch=None, rev=None):
24 self.author = author
24 self.author = author
25 self.date = date
25 self.date = date
26 self.desc = desc
26 self.desc = desc
27 self.parents = parents
27 self.parents = parents
28 self.branch = branch
28 self.branch = branch
29 self.rev = rev
29 self.rev = rev
30
30
31 class converter_source(object):
31 class converter_source(object):
32 """Conversion source interface"""
32 """Conversion source interface"""
33
33
34 def __init__(self, ui, path, rev=None):
34 def __init__(self, ui, path, rev=None):
35 """Initialize conversion source (or raise NoRepo("message")
35 """Initialize conversion source (or raise NoRepo("message")
36 exception if path is not a valid repository)"""
36 exception if path is not a valid repository)"""
37 self.ui = ui
37 self.ui = ui
38 self.path = path
38 self.path = path
39 self.rev = rev
39 self.rev = rev
40
40
41 self.encoding = 'utf-8'
41 self.encoding = 'utf-8'
42
42
43 def before(self):
43 def before(self):
44 pass
44 pass
45
45
46 def after(self):
46 def after(self):
47 pass
47 pass
48
48
49 def setrevmap(self, revmap, order):
49 def setrevmap(self, revmap, order):
50 """set the map of already-converted revisions
50 """set the map of already-converted revisions
51
51
52 order is a list with the keys from revmap in the order they
52 order is a list with the keys from revmap in the order they
53 appear in the revision map file."""
53 appear in the revision map file."""
54 pass
54 pass
55
55
56 def getheads(self):
56 def getheads(self):
57 """Return a list of this repository's heads"""
57 """Return a list of this repository's heads"""
58 raise NotImplementedError()
58 raise NotImplementedError()
59
59
60 def getfile(self, name, rev):
60 def getfile(self, name, rev):
61 """Return file contents as a string"""
61 """Return file contents as a string"""
62 raise NotImplementedError()
62 raise NotImplementedError()
63
63
64 def getmode(self, name, rev):
64 def getmode(self, name, rev):
65 """Return file mode, eg. '', 'x', or 'l'"""
65 """Return file mode, eg. '', 'x', or 'l'"""
66 raise NotImplementedError()
66 raise NotImplementedError()
67
67
68 def getchanges(self, version):
68 def getchanges(self, version):
69 """Returns a tuple of (files, copies)
69 """Returns a tuple of (files, copies)
70 Files is a sorted list of (filename, id) tuples for all files changed
70 Files is a sorted list of (filename, id) tuples for all files changed
71 in version, where id is the source revision id of the file.
71 in version, where id is the source revision id of the file.
72
72
73 copies is a dictionary of dest: source
73 copies is a dictionary of dest: source
74 """
74 """
75 raise NotImplementedError()
75 raise NotImplementedError()
76
76
77 def getcommit(self, version):
77 def getcommit(self, version):
78 """Return the commit object for version"""
78 """Return the commit object for version"""
79 raise NotImplementedError()
79 raise NotImplementedError()
80
80
81 def gettags(self):
81 def gettags(self):
82 """Return the tags as a dictionary of name: revision"""
82 """Return the tags as a dictionary of name: revision"""
83 raise NotImplementedError()
83 raise NotImplementedError()
84
84
85 def recode(self, s, encoding=None):
85 def recode(self, s, encoding=None):
86 if not encoding:
86 if not encoding:
87 encoding = self.encoding or 'utf-8'
87 encoding = self.encoding or 'utf-8'
88
88
89 if isinstance(s, unicode):
89 if isinstance(s, unicode):
90 return s.encode("utf-8")
90 return s.encode("utf-8")
91 try:
91 try:
92 return s.decode(encoding).encode("utf-8")
92 return s.decode(encoding).encode("utf-8")
93 except:
93 except:
94 try:
94 try:
95 return s.decode("latin-1").encode("utf-8")
95 return s.decode("latin-1").encode("utf-8")
96 except:
96 except:
97 return s.decode(encoding, "replace").encode("utf-8")
97 return s.decode(encoding, "replace").encode("utf-8")
98
98
99 def getchangedfiles(self, rev, i):
100 """Return the files changed by rev compared to parent[i].
101
102 i is an index selecting one of the parents of rev. The return
103 value should be the list of files that are different in rev and
104 this parent.
105
106 If rev has no parents, i is None.
107
108 This function is only needed to support --filemap
109 """
110 raise NotImplementedError()
111
99 class converter_sink(object):
112 class converter_sink(object):
100 """Conversion sink (target) interface"""
113 """Conversion sink (target) interface"""
101
114
102 def __init__(self, ui, path):
115 def __init__(self, ui, path):
103 """Initialize conversion sink (or raise NoRepo("message")
116 """Initialize conversion sink (or raise NoRepo("message")
104 exception if path is not a valid repository)"""
117 exception if path is not a valid repository)"""
105 raise NotImplementedError()
118 raise NotImplementedError()
106
119
107 def getheads(self):
120 def getheads(self):
108 """Return a list of this repository's heads"""
121 """Return a list of this repository's heads"""
109 raise NotImplementedError()
122 raise NotImplementedError()
110
123
111 def revmapfile(self):
124 def revmapfile(self):
112 """Path to a file that will contain lines
125 """Path to a file that will contain lines
113 source_rev_id sink_rev_id
126 source_rev_id sink_rev_id
114 mapping equivalent revision identifiers for each system."""
127 mapping equivalent revision identifiers for each system."""
115 raise NotImplementedError()
128 raise NotImplementedError()
116
129
117 def authorfile(self):
130 def authorfile(self):
118 """Path to a file that will contain lines
131 """Path to a file that will contain lines
119 srcauthor=dstauthor
132 srcauthor=dstauthor
120 mapping equivalent authors identifiers for each system."""
133 mapping equivalent authors identifiers for each system."""
121 return None
134 return None
122
135
123 def putfile(self, f, e, data):
136 def putfile(self, f, e, data):
124 """Put file for next putcommit().
137 """Put file for next putcommit().
125 f: path to file
138 f: path to file
126 e: '', 'x', or 'l' (regular file, executable, or symlink)
139 e: '', 'x', or 'l' (regular file, executable, or symlink)
127 data: file contents"""
140 data: file contents"""
128 raise NotImplementedError()
141 raise NotImplementedError()
129
142
130 def delfile(self, f):
143 def delfile(self, f):
131 """Delete file for next putcommit().
144 """Delete file for next putcommit().
132 f: path to file"""
145 f: path to file"""
133 raise NotImplementedError()
146 raise NotImplementedError()
134
147
135 def putcommit(self, files, parents, commit):
148 def putcommit(self, files, parents, commit):
136 """Create a revision with all changed files listed in 'files'
149 """Create a revision with all changed files listed in 'files'
137 and having listed parents. 'commit' is a commit object containing
150 and having listed parents. 'commit' is a commit object containing
138 at a minimum the author, date, and message for this changeset.
151 at a minimum the author, date, and message for this changeset.
139 Called after putfile() and delfile() calls. Note that the sink
152 Called after putfile() and delfile() calls. Note that the sink
140 repository is not told to update itself to a particular revision
153 repository is not told to update itself to a particular revision
141 (or even what that revision would be) before it receives the
154 (or even what that revision would be) before it receives the
142 file data."""
155 file data."""
143 raise NotImplementedError()
156 raise NotImplementedError()
144
157
145 def puttags(self, tags):
158 def puttags(self, tags):
146 """Put tags into sink.
159 """Put tags into sink.
147 tags: {tagname: sink_rev_id, ...}"""
160 tags: {tagname: sink_rev_id, ...}"""
148 raise NotImplementedError()
161 raise NotImplementedError()
149
162
150 def setbranch(self, branch, pbranch, parents):
163 def setbranch(self, branch, pbranch, parents):
151 """Set the current branch name. Called before the first putfile
164 """Set the current branch name. Called before the first putfile
152 on the branch.
165 on the branch.
153 branch: branch name for subsequent commits
166 branch: branch name for subsequent commits
154 pbranch: branch name of parent commit
167 pbranch: branch name of parent commit
155 parents: destination revisions of parent"""
168 parents: destination revisions of parent"""
156 pass
169 pass
@@ -1,94 +1,330
1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
2 #
3 #
3 # This software may be used and distributed according to the terms of
4 # This software may be used and distributed according to the terms of
4 # the GNU General Public License, incorporated herein by reference.
5 # the GNU General Public License, incorporated herein by reference.
5
6
6 import shlex
7 import shlex
7 from mercurial.i18n import _
8 from mercurial.i18n import _
8 from mercurial import util
9 from mercurial import util
10 from common import SKIPREV
9
11
10 def rpairs(name):
12 def rpairs(name):
11 e = len(name)
13 e = len(name)
12 while e != -1:
14 while e != -1:
13 yield name[:e], name[e+1:]
15 yield name[:e], name[e+1:]
14 e = name.rfind('/', 0, e)
16 e = name.rfind('/', 0, e)
15
17
16 class filemapper(object):
18 class filemapper(object):
17 '''Map and filter filenames when importing.
19 '''Map and filter filenames when importing.
18 A name can be mapped to itself, a new name, or None (omit from new
20 A name can be mapped to itself, a new name, or None (omit from new
19 repository).'''
21 repository).'''
20
22
21 def __init__(self, ui, path=None):
23 def __init__(self, ui, path=None):
22 self.ui = ui
24 self.ui = ui
23 self.include = {}
25 self.include = {}
24 self.exclude = {}
26 self.exclude = {}
25 self.rename = {}
27 self.rename = {}
26 if path:
28 if path:
27 if self.parse(path):
29 if self.parse(path):
28 raise util.Abort(_('errors in filemap'))
30 raise util.Abort(_('errors in filemap'))
29
31
30 def parse(self, path):
32 def parse(self, path):
31 errs = 0
33 errs = 0
32 def check(name, mapping, listname):
34 def check(name, mapping, listname):
33 if name in mapping:
35 if name in mapping:
34 self.ui.warn(_('%s:%d: %r already in %s list\n') %
36 self.ui.warn(_('%s:%d: %r already in %s list\n') %
35 (lex.infile, lex.lineno, name, listname))
37 (lex.infile, lex.lineno, name, listname))
36 return 1
38 return 1
37 return 0
39 return 0
38 lex = shlex.shlex(open(path), path, True)
40 lex = shlex.shlex(open(path), path, True)
39 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
41 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
40 cmd = lex.get_token()
42 cmd = lex.get_token()
41 while cmd:
43 while cmd:
42 if cmd == 'include':
44 if cmd == 'include':
43 name = lex.get_token()
45 name = lex.get_token()
44 errs += check(name, self.exclude, 'exclude')
46 errs += check(name, self.exclude, 'exclude')
45 self.include[name] = name
47 self.include[name] = name
46 elif cmd == 'exclude':
48 elif cmd == 'exclude':
47 name = lex.get_token()
49 name = lex.get_token()
48 errs += check(name, self.include, 'include')
50 errs += check(name, self.include, 'include')
49 errs += check(name, self.rename, 'rename')
51 errs += check(name, self.rename, 'rename')
50 self.exclude[name] = name
52 self.exclude[name] = name
51 elif cmd == 'rename':
53 elif cmd == 'rename':
52 src = lex.get_token()
54 src = lex.get_token()
53 dest = lex.get_token()
55 dest = lex.get_token()
54 errs += check(src, self.exclude, 'exclude')
56 errs += check(src, self.exclude, 'exclude')
55 self.rename[src] = dest
57 self.rename[src] = dest
56 elif cmd == 'source':
58 elif cmd == 'source':
57 errs += self.parse(lex.get_token())
59 errs += self.parse(lex.get_token())
58 else:
60 else:
59 self.ui.warn(_('%s:%d: unknown directive %r\n') %
61 self.ui.warn(_('%s:%d: unknown directive %r\n') %
60 (lex.infile, lex.lineno, cmd))
62 (lex.infile, lex.lineno, cmd))
61 errs += 1
63 errs += 1
62 cmd = lex.get_token()
64 cmd = lex.get_token()
63 return errs
65 return errs
64
66
65 def lookup(self, name, mapping):
67 def lookup(self, name, mapping):
66 for pre, suf in rpairs(name):
68 for pre, suf in rpairs(name):
67 try:
69 try:
68 return mapping[pre], pre, suf
70 return mapping[pre], pre, suf
69 except KeyError, err:
71 except KeyError, err:
70 pass
72 pass
71 return '', name, ''
73 return '', name, ''
72
74
73 def __call__(self, name):
75 def __call__(self, name):
74 if self.include:
76 if self.include:
75 inc = self.lookup(name, self.include)[0]
77 inc = self.lookup(name, self.include)[0]
76 else:
78 else:
77 inc = name
79 inc = name
78 if self.exclude:
80 if self.exclude:
79 exc = self.lookup(name, self.exclude)[0]
81 exc = self.lookup(name, self.exclude)[0]
80 else:
82 else:
81 exc = ''
83 exc = ''
82 if not inc or exc:
84 if not inc or exc:
83 return None
85 return None
84 newpre, pre, suf = self.lookup(name, self.rename)
86 newpre, pre, suf = self.lookup(name, self.rename)
85 if newpre:
87 if newpre:
86 if newpre == '.':
88 if newpre == '.':
87 return suf
89 return suf
88 if suf:
90 if suf:
89 return newpre + '/' + suf
91 return newpre + '/' + suf
90 return newpre
92 return newpre
91 return name
93 return name
92
94
93 def active(self):
95 def active(self):
94 return bool(self.include or self.exclude or self.rename)
96 return bool(self.include or self.exclude or self.rename)
97
98 # This class does two additional things compared to a regular source:
99 #
100 # - Filter and rename files. This is mostly wrapped by the filemapper
101 # class above. We hide the original filename in the revision that is
102 # returned by getchanges to be able to find things later in getfile
103 # and getmode.
104 #
105 # - Return only revisions that matter for the files we're interested in.
106 # This involves rewriting the parents of the original revision to
107 # create a graph that is restricted to those revisions.
108 #
109 # This set of revisions includes not only revisions that directly
110 # touch files we're interested in, but also merges that merge two
111 # or more interesting revisions.
112
113 class filemap_source(object):
114 def __init__(self, ui, baseconverter, filemap):
115 self.ui = ui
116 self.base = baseconverter
117 self.filemapper = filemapper(ui, filemap)
118 self.commits = {}
119 # if a revision rev has parent p in the original revision graph, then
120 # rev will have parent self.parentmap[p] in the restricted graph.
121 self.parentmap = {}
122 # self.wantedancestors[rev] is the set of all ancestors of rev that
123 # are in the restricted graph.
124 self.wantedancestors = {}
125 self.convertedorder = None
126 self._rebuilt = False
127 self.origparents = {}
128
129 def setrevmap(self, revmap, order):
130 # rebuild our state to make things restartable
131 #
132 # To avoid calling getcommit for every revision that has already
133 # been converted, we rebuild only the parentmap, delaying the
134 # rebuild of wantedancestors until we need it (i.e. until a
135 # merge).
136 #
137 # We assume the order argument lists the revisions in
138 # topological order, so that we can infer which revisions were
139 # wanted by previous runs.
140 self._rebuilt = not revmap
141 seen = {SKIPREV: SKIPREV}
142 dummyset = util.set()
143 converted = []
144 for rev in order:
145 mapped = revmap[rev]
146 wanted = mapped not in seen
147 if wanted:
148 seen[mapped] = rev
149 self.parentmap[rev] = rev
150 else:
151 self.parentmap[rev] = seen[mapped]
152 self.wantedancestors[rev] = dummyset
153 arg = seen[mapped]
154 if arg == SKIPREV:
155 arg = None
156 converted.append((rev, wanted, arg))
157 self.convertedorder = converted
158 return self.base.setrevmap(revmap, order)
159
160 def rebuild(self):
161 if self._rebuilt:
162 return True
163 self._rebuilt = True
164 pmap = self.parentmap.copy()
165 self.parentmap.clear()
166 self.wantedancestors.clear()
167 for rev, wanted, arg in self.convertedorder:
168 parents = self.origparents.get(rev)
169 if parents is None:
170 parents = self.base.getcommit(rev).parents
171 if wanted:
172 self.mark_wanted(rev, parents)
173 else:
174 self.mark_not_wanted(rev, arg)
175
176 assert pmap == self.parentmap
177 return True
178
179 def getheads(self):
180 return self.base.getheads()
181
182 def getcommit(self, rev):
183 # We want to save a reference to the commit objects to be able
184 # to rewrite their parents later on.
185 self.commits[rev] = self.base.getcommit(rev)
186 return self.commits[rev]
187
188 def wanted(self, rev, i):
189 # Return True if we're directly interested in rev.
190 #
191 # i is an index selecting one of the parents of rev (if rev
192 # has no parents, i is None). getchangedfiles will give us
193 # the list of files that are different in rev and in the parent
194 # indicated by i. If we're interested in any of these files,
195 # we're interested in rev.
196 try:
197 files = self.base.getchangedfiles(rev, i)
198 except NotImplementedError:
199 raise util.Abort(_("source repository doesn't support --filemap"))
200 for f in files:
201 if self.filemapper(f):
202 return True
203 return False
204
205 def mark_not_wanted(self, rev, p):
206 # Mark rev as not interesting and update data structures.
207
208 if p is None:
209 # A root revision. Use SKIPREV to indicate that it doesn't
210 # map to any revision in the restricted graph. Put SKIPREV
211 # in the set of wanted ancestors to simplify code elsewhere
212 self.parentmap[rev] = SKIPREV
213 self.wantedancestors[rev] = util.set((SKIPREV,))
214 return
215
216 # Reuse the data from our parent.
217 self.parentmap[rev] = self.parentmap[p]
218 self.wantedancestors[rev] = self.wantedancestors[p]
219
220 def mark_wanted(self, rev, parents):
221 # Mark rev ss wanted and update data structures.
222
223 # rev will be in the restricted graph, so children of rev in
224 # the original graph should still have rev as a parent in the
225 # restricted graph.
226 self.parentmap[rev] = rev
227
228 # The set of wanted ancestors of rev is the union of the sets
229 # of wanted ancestors of its parents. Plus rev itself.
230 wrev = util.set()
231 for p in parents:
232 wrev.update(self.wantedancestors[p])
233 wrev.add(rev)
234 self.wantedancestors[rev] = wrev
235
236 def getchanges(self, rev):
237 parents = self.commits[rev].parents
238 if len(parents) > 1:
239 self.rebuild()
240
241 # To decide whether we're interested in rev we:
242 #
243 # - calculate what parents rev will have if it turns out we're
244 # interested in it. If it's going to have more than 1 parent,
245 # we're interested in it.
246 #
247 # - otherwise, we'll compare it with the single parent we found.
248 # If any of the files we're interested in is different in the
249 # the two revisions, we're interested in rev.
250
251 # A parent p is interesting if its mapped version (self.parentmap[p]):
252 # - is not SKIPREV
253 # - is still not in the list of parents (we don't want duplicates)
254 # - is not an ancestor of the mapped versions of the other parents
255 mparents = []
256 wp = None
257 for i, p1 in enumerate(parents):
258 mp1 = self.parentmap[p1]
259 if mp1 == SKIPREV or mp1 in mparents:
260 continue
261 for p2 in parents:
262 if p1 == p2 or mp1 == self.parentmap[p2]:
263 continue
264 if mp1 in self.wantedancestors[p2]:
265 break
266 else:
267 mparents.append(mp1)
268 wp = i
269
270 if wp is None and parents:
271 wp = 0
272
273 self.origparents[rev] = parents
274
275 if len(mparents) < 2 and not self.wanted(rev, wp):
276 # We don't want this revision.
277 # Update our state and tell the convert process to map this
278 # revision to the same revision its parent as mapped to.
279 p = None
280 if parents:
281 p = parents[wp]
282 self.mark_not_wanted(rev, p)
283 self.convertedorder.append((rev, False, p))
284 return self.parentmap[rev]
285
286 # We want this revision.
287 # Rewrite the parents of the commit object
288 self.commits[rev].parents = mparents
289 self.mark_wanted(rev, parents)
290 self.convertedorder.append((rev, True, None))
291
292 # Get the real changes and do the filtering/mapping.
293 # To be able to get the files later on in getfile and getmode,
294 # we hide the original filename in the rev part of the return
295 # value.
296 changes, copies = self.base.getchanges(rev)
297 newnames = {}
298 files = []
299 for f, r in changes:
300 newf = self.filemapper(f)
301 if newf:
302 files.append((newf, (f, r)))
303 newnames[f] = newf
304
305 ncopies = {}
306 for c in copies:
307 newc = self.filemapper(c)
308 if newc:
309 newsource = self.filemapper(copies[c])
310 if newsource:
311 ncopies[newc] = newsource
312
313 return files, ncopies
314
315 def getfile(self, name, rev):
316 realname, realrev = rev
317 return self.base.getfile(realname, realrev)
318
319 def getmode(self, name, rev):
320 realname, realrev = rev
321 return self.base.getmode(realname, realrev)
322
323 def gettags(self):
324 return self.base.gettags()
325
326 def before(self):
327 pass
328
329 def after(self):
330 pass
General Comments 0
You need to be logged in to leave comments. Login now