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