##// END OF EJS Templates
convert: refactor sink initialisation, to remove hardcoding of hg...
Bryan O'Sullivan -
r5441:71e7c86a default
parent child Browse files
Show More
@@ -1,414 +1,398 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, 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 svn_source, debugsvnlog
13 from subversion import svn_source, debugsvnlog
14 import filemap
14 import filemap
15
15
16 import os, shutil
16 import os, shutil
17 from mercurial import hg, ui, util, commands
17 from mercurial import hg, ui, util, commands
18 from mercurial.i18n import _
18 from mercurial.i18n import _
19
19
20 commands.norepo += " convert debugsvnlog"
20 commands.norepo += " convert debugsvnlog"
21
21
22 sink_converters = [mercurial_sink]
22 source_converters = [
23 source_converters = [convert_cvs, convert_git, svn_source,
23 ('cvs', convert_cvs),
24 mercurial_source, darcs_source]
24 ('git', convert_git),
25 def convertsource(ui, path, **opts):
25 ('svn', svn_source),
26 for c in source_converters:
26 ('hg', mercurial_source),
27 ('darcs', darcs_source),
28 ]
29
30 sink_converters = [
31 ('hg', mercurial_sink),
32 ]
33
34 def convertsource(ui, path, type, rev):
35 for name, source in source_converters:
27 try:
36 try:
28 return c.getcommit and c(ui, path, **opts)
37 if not type or name == type:
29 except AttributeError:
38 return source(ui, path, rev)
30 pass
31 except NoRepo, inst:
39 except NoRepo, inst:
32 ui.note(_("convert: %s\n") % inst)
40 ui.note(_("convert: %s\n") % inst)
33 raise util.Abort('%s: unknown repository type' % path)
41 raise util.Abort('%s: unknown repository type' % path)
34
42
35 def convertsink(ui, path):
43 def convertsink(ui, path, type):
36 if not os.path.isdir(path):
44 for name, sink in sink_converters:
37 raise util.Abort("%s: not a directory" % path)
38 for c in sink_converters:
39 try:
45 try:
40 return c.putcommit and c(ui, path)
46 if not type or name == type:
41 except AttributeError:
47 return sink(ui, path)
42 pass
43 except NoRepo, inst:
48 except NoRepo, inst:
44 ui.note(_("convert: %s\n") % inst)
49 ui.note(_("convert: %s\n") % inst)
45 raise util.Abort('%s: unknown repository type' % path)
50 raise util.Abort('%s: unknown repository type' % path)
46
51
47 class converter(object):
52 class converter(object):
48 def __init__(self, ui, source, dest, revmapfile, opts):
53 def __init__(self, ui, source, dest, revmapfile, opts):
49
54
50 self.source = source
55 self.source = source
51 self.dest = dest
56 self.dest = dest
52 self.ui = ui
57 self.ui = ui
53 self.opts = opts
58 self.opts = opts
54 self.commitcache = {}
59 self.commitcache = {}
55 self.revmapfile = revmapfile
60 self.revmapfile = revmapfile
56 self.revmapfilefd = None
61 self.revmapfilefd = None
57 self.authors = {}
62 self.authors = {}
58 self.authorfile = None
63 self.authorfile = None
59
64
60 self.maporder = []
65 self.maporder = []
61 self.map = {}
66 self.map = {}
62 try:
67 try:
63 origrevmapfile = open(self.revmapfile, 'r')
68 origrevmapfile = open(self.revmapfile, 'r')
64 for l in origrevmapfile:
69 for l in origrevmapfile:
65 sv, dv = l[:-1].split()
70 sv, dv = l[:-1].split()
66 if sv not in self.map:
71 if sv not in self.map:
67 self.maporder.append(sv)
72 self.maporder.append(sv)
68 self.map[sv] = dv
73 self.map[sv] = dv
69 origrevmapfile.close()
74 origrevmapfile.close()
70 except IOError:
75 except IOError:
71 pass
76 pass
72
77
73 # Read first the dst author map if any
78 # Read first the dst author map if any
74 authorfile = self.dest.authorfile()
79 authorfile = self.dest.authorfile()
75 if authorfile and os.path.exists(authorfile):
80 if authorfile and os.path.exists(authorfile):
76 self.readauthormap(authorfile)
81 self.readauthormap(authorfile)
77 # Extend/Override with new author map if necessary
82 # Extend/Override with new author map if necessary
78 if opts.get('authors'):
83 if opts.get('authors'):
79 self.readauthormap(opts.get('authors'))
84 self.readauthormap(opts.get('authors'))
80 self.authorfile = self.dest.authorfile()
85 self.authorfile = self.dest.authorfile()
81
86
82 def walktree(self, heads):
87 def walktree(self, heads):
83 '''Return a mapping that identifies the uncommitted parents of every
88 '''Return a mapping that identifies the uncommitted parents of every
84 uncommitted changeset.'''
89 uncommitted changeset.'''
85 visit = heads
90 visit = heads
86 known = {}
91 known = {}
87 parents = {}
92 parents = {}
88 while visit:
93 while visit:
89 n = visit.pop(0)
94 n = visit.pop(0)
90 if n in known or n in self.map: continue
95 if n in known or n in self.map: continue
91 known[n] = 1
96 known[n] = 1
92 commit = self.cachecommit(n)
97 commit = self.cachecommit(n)
93 parents[n] = []
98 parents[n] = []
94 for p in commit.parents:
99 for p in commit.parents:
95 parents[n].append(p)
100 parents[n].append(p)
96 visit.append(p)
101 visit.append(p)
97
102
98 return parents
103 return parents
99
104
100 def toposort(self, parents):
105 def toposort(self, parents):
101 '''Return an ordering such that every uncommitted changeset is
106 '''Return an ordering such that every uncommitted changeset is
102 preceeded by all its uncommitted ancestors.'''
107 preceeded by all its uncommitted ancestors.'''
103 visit = parents.keys()
108 visit = parents.keys()
104 seen = {}
109 seen = {}
105 children = {}
110 children = {}
106
111
107 while visit:
112 while visit:
108 n = visit.pop(0)
113 n = visit.pop(0)
109 if n in seen: continue
114 if n in seen: continue
110 seen[n] = 1
115 seen[n] = 1
111 # Ensure that nodes without parents are present in the 'children'
116 # Ensure that nodes without parents are present in the 'children'
112 # mapping.
117 # mapping.
113 children.setdefault(n, [])
118 children.setdefault(n, [])
114 for p in parents[n]:
119 for p in parents[n]:
115 if not p in self.map:
120 if not p in self.map:
116 visit.append(p)
121 visit.append(p)
117 children.setdefault(p, []).append(n)
122 children.setdefault(p, []).append(n)
118
123
119 s = []
124 s = []
120 removed = {}
125 removed = {}
121 visit = children.keys()
126 visit = children.keys()
122 while visit:
127 while visit:
123 n = visit.pop(0)
128 n = visit.pop(0)
124 if n in removed: continue
129 if n in removed: continue
125 dep = 0
130 dep = 0
126 if n in parents:
131 if n in parents:
127 for p in parents[n]:
132 for p in parents[n]:
128 if p in self.map: continue
133 if p in self.map: continue
129 if p not in removed:
134 if p not in removed:
130 # we're still dependent
135 # we're still dependent
131 visit.append(n)
136 visit.append(n)
132 dep = 1
137 dep = 1
133 break
138 break
134
139
135 if not dep:
140 if not dep:
136 # all n's parents are in the list
141 # all n's parents are in the list
137 removed[n] = 1
142 removed[n] = 1
138 if n not in self.map:
143 if n not in self.map:
139 s.append(n)
144 s.append(n)
140 if n in children:
145 if n in children:
141 for c in children[n]:
146 for c in children[n]:
142 visit.insert(0, c)
147 visit.insert(0, c)
143
148
144 if self.opts.get('datesort'):
149 if self.opts.get('datesort'):
145 depth = {}
150 depth = {}
146 for n in s:
151 for n in s:
147 depth[n] = 0
152 depth[n] = 0
148 pl = [p for p in self.commitcache[n].parents
153 pl = [p for p in self.commitcache[n].parents
149 if p not in self.map]
154 if p not in self.map]
150 if pl:
155 if pl:
151 depth[n] = max([depth[p] for p in pl]) + 1
156 depth[n] = max([depth[p] for p in pl]) + 1
152
157
153 s = [(depth[n], self.commitcache[n].date, n) for n in s]
158 s = [(depth[n], self.commitcache[n].date, n) for n in s]
154 s.sort()
159 s.sort()
155 s = [e[2] for e in s]
160 s = [e[2] for e in s]
156
161
157 return s
162 return s
158
163
159 def mapentry(self, src, dst):
164 def mapentry(self, src, dst):
160 if self.revmapfilefd is None:
165 if self.revmapfilefd is None:
161 try:
166 try:
162 self.revmapfilefd = open(self.revmapfile, "a")
167 self.revmapfilefd = open(self.revmapfile, "a")
163 except IOError, (errno, strerror):
168 except IOError, (errno, strerror):
164 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
169 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.revmapfile, errno, strerror))
165 self.map[src] = dst
170 self.map[src] = dst
166 self.revmapfilefd.write("%s %s\n" % (src, dst))
171 self.revmapfilefd.write("%s %s\n" % (src, dst))
167 self.revmapfilefd.flush()
172 self.revmapfilefd.flush()
168
173
169 def writeauthormap(self):
174 def writeauthormap(self):
170 authorfile = self.authorfile
175 authorfile = self.authorfile
171 if authorfile:
176 if authorfile:
172 self.ui.status('Writing author map file %s\n' % authorfile)
177 self.ui.status('Writing author map file %s\n' % authorfile)
173 ofile = open(authorfile, 'w+')
178 ofile = open(authorfile, 'w+')
174 for author in self.authors:
179 for author in self.authors:
175 ofile.write("%s=%s\n" % (author, self.authors[author]))
180 ofile.write("%s=%s\n" % (author, self.authors[author]))
176 ofile.close()
181 ofile.close()
177
182
178 def readauthormap(self, authorfile):
183 def readauthormap(self, authorfile):
179 afile = open(authorfile, 'r')
184 afile = open(authorfile, 'r')
180 for line in afile:
185 for line in afile:
181 try:
186 try:
182 srcauthor = line.split('=')[0].strip()
187 srcauthor = line.split('=')[0].strip()
183 dstauthor = line.split('=')[1].strip()
188 dstauthor = line.split('=')[1].strip()
184 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
189 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
185 self.ui.status(
190 self.ui.status(
186 'Overriding mapping for author %s, was %s, will be %s\n'
191 'Overriding mapping for author %s, was %s, will be %s\n'
187 % (srcauthor, self.authors[srcauthor], dstauthor))
192 % (srcauthor, self.authors[srcauthor], dstauthor))
188 else:
193 else:
189 self.ui.debug('Mapping author %s to %s\n'
194 self.ui.debug('Mapping author %s to %s\n'
190 % (srcauthor, dstauthor))
195 % (srcauthor, dstauthor))
191 self.authors[srcauthor] = dstauthor
196 self.authors[srcauthor] = dstauthor
192 except IndexError:
197 except IndexError:
193 self.ui.warn(
198 self.ui.warn(
194 'Ignoring bad line in author file map %s: %s\n'
199 'Ignoring bad line in author file map %s: %s\n'
195 % (authorfile, line))
200 % (authorfile, line))
196 afile.close()
201 afile.close()
197
202
198 def cachecommit(self, rev):
203 def cachecommit(self, rev):
199 commit = self.source.getcommit(rev)
204 commit = self.source.getcommit(rev)
200 commit.author = self.authors.get(commit.author, commit.author)
205 commit.author = self.authors.get(commit.author, commit.author)
201 self.commitcache[rev] = commit
206 self.commitcache[rev] = commit
202 return commit
207 return commit
203
208
204 def copy(self, rev):
209 def copy(self, rev):
205 commit = self.commitcache[rev]
210 commit = self.commitcache[rev]
206 do_copies = hasattr(self.dest, 'copyfile')
211 do_copies = hasattr(self.dest, 'copyfile')
207 filenames = []
212 filenames = []
208
213
209 changes = self.source.getchanges(rev)
214 changes = self.source.getchanges(rev)
210 if isinstance(changes, basestring):
215 if isinstance(changes, basestring):
211 if changes == SKIPREV:
216 if changes == SKIPREV:
212 dest = SKIPREV
217 dest = SKIPREV
213 else:
218 else:
214 dest = self.map[changes]
219 dest = self.map[changes]
215 self.mapentry(rev, dest)
220 self.mapentry(rev, dest)
216 return
221 return
217 files, copies = changes
222 files, copies = changes
218 parents = [self.map[r] for r in commit.parents]
223 parents = [self.map[r] for r in commit.parents]
219 if commit.parents:
224 if commit.parents:
220 prev = commit.parents[0]
225 prev = commit.parents[0]
221 if prev not in self.commitcache:
226 if prev not in self.commitcache:
222 self.cachecommit(prev)
227 self.cachecommit(prev)
223 pbranch = self.commitcache[prev].branch
228 pbranch = self.commitcache[prev].branch
224 else:
229 else:
225 pbranch = None
230 pbranch = None
226 self.dest.setbranch(commit.branch, pbranch, parents)
231 self.dest.setbranch(commit.branch, pbranch, parents)
227 for f, v in files:
232 for f, v in files:
228 filenames.append(f)
233 filenames.append(f)
229 try:
234 try:
230 data = self.source.getfile(f, v)
235 data = self.source.getfile(f, v)
231 except IOError, inst:
236 except IOError, inst:
232 self.dest.delfile(f)
237 self.dest.delfile(f)
233 else:
238 else:
234 e = self.source.getmode(f, v)
239 e = self.source.getmode(f, v)
235 self.dest.putfile(f, e, data)
240 self.dest.putfile(f, e, data)
236 if do_copies:
241 if do_copies:
237 if f in copies:
242 if f in copies:
238 copyf = copies[f]
243 copyf = copies[f]
239 # Merely marks that a copy happened.
244 # Merely marks that a copy happened.
240 self.dest.copyfile(copyf, f)
245 self.dest.copyfile(copyf, f)
241
246
242 newnode = self.dest.putcommit(filenames, parents, commit)
247 newnode = self.dest.putcommit(filenames, parents, commit)
243 self.mapentry(rev, newnode)
248 self.mapentry(rev, newnode)
244
249
245 def convert(self):
250 def convert(self):
246 try:
251 try:
247 self.source.before()
252 self.source.before()
248 self.dest.before()
253 self.dest.before()
249 self.source.setrevmap(self.map, self.maporder)
254 self.source.setrevmap(self.map, self.maporder)
250 self.ui.status("scanning source...\n")
255 self.ui.status("scanning source...\n")
251 heads = self.source.getheads()
256 heads = self.source.getheads()
252 parents = self.walktree(heads)
257 parents = self.walktree(heads)
253 self.ui.status("sorting...\n")
258 self.ui.status("sorting...\n")
254 t = self.toposort(parents)
259 t = self.toposort(parents)
255 num = len(t)
260 num = len(t)
256 c = None
261 c = None
257
262
258 self.ui.status("converting...\n")
263 self.ui.status("converting...\n")
259 for c in t:
264 for c in t:
260 num -= 1
265 num -= 1
261 desc = self.commitcache[c].desc
266 desc = self.commitcache[c].desc
262 if "\n" in desc:
267 if "\n" in desc:
263 desc = desc.splitlines()[0]
268 desc = desc.splitlines()[0]
264 self.ui.status("%d %s\n" % (num, desc))
269 self.ui.status("%d %s\n" % (num, desc))
265 self.copy(c)
270 self.copy(c)
266
271
267 tags = self.source.gettags()
272 tags = self.source.gettags()
268 ctags = {}
273 ctags = {}
269 for k in tags:
274 for k in tags:
270 v = tags[k]
275 v = tags[k]
271 if self.map.get(v, SKIPREV) != SKIPREV:
276 if self.map.get(v, SKIPREV) != SKIPREV:
272 ctags[k] = self.map[v]
277 ctags[k] = self.map[v]
273
278
274 if c and ctags:
279 if c and ctags:
275 nrev = self.dest.puttags(ctags)
280 nrev = self.dest.puttags(ctags)
276 # write another hash correspondence to override the previous
281 # write another hash correspondence to override the previous
277 # one so we don't end up with extra tag heads
282 # one so we don't end up with extra tag heads
278 if nrev:
283 if nrev:
279 self.mapentry(c, nrev)
284 self.mapentry(c, nrev)
280
285
281 self.writeauthormap()
286 self.writeauthormap()
282 finally:
287 finally:
283 self.cleanup()
288 self.cleanup()
284
289
285 def cleanup(self):
290 def cleanup(self):
286 try:
291 try:
287 self.dest.after()
292 self.dest.after()
288 finally:
293 finally:
289 self.source.after()
294 self.source.after()
290 if self.revmapfilefd:
295 if self.revmapfilefd:
291 self.revmapfilefd.close()
296 self.revmapfilefd.close()
292
297
293 def convert(ui, src, dest=None, revmapfile=None, **opts):
298 def convert(ui, src, dest=None, revmapfile=None, **opts):
294 """Convert a foreign SCM repository to a Mercurial one.
299 """Convert a foreign SCM repository to a Mercurial one.
295
300
296 Accepted source formats:
301 Accepted source formats:
297 - CVS
302 - CVS
298 - Darcs
303 - Darcs
299 - git
304 - git
300 - Subversion
305 - Subversion
301
306
302 Accepted destination formats:
307 Accepted destination formats:
303 - Mercurial
308 - Mercurial
304
309
305 If no revision is given, all revisions will be converted. Otherwise,
310 If no revision is given, all revisions will be converted. Otherwise,
306 convert will only import up to the named revision (given in a format
311 convert will only import up to the named revision (given in a format
307 understood by the source).
312 understood by the source).
308
313
309 If no destination directory name is specified, it defaults to the
314 If no destination directory name is specified, it defaults to the
310 basename of the source with '-hg' appended. If the destination
315 basename of the source with '-hg' appended. If the destination
311 repository doesn't exist, it will be created.
316 repository doesn't exist, it will be created.
312
317
313 If <revmapfile> isn't given, it will be put in a default location
318 If <revmapfile> isn't given, it will be put in a default location
314 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
319 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
315 file that maps each source commit ID to the destination ID for
320 file that maps each source commit ID to the destination ID for
316 that revision, like so:
321 that revision, like so:
317 <source ID> <destination ID>
322 <source ID> <destination ID>
318
323
319 If the file doesn't exist, it's automatically created. It's updated
324 If the file doesn't exist, it's automatically created. It's updated
320 on each commit copied, so convert-repo can be interrupted and can
325 on each commit copied, so convert-repo can be interrupted and can
321 be run repeatedly to copy new commits.
326 be run repeatedly to copy new commits.
322
327
323 The [username mapping] file is a simple text file that maps each source
328 The [username mapping] file is a simple text file that maps each source
324 commit author to a destination commit author. It is handy for source SCMs
329 commit author to a destination commit author. It is handy for source SCMs
325 that use unix logins to identify authors (eg: CVS). One line per author
330 that use unix logins to identify authors (eg: CVS). One line per author
326 mapping and the line format is:
331 mapping and the line format is:
327 srcauthor=whatever string you want
332 srcauthor=whatever string you want
328
333
329 The filemap is a file that allows filtering and remapping of files
334 The filemap is a file that allows filtering and remapping of files
330 and directories. Comment lines start with '#'. Each line can
335 and directories. Comment lines start with '#'. Each line can
331 contain one of the following directives:
336 contain one of the following directives:
332
337
333 include path/to/file
338 include path/to/file
334
339
335 exclude path/to/file
340 exclude path/to/file
336
341
337 rename from/file to/file
342 rename from/file to/file
338
343
339 The 'include' directive causes a file, or all files under a
344 The 'include' directive causes a file, or all files under a
340 directory, to be included in the destination repository. The
345 directory, to be included in the destination repository. The
341 'exclude' directive causes files or directories to be omitted.
346 'exclude' directive causes files or directories to be omitted.
342 The 'rename' directive renames a file or directory. To rename
347 The 'rename' directive renames a file or directory. To rename
343 from a subdirectory into the root of the repository, use '.' as
348 from a subdirectory into the root of the repository, use '.' as
344 the path to rename to.
349 the path to rename to.
345 """
350 """
346
351
347 util._encoding = 'UTF-8'
352 util._encoding = 'UTF-8'
348
353
349 if not dest:
354 if not dest:
350 dest = hg.defaultdest(src) + "-hg"
355 dest = hg.defaultdest(src) + "-hg"
351 ui.status("assuming destination %s\n" % dest)
356 ui.status("assuming destination %s\n" % dest)
352
357
353 # Try to be smart and initalize things when required
358 destc = convertsink(ui, dest, opts.get('dest_type'))
354 created = False
355 if os.path.isdir(dest):
356 if len(os.listdir(dest)) > 0:
357 try:
358 hg.repository(ui, dest)
359 ui.status("destination %s is a Mercurial repository\n" % dest)
360 except hg.RepoError:
361 raise util.Abort(
362 "destination directory %s is not empty.\n"
363 "Please specify an empty directory to be initialized\n"
364 "or an already initialized mercurial repository"
365 % dest)
366 else:
367 ui.status("initializing destination %s repository\n" % dest)
368 hg.repository(ui, dest, create=True)
369 created = True
370 elif os.path.exists(dest):
371 raise util.Abort("destination %s exists and is not a directory" % dest)
372 else:
373 ui.status("initializing destination %s repository\n" % dest)
374 hg.repository(ui, dest, create=True)
375 created = True
376
377 destc = convertsink(ui, dest)
378
359
379 try:
360 try:
380 srcc = convertsource(ui, src, rev=opts.get('rev'))
361 srcc = convertsource(ui, src, opts.get('source_type'),
362 opts.get('rev'))
381 except Exception:
363 except Exception:
382 if created:
364 for path in destc.created:
383 shutil.rmtree(dest, True)
365 shutil.rmtree(path, True)
384 raise
366 raise
385
367
386 fmap = opts.get('filemap')
368 fmap = opts.get('filemap')
387 if fmap:
369 if fmap:
388 srcc = filemap.filemap_source(ui, srcc, fmap)
370 srcc = filemap.filemap_source(ui, srcc, fmap)
389 destc.setfilemapmode(True)
371 destc.setfilemapmode(True)
390
372
391 if not revmapfile:
373 if not revmapfile:
392 try:
374 try:
393 revmapfile = destc.revmapfile()
375 revmapfile = destc.revmapfile()
394 except:
376 except:
395 revmapfile = os.path.join(destc, "map")
377 revmapfile = os.path.join(destc, "map")
396
378
397 c = converter(ui, srcc, destc, revmapfile, opts)
379 c = converter(ui, srcc, destc, revmapfile, opts)
398 c.convert()
380 c.convert()
399
381
400
382
401 cmdtable = {
383 cmdtable = {
402 "convert":
384 "convert":
403 (convert,
385 (convert,
404 [('A', 'authors', '', 'username mapping filename'),
386 [('A', 'authors', '', 'username mapping filename'),
387 ('d', 'dest-type', '', 'destination repository type'),
405 ('', 'filemap', '', 'remap file names using contents of file'),
388 ('', 'filemap', '', 'remap file names using contents of file'),
406 ('r', 'rev', '', 'import up to target revision REV'),
389 ('r', 'rev', '', 'import up to target revision REV'),
390 ('s', 'source-type', '', 'source repository type'),
407 ('', 'datesort', None, 'try to sort changesets by date')],
391 ('', 'datesort', None, 'try to sort changesets by date')],
408 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
392 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
409 "debugsvnlog":
393 "debugsvnlog":
410 (debugsvnlog,
394 (debugsvnlog,
411 [],
395 [],
412 'hg debugsvnlog'),
396 'hg debugsvnlog'),
413 }
397 }
414
398
@@ -1,182 +1,186 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 SKIPREV = 'SKIP'
20 SKIPREV = 'SKIP'
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 extra={}):
24 extra={}):
25 self.author = author
25 self.author = author
26 self.date = date
26 self.date = date
27 self.desc = desc
27 self.desc = desc
28 self.parents = parents
28 self.parents = parents
29 self.branch = branch
29 self.branch = branch
30 self.rev = rev
30 self.rev = rev
31 self.extra = extra
31 self.extra = extra
32
32
33 class converter_source(object):
33 class converter_source(object):
34 """Conversion source interface"""
34 """Conversion source interface"""
35
35
36 def __init__(self, ui, path, rev=None):
36 def __init__(self, ui, path, rev=None):
37 """Initialize conversion source (or raise NoRepo("message")
37 """Initialize conversion source (or raise NoRepo("message")
38 exception if path is not a valid repository)"""
38 exception if path is not a valid repository)"""
39 self.ui = ui
39 self.ui = ui
40 self.path = path
40 self.path = path
41 self.rev = rev
41 self.rev = rev
42
42
43 self.encoding = 'utf-8'
43 self.encoding = 'utf-8'
44
44
45 def before(self):
45 def before(self):
46 pass
46 pass
47
47
48 def after(self):
48 def after(self):
49 pass
49 pass
50
50
51 def setrevmap(self, revmap, order):
51 def setrevmap(self, revmap, order):
52 """set the map of already-converted revisions
52 """set the map of already-converted revisions
53
53
54 order is a list with the keys from revmap in the order they
54 order is a list with the keys from revmap in the order they
55 appear in the revision map file."""
55 appear in the revision map file."""
56 pass
56 pass
57
57
58 def getheads(self):
58 def getheads(self):
59 """Return a list of this repository's heads"""
59 """Return a list of this repository's heads"""
60 raise NotImplementedError()
60 raise NotImplementedError()
61
61
62 def getfile(self, name, rev):
62 def getfile(self, name, rev):
63 """Return file contents as a string"""
63 """Return file contents as a string"""
64 raise NotImplementedError()
64 raise NotImplementedError()
65
65
66 def getmode(self, name, rev):
66 def getmode(self, name, rev):
67 """Return file mode, eg. '', 'x', or 'l'"""
67 """Return file mode, eg. '', 'x', or 'l'"""
68 raise NotImplementedError()
68 raise NotImplementedError()
69
69
70 def getchanges(self, version):
70 def getchanges(self, version):
71 """Returns a tuple of (files, copies)
71 """Returns a tuple of (files, copies)
72 Files is a sorted list of (filename, id) tuples for all files changed
72 Files is a sorted list of (filename, id) tuples for all files changed
73 in version, where id is the source revision id of the file.
73 in version, where id is the source revision id of the file.
74
74
75 copies is a dictionary of dest: source
75 copies is a dictionary of dest: source
76 """
76 """
77 raise NotImplementedError()
77 raise NotImplementedError()
78
78
79 def getcommit(self, version):
79 def getcommit(self, version):
80 """Return the commit object for version"""
80 """Return the commit object for version"""
81 raise NotImplementedError()
81 raise NotImplementedError()
82
82
83 def gettags(self):
83 def gettags(self):
84 """Return the tags as a dictionary of name: revision"""
84 """Return the tags as a dictionary of name: revision"""
85 raise NotImplementedError()
85 raise NotImplementedError()
86
86
87 def recode(self, s, encoding=None):
87 def recode(self, s, encoding=None):
88 if not encoding:
88 if not encoding:
89 encoding = self.encoding or 'utf-8'
89 encoding = self.encoding or 'utf-8'
90
90
91 if isinstance(s, unicode):
91 if isinstance(s, unicode):
92 return s.encode("utf-8")
92 return s.encode("utf-8")
93 try:
93 try:
94 return s.decode(encoding).encode("utf-8")
94 return s.decode(encoding).encode("utf-8")
95 except:
95 except:
96 try:
96 try:
97 return s.decode("latin-1").encode("utf-8")
97 return s.decode("latin-1").encode("utf-8")
98 except:
98 except:
99 return s.decode(encoding, "replace").encode("utf-8")
99 return s.decode(encoding, "replace").encode("utf-8")
100
100
101 def getchangedfiles(self, rev, i):
101 def getchangedfiles(self, rev, i):
102 """Return the files changed by rev compared to parent[i].
102 """Return the files changed by rev compared to parent[i].
103
103
104 i is an index selecting one of the parents of rev. The return
104 i is an index selecting one of the parents of rev. The return
105 value should be the list of files that are different in rev and
105 value should be the list of files that are different in rev and
106 this parent.
106 this parent.
107
107
108 If rev has no parents, i is None.
108 If rev has no parents, i is None.
109
109
110 This function is only needed to support --filemap
110 This function is only needed to support --filemap
111 """
111 """
112 raise NotImplementedError()
112 raise NotImplementedError()
113
113
114 class converter_sink(object):
114 class converter_sink(object):
115 """Conversion sink (target) interface"""
115 """Conversion sink (target) interface"""
116
116
117 def __init__(self, ui, path):
117 def __init__(self, ui, path):
118 """Initialize conversion sink (or raise NoRepo("message")
118 """Initialize conversion sink (or raise NoRepo("message")
119 exception if path is not a valid repository)"""
119 exception if path is not a valid repository)
120
121 created is a list of paths to remove if a fatal error occurs
122 later"""
123 self.ui = ui
120 self.path = path
124 self.path = path
121 self.ui = ui
125 self.created = []
122
126
123 def getheads(self):
127 def getheads(self):
124 """Return a list of this repository's heads"""
128 """Return a list of this repository's heads"""
125 raise NotImplementedError()
129 raise NotImplementedError()
126
130
127 def revmapfile(self):
131 def revmapfile(self):
128 """Path to a file that will contain lines
132 """Path to a file that will contain lines
129 source_rev_id sink_rev_id
133 source_rev_id sink_rev_id
130 mapping equivalent revision identifiers for each system."""
134 mapping equivalent revision identifiers for each system."""
131 raise NotImplementedError()
135 raise NotImplementedError()
132
136
133 def authorfile(self):
137 def authorfile(self):
134 """Path to a file that will contain lines
138 """Path to a file that will contain lines
135 srcauthor=dstauthor
139 srcauthor=dstauthor
136 mapping equivalent authors identifiers for each system."""
140 mapping equivalent authors identifiers for each system."""
137 return None
141 return None
138
142
139 def putfile(self, f, e, data):
143 def putfile(self, f, e, data):
140 """Put file for next putcommit().
144 """Put file for next putcommit().
141 f: path to file
145 f: path to file
142 e: '', 'x', or 'l' (regular file, executable, or symlink)
146 e: '', 'x', or 'l' (regular file, executable, or symlink)
143 data: file contents"""
147 data: file contents"""
144 raise NotImplementedError()
148 raise NotImplementedError()
145
149
146 def delfile(self, f):
150 def delfile(self, f):
147 """Delete file for next putcommit().
151 """Delete file for next putcommit().
148 f: path to file"""
152 f: path to file"""
149 raise NotImplementedError()
153 raise NotImplementedError()
150
154
151 def putcommit(self, files, parents, commit):
155 def putcommit(self, files, parents, commit):
152 """Create a revision with all changed files listed in 'files'
156 """Create a revision with all changed files listed in 'files'
153 and having listed parents. 'commit' is a commit object containing
157 and having listed parents. 'commit' is a commit object containing
154 at a minimum the author, date, and message for this changeset.
158 at a minimum the author, date, and message for this changeset.
155 Called after putfile() and delfile() calls. Note that the sink
159 Called after putfile() and delfile() calls. Note that the sink
156 repository is not told to update itself to a particular revision
160 repository is not told to update itself to a particular revision
157 (or even what that revision would be) before it receives the
161 (or even what that revision would be) before it receives the
158 file data."""
162 file data."""
159 raise NotImplementedError()
163 raise NotImplementedError()
160
164
161 def puttags(self, tags):
165 def puttags(self, tags):
162 """Put tags into sink.
166 """Put tags into sink.
163 tags: {tagname: sink_rev_id, ...}"""
167 tags: {tagname: sink_rev_id, ...}"""
164 raise NotImplementedError()
168 raise NotImplementedError()
165
169
166 def setbranch(self, branch, pbranch, parents):
170 def setbranch(self, branch, pbranch, parents):
167 """Set the current branch name. Called before the first putfile
171 """Set the current branch name. Called before the first putfile
168 on the branch.
172 on the branch.
169 branch: branch name for subsequent commits
173 branch: branch name for subsequent commits
170 pbranch: branch name of parent commit
174 pbranch: branch name of parent commit
171 parents: destination revisions of parent"""
175 parents: destination revisions of parent"""
172 pass
176 pass
173
177
174 def setfilemapmode(self, active):
178 def setfilemapmode(self, active):
175 """Tell the destination that we're using a filemap
179 """Tell the destination that we're using a filemap
176
180
177 Some converter_sources (svn in particular) can claim that a file
181 Some converter_sources (svn in particular) can claim that a file
178 was changed in a revision, even if there was no change. This method
182 was changed in a revision, even if there was no change. This method
179 tells the destination that we're using a filemap and that it should
183 tells the destination that we're using a filemap and that it should
180 filter empty revisions.
184 filter empty revisions.
181 """
185 """
182 pass
186 pass
@@ -1,247 +1,259 b''
1 # hg backend for convert extension
1 # hg backend for convert extension
2
2
3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
3 # Note for hg->hg conversion: Old versions of Mercurial didn't trim
4 # the whitespace from the ends of commit messages, but new versions
4 # the whitespace from the ends of commit messages, but new versions
5 # do. Changesets created by those older versions, then converted, may
5 # do. Changesets created by those older versions, then converted, may
6 # thus have different hashes for changesets that are otherwise
6 # thus have different hashes for changesets that are otherwise
7 # identical.
7 # identical.
8
8
9
9
10 import os, time
10 import os, time
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import *
12 from mercurial.node import *
13 from mercurial import hg, lock, revlog, util
13 from mercurial import hg, lock, revlog, util
14
14
15 from common import NoRepo, commit, converter_source, converter_sink
15 from common import NoRepo, commit, converter_source, converter_sink
16
16
17 class mercurial_sink(converter_sink):
17 class mercurial_sink(converter_sink):
18 def __init__(self, ui, path):
18 def __init__(self, ui, path):
19 converter_sink.__init__(self, ui, path)
19 converter_sink.__init__(self, ui, path)
20 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
20 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
21 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
21 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
22 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
22 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
23 self.lastbranch = None
23 self.lastbranch = None
24 try:
24 if os.path.isdir(path) and len(os.listdir(path)) > 0:
25 self.repo = hg.repository(self.ui, path)
25 try:
26 except:
26 self.repo = hg.repository(self.ui, path)
27 raise NoRepo("could not open hg repo %s as sink" % path)
27 ui.status(_('destination %s is a Mercurial repository\n') %
28 path)
29 except hg.RepoError, err:
30 ui.print_exc()
31 raise NoRepo(err.args[0])
32 else:
33 try:
34 ui.status(_('initializing destination %s repository\n') % path)
35 self.repo = hg.repository(self.ui, path, create=True)
36 self.created.append(path)
37 except hg.RepoError, err:
38 ui.print_exc()
39 raise NoRepo("could not create hg repo %s as sink" % path)
28 self.lock = None
40 self.lock = None
29 self.wlock = None
41 self.wlock = None
30 self.filemapmode = False
42 self.filemapmode = False
31
43
32 def before(self):
44 def before(self):
33 self.wlock = self.repo.wlock()
45 self.wlock = self.repo.wlock()
34 self.lock = self.repo.lock()
46 self.lock = self.repo.lock()
35 self.repo.dirstate.clear()
47 self.repo.dirstate.clear()
36
48
37 def after(self):
49 def after(self):
38 self.repo.dirstate.invalidate()
50 self.repo.dirstate.invalidate()
39 self.lock = None
51 self.lock = None
40 self.wlock = None
52 self.wlock = None
41
53
42 def revmapfile(self):
54 def revmapfile(self):
43 return os.path.join(self.path, ".hg", "shamap")
55 return os.path.join(self.path, ".hg", "shamap")
44
56
45 def authorfile(self):
57 def authorfile(self):
46 return os.path.join(self.path, ".hg", "authormap")
58 return os.path.join(self.path, ".hg", "authormap")
47
59
48 def getheads(self):
60 def getheads(self):
49 h = self.repo.changelog.heads()
61 h = self.repo.changelog.heads()
50 return [ hex(x) for x in h ]
62 return [ hex(x) for x in h ]
51
63
52 def putfile(self, f, e, data):
64 def putfile(self, f, e, data):
53 self.repo.wwrite(f, data, e)
65 self.repo.wwrite(f, data, e)
54 if f not in self.repo.dirstate:
66 if f not in self.repo.dirstate:
55 self.repo.dirstate.normallookup(f)
67 self.repo.dirstate.normallookup(f)
56
68
57 def copyfile(self, source, dest):
69 def copyfile(self, source, dest):
58 self.repo.copy(source, dest)
70 self.repo.copy(source, dest)
59
71
60 def delfile(self, f):
72 def delfile(self, f):
61 try:
73 try:
62 util.unlink(self.repo.wjoin(f))
74 util.unlink(self.repo.wjoin(f))
63 #self.repo.remove([f])
75 #self.repo.remove([f])
64 except OSError:
76 except OSError:
65 pass
77 pass
66
78
67 def setbranch(self, branch, pbranch, parents):
79 def setbranch(self, branch, pbranch, parents):
68 if (not self.clonebranches) or (branch == self.lastbranch):
80 if (not self.clonebranches) or (branch == self.lastbranch):
69 return
81 return
70
82
71 self.lastbranch = branch
83 self.lastbranch = branch
72 self.after()
84 self.after()
73 if not branch:
85 if not branch:
74 branch = 'default'
86 branch = 'default'
75 if not pbranch:
87 if not pbranch:
76 pbranch = 'default'
88 pbranch = 'default'
77
89
78 branchpath = os.path.join(self.path, branch)
90 branchpath = os.path.join(self.path, branch)
79 try:
91 try:
80 self.repo = hg.repository(self.ui, branchpath)
92 self.repo = hg.repository(self.ui, branchpath)
81 except:
93 except:
82 if not parents:
94 if not parents:
83 self.repo = hg.repository(self.ui, branchpath, create=True)
95 self.repo = hg.repository(self.ui, branchpath, create=True)
84 else:
96 else:
85 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
97 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
86 hg.clone(self.ui, os.path.join(self.path, pbranch),
98 hg.clone(self.ui, os.path.join(self.path, pbranch),
87 branchpath, rev=parents, update=False,
99 branchpath, rev=parents, update=False,
88 stream=True)
100 stream=True)
89 self.repo = hg.repository(self.ui, branchpath)
101 self.repo = hg.repository(self.ui, branchpath)
90 self.before()
102 self.before()
91
103
92 def putcommit(self, files, parents, commit):
104 def putcommit(self, files, parents, commit):
93 seen = {}
105 seen = {}
94 pl = []
106 pl = []
95 for p in parents:
107 for p in parents:
96 if p not in seen:
108 if p not in seen:
97 pl.append(p)
109 pl.append(p)
98 seen[p] = 1
110 seen[p] = 1
99 parents = pl
111 parents = pl
100 nparents = len(parents)
112 nparents = len(parents)
101 if self.filemapmode and nparents == 1:
113 if self.filemapmode and nparents == 1:
102 m1node = self.repo.changelog.read(bin(parents[0]))[0]
114 m1node = self.repo.changelog.read(bin(parents[0]))[0]
103 parent = parents[0]
115 parent = parents[0]
104
116
105 if len(parents) < 2: parents.append("0" * 40)
117 if len(parents) < 2: parents.append("0" * 40)
106 if len(parents) < 2: parents.append("0" * 40)
118 if len(parents) < 2: parents.append("0" * 40)
107 p2 = parents.pop(0)
119 p2 = parents.pop(0)
108
120
109 text = commit.desc
121 text = commit.desc
110 extra = commit.extra.copy()
122 extra = commit.extra.copy()
111 if self.branchnames and commit.branch:
123 if self.branchnames and commit.branch:
112 extra['branch'] = commit.branch
124 extra['branch'] = commit.branch
113 if commit.rev:
125 if commit.rev:
114 extra['convert_revision'] = commit.rev
126 extra['convert_revision'] = commit.rev
115
127
116 while parents:
128 while parents:
117 p1 = p2
129 p1 = p2
118 p2 = parents.pop(0)
130 p2 = parents.pop(0)
119 a = self.repo.rawcommit(files, text, commit.author, commit.date,
131 a = self.repo.rawcommit(files, text, commit.author, commit.date,
120 bin(p1), bin(p2), extra=extra)
132 bin(p1), bin(p2), extra=extra)
121 self.repo.dirstate.clear()
133 self.repo.dirstate.clear()
122 text = "(octopus merge fixup)\n"
134 text = "(octopus merge fixup)\n"
123 p2 = hg.hex(self.repo.changelog.tip())
135 p2 = hg.hex(self.repo.changelog.tip())
124
136
125 if self.filemapmode and nparents == 1:
137 if self.filemapmode and nparents == 1:
126 man = self.repo.manifest
138 man = self.repo.manifest
127 mnode = self.repo.changelog.read(bin(p2))[0]
139 mnode = self.repo.changelog.read(bin(p2))[0]
128 if not man.cmp(m1node, man.revision(mnode)):
140 if not man.cmp(m1node, man.revision(mnode)):
129 self.repo.rollback()
141 self.repo.rollback()
130 self.repo.dirstate.clear()
142 self.repo.dirstate.clear()
131 return parent
143 return parent
132 return p2
144 return p2
133
145
134 def puttags(self, tags):
146 def puttags(self, tags):
135 try:
147 try:
136 old = self.repo.wfile(".hgtags").read()
148 old = self.repo.wfile(".hgtags").read()
137 oldlines = old.splitlines(1)
149 oldlines = old.splitlines(1)
138 oldlines.sort()
150 oldlines.sort()
139 except:
151 except:
140 oldlines = []
152 oldlines = []
141
153
142 k = tags.keys()
154 k = tags.keys()
143 k.sort()
155 k.sort()
144 newlines = []
156 newlines = []
145 for tag in k:
157 for tag in k:
146 newlines.append("%s %s\n" % (tags[tag], tag))
158 newlines.append("%s %s\n" % (tags[tag], tag))
147
159
148 newlines.sort()
160 newlines.sort()
149
161
150 if newlines != oldlines:
162 if newlines != oldlines:
151 self.ui.status("updating tags\n")
163 self.ui.status("updating tags\n")
152 f = self.repo.wfile(".hgtags", "w")
164 f = self.repo.wfile(".hgtags", "w")
153 f.write("".join(newlines))
165 f.write("".join(newlines))
154 f.close()
166 f.close()
155 if not oldlines: self.repo.add([".hgtags"])
167 if not oldlines: self.repo.add([".hgtags"])
156 date = "%s 0" % int(time.mktime(time.gmtime()))
168 date = "%s 0" % int(time.mktime(time.gmtime()))
157 extra = {}
169 extra = {}
158 if self.tagsbranch != 'default':
170 if self.tagsbranch != 'default':
159 extra['branch'] = self.tagsbranch
171 extra['branch'] = self.tagsbranch
160 try:
172 try:
161 tagparent = self.repo.changectx(self.tagsbranch).node()
173 tagparent = self.repo.changectx(self.tagsbranch).node()
162 except hg.RepoError, inst:
174 except hg.RepoError, inst:
163 tagparent = nullid
175 tagparent = nullid
164 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
176 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
165 date, tagparent, nullid)
177 date, tagparent, nullid)
166 return hex(self.repo.changelog.tip())
178 return hex(self.repo.changelog.tip())
167
179
168 def setfilemapmode(self, active):
180 def setfilemapmode(self, active):
169 self.filemapmode = active
181 self.filemapmode = active
170
182
171 class mercurial_source(converter_source):
183 class mercurial_source(converter_source):
172 def __init__(self, ui, path, rev=None):
184 def __init__(self, ui, path, rev=None):
173 converter_source.__init__(self, ui, path, rev)
185 converter_source.__init__(self, ui, path, rev)
174 try:
186 try:
175 self.repo = hg.repository(self.ui, path)
187 self.repo = hg.repository(self.ui, path)
176 # try to provoke an exception if this isn't really a hg
188 # try to provoke an exception if this isn't really a hg
177 # repo, but some other bogus compatible-looking url
189 # repo, but some other bogus compatible-looking url
178 self.repo.heads()
190 self.repo.heads()
179 except hg.RepoError:
191 except hg.RepoError:
180 ui.print_exc()
192 ui.print_exc()
181 raise NoRepo("could not open hg repo %s as source" % path)
193 raise NoRepo("could not open hg repo %s as source" % path)
182 self.lastrev = None
194 self.lastrev = None
183 self.lastctx = None
195 self.lastctx = None
184 self._changescache = None
196 self._changescache = None
185
197
186 def changectx(self, rev):
198 def changectx(self, rev):
187 if self.lastrev != rev:
199 if self.lastrev != rev:
188 self.lastctx = self.repo.changectx(rev)
200 self.lastctx = self.repo.changectx(rev)
189 self.lastrev = rev
201 self.lastrev = rev
190 return self.lastctx
202 return self.lastctx
191
203
192 def getheads(self):
204 def getheads(self):
193 if self.rev:
205 if self.rev:
194 return [hex(self.repo.changectx(self.rev).node())]
206 return [hex(self.repo.changectx(self.rev).node())]
195 else:
207 else:
196 return [hex(node) for node in self.repo.heads()]
208 return [hex(node) for node in self.repo.heads()]
197
209
198 def getfile(self, name, rev):
210 def getfile(self, name, rev):
199 try:
211 try:
200 return self.changectx(rev).filectx(name).data()
212 return self.changectx(rev).filectx(name).data()
201 except revlog.LookupError, err:
213 except revlog.LookupError, err:
202 raise IOError(err)
214 raise IOError(err)
203
215
204 def getmode(self, name, rev):
216 def getmode(self, name, rev):
205 m = self.changectx(rev).manifest()
217 m = self.changectx(rev).manifest()
206 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
218 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
207
219
208 def getchanges(self, rev):
220 def getchanges(self, rev):
209 ctx = self.changectx(rev)
221 ctx = self.changectx(rev)
210 if self._changescache and self._changescache[0] == rev:
222 if self._changescache and self._changescache[0] == rev:
211 m, a, r = self._changescache[1]
223 m, a, r = self._changescache[1]
212 else:
224 else:
213 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
225 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
214 changes = [(name, rev) for name in m + a + r]
226 changes = [(name, rev) for name in m + a + r]
215 changes.sort()
227 changes.sort()
216 return (changes, self.getcopies(ctx, m + a))
228 return (changes, self.getcopies(ctx, m + a))
217
229
218 def getcopies(self, ctx, files):
230 def getcopies(self, ctx, files):
219 copies = {}
231 copies = {}
220 for name in files:
232 for name in files:
221 try:
233 try:
222 copies[name] = ctx.filectx(name).renamed()[0]
234 copies[name] = ctx.filectx(name).renamed()[0]
223 except TypeError:
235 except TypeError:
224 pass
236 pass
225 return copies
237 return copies
226
238
227 def getcommit(self, rev):
239 def getcommit(self, rev):
228 ctx = self.changectx(rev)
240 ctx = self.changectx(rev)
229 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
241 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
230 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
242 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
231 desc=ctx.description(), parents=parents,
243 desc=ctx.description(), parents=parents,
232 branch=ctx.branch(), extra=ctx.extra())
244 branch=ctx.branch(), extra=ctx.extra())
233
245
234 def gettags(self):
246 def gettags(self):
235 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
247 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
236 return dict([(name, hex(node)) for name, node in tags])
248 return dict([(name, hex(node)) for name, node in tags])
237
249
238 def getchangedfiles(self, rev, i):
250 def getchangedfiles(self, rev, i):
239 ctx = self.changectx(rev)
251 ctx = self.changectx(rev)
240 i = i or 0
252 i = i or 0
241 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
253 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
242
254
243 if i == 0:
255 if i == 0:
244 self._changescache = (rev, changes)
256 self._changescache = (rev, changes)
245
257
246 return changes[0] + changes[1] + changes[2]
258 return changes[0] + changes[1] + changes[2]
247
259
@@ -1,21 +1,37 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 echo "[extensions]" >> $HGRCPATH
3 echo "[extensions]" >> $HGRCPATH
4 echo "convert=" >> $HGRCPATH
4 echo "convert=" >> $HGRCPATH
5
5
6 hg help convert
7
6 hg init a
8 hg init a
7 cd a
9 cd a
8 echo a > a
10 echo a > a
9 hg ci -d'0 0' -Ama
11 hg ci -d'0 0' -Ama
10 hg cp a b
12 hg cp a b
11 hg ci -d'1 0' -mb
13 hg ci -d'1 0' -mb
12 hg rm a
14 hg rm a
13 hg ci -d'2 0' -mc
15 hg ci -d'2 0' -mc
14 hg mv b a
16 hg mv b a
15 hg ci -d'3 0' -md
17 hg ci -d'3 0' -md
16 echo a >> a
18 echo a >> a
17 hg ci -d'4 0' -me
19 hg ci -d'4 0' -me
18
20
19 cd ..
21 cd ..
20 hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
22 hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
21 hg --cwd a-hg pull ../a
23 hg --cwd a-hg pull ../a
24
25 touch bogusfile
26 echo % should fail
27 hg convert a bogusfile
28
29 mkdir bogusdir
30 chmod 000 bogusdir
31
32 echo % should fail
33 hg convert a bogusdir
34
35 echo % should succeed
36 chmod 700 bogusdir
37 hg convert a bogusdir
@@ -1,14 +1,93 b''
1 hg convert [OPTION]... SOURCE [DEST [MAPFILE]]
2
3 Convert a foreign SCM repository to a Mercurial one.
4
5 Accepted source formats:
6 - CVS
7 - Darcs
8 - git
9 - Subversion
10
11 Accepted destination formats:
12 - Mercurial
13
14 If no revision is given, all revisions will be converted. Otherwise,
15 convert will only import up to the named revision (given in a format
16 understood by the source).
17
18 If no destination directory name is specified, it defaults to the
19 basename of the source with '-hg' appended. If the destination
20 repository doesn't exist, it will be created.
21
22 If <revmapfile> isn't given, it will be put in a default location
23 (<dest>/.hg/shamap by default). The <revmapfile> is a simple text
24 file that maps each source commit ID to the destination ID for
25 that revision, like so:
26 <source ID> <destination ID>
27
28 If the file doesn't exist, it's automatically created. It's updated
29 on each commit copied, so convert-repo can be interrupted and can
30 be run repeatedly to copy new commits.
31
32 The [username mapping] file is a simple text file that maps each source
33 commit author to a destination commit author. It is handy for source SCMs
34 that use unix logins to identify authors (eg: CVS). One line per author
35 mapping and the line format is:
36 srcauthor=whatever string you want
37
38 The filemap is a file that allows filtering and remapping of files
39 and directories. Comment lines start with '#'. Each line can
40 contain one of the following directives:
41
42 include path/to/file
43
44 exclude path/to/file
45
46 rename from/file to/file
47
48 The 'include' directive causes a file, or all files under a
49 directory, to be included in the destination repository. The
50 'exclude' directive causes files or directories to be omitted.
51 The 'rename' directive renames a file or directory. To rename
52 from a subdirectory into the root of the repository, use '.' as
53 the path to rename to.
54
55 options:
56
57 -A --authors username mapping filename
58 -d --dest-type destination repository type
59 --filemap remap file names using contents of file
60 -r --rev import up to target revision REV
61 -s --source-type source repository type
62 --datesort try to sort changesets by date
63
64 use "hg -v help convert" to show global options
1 adding a
65 adding a
2 assuming destination a-hg
66 assuming destination a-hg
3 initializing destination a-hg repository
67 initializing destination a-hg repository
4 scanning source...
68 scanning source...
5 sorting...
69 sorting...
6 converting...
70 converting...
7 4 a
71 4 a
8 3 b
72 3 b
9 2 c
73 2 c
10 1 d
74 1 d
11 0 e
75 0 e
12 pulling from ../a
76 pulling from ../a
13 searching for changes
77 searching for changes
14 no changes found
78 no changes found
79 % should fail
80 initializing destination bogusfile repository
81 abort: cannot create new bundle repository
82 % should fail
83 abort: Permission denied: bogusdir
84 % should succeed
85 initializing destination bogusdir repository
86 scanning source...
87 sorting...
88 converting...
89 4 a
90 3 b
91 2 c
92 1 d
93 0 e
General Comments 0
You need to be logged in to leave comments. Login now