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