##// END OF EJS Templates
convert: some tidyups, doc improvements, and test fixes...
Bryan O'Sullivan -
r5556:61fdf255 default
parent child Browse files
Show More
@@ -1,383 +1,401 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, mapfile
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 debugsvnlog, svn_source, svn_sink
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 22 source_converters = [
23 23 ('cvs', convert_cvs),
24 24 ('git', convert_git),
25 25 ('svn', svn_source),
26 26 ('hg', mercurial_source),
27 27 ('darcs', darcs_source),
28 28 ]
29 29
30 30 sink_converters = [
31 31 ('hg', mercurial_sink),
32 32 ('svn', svn_sink),
33 33 ]
34 34
35 35 def convertsource(ui, path, type, rev):
36 36 exceptions = []
37 37 for name, source in source_converters:
38 38 try:
39 39 if not type or name == type:
40 40 return source(ui, path, rev)
41 41 except NoRepo, inst:
42 42 exceptions.append(inst)
43 43 if not ui.quiet:
44 44 for inst in exceptions:
45 45 ui.write(_("%s\n") % inst)
46 46 raise util.Abort('%s: unknown repository type' % path)
47 47
48 48 def convertsink(ui, path, type):
49 49 for name, sink in sink_converters:
50 50 try:
51 51 if not type or name == type:
52 52 return sink(ui, path)
53 53 except NoRepo, inst:
54 54 ui.note(_("convert: %s\n") % inst)
55 55 raise util.Abort('%s: unknown repository type' % path)
56 56
57 57 class converter(object):
58 58 def __init__(self, ui, source, dest, revmapfile, opts):
59 59
60 60 self.source = source
61 61 self.dest = dest
62 62 self.ui = ui
63 63 self.opts = opts
64 64 self.commitcache = {}
65 65 self.authors = {}
66 66 self.authorfile = None
67 67
68 68 self.map = mapfile(ui, revmapfile)
69 69
70 70 # Read first the dst author map if any
71 71 authorfile = self.dest.authorfile()
72 72 if authorfile and os.path.exists(authorfile):
73 73 self.readauthormap(authorfile)
74 74 # Extend/Override with new author map if necessary
75 75 if opts.get('authors'):
76 76 self.readauthormap(opts.get('authors'))
77 77 self.authorfile = self.dest.authorfile()
78 78
79 79 def walktree(self, heads):
80 80 '''Return a mapping that identifies the uncommitted parents of every
81 81 uncommitted changeset.'''
82 82 visit = heads
83 83 known = {}
84 84 parents = {}
85 85 while visit:
86 86 n = visit.pop(0)
87 87 if n in known or n in self.map: continue
88 88 known[n] = 1
89 89 commit = self.cachecommit(n)
90 90 parents[n] = []
91 91 for p in commit.parents:
92 92 parents[n].append(p)
93 93 visit.append(p)
94 94
95 95 return parents
96 96
97 97 def toposort(self, parents):
98 98 '''Return an ordering such that every uncommitted changeset is
99 99 preceeded by all its uncommitted ancestors.'''
100 100 visit = parents.keys()
101 101 seen = {}
102 102 children = {}
103 103
104 104 while visit:
105 105 n = visit.pop(0)
106 106 if n in seen: continue
107 107 seen[n] = 1
108 108 # Ensure that nodes without parents are present in the 'children'
109 109 # mapping.
110 110 children.setdefault(n, [])
111 111 for p in parents[n]:
112 112 if not p in self.map:
113 113 visit.append(p)
114 114 children.setdefault(p, []).append(n)
115 115
116 116 s = []
117 117 removed = {}
118 118 visit = children.keys()
119 119 while visit:
120 120 n = visit.pop(0)
121 121 if n in removed: continue
122 122 dep = 0
123 123 if n in parents:
124 124 for p in parents[n]:
125 125 if p in self.map: continue
126 126 if p not in removed:
127 127 # we're still dependent
128 128 visit.append(n)
129 129 dep = 1
130 130 break
131 131
132 132 if not dep:
133 133 # all n's parents are in the list
134 134 removed[n] = 1
135 135 if n not in self.map:
136 136 s.append(n)
137 137 if n in children:
138 138 for c in children[n]:
139 139 visit.insert(0, c)
140 140
141 141 if self.opts.get('datesort'):
142 142 depth = {}
143 143 for n in s:
144 144 depth[n] = 0
145 145 pl = [p for p in self.commitcache[n].parents
146 146 if p not in self.map]
147 147 if pl:
148 148 depth[n] = max([depth[p] for p in pl]) + 1
149 149
150 150 s = [(depth[n], self.commitcache[n].date, n) for n in s]
151 151 s.sort()
152 152 s = [e[2] for e in s]
153 153
154 154 return s
155 155
156 156 def writeauthormap(self):
157 157 authorfile = self.authorfile
158 158 if authorfile:
159 159 self.ui.status('Writing author map file %s\n' % authorfile)
160 160 ofile = open(authorfile, 'w+')
161 161 for author in self.authors:
162 162 ofile.write("%s=%s\n" % (author, self.authors[author]))
163 163 ofile.close()
164 164
165 165 def readauthormap(self, authorfile):
166 166 afile = open(authorfile, 'r')
167 167 for line in afile:
168 168 try:
169 169 srcauthor = line.split('=')[0].strip()
170 170 dstauthor = line.split('=')[1].strip()
171 171 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
172 172 self.ui.status(
173 173 'Overriding mapping for author %s, was %s, will be %s\n'
174 174 % (srcauthor, self.authors[srcauthor], dstauthor))
175 175 else:
176 176 self.ui.debug('Mapping author %s to %s\n'
177 177 % (srcauthor, dstauthor))
178 178 self.authors[srcauthor] = dstauthor
179 179 except IndexError:
180 180 self.ui.warn(
181 181 'Ignoring bad line in author file map %s: %s\n'
182 182 % (authorfile, line))
183 183 afile.close()
184 184
185 185 def cachecommit(self, rev):
186 186 commit = self.source.getcommit(rev)
187 187 commit.author = self.authors.get(commit.author, commit.author)
188 188 self.commitcache[rev] = commit
189 189 return commit
190 190
191 191 def copy(self, rev):
192 192 commit = self.commitcache[rev]
193 193 do_copies = hasattr(self.dest, 'copyfile')
194 194 filenames = []
195 195
196 196 changes = self.source.getchanges(rev)
197 197 if isinstance(changes, basestring):
198 198 if changes == SKIPREV:
199 199 dest = SKIPREV
200 200 else:
201 201 dest = self.map[changes]
202 202 self.map[rev] = dest
203 203 return
204 204 files, copies = changes
205 205 parents = [self.map[r] for r in commit.parents]
206 206 if commit.parents:
207 207 prev = commit.parents[0]
208 208 if prev not in self.commitcache:
209 209 self.cachecommit(prev)
210 210 pbranch = self.commitcache[prev].branch
211 211 else:
212 212 pbranch = None
213 213 self.dest.setbranch(commit.branch, pbranch, parents)
214 214 for f, v in files:
215 215 filenames.append(f)
216 216 try:
217 217 data = self.source.getfile(f, v)
218 218 except IOError, inst:
219 219 self.dest.delfile(f)
220 220 else:
221 221 e = self.source.getmode(f, v)
222 222 self.dest.putfile(f, e, data)
223 223 if do_copies:
224 224 if f in copies:
225 225 copyf = copies[f]
226 226 # Merely marks that a copy happened.
227 227 self.dest.copyfile(copyf, f)
228 228
229 229 newnode = self.dest.putcommit(filenames, parents, commit)
230 230 self.source.converted(rev, newnode)
231 231 self.map[rev] = newnode
232 232
233 233 def convert(self):
234 234 try:
235 235 self.source.before()
236 236 self.dest.before()
237 237 self.source.setrevmap(self.map)
238 238 self.ui.status("scanning source...\n")
239 239 heads = self.source.getheads()
240 240 parents = self.walktree(heads)
241 241 self.ui.status("sorting...\n")
242 242 t = self.toposort(parents)
243 243 num = len(t)
244 244 c = None
245 245
246 246 self.ui.status("converting...\n")
247 247 for c in t:
248 248 num -= 1
249 249 desc = self.commitcache[c].desc
250 250 if "\n" in desc:
251 251 desc = desc.splitlines()[0]
252 252 self.ui.status("%d %s\n" % (num, desc))
253 253 self.copy(c)
254 254
255 255 tags = self.source.gettags()
256 256 ctags = {}
257 257 for k in tags:
258 258 v = tags[k]
259 259 if self.map.get(v, SKIPREV) != SKIPREV:
260 260 ctags[k] = self.map[v]
261 261
262 262 if c and ctags:
263 263 nrev = self.dest.puttags(ctags)
264 264 # write another hash correspondence to override the previous
265 265 # one so we don't end up with extra tag heads
266 266 if nrev:
267 267 self.map[c] = nrev
268 268
269 269 self.writeauthormap()
270 270 finally:
271 271 self.cleanup()
272 272
273 273 def cleanup(self):
274 274 try:
275 275 self.dest.after()
276 276 finally:
277 277 self.source.after()
278 278 self.map.close()
279 279
280 280 def convert(ui, src, dest=None, revmapfile=None, **opts):
281 281 """Convert a foreign SCM repository to a Mercurial one.
282 282
283 283 Accepted source formats:
284 284 - Mercurial
285 285 - CVS
286 286 - Darcs
287 287 - git
288 288 - Subversion
289 289
290 290 Accepted destination formats:
291 291 - Mercurial
292 292 - Subversion (history on branches is not preserved)
293 293
294 294 If no revision is given, all revisions will be converted. Otherwise,
295 295 convert will only import up to the named revision (given in a format
296 296 understood by the source).
297 297
298 298 If no destination directory name is specified, it defaults to the
299 299 basename of the source with '-hg' appended. If the destination
300 300 repository doesn't exist, it will be created.
301 301
302 302 If <MAPFILE> isn't given, it will be put in a default location
303 303 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
304 304 file that maps each source commit ID to the destination ID for
305 305 that revision, like so:
306 306 <source ID> <destination ID>
307 307
308 308 If the file doesn't exist, it's automatically created. It's updated
309 309 on each commit copied, so convert-repo can be interrupted and can
310 310 be run repeatedly to copy new commits.
311 311
312 312 The [username mapping] file is a simple text file that maps each source
313 313 commit author to a destination commit author. It is handy for source SCMs
314 314 that use unix logins to identify authors (eg: CVS). One line per author
315 315 mapping and the line format is:
316 316 srcauthor=whatever string you want
317 317
318 318 The filemap is a file that allows filtering and remapping of files
319 319 and directories. Comment lines start with '#'. Each line can
320 320 contain one of the following directives:
321 321
322 322 include path/to/file
323 323
324 324 exclude path/to/file
325 325
326 326 rename from/file to/file
327 327
328 328 The 'include' directive causes a file, or all files under a
329 329 directory, to be included in the destination repository, and the
330 330 exclusion of all other files and dirs not explicitely included.
331 331 The 'exclude' directive causes files or directories to be omitted.
332 332 The 'rename' directive renames a file or directory. To rename from a
333 333 subdirectory into the root of the repository, use '.' as the path to
334 334 rename to.
335
336 Back end options:
337
338 --config convert.hg.clonebranches=False (boolean)
339 hg target: XXX not documented
340 --config convert.hg.saverev=True (boolean)
341 hg source: allow target to preserve source revision ID
342 --config convert.hg.tagsbranch=default (branch name)
343 hg target: XXX not documented
344 --config convert.hg.usebranchnames=True (boolean)
345 hg target: preserve branch names
346
347 --config convert.svn.branches=branches (directory name)
348 svn source: specify the directory containing branches
349 --config convert.svn.tags=tags (directory name)
350 svn source: specify the directory containing tags
351 --config convert.svn.trunk=trunk (directory name)
352 svn source: specify the name of the trunk branch
335 353 """
336 354
337 355 util._encoding = 'UTF-8'
338 356
339 357 if not dest:
340 358 dest = hg.defaultdest(src) + "-hg"
341 359 ui.status("assuming destination %s\n" % dest)
342 360
343 361 destc = convertsink(ui, dest, opts.get('dest_type'))
344 362
345 363 try:
346 364 srcc = convertsource(ui, src, opts.get('source_type'),
347 365 opts.get('rev'))
348 366 except Exception:
349 367 for path in destc.created:
350 368 shutil.rmtree(path, True)
351 369 raise
352 370
353 371 fmap = opts.get('filemap')
354 372 if fmap:
355 373 srcc = filemap.filemap_source(ui, srcc, fmap)
356 374 destc.setfilemapmode(True)
357 375
358 376 if not revmapfile:
359 377 try:
360 378 revmapfile = destc.revmapfile()
361 379 except:
362 380 revmapfile = os.path.join(destc, "map")
363 381
364 382 c = converter(ui, srcc, destc, revmapfile, opts)
365 383 c.convert()
366 384
367 385
368 386 cmdtable = {
369 387 "convert":
370 388 (convert,
371 389 [('A', 'authors', '', 'username mapping filename'),
372 390 ('d', 'dest-type', '', 'destination repository type'),
373 391 ('', 'filemap', '', 'remap file names using contents of file'),
374 392 ('r', 'rev', '', 'import up to target revision REV'),
375 393 ('s', 'source-type', '', 'source repository type'),
376 394 ('', 'datesort', None, 'try to sort changesets by date')],
377 395 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
378 396 "debugsvnlog":
379 397 (debugsvnlog,
380 398 [],
381 399 'hg debugsvnlog'),
382 400 }
383 401
@@ -1,297 +1,297 b''
1 1 # common code for the convert extension
2 2 import base64, errno
3 3 import cPickle as pickle
4 4 from mercurial import util
5 5 from mercurial.i18n import _
6 6
7 7 def encodeargs(args):
8 8 def encodearg(s):
9 9 lines = base64.encodestring(s)
10 10 lines = [l.splitlines()[0] for l in lines]
11 11 return ''.join(lines)
12 12
13 13 s = pickle.dumps(args)
14 14 return encodearg(s)
15 15
16 16 def decodeargs(s):
17 17 s = base64.decodestring(s)
18 18 return pickle.loads(s)
19 19
20 20 def checktool(exe, name=None):
21 21 name = name or exe
22 22 if not util.find_exe(exe):
23 23 raise util.Abort('cannot find required "%s" tool' % name)
24 24
25 25 class NoRepo(Exception): pass
26 26
27 27 SKIPREV = 'SKIP'
28 28
29 29 class commit(object):
30 30 def __init__(self, author, date, desc, parents, branch=None, rev=None,
31 31 extra={}):
32 32 self.author = author
33 33 self.date = date
34 34 self.desc = desc
35 35 self.parents = parents
36 36 self.branch = branch
37 37 self.rev = rev
38 38 self.extra = extra
39 39
40 40 class converter_source(object):
41 41 """Conversion source interface"""
42 42
43 def __init__(self, ui, path, rev=None):
43 def __init__(self, ui, path=None, rev=None):
44 44 """Initialize conversion source (or raise NoRepo("message")
45 45 exception if path is not a valid repository)"""
46 46 self.ui = ui
47 47 self.path = path
48 48 self.rev = rev
49 49
50 50 self.encoding = 'utf-8'
51 51
52 52 def before(self):
53 53 pass
54 54
55 55 def after(self):
56 56 pass
57 57
58 58 def setrevmap(self, revmap):
59 59 """set the map of already-converted revisions"""
60 60 pass
61 61
62 62 def getheads(self):
63 63 """Return a list of this repository's heads"""
64 64 raise NotImplementedError()
65 65
66 66 def getfile(self, name, rev):
67 67 """Return file contents as a string"""
68 68 raise NotImplementedError()
69 69
70 70 def getmode(self, name, rev):
71 71 """Return file mode, eg. '', 'x', or 'l'"""
72 72 raise NotImplementedError()
73 73
74 74 def getchanges(self, version):
75 75 """Returns a tuple of (files, copies)
76 76 Files is a sorted list of (filename, id) tuples for all files changed
77 77 in version, where id is the source revision id of the file.
78 78
79 79 copies is a dictionary of dest: source
80 80 """
81 81 raise NotImplementedError()
82 82
83 83 def getcommit(self, version):
84 84 """Return the commit object for version"""
85 85 raise NotImplementedError()
86 86
87 87 def gettags(self):
88 88 """Return the tags as a dictionary of name: revision"""
89 89 raise NotImplementedError()
90 90
91 91 def recode(self, s, encoding=None):
92 92 if not encoding:
93 93 encoding = self.encoding or 'utf-8'
94 94
95 95 if isinstance(s, unicode):
96 96 return s.encode("utf-8")
97 97 try:
98 98 return s.decode(encoding).encode("utf-8")
99 99 except:
100 100 try:
101 101 return s.decode("latin-1").encode("utf-8")
102 102 except:
103 103 return s.decode(encoding, "replace").encode("utf-8")
104 104
105 105 def getchangedfiles(self, rev, i):
106 106 """Return the files changed by rev compared to parent[i].
107 107
108 108 i is an index selecting one of the parents of rev. The return
109 109 value should be the list of files that are different in rev and
110 110 this parent.
111 111
112 112 If rev has no parents, i is None.
113 113
114 114 This function is only needed to support --filemap
115 115 """
116 116 raise NotImplementedError()
117 117
118 118 def converted(self, rev, sinkrev):
119 119 '''Notify the source that a revision has been converted.'''
120 120 pass
121 121
122 122
123 123 class converter_sink(object):
124 124 """Conversion sink (target) interface"""
125 125
126 126 def __init__(self, ui, path):
127 127 """Initialize conversion sink (or raise NoRepo("message")
128 128 exception if path is not a valid repository)
129 129
130 130 created is a list of paths to remove if a fatal error occurs
131 131 later"""
132 132 self.ui = ui
133 133 self.path = path
134 134 self.created = []
135 135
136 136 def getheads(self):
137 137 """Return a list of this repository's heads"""
138 138 raise NotImplementedError()
139 139
140 140 def revmapfile(self):
141 141 """Path to a file that will contain lines
142 142 source_rev_id sink_rev_id
143 143 mapping equivalent revision identifiers for each system."""
144 144 raise NotImplementedError()
145 145
146 146 def authorfile(self):
147 147 """Path to a file that will contain lines
148 148 srcauthor=dstauthor
149 149 mapping equivalent authors identifiers for each system."""
150 150 return None
151 151
152 152 def putfile(self, f, e, data):
153 153 """Put file for next putcommit().
154 154 f: path to file
155 155 e: '', 'x', or 'l' (regular file, executable, or symlink)
156 156 data: file contents"""
157 157 raise NotImplementedError()
158 158
159 159 def delfile(self, f):
160 160 """Delete file for next putcommit().
161 161 f: path to file"""
162 162 raise NotImplementedError()
163 163
164 164 def putcommit(self, files, parents, commit):
165 165 """Create a revision with all changed files listed in 'files'
166 166 and having listed parents. 'commit' is a commit object containing
167 167 at a minimum the author, date, and message for this changeset.
168 168 Called after putfile() and delfile() calls. Note that the sink
169 169 repository is not told to update itself to a particular revision
170 170 (or even what that revision would be) before it receives the
171 171 file data."""
172 172 raise NotImplementedError()
173 173
174 174 def puttags(self, tags):
175 175 """Put tags into sink.
176 176 tags: {tagname: sink_rev_id, ...}"""
177 177 raise NotImplementedError()
178 178
179 179 def setbranch(self, branch, pbranch, parents):
180 180 """Set the current branch name. Called before the first putfile
181 181 on the branch.
182 182 branch: branch name for subsequent commits
183 183 pbranch: branch name of parent commit
184 184 parents: destination revisions of parent"""
185 185 pass
186 186
187 187 def setfilemapmode(self, active):
188 188 """Tell the destination that we're using a filemap
189 189
190 190 Some converter_sources (svn in particular) can claim that a file
191 191 was changed in a revision, even if there was no change. This method
192 192 tells the destination that we're using a filemap and that it should
193 193 filter empty revisions.
194 194 """
195 195 pass
196 196
197 197 def before(self):
198 198 pass
199 199
200 200 def after(self):
201 201 pass
202 202
203 203
204 204 class commandline(object):
205 205 def __init__(self, ui, command):
206 206 self.ui = ui
207 207 self.command = command
208 208
209 209 def prerun(self):
210 210 pass
211 211
212 212 def postrun(self):
213 213 pass
214 214
215 215 def _run(self, cmd, *args, **kwargs):
216 216 cmdline = [self.command, cmd] + list(args)
217 217 for k, v in kwargs.iteritems():
218 218 if len(k) == 1:
219 219 cmdline.append('-' + k)
220 220 else:
221 221 cmdline.append('--' + k.replace('_', '-'))
222 222 try:
223 223 if len(k) == 1:
224 224 cmdline.append('' + v)
225 225 else:
226 226 cmdline[-1] += '=' + v
227 227 except TypeError:
228 228 pass
229 229 cmdline = [util.shellquote(arg) for arg in cmdline]
230 230 cmdline += ['<', util.nulldev]
231 231 cmdline = ' '.join(cmdline)
232 232 self.ui.debug(cmdline, '\n')
233 233
234 234 self.prerun()
235 235 try:
236 236 return util.popen(cmdline)
237 237 finally:
238 238 self.postrun()
239 239
240 240 def run(self, cmd, *args, **kwargs):
241 241 fp = self._run(cmd, *args, **kwargs)
242 242 output = fp.read()
243 243 self.ui.debug(output)
244 244 return output, fp.close()
245 245
246 246 def checkexit(self, status, output=''):
247 247 if status:
248 248 if output:
249 249 self.ui.warn(_('%s error:\n') % self.command)
250 250 self.ui.warn(output)
251 251 msg = util.explain_exit(status)[0]
252 252 raise util.Abort(_('%s %s') % (self.command, msg))
253 253
254 254 def run0(self, cmd, *args, **kwargs):
255 255 output, status = self.run(cmd, *args, **kwargs)
256 256 self.checkexit(status, output)
257 257 return output
258 258
259 259
260 260 class mapfile(dict):
261 261 def __init__(self, ui, path):
262 262 super(mapfile, self).__init__()
263 263 self.ui = ui
264 264 self.path = path
265 265 self.fp = None
266 266 self.order = []
267 267 self._read()
268 268
269 269 def _read(self):
270 270 try:
271 271 fp = open(self.path, 'r')
272 272 except IOError, err:
273 273 if err.errno != errno.ENOENT:
274 274 raise
275 275 return
276 276 for line in fp:
277 277 key, value = line[:-1].split(' ', 1)
278 278 if key not in self:
279 279 self.order.append(key)
280 280 super(mapfile, self).__setitem__(key, value)
281 281 fp.close()
282 282
283 283 def __setitem__(self, key, value):
284 284 if self.fp is None:
285 285 try:
286 286 self.fp = open(self.path, 'a')
287 287 except IOError, err:
288 288 raise util.Abort(_('could not open map file %r: %s') %
289 289 (self.path, err.strerror))
290 290 self.fp.write('%s %s\n' % (key, value))
291 291 self.fp.flush()
292 292 super(mapfile, self).__setitem__(key, value)
293 293
294 294 def close(self):
295 295 if self.fp:
296 296 self.fp.close()
297 297 self.fp = None
@@ -1,352 +1,346 b''
1 1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
2 2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
3 3 #
4 4 # This software may be used and distributed according to the terms of
5 5 # the GNU General Public License, incorporated herein by reference.
6 6
7 7 import shlex
8 8 from mercurial.i18n import _
9 9 from mercurial import util
10 from common import SKIPREV
10 from common import SKIPREV, converter_source
11 11
12 12 def rpairs(name):
13 13 e = len(name)
14 14 while e != -1:
15 15 yield name[:e], name[e+1:]
16 16 e = name.rfind('/', 0, e)
17 17
18 18 class filemapper(object):
19 19 '''Map and filter filenames when importing.
20 20 A name can be mapped to itself, a new name, or None (omit from new
21 21 repository).'''
22 22
23 23 def __init__(self, ui, path=None):
24 24 self.ui = ui
25 25 self.include = {}
26 26 self.exclude = {}
27 27 self.rename = {}
28 28 if path:
29 29 if self.parse(path):
30 30 raise util.Abort(_('errors in filemap'))
31 31
32 32 def parse(self, path):
33 33 errs = 0
34 34 def check(name, mapping, listname):
35 35 if name in mapping:
36 36 self.ui.warn(_('%s:%d: %r already in %s list\n') %
37 37 (lex.infile, lex.lineno, name, listname))
38 38 return 1
39 39 return 0
40 40 lex = shlex.shlex(open(path), path, True)
41 41 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
42 42 cmd = lex.get_token()
43 43 while cmd:
44 44 if cmd == 'include':
45 45 name = lex.get_token()
46 46 errs += check(name, self.exclude, 'exclude')
47 47 self.include[name] = name
48 48 elif cmd == 'exclude':
49 49 name = lex.get_token()
50 50 errs += check(name, self.include, 'include')
51 51 errs += check(name, self.rename, 'rename')
52 52 self.exclude[name] = name
53 53 elif cmd == 'rename':
54 54 src = lex.get_token()
55 55 dest = lex.get_token()
56 56 errs += check(src, self.exclude, 'exclude')
57 57 self.rename[src] = dest
58 58 elif cmd == 'source':
59 59 errs += self.parse(lex.get_token())
60 60 else:
61 61 self.ui.warn(_('%s:%d: unknown directive %r\n') %
62 62 (lex.infile, lex.lineno, cmd))
63 63 errs += 1
64 64 cmd = lex.get_token()
65 65 return errs
66 66
67 67 def lookup(self, name, mapping):
68 68 for pre, suf in rpairs(name):
69 69 try:
70 70 return mapping[pre], pre, suf
71 71 except KeyError, err:
72 72 pass
73 73 return '', name, ''
74 74
75 75 def __call__(self, name):
76 76 if self.include:
77 77 inc = self.lookup(name, self.include)[0]
78 78 else:
79 79 inc = name
80 80 if self.exclude:
81 81 exc = self.lookup(name, self.exclude)[0]
82 82 else:
83 83 exc = ''
84 84 if not inc or exc:
85 85 return None
86 86 newpre, pre, suf = self.lookup(name, self.rename)
87 87 if newpre:
88 88 if newpre == '.':
89 89 return suf
90 90 if suf:
91 91 return newpre + '/' + suf
92 92 return newpre
93 93 return name
94 94
95 95 def active(self):
96 96 return bool(self.include or self.exclude or self.rename)
97 97
98 98 # This class does two additional things compared to a regular source:
99 99 #
100 100 # - Filter and rename files. This is mostly wrapped by the filemapper
101 101 # class above. We hide the original filename in the revision that is
102 102 # returned by getchanges to be able to find things later in getfile
103 103 # and getmode.
104 104 #
105 105 # - Return only revisions that matter for the files we're interested in.
106 106 # This involves rewriting the parents of the original revision to
107 107 # create a graph that is restricted to those revisions.
108 108 #
109 109 # This set of revisions includes not only revisions that directly
110 110 # touch files we're interested in, but also merges that merge two
111 111 # or more interesting revisions.
112 112
113 class filemap_source(object):
113 class filemap_source(converter_source):
114 114 def __init__(self, ui, baseconverter, filemap):
115 self.ui = ui
115 super(filemap_source, self).__init__(ui)
116 116 self.base = baseconverter
117 117 self.filemapper = filemapper(ui, filemap)
118 118 self.commits = {}
119 119 # if a revision rev has parent p in the original revision graph, then
120 120 # rev will have parent self.parentmap[p] in the restricted graph.
121 121 self.parentmap = {}
122 122 # self.wantedancestors[rev] is the set of all ancestors of rev that
123 123 # are in the restricted graph.
124 124 self.wantedancestors = {}
125 125 self.convertedorder = None
126 126 self._rebuilt = False
127 127 self.origparents = {}
128 128 self.children = {}
129 129 self.seenchildren = {}
130 130
131 131 def setrevmap(self, revmap):
132 132 # rebuild our state to make things restartable
133 133 #
134 134 # To avoid calling getcommit for every revision that has already
135 135 # been converted, we rebuild only the parentmap, delaying the
136 136 # rebuild of wantedancestors until we need it (i.e. until a
137 137 # merge).
138 138 #
139 139 # We assume the order argument lists the revisions in
140 140 # topological order, so that we can infer which revisions were
141 141 # wanted by previous runs.
142 142 self._rebuilt = not revmap
143 143 seen = {SKIPREV: SKIPREV}
144 144 dummyset = util.set()
145 145 converted = []
146 146 for rev in revmap.order:
147 147 mapped = revmap[rev]
148 148 wanted = mapped not in seen
149 149 if wanted:
150 150 seen[mapped] = rev
151 151 self.parentmap[rev] = rev
152 152 else:
153 153 self.parentmap[rev] = seen[mapped]
154 154 self.wantedancestors[rev] = dummyset
155 155 arg = seen[mapped]
156 156 if arg == SKIPREV:
157 157 arg = None
158 158 converted.append((rev, wanted, arg))
159 159 self.convertedorder = converted
160 160 return self.base.setrevmap(revmap)
161 161
162 162 def rebuild(self):
163 163 if self._rebuilt:
164 164 return True
165 165 self._rebuilt = True
166 166 self.parentmap.clear()
167 167 self.wantedancestors.clear()
168 168 self.seenchildren.clear()
169 169 for rev, wanted, arg in self.convertedorder:
170 170 if rev not in self.origparents:
171 171 self.origparents[rev] = self.getcommit(rev).parents
172 172 if arg is not None:
173 173 self.children[arg] = self.children.get(arg, 0) + 1
174 174
175 175 for rev, wanted, arg in self.convertedorder:
176 176 parents = self.origparents[rev]
177 177 if wanted:
178 178 self.mark_wanted(rev, parents)
179 179 else:
180 180 self.mark_not_wanted(rev, arg)
181 181 self._discard(arg, *parents)
182 182
183 183 return True
184 184
185 185 def getheads(self):
186 186 return self.base.getheads()
187 187
188 188 def getcommit(self, rev):
189 189 # We want to save a reference to the commit objects to be able
190 190 # to rewrite their parents later on.
191 191 c = self.commits[rev] = self.base.getcommit(rev)
192 192 for p in c.parents:
193 193 self.children[p] = self.children.get(p, 0) + 1
194 194 return c
195 195
196 196 def _discard(self, *revs):
197 197 for r in revs:
198 198 if r is None:
199 199 continue
200 200 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
201 201 if self.seenchildren[r] == self.children[r]:
202 202 del self.wantedancestors[r]
203 203 del self.parentmap[r]
204 204 del self.seenchildren[r]
205 205 if self._rebuilt:
206 206 del self.children[r]
207 207
208 208 def wanted(self, rev, i):
209 209 # Return True if we're directly interested in rev.
210 210 #
211 211 # i is an index selecting one of the parents of rev (if rev
212 212 # has no parents, i is None). getchangedfiles will give us
213 213 # the list of files that are different in rev and in the parent
214 214 # indicated by i. If we're interested in any of these files,
215 215 # we're interested in rev.
216 216 try:
217 217 files = self.base.getchangedfiles(rev, i)
218 218 except NotImplementedError:
219 219 raise util.Abort(_("source repository doesn't support --filemap"))
220 220 for f in files:
221 221 if self.filemapper(f):
222 222 return True
223 223 return False
224 224
225 225 def mark_not_wanted(self, rev, p):
226 226 # Mark rev as not interesting and update data structures.
227 227
228 228 if p is None:
229 229 # A root revision. Use SKIPREV to indicate that it doesn't
230 230 # map to any revision in the restricted graph. Put SKIPREV
231 231 # in the set of wanted ancestors to simplify code elsewhere
232 232 self.parentmap[rev] = SKIPREV
233 233 self.wantedancestors[rev] = util.set((SKIPREV,))
234 234 return
235 235
236 236 # Reuse the data from our parent.
237 237 self.parentmap[rev] = self.parentmap[p]
238 238 self.wantedancestors[rev] = self.wantedancestors[p]
239 239
240 240 def mark_wanted(self, rev, parents):
241 241 # Mark rev ss wanted and update data structures.
242 242
243 243 # rev will be in the restricted graph, so children of rev in
244 244 # the original graph should still have rev as a parent in the
245 245 # restricted graph.
246 246 self.parentmap[rev] = rev
247 247
248 248 # The set of wanted ancestors of rev is the union of the sets
249 249 # of wanted ancestors of its parents. Plus rev itself.
250 250 wrev = util.set()
251 251 for p in parents:
252 252 wrev.update(self.wantedancestors[p])
253 253 wrev.add(rev)
254 254 self.wantedancestors[rev] = wrev
255 255
256 256 def getchanges(self, rev):
257 257 parents = self.commits[rev].parents
258 258 if len(parents) > 1:
259 259 self.rebuild()
260 260
261 261 # To decide whether we're interested in rev we:
262 262 #
263 263 # - calculate what parents rev will have if it turns out we're
264 264 # interested in it. If it's going to have more than 1 parent,
265 265 # we're interested in it.
266 266 #
267 267 # - otherwise, we'll compare it with the single parent we found.
268 268 # If any of the files we're interested in is different in the
269 269 # the two revisions, we're interested in rev.
270 270
271 271 # A parent p is interesting if its mapped version (self.parentmap[p]):
272 272 # - is not SKIPREV
273 273 # - is still not in the list of parents (we don't want duplicates)
274 274 # - is not an ancestor of the mapped versions of the other parents
275 275 mparents = []
276 276 wp = None
277 277 for i, p1 in enumerate(parents):
278 278 mp1 = self.parentmap[p1]
279 279 if mp1 == SKIPREV or mp1 in mparents:
280 280 continue
281 281 for p2 in parents:
282 282 if p1 == p2 or mp1 == self.parentmap[p2]:
283 283 continue
284 284 if mp1 in self.wantedancestors[p2]:
285 285 break
286 286 else:
287 287 mparents.append(mp1)
288 288 wp = i
289 289
290 290 if wp is None and parents:
291 291 wp = 0
292 292
293 293 self.origparents[rev] = parents
294 294
295 295 if len(mparents) < 2 and not self.wanted(rev, wp):
296 296 # We don't want this revision.
297 297 # Update our state and tell the convert process to map this
298 298 # revision to the same revision its parent as mapped to.
299 299 p = None
300 300 if parents:
301 301 p = parents[wp]
302 302 self.mark_not_wanted(rev, p)
303 303 self.convertedorder.append((rev, False, p))
304 304 self._discard(*parents)
305 305 return self.parentmap[rev]
306 306
307 307 # We want this revision.
308 308 # Rewrite the parents of the commit object
309 309 self.commits[rev].parents = mparents
310 310 self.mark_wanted(rev, parents)
311 311 self.convertedorder.append((rev, True, None))
312 312 self._discard(*parents)
313 313
314 314 # Get the real changes and do the filtering/mapping.
315 315 # To be able to get the files later on in getfile and getmode,
316 316 # we hide the original filename in the rev part of the return
317 317 # value.
318 318 changes, copies = self.base.getchanges(rev)
319 319 newnames = {}
320 320 files = []
321 321 for f, r in changes:
322 322 newf = self.filemapper(f)
323 323 if newf:
324 324 files.append((newf, (f, r)))
325 325 newnames[f] = newf
326 326
327 327 ncopies = {}
328 328 for c in copies:
329 329 newc = self.filemapper(c)
330 330 if newc:
331 331 newsource = self.filemapper(copies[c])
332 332 if newsource:
333 333 ncopies[newc] = newsource
334 334
335 335 return files, ncopies
336 336
337 337 def getfile(self, name, rev):
338 338 realname, realrev = rev
339 339 return self.base.getfile(realname, realrev)
340 340
341 341 def getmode(self, name, rev):
342 342 realname, realrev = rev
343 343 return self.base.getmode(realname, realrev)
344 344
345 345 def gettags(self):
346 346 return self.base.gettags()
347
348 def before(self):
349 pass
350
351 def after(self):
352 pass
@@ -1,265 +1,276 b''
1 1 # hg backend for convert extension
2 2
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
5 # do. Changesets created by those older versions, then converted, may
6 # thus have different hashes for changesets that are otherwise
7 # identical.
3 # Notes for hg->hg conversion:
4 #
5 # * Old versions of Mercurial didn't trim the whitespace from the ends
6 # of commit messages, but new versions do. Changesets created by
7 # those older versions, then converted, may thus have different
8 # hashes for changesets that are otherwise identical.
9 #
10 # * By default, the source revision is stored in the converted
11 # revision. This will cause the converted revision to have a
12 # different identity than the source. To avoid this, use the
13 # following option: "--config convert.hg.saverev=false"
8 14
9 15
10 16 import os, time
11 17 from mercurial.i18n import _
12 18 from mercurial.node import *
13 19 from mercurial import hg, lock, revlog, util
14 20
15 21 from common import NoRepo, commit, converter_source, converter_sink
16 22
17 23 class mercurial_sink(converter_sink):
18 24 def __init__(self, ui, path):
19 25 converter_sink.__init__(self, ui, path)
20 26 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
21 27 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
22 28 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
23 29 self.lastbranch = None
24 30 if os.path.isdir(path) and len(os.listdir(path)) > 0:
25 31 try:
26 32 self.repo = hg.repository(self.ui, path)
27 33 except hg.RepoError, err:
28 34 ui.print_exc()
29 35 raise NoRepo(err.args[0])
30 36 else:
31 37 try:
32 38 ui.status(_('initializing destination %s repository\n') % path)
33 39 self.repo = hg.repository(self.ui, path, create=True)
34 40 self.created.append(path)
35 41 except hg.RepoError, err:
36 42 ui.print_exc()
37 43 raise NoRepo("could not create hg repo %s as sink" % path)
38 44 self.lock = None
39 45 self.wlock = None
40 46 self.filemapmode = False
41 47
42 48 def before(self):
43 49 self.wlock = self.repo.wlock()
44 50 self.lock = self.repo.lock()
45 51 self.repo.dirstate.clear()
46 52
47 53 def after(self):
48 54 self.repo.dirstate.invalidate()
49 55 self.lock = None
50 56 self.wlock = None
51 57
52 58 def revmapfile(self):
53 59 return os.path.join(self.path, ".hg", "shamap")
54 60
55 61 def authorfile(self):
56 62 return os.path.join(self.path, ".hg", "authormap")
57 63
58 64 def getheads(self):
59 65 h = self.repo.changelog.heads()
60 66 return [ hex(x) for x in h ]
61 67
62 68 def putfile(self, f, e, data):
63 69 self.repo.wwrite(f, data, e)
64 70 if f not in self.repo.dirstate:
65 71 self.repo.dirstate.normallookup(f)
66 72
67 73 def copyfile(self, source, dest):
68 74 self.repo.copy(source, dest)
69 75
70 76 def delfile(self, f):
71 77 try:
72 78 util.unlink(self.repo.wjoin(f))
73 79 #self.repo.remove([f])
74 80 except OSError:
75 81 pass
76 82
77 83 def setbranch(self, branch, pbranch, parents):
78 84 if (not self.clonebranches) or (branch == self.lastbranch):
79 85 return
80 86
81 87 self.lastbranch = branch
82 88 self.after()
83 89 if not branch:
84 90 branch = 'default'
85 91 if not pbranch:
86 92 pbranch = 'default'
87 93
88 94 branchpath = os.path.join(self.path, branch)
89 95 try:
90 96 self.repo = hg.repository(self.ui, branchpath)
91 97 except:
92 98 if not parents:
93 99 self.repo = hg.repository(self.ui, branchpath, create=True)
94 100 else:
95 101 self.ui.note(_('cloning branch %s to %s\n') % (pbranch, branch))
96 102 hg.clone(self.ui, os.path.join(self.path, pbranch),
97 103 branchpath, rev=parents, update=False,
98 104 stream=True)
99 105 self.repo = hg.repository(self.ui, branchpath)
100 106 self.before()
101 107
102 108 def putcommit(self, files, parents, commit):
103 109 seen = {}
104 110 pl = []
105 111 for p in parents:
106 112 if p not in seen:
107 113 pl.append(p)
108 114 seen[p] = 1
109 115 parents = pl
110 116 nparents = len(parents)
111 117 if self.filemapmode and nparents == 1:
112 118 m1node = self.repo.changelog.read(bin(parents[0]))[0]
113 119 parent = parents[0]
114 120
115 121 if len(parents) < 2: parents.append("0" * 40)
116 122 if len(parents) < 2: parents.append("0" * 40)
117 123 p2 = parents.pop(0)
118 124
119 125 text = commit.desc
120 126 extra = commit.extra.copy()
121 127 if self.branchnames and commit.branch:
122 128 extra['branch'] = commit.branch
123 129 if commit.rev:
124 130 extra['convert_revision'] = commit.rev
125 131
126 132 while parents:
127 133 p1 = p2
128 134 p2 = parents.pop(0)
129 135 a = self.repo.rawcommit(files, text, commit.author, commit.date,
130 136 bin(p1), bin(p2), extra=extra)
131 137 self.repo.dirstate.clear()
132 138 text = "(octopus merge fixup)\n"
133 139 p2 = hg.hex(self.repo.changelog.tip())
134 140
135 141 if self.filemapmode and nparents == 1:
136 142 man = self.repo.manifest
137 143 mnode = self.repo.changelog.read(bin(p2))[0]
138 144 if not man.cmp(m1node, man.revision(mnode)):
139 145 self.repo.rollback()
140 146 self.repo.dirstate.clear()
141 147 return parent
142 148 return p2
143 149
144 150 def puttags(self, tags):
145 151 try:
146 152 old = self.repo.wfile(".hgtags").read()
147 153 oldlines = old.splitlines(1)
148 154 oldlines.sort()
149 155 except:
150 156 oldlines = []
151 157
152 158 k = tags.keys()
153 159 k.sort()
154 160 newlines = []
155 161 for tag in k:
156 162 newlines.append("%s %s\n" % (tags[tag], tag))
157 163
158 164 newlines.sort()
159 165
160 166 if newlines != oldlines:
161 167 self.ui.status("updating tags\n")
162 168 f = self.repo.wfile(".hgtags", "w")
163 169 f.write("".join(newlines))
164 170 f.close()
165 171 if not oldlines: self.repo.add([".hgtags"])
166 172 date = "%s 0" % int(time.mktime(time.gmtime()))
167 173 extra = {}
168 174 if self.tagsbranch != 'default':
169 175 extra['branch'] = self.tagsbranch
170 176 try:
171 177 tagparent = self.repo.changectx(self.tagsbranch).node()
172 178 except hg.RepoError, inst:
173 179 tagparent = nullid
174 180 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
175 181 date, tagparent, nullid)
176 182 return hex(self.repo.changelog.tip())
177 183
178 184 def setfilemapmode(self, active):
179 185 self.filemapmode = active
180 186
181 187 class mercurial_source(converter_source):
182 188 def __init__(self, ui, path, rev=None):
183 189 converter_source.__init__(self, ui, path, rev)
190 self.saverev = ui.configbool('convert', 'hg.saverev', True)
184 191 try:
185 192 self.repo = hg.repository(self.ui, path)
186 193 # try to provoke an exception if this isn't really a hg
187 194 # repo, but some other bogus compatible-looking url
188 195 if not self.repo.local():
189 196 raise hg.RepoError()
190 197 except hg.RepoError:
191 198 ui.print_exc()
192 199 raise NoRepo("%s is not a local Mercurial repo" % path)
193 200 self.lastrev = None
194 201 self.lastctx = None
195 202 self._changescache = None
196 203 self.convertfp = None
197 204
198 205 def changectx(self, rev):
199 206 if self.lastrev != rev:
200 207 self.lastctx = self.repo.changectx(rev)
201 208 self.lastrev = rev
202 209 return self.lastctx
203 210
204 211 def getheads(self):
205 212 if self.rev:
206 213 return [hex(self.repo.changectx(self.rev).node())]
207 214 else:
208 215 return [hex(node) for node in self.repo.heads()]
209 216
210 217 def getfile(self, name, rev):
211 218 try:
212 219 return self.changectx(rev).filectx(name).data()
213 220 except revlog.LookupError, err:
214 221 raise IOError(err)
215 222
216 223 def getmode(self, name, rev):
217 224 m = self.changectx(rev).manifest()
218 225 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
219 226
220 227 def getchanges(self, rev):
221 228 ctx = self.changectx(rev)
222 229 if self._changescache and self._changescache[0] == rev:
223 230 m, a, r = self._changescache[1]
224 231 else:
225 232 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
226 233 changes = [(name, rev) for name in m + a + r]
227 234 changes.sort()
228 235 return (changes, self.getcopies(ctx, m + a))
229 236
230 237 def getcopies(self, ctx, files):
231 238 copies = {}
232 239 for name in files:
233 240 try:
234 241 copies[name] = ctx.filectx(name).renamed()[0]
235 242 except TypeError:
236 243 pass
237 244 return copies
238 245
239 246 def getcommit(self, rev):
240 247 ctx = self.changectx(rev)
241 248 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
249 if self.saverev:
250 crev = rev
251 else:
252 crev = None
242 253 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
243 desc=ctx.description(), rev=rev, parents=parents,
254 desc=ctx.description(), rev=crev, parents=parents,
244 255 branch=ctx.branch(), extra=ctx.extra())
245 256
246 257 def gettags(self):
247 258 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
248 259 return dict([(name, hex(node)) for name, node in tags])
249 260
250 261 def getchangedfiles(self, rev, i):
251 262 ctx = self.changectx(rev)
252 263 i = i or 0
253 264 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
254 265
255 266 if i == 0:
256 267 self._changescache = (rev, changes)
257 268
258 269 return changes[0] + changes[1] + changes[2]
259 270
260 271 def converted(self, rev, destrev):
261 272 if self.convertfp is None:
262 273 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
263 274 'a')
264 275 self.convertfp.write('%s %s\n' % (destrev, rev))
265 276 self.convertfp.flush()
@@ -1,37 +1,41 b''
1 1 #!/bin/sh
2 2
3 echo "[extensions]" >> $HGRCPATH
4 echo "convert=" >> $HGRCPATH
3 cat >> $HGRCPATH <<EOF
4 [extensions]
5 convert=
6 [convert]
7 hg.saverev=False
8 EOF
5 9
6 10 hg help convert
7 11
8 12 hg init a
9 13 cd a
10 14 echo a > a
11 15 hg ci -d'0 0' -Ama
12 16 hg cp a b
13 17 hg ci -d'1 0' -mb
14 18 hg rm a
15 19 hg ci -d'2 0' -mc
16 20 hg mv b a
17 21 hg ci -d'3 0' -md
18 22 echo a >> a
19 23 hg ci -d'4 0' -me
20 24
21 25 cd ..
22 26 hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
23 27 hg --cwd a-hg pull ../a
24 28
25 29 touch bogusfile
26 30 echo % should fail
27 31 hg convert a bogusfile
28 32
29 33 mkdir bogusdir
30 34 chmod 000 bogusdir
31 35
32 36 echo % should fail
33 37 hg convert a bogusdir
34 38
35 39 echo % should succeed
36 40 chmod 700 bogusdir
37 41 hg convert a bogusdir
@@ -1,71 +1,69 b''
1 1 % create cvs repository
2 2 % create source directory
3 3 % import source directory
4 4 N src/a
5 5 N src/b/c
6 6
7 7 No conflicts created by this import
8 8
9 9 % checkout source directory
10 10 U src/a
11 11 U src/b/c
12 12 % commit a new revision changing b/c
13 13 checking in src/b/c,v
14 14 % convert fresh repo
15 15 initializing destination src-hg repository
16 16 connecting to cvsrepo
17 17 scanning source...
18 18 sorting...
19 19 converting...
20 20 2 Initial revision
21 21 1 import
22 22 0 ci0
23 23 updating tags
24 24 a
25 25 c
26 26 c
27 27 % convert fresh repo with --filemap
28 28 initializing destination src-filemap repository
29 29 connecting to cvsrepo
30 30 scanning source...
31 31 sorting...
32 32 converting...
33 33 2 Initial revision
34 34 1 import
35 35 rolling back last transaction
36 36 0 ci0
37 37 updating tags
38 38 c
39 39 c
40 40 2 update tags files: .hgtags
41 41 1 ci0 files: b/c
42 42 0 Initial revision files: b/c
43 43 % commit new file revisions
44 44 checking in src/a,v
45 45 checking in src/b/c,v
46 46 % convert again
47 destination src-hg is a Mercurial repository
48 47 connecting to cvsrepo
49 48 scanning source...
50 49 sorting...
51 50 converting...
52 51 0 ci1
53 52 a
54 53 a
55 54 c
56 55 c
57 56 c
58 57 % convert again with --filemap
59 destination src-filemap is a Mercurial repository
60 58 connecting to cvsrepo
61 59 scanning source...
62 60 sorting...
63 61 converting...
64 62 0 ci1
65 63 c
66 64 c
67 65 c
68 66 3 ci1 files: b/c
69 67 2 update tags files: .hgtags
70 68 1 ci0 files: b/c
71 69 0 Initial revision files: b/c
@@ -1,53 +1,57 b''
1 1 #!/bin/sh
2 2
3 echo "[extensions]" >> $HGRCPATH
4 echo "hgext.convert=" >> $HGRCPATH
3 cat >> $HGRCPATH <<EOF
4 [extensions]
5 convert=
6 [convert]
7 hg.saverev=False
8 EOF
5 9
6 10 hg init orig
7 11 cd orig
8 12 echo foo > foo
9 13 echo bar > bar
10 14 hg ci -qAm 'add foo and bar' -d '0 0'
11 15
12 16 hg rm foo
13 17 hg ci -m 'remove foo' -d '0 0'
14 18
15 19 mkdir foo
16 20 echo file > foo/file
17 21 hg ci -qAm 'add foo/file' -d '0 0'
18 22
19 23 hg tag -d '0 0' some-tag
20 24
21 25 hg log
22 26 cd ..
23 27
24 28 hg convert orig new 2>&1 | grep -v 'subversion python bindings could not be loaded'
25 29 cd new
26 30 hg out ../orig
27 31
28 32 echo '% dirstate should be empty:'
29 33 hg debugstate
30 34 hg parents -q
31 35
32 36 hg up -C
33 37 hg copy bar baz
34 38 echo '% put something in the dirstate:'
35 39 hg debugstate > debugstate
36 40 grep baz debugstate
37 41
38 42 echo '% add a new revision in the original repo'
39 43 cd ../orig
40 44 echo baz > baz
41 45 hg ci -qAm 'add baz'
42 46
43 47 cd ..
44 48 hg convert orig new 2>&1 | grep -v 'subversion python bindings could not be loaded'
45 49 cd new
46 50 hg out ../orig
47 51 echo '% dirstate should be the same (no output below):'
48 52 hg debugstate > new-debugstate
49 53 diff debugstate new-debugstate
50 54
51 55 echo '% no copies'
52 56 hg up -C
53 57 hg debugrename baz
@@ -1,51 +1,50 b''
1 1 changeset: 3:593cbf6fb2b4
2 2 tag: tip
3 3 user: test
4 4 date: Thu Jan 01 00:00:00 1970 +0000
5 5 summary: Added tag some-tag for changeset ad681a868e44
6 6
7 7 changeset: 2:ad681a868e44
8 8 tag: some-tag
9 9 user: test
10 10 date: Thu Jan 01 00:00:00 1970 +0000
11 11 summary: add foo/file
12 12
13 13 changeset: 1:cbba8ecc03b7
14 14 user: test
15 15 date: Thu Jan 01 00:00:00 1970 +0000
16 16 summary: remove foo
17 17
18 18 changeset: 0:327daa9251fa
19 19 user: test
20 20 date: Thu Jan 01 00:00:00 1970 +0000
21 21 summary: add foo and bar
22 22
23 23 initializing destination new repository
24 24 scanning source...
25 25 sorting...
26 26 converting...
27 27 3 add foo and bar
28 28 2 remove foo
29 29 1 add foo/file
30 30 0 Added tag some-tag for changeset ad681a868e44
31 31 comparing with ../orig
32 32 searching for changes
33 33 no changes found
34 34 % dirstate should be empty:
35 35 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 36 % put something in the dirstate:
37 37 a 0 -1 unset baz
38 38 copy: bar -> baz
39 39 % add a new revision in the original repo
40 destination new is a Mercurial repository
41 40 scanning source...
42 41 sorting...
43 42 converting...
44 43 0 add baz
45 44 comparing with ../orig
46 45 searching for changes
47 46 no changes found
48 47 % dirstate should be the same (no output below):
49 48 % no copies
50 49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
51 50 baz not renamed
@@ -1,33 +1,37 b''
1 1 #!/bin/sh
2 2
3 echo "[extensions]" >> $HGRCPATH
4 echo "hgext.convert=" >> $HGRCPATH
3 cat >> $HGRCPATH <<EOF
4 [extensions]
5 convert=
6 [convert]
7 hg.saverev=False
8 EOF
5 9
6 10 hg init orig
7 11 cd orig
8 12
9 13 echo foo > foo
10 14 echo bar > bar
11 15 hg ci -qAm 'add foo bar' -d '0 0'
12 16
13 17 echo >> foo
14 18 hg ci -m 'change foo' -d '1 0'
15 19
16 20 hg up -qC 0
17 21 hg copy --after --force foo bar
18 22 hg copy foo baz
19 23 hg ci -m 'make bar and baz copies of foo' -d '2 0'
20 24
21 25 hg merge
22 26 hg ci -m 'merge local copy' -d '3 0'
23 27
24 28 hg up -C 1
25 29 hg merge 2
26 30 hg ci -m 'merge remote copy' -d '4 0'
27 31
28 32 cd ..
29 33 hg convert --datesort orig new 2>&1 | grep -v 'subversion python bindings could not be loaded'
30 34 cd new
31 35 hg out ../orig
32 36
33 37 true
@@ -1,114 +1,112 b''
1 1 % initial svn import
2 2 Adding t/a
3 3
4 4 Committed revision 1.
5 5 % update svn repository
6 6 A t2/a
7 7 Checked out revision 1.
8 8 A b
9 9 Sending a
10 10 Adding b
11 11 Transmitting file data ..
12 12 Committed revision 2.
13 13 % convert to hg once
14 14 assuming destination trunk-hg
15 15 initializing destination trunk-hg repository
16 16 scanning source...
17 17 sorting...
18 18 converting...
19 19 1 init
20 20 0 changea
21 21 % update svn repository again
22 22 Sending a
23 23 Sending b
24 24 Transmitting file data ..
25 25 Committed revision 3.
26 26 % test incremental conversion
27 27 assuming destination trunk-hg
28 destination trunk-hg is a Mercurial repository
29 28 scanning source...
30 29 sorting...
31 30 converting...
32 31 0 changeb
33 32 % test filemap
34 33 initializing destination fmap repository
35 34 scanning source...
36 35 sorting...
37 36 converting...
38 37 2 init
39 38 1 changea
40 39 0 changeb
41 40 o 1 changeb files: b
42 41 |
43 42 o 0 changea files: b
44 43
45 44 # now tests that it works with trunk/branches/tags layout
46 45
47 46 % initial svn import
48 47 Adding projA/trunk
49 48 Adding projA/branches
50 49 Adding projA/tags
51 50
52 51 Committed revision 4.
53 52 % update svn repository
54 53 Checked out revision 4.
55 54 A letter.txt
56 55 Adding letter.txt
57 56 Transmitting file data .
58 57 Committed revision 5.
59 58 Sending letter.txt
60 59 Transmitting file data .
61 60 Committed revision 6.
62 61
63 62 Committed revision 7.
64 63 Sending letter.txt
65 64 Transmitting file data .
66 65 Committed revision 8.
67 66 % convert to hg once
68 67 initializing destination A-hg repository
69 68 scanning source...
70 69 sorting...
71 70 converting...
72 71 3 init projA
73 72 2 hello
74 73 1 world
75 74 0 nice day
76 75 updating tags
77 76 % update svn repository again
78 77 A letter2.txt
79 78 Sending letter.txt
80 79 Adding letter2.txt
81 80 Transmitting file data ..
82 81 Committed revision 9.
83 82
84 83 Committed revision 10.
85 84 Sending letter2.txt
86 85 Transmitting file data .
87 86 Committed revision 11.
88 87 % test incremental conversion
89 destination A-hg is a Mercurial repository
90 88 scanning source...
91 89 sorting...
92 90 converting...
93 91 1 second letter
94 92 0 work in progress
95 93 updating tags
96 94 o 7 update tags files: .hgtags
97 95 |
98 96 o 6 work in progress files: letter2.txt
99 97 |
100 98 o 5 second letter files: letter.txt letter2.txt
101 99 |
102 100 o 4 update tags files: .hgtags
103 101 |
104 102 o 3 nice day files: letter.txt
105 103 |
106 104 o 2 world files: letter.txt
107 105 |
108 106 o 1 hello files: letter.txt
109 107 |
110 108 o 0 init projA files:
111 109
112 110 tip
113 111 v0.2
114 112 v0.1
@@ -1,96 +1,114 b''
1 1 hg convert [OPTION]... SOURCE [DEST [MAPFILE]]
2 2
3 3 Convert a foreign SCM repository to a Mercurial one.
4 4
5 5 Accepted source formats:
6 6 - Mercurial
7 7 - CVS
8 8 - Darcs
9 9 - git
10 10 - Subversion
11 11
12 12 Accepted destination formats:
13 13 - Mercurial
14 14 - Subversion (history on branches is not preserved)
15 15
16 16 If no revision is given, all revisions will be converted. Otherwise,
17 17 convert will only import up to the named revision (given in a format
18 18 understood by the source).
19 19
20 20 If no destination directory name is specified, it defaults to the
21 21 basename of the source with '-hg' appended. If the destination
22 22 repository doesn't exist, it will be created.
23 23
24 24 If <MAPFILE> isn't given, it will be put in a default location
25 25 (<dest>/.hg/shamap by default). The <MAPFILE> is a simple text
26 26 file that maps each source commit ID to the destination ID for
27 27 that revision, like so:
28 28 <source ID> <destination ID>
29 29
30 30 If the file doesn't exist, it's automatically created. It's updated
31 31 on each commit copied, so convert-repo can be interrupted and can
32 32 be run repeatedly to copy new commits.
33 33
34 34 The [username mapping] file is a simple text file that maps each source
35 35 commit author to a destination commit author. It is handy for source SCMs
36 36 that use unix logins to identify authors (eg: CVS). One line per author
37 37 mapping and the line format is:
38 38 srcauthor=whatever string you want
39 39
40 40 The filemap is a file that allows filtering and remapping of files
41 41 and directories. Comment lines start with '#'. Each line can
42 42 contain one of the following directives:
43 43
44 44 include path/to/file
45 45
46 46 exclude path/to/file
47 47
48 48 rename from/file to/file
49 49
50 50 The 'include' directive causes a file, or all files under a
51 51 directory, to be included in the destination repository, and the
52 52 exclusion of all other files and dirs not explicitely included.
53 53 The 'exclude' directive causes files or directories to be omitted.
54 54 The 'rename' directive renames a file or directory. To rename from a
55 55 subdirectory into the root of the repository, use '.' as the path to
56 56 rename to.
57 57
58 Back end options:
59
60 --config convert.hg.clonebranches=False (boolean)
61 hg target: XXX not documented
62 --config convert.hg.saverev=True (boolean)
63 hg source: allow target to preserve source revision ID
64 --config convert.hg.tagsbranch=default (branch name)
65 hg target: XXX not documented
66 --config convert.hg.usebranchnames=True (boolean)
67 hg target: preserve branch names
68
69 --config convert.svn.branches=branches (directory name)
70 svn source: specify the directory containing branches
71 --config convert.svn.tags=tags (directory name)
72 svn source: specify the directory containing tags
73 --config convert.svn.trunk=trunk (directory name)
74 svn source: specify the name of the trunk branch
75
58 76 options:
59 77
60 78 -A --authors username mapping filename
61 79 -d --dest-type destination repository type
62 80 --filemap remap file names using contents of file
63 81 -r --rev import up to target revision REV
64 82 -s --source-type source repository type
65 83 --datesort try to sort changesets by date
66 84
67 85 use "hg -v help convert" to show global options
68 86 adding a
69 87 assuming destination a-hg
70 88 initializing destination a-hg repository
71 89 scanning source...
72 90 sorting...
73 91 converting...
74 92 4 a
75 93 3 b
76 94 2 c
77 95 1 d
78 96 0 e
79 97 pulling from ../a
80 98 searching for changes
81 99 no changes found
82 100 % should fail
83 101 initializing destination bogusfile repository
84 102 abort: cannot create new bundle repository
85 103 % should fail
86 104 abort: Permission denied: bogusdir
87 105 % should succeed
88 106 initializing destination bogusdir repository
89 107 scanning source...
90 108 sorting...
91 109 converting...
92 110 4 a
93 111 3 b
94 112 2 c
95 113 1 d
96 114 0 e
General Comments 0
You need to be logged in to leave comments. Login now