##// END OF EJS Templates
convert: add config option for disabling ancestor parent checks...
Durham Goode -
r25742:d859123e default
parent child Browse files
Show More
@@ -1,425 +1,430
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 the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 import posixpath
8 8 import shlex
9 9 from mercurial.i18n import _
10 10 from mercurial import util, error
11 11 from common import SKIPREV, converter_source
12 12
13 13 def rpairs(path):
14 14 '''Yield tuples with path split at '/', starting with the full path.
15 15 No leading, trailing or double '/', please.
16 16 >>> for x in rpairs('foo/bar/baz'): print x
17 17 ('foo/bar/baz', '')
18 18 ('foo/bar', 'baz')
19 19 ('foo', 'bar/baz')
20 20 ('.', 'foo/bar/baz')
21 21 '''
22 22 i = len(path)
23 23 while i != -1:
24 24 yield path[:i], path[i + 1:]
25 25 i = path.rfind('/', 0, i)
26 26 yield '.', path
27 27
28 28 def normalize(path):
29 29 ''' We use posixpath.normpath to support cross-platform path format.
30 30 However, it doesn't handle None input. So we wrap it up. '''
31 31 if path is None:
32 32 return None
33 33 return posixpath.normpath(path)
34 34
35 35 class filemapper(object):
36 36 '''Map and filter filenames when importing.
37 37 A name can be mapped to itself, a new name, or None (omit from new
38 38 repository).'''
39 39
40 40 def __init__(self, ui, path=None):
41 41 self.ui = ui
42 42 self.include = {}
43 43 self.exclude = {}
44 44 self.rename = {}
45 45 if path:
46 46 if self.parse(path):
47 47 raise util.Abort(_('errors in filemap'))
48 48
49 49 def parse(self, path):
50 50 errs = 0
51 51 def check(name, mapping, listname):
52 52 if not name:
53 53 self.ui.warn(_('%s:%d: path to %s is missing\n') %
54 54 (lex.infile, lex.lineno, listname))
55 55 return 1
56 56 if name in mapping:
57 57 self.ui.warn(_('%s:%d: %r already in %s list\n') %
58 58 (lex.infile, lex.lineno, name, listname))
59 59 return 1
60 60 if (name.startswith('/') or
61 61 name.endswith('/') or
62 62 '//' in name):
63 63 self.ui.warn(_('%s:%d: superfluous / in %s %r\n') %
64 64 (lex.infile, lex.lineno, listname, name))
65 65 return 1
66 66 return 0
67 67 lex = shlex.shlex(open(path), path, True)
68 68 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
69 69 cmd = lex.get_token()
70 70 while cmd:
71 71 if cmd == 'include':
72 72 name = normalize(lex.get_token())
73 73 errs += check(name, self.exclude, 'exclude')
74 74 self.include[name] = name
75 75 elif cmd == 'exclude':
76 76 name = normalize(lex.get_token())
77 77 errs += check(name, self.include, 'include')
78 78 errs += check(name, self.rename, 'rename')
79 79 self.exclude[name] = name
80 80 elif cmd == 'rename':
81 81 src = normalize(lex.get_token())
82 82 dest = normalize(lex.get_token())
83 83 errs += check(src, self.exclude, 'exclude')
84 84 self.rename[src] = dest
85 85 elif cmd == 'source':
86 86 errs += self.parse(normalize(lex.get_token()))
87 87 else:
88 88 self.ui.warn(_('%s:%d: unknown directive %r\n') %
89 89 (lex.infile, lex.lineno, cmd))
90 90 errs += 1
91 91 cmd = lex.get_token()
92 92 return errs
93 93
94 94 def lookup(self, name, mapping):
95 95 name = normalize(name)
96 96 for pre, suf in rpairs(name):
97 97 try:
98 98 return mapping[pre], pre, suf
99 99 except KeyError:
100 100 pass
101 101 return '', name, ''
102 102
103 103 def __call__(self, name):
104 104 if self.include:
105 105 inc = self.lookup(name, self.include)[0]
106 106 else:
107 107 inc = name
108 108 if self.exclude:
109 109 exc = self.lookup(name, self.exclude)[0]
110 110 else:
111 111 exc = ''
112 112 if (not self.include and exc) or (len(inc) <= len(exc)):
113 113 return None
114 114 newpre, pre, suf = self.lookup(name, self.rename)
115 115 if newpre:
116 116 if newpre == '.':
117 117 return suf
118 118 if suf:
119 119 if newpre.endswith('/'):
120 120 return newpre + suf
121 121 return newpre + '/' + suf
122 122 return newpre
123 123 return name
124 124
125 125 def active(self):
126 126 return bool(self.include or self.exclude or self.rename)
127 127
128 128 # This class does two additional things compared to a regular source:
129 129 #
130 130 # - Filter and rename files. This is mostly wrapped by the filemapper
131 131 # class above. We hide the original filename in the revision that is
132 132 # returned by getchanges to be able to find things later in getfile.
133 133 #
134 134 # - Return only revisions that matter for the files we're interested in.
135 135 # This involves rewriting the parents of the original revision to
136 136 # create a graph that is restricted to those revisions.
137 137 #
138 138 # This set of revisions includes not only revisions that directly
139 139 # touch files we're interested in, but also merges that merge two
140 140 # or more interesting revisions.
141 141
142 142 class filemap_source(converter_source):
143 143 def __init__(self, ui, baseconverter, filemap):
144 144 super(filemap_source, self).__init__(ui)
145 145 self.base = baseconverter
146 146 self.filemapper = filemapper(ui, filemap)
147 147 self.commits = {}
148 148 # if a revision rev has parent p in the original revision graph, then
149 149 # rev will have parent self.parentmap[p] in the restricted graph.
150 150 self.parentmap = {}
151 151 # self.wantedancestors[rev] is the set of all ancestors of rev that
152 152 # are in the restricted graph.
153 153 self.wantedancestors = {}
154 154 self.convertedorder = None
155 155 self._rebuilt = False
156 156 self.origparents = {}
157 157 self.children = {}
158 158 self.seenchildren = {}
159 # experimental config: convert.ignoreancestorcheck
160 self.ignoreancestorcheck = self.ui.configbool('convert',
161 'ignoreancestorcheck')
159 162
160 163 def before(self):
161 164 self.base.before()
162 165
163 166 def after(self):
164 167 self.base.after()
165 168
166 169 def setrevmap(self, revmap):
167 170 # rebuild our state to make things restartable
168 171 #
169 172 # To avoid calling getcommit for every revision that has already
170 173 # been converted, we rebuild only the parentmap, delaying the
171 174 # rebuild of wantedancestors until we need it (i.e. until a
172 175 # merge).
173 176 #
174 177 # We assume the order argument lists the revisions in
175 178 # topological order, so that we can infer which revisions were
176 179 # wanted by previous runs.
177 180 self._rebuilt = not revmap
178 181 seen = {SKIPREV: SKIPREV}
179 182 dummyset = set()
180 183 converted = []
181 184 for rev in revmap.order:
182 185 mapped = revmap[rev]
183 186 wanted = mapped not in seen
184 187 if wanted:
185 188 seen[mapped] = rev
186 189 self.parentmap[rev] = rev
187 190 else:
188 191 self.parentmap[rev] = seen[mapped]
189 192 self.wantedancestors[rev] = dummyset
190 193 arg = seen[mapped]
191 194 if arg == SKIPREV:
192 195 arg = None
193 196 converted.append((rev, wanted, arg))
194 197 self.convertedorder = converted
195 198 return self.base.setrevmap(revmap)
196 199
197 200 def rebuild(self):
198 201 if self._rebuilt:
199 202 return True
200 203 self._rebuilt = True
201 204 self.parentmap.clear()
202 205 self.wantedancestors.clear()
203 206 self.seenchildren.clear()
204 207 for rev, wanted, arg in self.convertedorder:
205 208 if rev not in self.origparents:
206 209 try:
207 210 self.origparents[rev] = self.getcommit(rev).parents
208 211 except error.RepoLookupError:
209 212 self.ui.debug("unknown revmap source: %s\n" % rev)
210 213 continue
211 214 if arg is not None:
212 215 self.children[arg] = self.children.get(arg, 0) + 1
213 216
214 217 for rev, wanted, arg in self.convertedorder:
215 218 try:
216 219 parents = self.origparents[rev]
217 220 except KeyError:
218 221 continue # unknown revmap source
219 222 if wanted:
220 223 self.mark_wanted(rev, parents)
221 224 else:
222 225 self.mark_not_wanted(rev, arg)
223 226 self._discard(arg, *parents)
224 227
225 228 return True
226 229
227 230 def getheads(self):
228 231 return self.base.getheads()
229 232
230 233 def getcommit(self, rev):
231 234 # We want to save a reference to the commit objects to be able
232 235 # to rewrite their parents later on.
233 236 c = self.commits[rev] = self.base.getcommit(rev)
234 237 for p in c.parents:
235 238 self.children[p] = self.children.get(p, 0) + 1
236 239 return c
237 240
238 241 def _cachedcommit(self, rev):
239 242 if rev in self.commits:
240 243 return self.commits[rev]
241 244 return self.base.getcommit(rev)
242 245
243 246 def _discard(self, *revs):
244 247 for r in revs:
245 248 if r is None:
246 249 continue
247 250 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
248 251 if self.seenchildren[r] == self.children[r]:
249 252 self.wantedancestors.pop(r, None)
250 253 self.parentmap.pop(r, None)
251 254 del self.seenchildren[r]
252 255 if self._rebuilt:
253 256 del self.children[r]
254 257
255 258 def wanted(self, rev, i):
256 259 # Return True if we're directly interested in rev.
257 260 #
258 261 # i is an index selecting one of the parents of rev (if rev
259 262 # has no parents, i is None). getchangedfiles will give us
260 263 # the list of files that are different in rev and in the parent
261 264 # indicated by i. If we're interested in any of these files,
262 265 # we're interested in rev.
263 266 try:
264 267 files = self.base.getchangedfiles(rev, i)
265 268 except NotImplementedError:
266 269 raise util.Abort(_("source repository doesn't support --filemap"))
267 270 for f in files:
268 271 if self.filemapper(f):
269 272 return True
270 273 return False
271 274
272 275 def mark_not_wanted(self, rev, p):
273 276 # Mark rev as not interesting and update data structures.
274 277
275 278 if p is None:
276 279 # A root revision. Use SKIPREV to indicate that it doesn't
277 280 # map to any revision in the restricted graph. Put SKIPREV
278 281 # in the set of wanted ancestors to simplify code elsewhere
279 282 self.parentmap[rev] = SKIPREV
280 283 self.wantedancestors[rev] = set((SKIPREV,))
281 284 return
282 285
283 286 # Reuse the data from our parent.
284 287 self.parentmap[rev] = self.parentmap[p]
285 288 self.wantedancestors[rev] = self.wantedancestors[p]
286 289
287 290 def mark_wanted(self, rev, parents):
288 291 # Mark rev ss wanted and update data structures.
289 292
290 293 # rev will be in the restricted graph, so children of rev in
291 294 # the original graph should still have rev as a parent in the
292 295 # restricted graph.
293 296 self.parentmap[rev] = rev
294 297
295 298 # The set of wanted ancestors of rev is the union of the sets
296 299 # of wanted ancestors of its parents. Plus rev itself.
297 300 wrev = set()
298 301 for p in parents:
299 302 if p in self.wantedancestors:
300 303 wrev.update(self.wantedancestors[p])
301 304 else:
302 305 self.ui.warn(_('warning: %s parent %s is missing\n') %
303 306 (rev, p))
304 307 wrev.add(rev)
305 308 self.wantedancestors[rev] = wrev
306 309
307 310 def getchanges(self, rev, full):
308 311 parents = self.commits[rev].parents
309 if len(parents) > 1:
312 if len(parents) > 1 and not self.ignoreancestorcheck:
310 313 self.rebuild()
311 314
312 315 # To decide whether we're interested in rev we:
313 316 #
314 317 # - calculate what parents rev will have if it turns out we're
315 318 # interested in it. If it's going to have more than 1 parent,
316 319 # we're interested in it.
317 320 #
318 321 # - otherwise, we'll compare it with the single parent we found.
319 322 # If any of the files we're interested in is different in the
320 323 # the two revisions, we're interested in rev.
321 324
322 325 # A parent p is interesting if its mapped version (self.parentmap[p]):
323 326 # - is not SKIPREV
324 327 # - is still not in the list of parents (we don't want duplicates)
325 328 # - is not an ancestor of the mapped versions of the other parents or
326 329 # there is no parent in the same branch than the current revision.
327 330 mparents = []
328 331 knownparents = set()
329 332 branch = self.commits[rev].branch
330 333 hasbranchparent = False
331 334 for i, p1 in enumerate(parents):
332 335 mp1 = self.parentmap[p1]
333 336 if mp1 == SKIPREV or mp1 in knownparents:
334 337 continue
335 isancestor = any(p2 for p2 in parents
338
339 isancestor = (not self.ignoreancestorcheck and
340 any(p2 for p2 in parents
336 341 if p1 != p2 and mp1 != self.parentmap[p2]
337 and mp1 in self.wantedancestors[p2])
342 and mp1 in self.wantedancestors[p2]))
338 343 if not isancestor and not hasbranchparent and len(parents) > 1:
339 344 # This could be expensive, avoid unnecessary calls.
340 345 if self._cachedcommit(p1).branch == branch:
341 346 hasbranchparent = True
342 347 mparents.append((p1, mp1, i, isancestor))
343 348 knownparents.add(mp1)
344 349 # Discard parents ancestors of other parents if there is a
345 350 # non-ancestor one on the same branch than current revision.
346 351 if hasbranchparent:
347 352 mparents = [p for p in mparents if not p[3]]
348 353 wp = None
349 354 if mparents:
350 355 wp = max(p[2] for p in mparents)
351 356 mparents = [p[1] for p in mparents]
352 357 elif parents:
353 358 wp = 0
354 359
355 360 self.origparents[rev] = parents
356 361
357 362 closed = False
358 363 if 'close' in self.commits[rev].extra:
359 364 # A branch closing revision is only useful if one of its
360 365 # parents belong to the branch being closed
361 366 pbranches = [self._cachedcommit(p).branch for p in mparents]
362 367 if branch in pbranches:
363 368 closed = True
364 369
365 370 if len(mparents) < 2 and not closed and not self.wanted(rev, wp):
366 371 # We don't want this revision.
367 372 # Update our state and tell the convert process to map this
368 373 # revision to the same revision its parent as mapped to.
369 374 p = None
370 375 if parents:
371 376 p = parents[wp]
372 377 self.mark_not_wanted(rev, p)
373 378 self.convertedorder.append((rev, False, p))
374 379 self._discard(*parents)
375 380 return self.parentmap[rev]
376 381
377 382 # We want this revision.
378 383 # Rewrite the parents of the commit object
379 384 self.commits[rev].parents = mparents
380 385 self.mark_wanted(rev, parents)
381 386 self.convertedorder.append((rev, True, None))
382 387 self._discard(*parents)
383 388
384 389 # Get the real changes and do the filtering/mapping. To be
385 390 # able to get the files later on in getfile, we hide the
386 391 # original filename in the rev part of the return value.
387 392 changes, copies, cleanp2 = self.base.getchanges(rev, full)
388 393 files = {}
389 394 ncleanp2 = set(cleanp2)
390 395 for f, r in changes:
391 396 newf = self.filemapper(f)
392 397 if newf and (newf != f or newf not in files):
393 398 files[newf] = (f, r)
394 399 if newf != f:
395 400 ncleanp2.discard(f)
396 401 files = sorted(files.items())
397 402
398 403 ncopies = {}
399 404 for c in copies:
400 405 newc = self.filemapper(c)
401 406 if newc:
402 407 newsource = self.filemapper(copies[c])
403 408 if newsource:
404 409 ncopies[newc] = newsource
405 410
406 411 return files, ncopies, ncleanp2
407 412
408 413 def getfile(self, name, rev):
409 414 realname, realrev = rev
410 415 return self.base.getfile(realname, realrev)
411 416
412 417 def gettags(self):
413 418 return self.base.gettags()
414 419
415 420 def hasnativeorder(self):
416 421 return self.base.hasnativeorder()
417 422
418 423 def lookuprev(self, rev):
419 424 return self.base.lookuprev(rev)
420 425
421 426 def getbookmarks(self):
422 427 return self.base.getbookmarks()
423 428
424 429 def converted(self, rev, sinkrev):
425 430 self.base.converted(rev, sinkrev)
General Comments 0
You need to be logged in to leave comments. Login now