##// END OF EJS Templates
copies-rust: parse the changed-file sidedata directly in rust...
marmoute -
r46674:e0313b0a default
parent child Browse files
Show More
@@ -1,1153 +1,1178 b''
1 # copies.py - copy detection for Mercurial
1 # copies.py - copy detection for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import os
11 import os
12
12
13 from .i18n import _
13 from .i18n import _
14
14
15
15
16 from . import (
16 from . import (
17 match as matchmod,
17 match as matchmod,
18 node,
18 node,
19 pathutil,
19 pathutil,
20 policy,
20 policy,
21 pycompat,
21 pycompat,
22 util,
22 util,
23 )
23 )
24
24
25
25
26 from .utils import stringutil
26 from .utils import stringutil
27
27
28 from .revlogutils import flagutil
28 from .revlogutils import (
29 flagutil,
30 sidedata as sidedatamod,
31 )
29
32
30 rustmod = policy.importrust("copy_tracing")
33 rustmod = policy.importrust("copy_tracing")
31
34
32
35
33 def _filter(src, dst, t):
36 def _filter(src, dst, t):
34 """filters out invalid copies after chaining"""
37 """filters out invalid copies after chaining"""
35
38
36 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
39 # When _chain()'ing copies in 'a' (from 'src' via some other commit 'mid')
37 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
40 # with copies in 'b' (from 'mid' to 'dst'), we can get the different cases
38 # in the following table (not including trivial cases). For example, case 2
41 # in the following table (not including trivial cases). For example, case 2
39 # is where a file existed in 'src' and remained under that name in 'mid' and
42 # is where a file existed in 'src' and remained under that name in 'mid' and
40 # then was renamed between 'mid' and 'dst'.
43 # then was renamed between 'mid' and 'dst'.
41 #
44 #
42 # case src mid dst result
45 # case src mid dst result
43 # 1 x y - -
46 # 1 x y - -
44 # 2 x y y x->y
47 # 2 x y y x->y
45 # 3 x y x -
48 # 3 x y x -
46 # 4 x y z x->z
49 # 4 x y z x->z
47 # 5 - x y -
50 # 5 - x y -
48 # 6 x x y x->y
51 # 6 x x y x->y
49 #
52 #
50 # _chain() takes care of chaining the copies in 'a' and 'b', but it
53 # _chain() takes care of chaining the copies in 'a' and 'b', but it
51 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
54 # cannot tell the difference between cases 1 and 2, between 3 and 4, or
52 # between 5 and 6, so it includes all cases in its result.
55 # between 5 and 6, so it includes all cases in its result.
53 # Cases 1, 3, and 5 are then removed by _filter().
56 # Cases 1, 3, and 5 are then removed by _filter().
54
57
55 for k, v in list(t.items()):
58 for k, v in list(t.items()):
56 # remove copies from files that didn't exist
59 # remove copies from files that didn't exist
57 if v not in src:
60 if v not in src:
58 del t[k]
61 del t[k]
59 # remove criss-crossed copies
62 # remove criss-crossed copies
60 elif k in src and v in dst:
63 elif k in src and v in dst:
61 del t[k]
64 del t[k]
62 # remove copies to files that were then removed
65 # remove copies to files that were then removed
63 elif k not in dst:
66 elif k not in dst:
64 del t[k]
67 del t[k]
65
68
66
69
67 def _chain(prefix, suffix):
70 def _chain(prefix, suffix):
68 """chain two sets of copies 'prefix' and 'suffix'"""
71 """chain two sets of copies 'prefix' and 'suffix'"""
69 result = prefix.copy()
72 result = prefix.copy()
70 for key, value in pycompat.iteritems(suffix):
73 for key, value in pycompat.iteritems(suffix):
71 result[key] = prefix.get(value, value)
74 result[key] = prefix.get(value, value)
72 return result
75 return result
73
76
74
77
75 def _tracefile(fctx, am, basemf):
78 def _tracefile(fctx, am, basemf):
76 """return file context that is the ancestor of fctx present in ancestor
79 """return file context that is the ancestor of fctx present in ancestor
77 manifest am
80 manifest am
78
81
79 Note: we used to try and stop after a given limit, however checking if that
82 Note: we used to try and stop after a given limit, however checking if that
80 limit is reached turned out to be very expensive. we are better off
83 limit is reached turned out to be very expensive. we are better off
81 disabling that feature."""
84 disabling that feature."""
82
85
83 for f in fctx.ancestors():
86 for f in fctx.ancestors():
84 path = f.path()
87 path = f.path()
85 if am.get(path, None) == f.filenode():
88 if am.get(path, None) == f.filenode():
86 return path
89 return path
87 if basemf and basemf.get(path, None) == f.filenode():
90 if basemf and basemf.get(path, None) == f.filenode():
88 return path
91 return path
89
92
90
93
91 def _dirstatecopies(repo, match=None):
94 def _dirstatecopies(repo, match=None):
92 ds = repo.dirstate
95 ds = repo.dirstate
93 c = ds.copies().copy()
96 c = ds.copies().copy()
94 for k in list(c):
97 for k in list(c):
95 if ds[k] not in b'anm' or (match and not match(k)):
98 if ds[k] not in b'anm' or (match and not match(k)):
96 del c[k]
99 del c[k]
97 return c
100 return c
98
101
99
102
100 def _computeforwardmissing(a, b, match=None):
103 def _computeforwardmissing(a, b, match=None):
101 """Computes which files are in b but not a.
104 """Computes which files are in b but not a.
102 This is its own function so extensions can easily wrap this call to see what
105 This is its own function so extensions can easily wrap this call to see what
103 files _forwardcopies is about to process.
106 files _forwardcopies is about to process.
104 """
107 """
105 ma = a.manifest()
108 ma = a.manifest()
106 mb = b.manifest()
109 mb = b.manifest()
107 return mb.filesnotin(ma, match=match)
110 return mb.filesnotin(ma, match=match)
108
111
109
112
110 def usechangesetcentricalgo(repo):
113 def usechangesetcentricalgo(repo):
111 """Checks if we should use changeset-centric copy algorithms"""
114 """Checks if we should use changeset-centric copy algorithms"""
112 if repo.filecopiesmode == b'changeset-sidedata':
115 if repo.filecopiesmode == b'changeset-sidedata':
113 return True
116 return True
114 readfrom = repo.ui.config(b'experimental', b'copies.read-from')
117 readfrom = repo.ui.config(b'experimental', b'copies.read-from')
115 changesetsource = (b'changeset-only', b'compatibility')
118 changesetsource = (b'changeset-only', b'compatibility')
116 return readfrom in changesetsource
119 return readfrom in changesetsource
117
120
118
121
119 def _committedforwardcopies(a, b, base, match):
122 def _committedforwardcopies(a, b, base, match):
120 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
123 """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
121 # files might have to be traced back to the fctx parent of the last
124 # files might have to be traced back to the fctx parent of the last
122 # one-side-only changeset, but not further back than that
125 # one-side-only changeset, but not further back than that
123 repo = a._repo
126 repo = a._repo
124
127
125 if usechangesetcentricalgo(repo):
128 if usechangesetcentricalgo(repo):
126 return _changesetforwardcopies(a, b, match)
129 return _changesetforwardcopies(a, b, match)
127
130
128 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
131 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
129 dbg = repo.ui.debug
132 dbg = repo.ui.debug
130 if debug:
133 if debug:
131 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b))
134 dbg(b'debug.copies: looking into rename from %s to %s\n' % (a, b))
132 am = a.manifest()
135 am = a.manifest()
133 basemf = None if base is None else base.manifest()
136 basemf = None if base is None else base.manifest()
134
137
135 # find where new files came from
138 # find where new files came from
136 # we currently don't try to find where old files went, too expensive
139 # we currently don't try to find where old files went, too expensive
137 # this means we can miss a case like 'hg rm b; hg cp a b'
140 # this means we can miss a case like 'hg rm b; hg cp a b'
138 cm = {}
141 cm = {}
139
142
140 # Computing the forward missing is quite expensive on large manifests, since
143 # Computing the forward missing is quite expensive on large manifests, since
141 # it compares the entire manifests. We can optimize it in the common use
144 # it compares the entire manifests. We can optimize it in the common use
142 # case of computing what copies are in a commit versus its parent (like
145 # case of computing what copies are in a commit versus its parent (like
143 # during a rebase or histedit). Note, we exclude merge commits from this
146 # during a rebase or histedit). Note, we exclude merge commits from this
144 # optimization, since the ctx.files() for a merge commit is not correct for
147 # optimization, since the ctx.files() for a merge commit is not correct for
145 # this comparison.
148 # this comparison.
146 forwardmissingmatch = match
149 forwardmissingmatch = match
147 if b.p1() == a and b.p2().node() == node.nullid:
150 if b.p1() == a and b.p2().node() == node.nullid:
148 filesmatcher = matchmod.exact(b.files())
151 filesmatcher = matchmod.exact(b.files())
149 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
152 forwardmissingmatch = matchmod.intersectmatchers(match, filesmatcher)
150 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
153 missing = _computeforwardmissing(a, b, match=forwardmissingmatch)
151
154
152 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
155 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
153
156
154 if debug:
157 if debug:
155 dbg(b'debug.copies: missing files to search: %d\n' % len(missing))
158 dbg(b'debug.copies: missing files to search: %d\n' % len(missing))
156
159
157 for f in sorted(missing):
160 for f in sorted(missing):
158 if debug:
161 if debug:
159 dbg(b'debug.copies: tracing file: %s\n' % f)
162 dbg(b'debug.copies: tracing file: %s\n' % f)
160 fctx = b[f]
163 fctx = b[f]
161 fctx._ancestrycontext = ancestrycontext
164 fctx._ancestrycontext = ancestrycontext
162
165
163 if debug:
166 if debug:
164 start = util.timer()
167 start = util.timer()
165 opath = _tracefile(fctx, am, basemf)
168 opath = _tracefile(fctx, am, basemf)
166 if opath:
169 if opath:
167 if debug:
170 if debug:
168 dbg(b'debug.copies: rename of: %s\n' % opath)
171 dbg(b'debug.copies: rename of: %s\n' % opath)
169 cm[f] = opath
172 cm[f] = opath
170 if debug:
173 if debug:
171 dbg(
174 dbg(
172 b'debug.copies: time: %f seconds\n'
175 b'debug.copies: time: %f seconds\n'
173 % (util.timer() - start)
176 % (util.timer() - start)
174 )
177 )
175 return cm
178 return cm
176
179
177
180
178 def _revinfo_getter(repo):
181 def _revinfo_getter(repo, match):
179 """returns a function that returns the following data given a <rev>"
182 """returns a function that returns the following data given a <rev>"
180
183
181 * p1: revision number of first parent
184 * p1: revision number of first parent
182 * p2: revision number of first parent
185 * p2: revision number of first parent
183 * changes: a ChangingFiles object
186 * changes: a ChangingFiles object
184 """
187 """
185 cl = repo.changelog
188 cl = repo.changelog
186 parents = cl.parentrevs
189 parents = cl.parentrevs
187 flags = cl.flags
190 flags = cl.flags
188
191
189 HASCOPIESINFO = flagutil.REVIDX_HASCOPIESINFO
192 HASCOPIESINFO = flagutil.REVIDX_HASCOPIESINFO
190
193
191 changelogrevision = cl.changelogrevision
194 changelogrevision = cl.changelogrevision
192
195
193 # A small cache to avoid doing the work twice for merges
196 # A small cache to avoid doing the work twice for merges
194 #
197 #
195 # In the vast majority of cases, if we ask information for a revision
198 # In the vast majority of cases, if we ask information for a revision
196 # about 1 parent, we'll later ask it for the other. So it make sense to
199 # about 1 parent, we'll later ask it for the other. So it make sense to
197 # keep the information around when reaching the first parent of a merge
200 # keep the information around when reaching the first parent of a merge
198 # and dropping it after it was provided for the second parents.
201 # and dropping it after it was provided for the second parents.
199 #
202 #
200 # It exists cases were only one parent of the merge will be walked. It
203 # It exists cases were only one parent of the merge will be walked. It
201 # happens when the "destination" the copy tracing is descendant from a
204 # happens when the "destination" the copy tracing is descendant from a
202 # new root, not common with the "source". In that case, we will only walk
205 # new root, not common with the "source". In that case, we will only walk
203 # through merge parents that are descendant of changesets common
206 # through merge parents that are descendant of changesets common
204 # between "source" and "destination".
207 # between "source" and "destination".
205 #
208 #
206 # With the current case implementation if such changesets have a copy
209 # With the current case implementation if such changesets have a copy
207 # information, we'll keep them in memory until the end of
210 # information, we'll keep them in memory until the end of
208 # _changesetforwardcopies. We don't expect the case to be frequent
211 # _changesetforwardcopies. We don't expect the case to be frequent
209 # enough to matters.
212 # enough to matters.
210 #
213 #
211 # In addition, it would be possible to reach pathological case, were
214 # In addition, it would be possible to reach pathological case, were
212 # many first parent are met before any second parent is reached. In
215 # many first parent are met before any second parent is reached. In
213 # that case the cache could grow. If this even become an issue one can
216 # that case the cache could grow. If this even become an issue one can
214 # safely introduce a maximum cache size. This would trade extra CPU/IO
217 # safely introduce a maximum cache size. This would trade extra CPU/IO
215 # time to save memory.
218 # time to save memory.
216 merge_caches = {}
219 merge_caches = {}
217
220
221 alwaysmatch = match.always()
222
223 if rustmod is not None and alwaysmatch:
224
225 def revinfo(rev):
226 p1, p2 = parents(rev)
227 value = None
228 e = merge_caches.pop(rev, None)
229 if e is not None:
230 return e
231 if flags(rev) & HASCOPIESINFO:
232 raw = changelogrevision(rev)._sidedata.get(sidedatamod.SD_FILES)
233 else:
234 raw = None
235 value = (p1, p2, raw)
236 if p1 != node.nullrev and p2 != node.nullrev:
237 # XXX some case we over cache, IGNORE
238 merge_caches[rev] = value
239 return value
240
241 else:
242
218 def revinfo(rev):
243 def revinfo(rev):
219 p1, p2 = parents(rev)
244 p1, p2 = parents(rev)
220 value = None
245 value = None
221 e = merge_caches.pop(rev, None)
246 e = merge_caches.pop(rev, None)
222 if e is not None:
247 if e is not None:
223 return e
248 return e
224 changes = None
249 changes = None
225 if flags(rev) & HASCOPIESINFO:
250 if flags(rev) & HASCOPIESINFO:
226 changes = changelogrevision(rev).changes
251 changes = changelogrevision(rev).changes
227 value = (p1, p2, changes)
252 value = (p1, p2, changes)
228 if p1 != node.nullrev and p2 != node.nullrev:
253 if p1 != node.nullrev and p2 != node.nullrev:
229 # XXX some case we over cache, IGNORE
254 # XXX some case we over cache, IGNORE
230 merge_caches[rev] = value
255 merge_caches[rev] = value
231 return value
256 return value
232
257
233 return revinfo
258 return revinfo
234
259
235
260
236 def cached_is_ancestor(is_ancestor):
261 def cached_is_ancestor(is_ancestor):
237 """return a cached version of is_ancestor"""
262 """return a cached version of is_ancestor"""
238 cache = {}
263 cache = {}
239
264
240 def _is_ancestor(anc, desc):
265 def _is_ancestor(anc, desc):
241 if anc > desc:
266 if anc > desc:
242 return False
267 return False
243 elif anc == desc:
268 elif anc == desc:
244 return True
269 return True
245 key = (anc, desc)
270 key = (anc, desc)
246 ret = cache.get(key)
271 ret = cache.get(key)
247 if ret is None:
272 if ret is None:
248 ret = cache[key] = is_ancestor(anc, desc)
273 ret = cache[key] = is_ancestor(anc, desc)
249 return ret
274 return ret
250
275
251 return _is_ancestor
276 return _is_ancestor
252
277
253
278
254 def _changesetforwardcopies(a, b, match):
279 def _changesetforwardcopies(a, b, match):
255 if a.rev() in (node.nullrev, b.rev()):
280 if a.rev() in (node.nullrev, b.rev()):
256 return {}
281 return {}
257
282
258 repo = a.repo().unfiltered()
283 repo = a.repo().unfiltered()
259 children = {}
284 children = {}
260
285
261 cl = repo.changelog
286 cl = repo.changelog
262 isancestor = cl.isancestorrev
287 isancestor = cl.isancestorrev
263 missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
288 missingrevs = cl.findmissingrevs(common=[a.rev()], heads=[b.rev()])
264 mrset = set(missingrevs)
289 mrset = set(missingrevs)
265 roots = set()
290 roots = set()
266 for r in missingrevs:
291 for r in missingrevs:
267 for p in cl.parentrevs(r):
292 for p in cl.parentrevs(r):
268 if p == node.nullrev:
293 if p == node.nullrev:
269 continue
294 continue
270 if p not in children:
295 if p not in children:
271 children[p] = [r]
296 children[p] = [r]
272 else:
297 else:
273 children[p].append(r)
298 children[p].append(r)
274 if p not in mrset:
299 if p not in mrset:
275 roots.add(p)
300 roots.add(p)
276 if not roots:
301 if not roots:
277 # no common revision to track copies from
302 # no common revision to track copies from
278 return {}
303 return {}
279 min_root = min(roots)
304 min_root = min(roots)
280
305
281 from_head = set(
306 from_head = set(
282 cl.reachableroots(min_root, [b.rev()], list(roots), includepath=True)
307 cl.reachableroots(min_root, [b.rev()], list(roots), includepath=True)
283 )
308 )
284
309
285 iterrevs = set(from_head)
310 iterrevs = set(from_head)
286 iterrevs &= mrset
311 iterrevs &= mrset
287 iterrevs.update(roots)
312 iterrevs.update(roots)
288 iterrevs.remove(b.rev())
313 iterrevs.remove(b.rev())
289 revs = sorted(iterrevs)
314 revs = sorted(iterrevs)
290
315
291 if repo.filecopiesmode == b'changeset-sidedata':
316 if repo.filecopiesmode == b'changeset-sidedata':
292 revinfo = _revinfo_getter(repo)
317 revinfo = _revinfo_getter(repo, match)
293 return _combine_changeset_copies(
318 return _combine_changeset_copies(
294 revs, children, b.rev(), revinfo, match, isancestor
319 revs, children, b.rev(), revinfo, match, isancestor
295 )
320 )
296 else:
321 else:
297 revinfo = _revinfo_getter_extra(repo)
322 revinfo = _revinfo_getter_extra(repo)
298 return _combine_changeset_copies_extra(
323 return _combine_changeset_copies_extra(
299 revs, children, b.rev(), revinfo, match, isancestor
324 revs, children, b.rev(), revinfo, match, isancestor
300 )
325 )
301
326
302
327
303 def _combine_changeset_copies(
328 def _combine_changeset_copies(
304 revs, children, targetrev, revinfo, match, isancestor
329 revs, children, targetrev, revinfo, match, isancestor
305 ):
330 ):
306 """combine the copies information for each item of iterrevs
331 """combine the copies information for each item of iterrevs
307
332
308 revs: sorted iterable of revision to visit
333 revs: sorted iterable of revision to visit
309 children: a {parent: [children]} mapping.
334 children: a {parent: [children]} mapping.
310 targetrev: the final copies destination revision (not in iterrevs)
335 targetrev: the final copies destination revision (not in iterrevs)
311 revinfo(rev): a function that return (p1, p2, p1copies, p2copies, removed)
336 revinfo(rev): a function that return (p1, p2, p1copies, p2copies, removed)
312 match: a matcher
337 match: a matcher
313
338
314 It returns the aggregated copies information for `targetrev`.
339 It returns the aggregated copies information for `targetrev`.
315 """
340 """
316
341
317 alwaysmatch = match.always()
342 alwaysmatch = match.always()
318
343
319 if rustmod is not None and alwaysmatch:
344 if rustmod is not None and alwaysmatch:
320 return rustmod.combine_changeset_copies(
345 return rustmod.combine_changeset_copies(
321 list(revs), children, targetrev, revinfo, isancestor
346 list(revs), children, targetrev, revinfo, isancestor
322 )
347 )
323
348
324 isancestor = cached_is_ancestor(isancestor)
349 isancestor = cached_is_ancestor(isancestor)
325
350
326 all_copies = {}
351 all_copies = {}
327 for r in revs:
352 for r in revs:
328 copies = all_copies.pop(r, None)
353 copies = all_copies.pop(r, None)
329 if copies is None:
354 if copies is None:
330 # this is a root
355 # this is a root
331 copies = {}
356 copies = {}
332 for i, c in enumerate(children[r]):
357 for i, c in enumerate(children[r]):
333 p1, p2, changes = revinfo(c)
358 p1, p2, changes = revinfo(c)
334 childcopies = {}
359 childcopies = {}
335 if r == p1:
360 if r == p1:
336 parent = 1
361 parent = 1
337 if changes is not None:
362 if changes is not None:
338 childcopies = changes.copied_from_p1
363 childcopies = changes.copied_from_p1
339 else:
364 else:
340 assert r == p2
365 assert r == p2
341 parent = 2
366 parent = 2
342 if changes is not None:
367 if changes is not None:
343 childcopies = changes.copied_from_p2
368 childcopies = changes.copied_from_p2
344 if not alwaysmatch:
369 if not alwaysmatch:
345 childcopies = {
370 childcopies = {
346 dst: src for dst, src in childcopies.items() if match(dst)
371 dst: src for dst, src in childcopies.items() if match(dst)
347 }
372 }
348 newcopies = copies
373 newcopies = copies
349 if childcopies:
374 if childcopies:
350 newcopies = copies.copy()
375 newcopies = copies.copy()
351 for dest, source in pycompat.iteritems(childcopies):
376 for dest, source in pycompat.iteritems(childcopies):
352 prev = copies.get(source)
377 prev = copies.get(source)
353 if prev is not None and prev[1] is not None:
378 if prev is not None and prev[1] is not None:
354 source = prev[1]
379 source = prev[1]
355 newcopies[dest] = (c, source)
380 newcopies[dest] = (c, source)
356 assert newcopies is not copies
381 assert newcopies is not copies
357 if changes is not None and changes.removed:
382 if changes is not None and changes.removed:
358 if newcopies is copies:
383 if newcopies is copies:
359 newcopies = copies.copy()
384 newcopies = copies.copy()
360 for f in changes.removed:
385 for f in changes.removed:
361 if f in newcopies:
386 if f in newcopies:
362 if newcopies is copies:
387 if newcopies is copies:
363 # copy on write to avoid affecting potential other
388 # copy on write to avoid affecting potential other
364 # branches. when there are no other branches, this
389 # branches. when there are no other branches, this
365 # could be avoided.
390 # could be avoided.
366 newcopies = copies.copy()
391 newcopies = copies.copy()
367 newcopies[f] = (c, None)
392 newcopies[f] = (c, None)
368 othercopies = all_copies.get(c)
393 othercopies = all_copies.get(c)
369 if othercopies is None:
394 if othercopies is None:
370 all_copies[c] = newcopies
395 all_copies[c] = newcopies
371 elif newcopies is othercopies:
396 elif newcopies is othercopies:
372 # nothing to merge:
397 # nothing to merge:
373 pass
398 pass
374 else:
399 else:
375 # we are the second parent to work on c, we need to merge our
400 # we are the second parent to work on c, we need to merge our
376 # work with the other.
401 # work with the other.
377 #
402 #
378 # In case of conflict, parent 1 take precedence over parent 2.
403 # In case of conflict, parent 1 take precedence over parent 2.
379 # This is an arbitrary choice made anew when implementing
404 # This is an arbitrary choice made anew when implementing
380 # changeset based copies. It was made without regards with
405 # changeset based copies. It was made without regards with
381 # potential filelog related behavior.
406 # potential filelog related behavior.
382 if parent == 1:
407 if parent == 1:
383 if newcopies is copies:
408 if newcopies is copies:
384 newcopies = copies.copy()
409 newcopies = copies.copy()
385 minor, major = othercopies, newcopies
410 minor, major = othercopies, newcopies
386 else:
411 else:
387 # we do not know if the other dict is a copy or not, so we
412 # we do not know if the other dict is a copy or not, so we
388 # need to blindly copy it. Future change should make this
413 # need to blindly copy it. Future change should make this
389 # unnecessary.
414 # unnecessary.
390 minor, major = newcopies, othercopies.copy()
415 minor, major = newcopies, othercopies.copy()
391 copies = _merge_copies_dict(minor, major, isancestor, changes)
416 copies = _merge_copies_dict(minor, major, isancestor, changes)
392 all_copies[c] = copies
417 all_copies[c] = copies
393
418
394 final_copies = {}
419 final_copies = {}
395 for dest, (tt, source) in all_copies[targetrev].items():
420 for dest, (tt, source) in all_copies[targetrev].items():
396 if source is not None:
421 if source is not None:
397 final_copies[dest] = source
422 final_copies[dest] = source
398 return final_copies
423 return final_copies
399
424
400
425
401 def _merge_copies_dict(minor, major, isancestor, changes):
426 def _merge_copies_dict(minor, major, isancestor, changes):
402 """merge two copies-mapping together, minor and major
427 """merge two copies-mapping together, minor and major
403
428
404 In case of conflict, value from "major" will be picked.
429 In case of conflict, value from "major" will be picked.
405
430
406 - `isancestors(low_rev, high_rev)`: callable return True if `low_rev` is an
431 - `isancestors(low_rev, high_rev)`: callable return True if `low_rev` is an
407 ancestors of `high_rev`,
432 ancestors of `high_rev`,
408
433
409 - `ismerged(path)`: callable return True if `path` have been merged in the
434 - `ismerged(path)`: callable return True if `path` have been merged in the
410 current revision,
435 current revision,
411
436
412 return the resulting dict (in practice, the "minor" object, updated)
437 return the resulting dict (in practice, the "minor" object, updated)
413 """
438 """
414 for dest, value in major.items():
439 for dest, value in major.items():
415 other = minor.get(dest)
440 other = minor.get(dest)
416 if other is None:
441 if other is None:
417 minor[dest] = value
442 minor[dest] = value
418 else:
443 else:
419 new_tt = value[0]
444 new_tt = value[0]
420 other_tt = other[0]
445 other_tt = other[0]
421 if value[1] == other[1]:
446 if value[1] == other[1]:
422 continue
447 continue
423 # content from "major" wins, unless it is older
448 # content from "major" wins, unless it is older
424 # than the branch point or there is a merge
449 # than the branch point or there is a merge
425 if new_tt == other_tt:
450 if new_tt == other_tt:
426 minor[dest] = value
451 minor[dest] = value
427 elif (
452 elif (
428 changes is not None
453 changes is not None
429 and value[1] is None
454 and value[1] is None
430 and dest in changes.salvaged
455 and dest in changes.salvaged
431 ):
456 ):
432 pass
457 pass
433 elif (
458 elif (
434 changes is not None
459 changes is not None
435 and other[1] is None
460 and other[1] is None
436 and dest in changes.salvaged
461 and dest in changes.salvaged
437 ):
462 ):
438 minor[dest] = value
463 minor[dest] = value
439 elif changes is not None and dest in changes.merged:
464 elif changes is not None and dest in changes.merged:
440 minor[dest] = value
465 minor[dest] = value
441 elif not isancestor(new_tt, other_tt):
466 elif not isancestor(new_tt, other_tt):
442 if value[1] is not None:
467 if value[1] is not None:
443 minor[dest] = value
468 minor[dest] = value
444 elif isancestor(other_tt, new_tt):
469 elif isancestor(other_tt, new_tt):
445 minor[dest] = value
470 minor[dest] = value
446 return minor
471 return minor
447
472
448
473
449 def _revinfo_getter_extra(repo):
474 def _revinfo_getter_extra(repo):
450 """return a function that return multiple data given a <rev>"i
475 """return a function that return multiple data given a <rev>"i
451
476
452 * p1: revision number of first parent
477 * p1: revision number of first parent
453 * p2: revision number of first parent
478 * p2: revision number of first parent
454 * p1copies: mapping of copies from p1
479 * p1copies: mapping of copies from p1
455 * p2copies: mapping of copies from p2
480 * p2copies: mapping of copies from p2
456 * removed: a list of removed files
481 * removed: a list of removed files
457 * ismerged: a callback to know if file was merged in that revision
482 * ismerged: a callback to know if file was merged in that revision
458 """
483 """
459 cl = repo.changelog
484 cl = repo.changelog
460 parents = cl.parentrevs
485 parents = cl.parentrevs
461
486
462 def get_ismerged(rev):
487 def get_ismerged(rev):
463 ctx = repo[rev]
488 ctx = repo[rev]
464
489
465 def ismerged(path):
490 def ismerged(path):
466 if path not in ctx.files():
491 if path not in ctx.files():
467 return False
492 return False
468 fctx = ctx[path]
493 fctx = ctx[path]
469 parents = fctx._filelog.parents(fctx._filenode)
494 parents = fctx._filelog.parents(fctx._filenode)
470 nb_parents = 0
495 nb_parents = 0
471 for n in parents:
496 for n in parents:
472 if n != node.nullid:
497 if n != node.nullid:
473 nb_parents += 1
498 nb_parents += 1
474 return nb_parents >= 2
499 return nb_parents >= 2
475
500
476 return ismerged
501 return ismerged
477
502
478 def revinfo(rev):
503 def revinfo(rev):
479 p1, p2 = parents(rev)
504 p1, p2 = parents(rev)
480 ctx = repo[rev]
505 ctx = repo[rev]
481 p1copies, p2copies = ctx._copies
506 p1copies, p2copies = ctx._copies
482 removed = ctx.filesremoved()
507 removed = ctx.filesremoved()
483 return p1, p2, p1copies, p2copies, removed, get_ismerged(rev)
508 return p1, p2, p1copies, p2copies, removed, get_ismerged(rev)
484
509
485 return revinfo
510 return revinfo
486
511
487
512
488 def _combine_changeset_copies_extra(
513 def _combine_changeset_copies_extra(
489 revs, children, targetrev, revinfo, match, isancestor
514 revs, children, targetrev, revinfo, match, isancestor
490 ):
515 ):
491 """version of `_combine_changeset_copies` that works with the Google
516 """version of `_combine_changeset_copies` that works with the Google
492 specific "extra" based storage for copy information"""
517 specific "extra" based storage for copy information"""
493 all_copies = {}
518 all_copies = {}
494 alwaysmatch = match.always()
519 alwaysmatch = match.always()
495 for r in revs:
520 for r in revs:
496 copies = all_copies.pop(r, None)
521 copies = all_copies.pop(r, None)
497 if copies is None:
522 if copies is None:
498 # this is a root
523 # this is a root
499 copies = {}
524 copies = {}
500 for i, c in enumerate(children[r]):
525 for i, c in enumerate(children[r]):
501 p1, p2, p1copies, p2copies, removed, ismerged = revinfo(c)
526 p1, p2, p1copies, p2copies, removed, ismerged = revinfo(c)
502 if r == p1:
527 if r == p1:
503 parent = 1
528 parent = 1
504 childcopies = p1copies
529 childcopies = p1copies
505 else:
530 else:
506 assert r == p2
531 assert r == p2
507 parent = 2
532 parent = 2
508 childcopies = p2copies
533 childcopies = p2copies
509 if not alwaysmatch:
534 if not alwaysmatch:
510 childcopies = {
535 childcopies = {
511 dst: src for dst, src in childcopies.items() if match(dst)
536 dst: src for dst, src in childcopies.items() if match(dst)
512 }
537 }
513 newcopies = copies
538 newcopies = copies
514 if childcopies:
539 if childcopies:
515 newcopies = copies.copy()
540 newcopies = copies.copy()
516 for dest, source in pycompat.iteritems(childcopies):
541 for dest, source in pycompat.iteritems(childcopies):
517 prev = copies.get(source)
542 prev = copies.get(source)
518 if prev is not None and prev[1] is not None:
543 if prev is not None and prev[1] is not None:
519 source = prev[1]
544 source = prev[1]
520 newcopies[dest] = (c, source)
545 newcopies[dest] = (c, source)
521 assert newcopies is not copies
546 assert newcopies is not copies
522 for f in removed:
547 for f in removed:
523 if f in newcopies:
548 if f in newcopies:
524 if newcopies is copies:
549 if newcopies is copies:
525 # copy on write to avoid affecting potential other
550 # copy on write to avoid affecting potential other
526 # branches. when there are no other branches, this
551 # branches. when there are no other branches, this
527 # could be avoided.
552 # could be avoided.
528 newcopies = copies.copy()
553 newcopies = copies.copy()
529 newcopies[f] = (c, None)
554 newcopies[f] = (c, None)
530 othercopies = all_copies.get(c)
555 othercopies = all_copies.get(c)
531 if othercopies is None:
556 if othercopies is None:
532 all_copies[c] = newcopies
557 all_copies[c] = newcopies
533 else:
558 else:
534 # we are the second parent to work on c, we need to merge our
559 # we are the second parent to work on c, we need to merge our
535 # work with the other.
560 # work with the other.
536 #
561 #
537 # In case of conflict, parent 1 take precedence over parent 2.
562 # In case of conflict, parent 1 take precedence over parent 2.
538 # This is an arbitrary choice made anew when implementing
563 # This is an arbitrary choice made anew when implementing
539 # changeset based copies. It was made without regards with
564 # changeset based copies. It was made without regards with
540 # potential filelog related behavior.
565 # potential filelog related behavior.
541 if parent == 1:
566 if parent == 1:
542 _merge_copies_dict_extra(
567 _merge_copies_dict_extra(
543 othercopies, newcopies, isancestor, ismerged
568 othercopies, newcopies, isancestor, ismerged
544 )
569 )
545 else:
570 else:
546 _merge_copies_dict_extra(
571 _merge_copies_dict_extra(
547 newcopies, othercopies, isancestor, ismerged
572 newcopies, othercopies, isancestor, ismerged
548 )
573 )
549 all_copies[c] = newcopies
574 all_copies[c] = newcopies
550
575
551 final_copies = {}
576 final_copies = {}
552 for dest, (tt, source) in all_copies[targetrev].items():
577 for dest, (tt, source) in all_copies[targetrev].items():
553 if source is not None:
578 if source is not None:
554 final_copies[dest] = source
579 final_copies[dest] = source
555 return final_copies
580 return final_copies
556
581
557
582
558 def _merge_copies_dict_extra(minor, major, isancestor, ismerged):
583 def _merge_copies_dict_extra(minor, major, isancestor, ismerged):
559 """version of `_merge_copies_dict` that works with the Google
584 """version of `_merge_copies_dict` that works with the Google
560 specific "extra" based storage for copy information"""
585 specific "extra" based storage for copy information"""
561 for dest, value in major.items():
586 for dest, value in major.items():
562 other = minor.get(dest)
587 other = minor.get(dest)
563 if other is None:
588 if other is None:
564 minor[dest] = value
589 minor[dest] = value
565 else:
590 else:
566 new_tt = value[0]
591 new_tt = value[0]
567 other_tt = other[0]
592 other_tt = other[0]
568 if value[1] == other[1]:
593 if value[1] == other[1]:
569 continue
594 continue
570 # content from "major" wins, unless it is older
595 # content from "major" wins, unless it is older
571 # than the branch point or there is a merge
596 # than the branch point or there is a merge
572 if (
597 if (
573 new_tt == other_tt
598 new_tt == other_tt
574 or not isancestor(new_tt, other_tt)
599 or not isancestor(new_tt, other_tt)
575 or ismerged(dest)
600 or ismerged(dest)
576 ):
601 ):
577 minor[dest] = value
602 minor[dest] = value
578
603
579
604
580 def _forwardcopies(a, b, base=None, match=None):
605 def _forwardcopies(a, b, base=None, match=None):
581 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
606 """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
582
607
583 if base is None:
608 if base is None:
584 base = a
609 base = a
585 match = a.repo().narrowmatch(match)
610 match = a.repo().narrowmatch(match)
586 # check for working copy
611 # check for working copy
587 if b.rev() is None:
612 if b.rev() is None:
588 cm = _committedforwardcopies(a, b.p1(), base, match)
613 cm = _committedforwardcopies(a, b.p1(), base, match)
589 # combine copies from dirstate if necessary
614 # combine copies from dirstate if necessary
590 copies = _chain(cm, _dirstatecopies(b._repo, match))
615 copies = _chain(cm, _dirstatecopies(b._repo, match))
591 else:
616 else:
592 copies = _committedforwardcopies(a, b, base, match)
617 copies = _committedforwardcopies(a, b, base, match)
593 return copies
618 return copies
594
619
595
620
596 def _backwardrenames(a, b, match):
621 def _backwardrenames(a, b, match):
597 if a._repo.ui.config(b'experimental', b'copytrace') == b'off':
622 if a._repo.ui.config(b'experimental', b'copytrace') == b'off':
598 return {}
623 return {}
599
624
600 # Even though we're not taking copies into account, 1:n rename situations
625 # Even though we're not taking copies into account, 1:n rename situations
601 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
626 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
602 # arbitrarily pick one of the renames.
627 # arbitrarily pick one of the renames.
603 # We don't want to pass in "match" here, since that would filter
628 # We don't want to pass in "match" here, since that would filter
604 # the destination by it. Since we're reversing the copies, we want
629 # the destination by it. Since we're reversing the copies, we want
605 # to filter the source instead.
630 # to filter the source instead.
606 f = _forwardcopies(b, a)
631 f = _forwardcopies(b, a)
607 r = {}
632 r = {}
608 for k, v in sorted(pycompat.iteritems(f)):
633 for k, v in sorted(pycompat.iteritems(f)):
609 if match and not match(v):
634 if match and not match(v):
610 continue
635 continue
611 # remove copies
636 # remove copies
612 if v in a:
637 if v in a:
613 continue
638 continue
614 r[v] = k
639 r[v] = k
615 return r
640 return r
616
641
617
642
618 def pathcopies(x, y, match=None):
643 def pathcopies(x, y, match=None):
619 """find {dst@y: src@x} copy mapping for directed compare"""
644 """find {dst@y: src@x} copy mapping for directed compare"""
620 repo = x._repo
645 repo = x._repo
621 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
646 debug = repo.ui.debugflag and repo.ui.configbool(b'devel', b'debug.copies')
622 if debug:
647 if debug:
623 repo.ui.debug(
648 repo.ui.debug(
624 b'debug.copies: searching copies from %s to %s\n' % (x, y)
649 b'debug.copies: searching copies from %s to %s\n' % (x, y)
625 )
650 )
626 if x == y or not x or not y:
651 if x == y or not x or not y:
627 return {}
652 return {}
628 if y.rev() is None and x == y.p1():
653 if y.rev() is None and x == y.p1():
629 if debug:
654 if debug:
630 repo.ui.debug(b'debug.copies: search mode: dirstate\n')
655 repo.ui.debug(b'debug.copies: search mode: dirstate\n')
631 # short-circuit to avoid issues with merge states
656 # short-circuit to avoid issues with merge states
632 return _dirstatecopies(repo, match)
657 return _dirstatecopies(repo, match)
633 a = y.ancestor(x)
658 a = y.ancestor(x)
634 if a == x:
659 if a == x:
635 if debug:
660 if debug:
636 repo.ui.debug(b'debug.copies: search mode: forward\n')
661 repo.ui.debug(b'debug.copies: search mode: forward\n')
637 copies = _forwardcopies(x, y, match=match)
662 copies = _forwardcopies(x, y, match=match)
638 elif a == y:
663 elif a == y:
639 if debug:
664 if debug:
640 repo.ui.debug(b'debug.copies: search mode: backward\n')
665 repo.ui.debug(b'debug.copies: search mode: backward\n')
641 copies = _backwardrenames(x, y, match=match)
666 copies = _backwardrenames(x, y, match=match)
642 else:
667 else:
643 if debug:
668 if debug:
644 repo.ui.debug(b'debug.copies: search mode: combined\n')
669 repo.ui.debug(b'debug.copies: search mode: combined\n')
645 base = None
670 base = None
646 if a.rev() != node.nullrev:
671 if a.rev() != node.nullrev:
647 base = x
672 base = x
648 copies = _chain(
673 copies = _chain(
649 _backwardrenames(x, a, match=match),
674 _backwardrenames(x, a, match=match),
650 _forwardcopies(a, y, base, match=match),
675 _forwardcopies(a, y, base, match=match),
651 )
676 )
652 _filter(x, y, copies)
677 _filter(x, y, copies)
653 return copies
678 return copies
654
679
655
680
656 def mergecopies(repo, c1, c2, base):
681 def mergecopies(repo, c1, c2, base):
657 """
682 """
658 Finds moves and copies between context c1 and c2 that are relevant for
683 Finds moves and copies between context c1 and c2 that are relevant for
659 merging. 'base' will be used as the merge base.
684 merging. 'base' will be used as the merge base.
660
685
661 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
686 Copytracing is used in commands like rebase, merge, unshelve, etc to merge
662 files that were moved/ copied in one merge parent and modified in another.
687 files that were moved/ copied in one merge parent and modified in another.
663 For example:
688 For example:
664
689
665 o ---> 4 another commit
690 o ---> 4 another commit
666 |
691 |
667 | o ---> 3 commit that modifies a.txt
692 | o ---> 3 commit that modifies a.txt
668 | /
693 | /
669 o / ---> 2 commit that moves a.txt to b.txt
694 o / ---> 2 commit that moves a.txt to b.txt
670 |/
695 |/
671 o ---> 1 merge base
696 o ---> 1 merge base
672
697
673 If we try to rebase revision 3 on revision 4, since there is no a.txt in
698 If we try to rebase revision 3 on revision 4, since there is no a.txt in
674 revision 4, and if user have copytrace disabled, we prints the following
699 revision 4, and if user have copytrace disabled, we prints the following
675 message:
700 message:
676
701
677 ```other changed <file> which local deleted```
702 ```other changed <file> which local deleted```
678
703
679 Returns a tuple where:
704 Returns a tuple where:
680
705
681 "branch_copies" an instance of branch_copies.
706 "branch_copies" an instance of branch_copies.
682
707
683 "diverge" is a mapping of source name -> list of destination names
708 "diverge" is a mapping of source name -> list of destination names
684 for divergent renames.
709 for divergent renames.
685
710
686 This function calls different copytracing algorithms based on config.
711 This function calls different copytracing algorithms based on config.
687 """
712 """
688 # avoid silly behavior for update from empty dir
713 # avoid silly behavior for update from empty dir
689 if not c1 or not c2 or c1 == c2:
714 if not c1 or not c2 or c1 == c2:
690 return branch_copies(), branch_copies(), {}
715 return branch_copies(), branch_copies(), {}
691
716
692 narrowmatch = c1.repo().narrowmatch()
717 narrowmatch = c1.repo().narrowmatch()
693
718
694 # avoid silly behavior for parent -> working dir
719 # avoid silly behavior for parent -> working dir
695 if c2.node() is None and c1.node() == repo.dirstate.p1():
720 if c2.node() is None and c1.node() == repo.dirstate.p1():
696 return (
721 return (
697 branch_copies(_dirstatecopies(repo, narrowmatch)),
722 branch_copies(_dirstatecopies(repo, narrowmatch)),
698 branch_copies(),
723 branch_copies(),
699 {},
724 {},
700 )
725 )
701
726
702 copytracing = repo.ui.config(b'experimental', b'copytrace')
727 copytracing = repo.ui.config(b'experimental', b'copytrace')
703 if stringutil.parsebool(copytracing) is False:
728 if stringutil.parsebool(copytracing) is False:
704 # stringutil.parsebool() returns None when it is unable to parse the
729 # stringutil.parsebool() returns None when it is unable to parse the
705 # value, so we should rely on making sure copytracing is on such cases
730 # value, so we should rely on making sure copytracing is on such cases
706 return branch_copies(), branch_copies(), {}
731 return branch_copies(), branch_copies(), {}
707
732
708 if usechangesetcentricalgo(repo):
733 if usechangesetcentricalgo(repo):
709 # The heuristics don't make sense when we need changeset-centric algos
734 # The heuristics don't make sense when we need changeset-centric algos
710 return _fullcopytracing(repo, c1, c2, base)
735 return _fullcopytracing(repo, c1, c2, base)
711
736
712 # Copy trace disabling is explicitly below the node == p1 logic above
737 # Copy trace disabling is explicitly below the node == p1 logic above
713 # because the logic above is required for a simple copy to be kept across a
738 # because the logic above is required for a simple copy to be kept across a
714 # rebase.
739 # rebase.
715 if copytracing == b'heuristics':
740 if copytracing == b'heuristics':
716 # Do full copytracing if only non-public revisions are involved as
741 # Do full copytracing if only non-public revisions are involved as
717 # that will be fast enough and will also cover the copies which could
742 # that will be fast enough and will also cover the copies which could
718 # be missed by heuristics
743 # be missed by heuristics
719 if _isfullcopytraceable(repo, c1, base):
744 if _isfullcopytraceable(repo, c1, base):
720 return _fullcopytracing(repo, c1, c2, base)
745 return _fullcopytracing(repo, c1, c2, base)
721 return _heuristicscopytracing(repo, c1, c2, base)
746 return _heuristicscopytracing(repo, c1, c2, base)
722 else:
747 else:
723 return _fullcopytracing(repo, c1, c2, base)
748 return _fullcopytracing(repo, c1, c2, base)
724
749
725
750
726 def _isfullcopytraceable(repo, c1, base):
751 def _isfullcopytraceable(repo, c1, base):
727 """Checks that if base, source and destination are all no-public branches,
752 """Checks that if base, source and destination are all no-public branches,
728 if yes let's use the full copytrace algorithm for increased capabilities
753 if yes let's use the full copytrace algorithm for increased capabilities
729 since it will be fast enough.
754 since it will be fast enough.
730
755
731 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
756 `experimental.copytrace.sourcecommitlimit` can be used to set a limit for
732 number of changesets from c1 to base such that if number of changesets are
757 number of changesets from c1 to base such that if number of changesets are
733 more than the limit, full copytracing algorithm won't be used.
758 more than the limit, full copytracing algorithm won't be used.
734 """
759 """
735 if c1.rev() is None:
760 if c1.rev() is None:
736 c1 = c1.p1()
761 c1 = c1.p1()
737 if c1.mutable() and base.mutable():
762 if c1.mutable() and base.mutable():
738 sourcecommitlimit = repo.ui.configint(
763 sourcecommitlimit = repo.ui.configint(
739 b'experimental', b'copytrace.sourcecommitlimit'
764 b'experimental', b'copytrace.sourcecommitlimit'
740 )
765 )
741 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev()))
766 commits = len(repo.revs(b'%d::%d', base.rev(), c1.rev()))
742 return commits < sourcecommitlimit
767 return commits < sourcecommitlimit
743 return False
768 return False
744
769
745
770
746 def _checksinglesidecopies(
771 def _checksinglesidecopies(
747 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
772 src, dsts1, m1, m2, mb, c2, base, copy, renamedelete
748 ):
773 ):
749 if src not in m2:
774 if src not in m2:
750 # deleted on side 2
775 # deleted on side 2
751 if src not in m1:
776 if src not in m1:
752 # renamed on side 1, deleted on side 2
777 # renamed on side 1, deleted on side 2
753 renamedelete[src] = dsts1
778 renamedelete[src] = dsts1
754 elif src not in mb:
779 elif src not in mb:
755 # Work around the "short-circuit to avoid issues with merge states"
780 # Work around the "short-circuit to avoid issues with merge states"
756 # thing in pathcopies(): pathcopies(x, y) can return a copy where the
781 # thing in pathcopies(): pathcopies(x, y) can return a copy where the
757 # destination doesn't exist in y.
782 # destination doesn't exist in y.
758 pass
783 pass
759 elif mb[src] != m2[src] and not _related(c2[src], base[src]):
784 elif mb[src] != m2[src] and not _related(c2[src], base[src]):
760 return
785 return
761 elif mb[src] != m2[src] or mb.flags(src) != m2.flags(src):
786 elif mb[src] != m2[src] or mb.flags(src) != m2.flags(src):
762 # modified on side 2
787 # modified on side 2
763 for dst in dsts1:
788 for dst in dsts1:
764 copy[dst] = src
789 copy[dst] = src
765
790
766
791
767 class branch_copies(object):
792 class branch_copies(object):
768 """Information about copies made on one side of a merge/graft.
793 """Information about copies made on one side of a merge/graft.
769
794
770 "copy" is a mapping from destination name -> source name,
795 "copy" is a mapping from destination name -> source name,
771 where source is in c1 and destination is in c2 or vice-versa.
796 where source is in c1 and destination is in c2 or vice-versa.
772
797
773 "movewithdir" is a mapping from source name -> destination name,
798 "movewithdir" is a mapping from source name -> destination name,
774 where the file at source present in one context but not the other
799 where the file at source present in one context but not the other
775 needs to be moved to destination by the merge process, because the
800 needs to be moved to destination by the merge process, because the
776 other context moved the directory it is in.
801 other context moved the directory it is in.
777
802
778 "renamedelete" is a mapping of source name -> list of destination
803 "renamedelete" is a mapping of source name -> list of destination
779 names for files deleted in c1 that were renamed in c2 or vice-versa.
804 names for files deleted in c1 that were renamed in c2 or vice-versa.
780
805
781 "dirmove" is a mapping of detected source dir -> destination dir renames.
806 "dirmove" is a mapping of detected source dir -> destination dir renames.
782 This is needed for handling changes to new files previously grafted into
807 This is needed for handling changes to new files previously grafted into
783 renamed directories.
808 renamed directories.
784 """
809 """
785
810
786 def __init__(
811 def __init__(
787 self, copy=None, renamedelete=None, dirmove=None, movewithdir=None
812 self, copy=None, renamedelete=None, dirmove=None, movewithdir=None
788 ):
813 ):
789 self.copy = {} if copy is None else copy
814 self.copy = {} if copy is None else copy
790 self.renamedelete = {} if renamedelete is None else renamedelete
815 self.renamedelete = {} if renamedelete is None else renamedelete
791 self.dirmove = {} if dirmove is None else dirmove
816 self.dirmove = {} if dirmove is None else dirmove
792 self.movewithdir = {} if movewithdir is None else movewithdir
817 self.movewithdir = {} if movewithdir is None else movewithdir
793
818
794 def __repr__(self):
819 def __repr__(self):
795 return '<branch_copies\n copy=%r\n renamedelete=%r\n dirmove=%r\n movewithdir=%r\n>' % (
820 return '<branch_copies\n copy=%r\n renamedelete=%r\n dirmove=%r\n movewithdir=%r\n>' % (
796 self.copy,
821 self.copy,
797 self.renamedelete,
822 self.renamedelete,
798 self.dirmove,
823 self.dirmove,
799 self.movewithdir,
824 self.movewithdir,
800 )
825 )
801
826
802
827
803 def _fullcopytracing(repo, c1, c2, base):
828 def _fullcopytracing(repo, c1, c2, base):
804 """The full copytracing algorithm which finds all the new files that were
829 """The full copytracing algorithm which finds all the new files that were
805 added from merge base up to the top commit and for each file it checks if
830 added from merge base up to the top commit and for each file it checks if
806 this file was copied from another file.
831 this file was copied from another file.
807
832
808 This is pretty slow when a lot of changesets are involved but will track all
833 This is pretty slow when a lot of changesets are involved but will track all
809 the copies.
834 the copies.
810 """
835 """
811 m1 = c1.manifest()
836 m1 = c1.manifest()
812 m2 = c2.manifest()
837 m2 = c2.manifest()
813 mb = base.manifest()
838 mb = base.manifest()
814
839
815 copies1 = pathcopies(base, c1)
840 copies1 = pathcopies(base, c1)
816 copies2 = pathcopies(base, c2)
841 copies2 = pathcopies(base, c2)
817
842
818 if not (copies1 or copies2):
843 if not (copies1 or copies2):
819 return branch_copies(), branch_copies(), {}
844 return branch_copies(), branch_copies(), {}
820
845
821 inversecopies1 = {}
846 inversecopies1 = {}
822 inversecopies2 = {}
847 inversecopies2 = {}
823 for dst, src in copies1.items():
848 for dst, src in copies1.items():
824 inversecopies1.setdefault(src, []).append(dst)
849 inversecopies1.setdefault(src, []).append(dst)
825 for dst, src in copies2.items():
850 for dst, src in copies2.items():
826 inversecopies2.setdefault(src, []).append(dst)
851 inversecopies2.setdefault(src, []).append(dst)
827
852
828 copy1 = {}
853 copy1 = {}
829 copy2 = {}
854 copy2 = {}
830 diverge = {}
855 diverge = {}
831 renamedelete1 = {}
856 renamedelete1 = {}
832 renamedelete2 = {}
857 renamedelete2 = {}
833 allsources = set(inversecopies1) | set(inversecopies2)
858 allsources = set(inversecopies1) | set(inversecopies2)
834 for src in allsources:
859 for src in allsources:
835 dsts1 = inversecopies1.get(src)
860 dsts1 = inversecopies1.get(src)
836 dsts2 = inversecopies2.get(src)
861 dsts2 = inversecopies2.get(src)
837 if dsts1 and dsts2:
862 if dsts1 and dsts2:
838 # copied/renamed on both sides
863 # copied/renamed on both sides
839 if src not in m1 and src not in m2:
864 if src not in m1 and src not in m2:
840 # renamed on both sides
865 # renamed on both sides
841 dsts1 = set(dsts1)
866 dsts1 = set(dsts1)
842 dsts2 = set(dsts2)
867 dsts2 = set(dsts2)
843 # If there's some overlap in the rename destinations, we
868 # If there's some overlap in the rename destinations, we
844 # consider it not divergent. For example, if side 1 copies 'a'
869 # consider it not divergent. For example, if side 1 copies 'a'
845 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
870 # to 'b' and 'c' and deletes 'a', and side 2 copies 'a' to 'c'
846 # and 'd' and deletes 'a'.
871 # and 'd' and deletes 'a'.
847 if dsts1 & dsts2:
872 if dsts1 & dsts2:
848 for dst in dsts1 & dsts2:
873 for dst in dsts1 & dsts2:
849 copy1[dst] = src
874 copy1[dst] = src
850 copy2[dst] = src
875 copy2[dst] = src
851 else:
876 else:
852 diverge[src] = sorted(dsts1 | dsts2)
877 diverge[src] = sorted(dsts1 | dsts2)
853 elif src in m1 and src in m2:
878 elif src in m1 and src in m2:
854 # copied on both sides
879 # copied on both sides
855 dsts1 = set(dsts1)
880 dsts1 = set(dsts1)
856 dsts2 = set(dsts2)
881 dsts2 = set(dsts2)
857 for dst in dsts1 & dsts2:
882 for dst in dsts1 & dsts2:
858 copy1[dst] = src
883 copy1[dst] = src
859 copy2[dst] = src
884 copy2[dst] = src
860 # TODO: Handle cases where it was renamed on one side and copied
885 # TODO: Handle cases where it was renamed on one side and copied
861 # on the other side
886 # on the other side
862 elif dsts1:
887 elif dsts1:
863 # copied/renamed only on side 1
888 # copied/renamed only on side 1
864 _checksinglesidecopies(
889 _checksinglesidecopies(
865 src, dsts1, m1, m2, mb, c2, base, copy1, renamedelete1
890 src, dsts1, m1, m2, mb, c2, base, copy1, renamedelete1
866 )
891 )
867 elif dsts2:
892 elif dsts2:
868 # copied/renamed only on side 2
893 # copied/renamed only on side 2
869 _checksinglesidecopies(
894 _checksinglesidecopies(
870 src, dsts2, m2, m1, mb, c1, base, copy2, renamedelete2
895 src, dsts2, m2, m1, mb, c1, base, copy2, renamedelete2
871 )
896 )
872
897
873 # find interesting file sets from manifests
898 # find interesting file sets from manifests
874 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
899 addedinm1 = m1.filesnotin(mb, repo.narrowmatch())
875 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
900 addedinm2 = m2.filesnotin(mb, repo.narrowmatch())
876 u1 = sorted(addedinm1 - addedinm2)
901 u1 = sorted(addedinm1 - addedinm2)
877 u2 = sorted(addedinm2 - addedinm1)
902 u2 = sorted(addedinm2 - addedinm1)
878
903
879 header = b" unmatched files in %s"
904 header = b" unmatched files in %s"
880 if u1:
905 if u1:
881 repo.ui.debug(b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1)))
906 repo.ui.debug(b"%s:\n %s\n" % (header % b'local', b"\n ".join(u1)))
882 if u2:
907 if u2:
883 repo.ui.debug(b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2)))
908 repo.ui.debug(b"%s:\n %s\n" % (header % b'other', b"\n ".join(u2)))
884
909
885 if repo.ui.debugflag:
910 if repo.ui.debugflag:
886 renamedeleteset = set()
911 renamedeleteset = set()
887 divergeset = set()
912 divergeset = set()
888 for dsts in diverge.values():
913 for dsts in diverge.values():
889 divergeset.update(dsts)
914 divergeset.update(dsts)
890 for dsts in renamedelete1.values():
915 for dsts in renamedelete1.values():
891 renamedeleteset.update(dsts)
916 renamedeleteset.update(dsts)
892 for dsts in renamedelete2.values():
917 for dsts in renamedelete2.values():
893 renamedeleteset.update(dsts)
918 renamedeleteset.update(dsts)
894
919
895 repo.ui.debug(
920 repo.ui.debug(
896 b" all copies found (* = to merge, ! = divergent, "
921 b" all copies found (* = to merge, ! = divergent, "
897 b"% = renamed and deleted):\n"
922 b"% = renamed and deleted):\n"
898 )
923 )
899 for side, copies in ((b"local", copies1), (b"remote", copies2)):
924 for side, copies in ((b"local", copies1), (b"remote", copies2)):
900 if not copies:
925 if not copies:
901 continue
926 continue
902 repo.ui.debug(b" on %s side:\n" % side)
927 repo.ui.debug(b" on %s side:\n" % side)
903 for f in sorted(copies):
928 for f in sorted(copies):
904 note = b""
929 note = b""
905 if f in copy1 or f in copy2:
930 if f in copy1 or f in copy2:
906 note += b"*"
931 note += b"*"
907 if f in divergeset:
932 if f in divergeset:
908 note += b"!"
933 note += b"!"
909 if f in renamedeleteset:
934 if f in renamedeleteset:
910 note += b"%"
935 note += b"%"
911 repo.ui.debug(
936 repo.ui.debug(
912 b" src: '%s' -> dst: '%s' %s\n" % (copies[f], f, note)
937 b" src: '%s' -> dst: '%s' %s\n" % (copies[f], f, note)
913 )
938 )
914 del renamedeleteset
939 del renamedeleteset
915 del divergeset
940 del divergeset
916
941
917 repo.ui.debug(b" checking for directory renames\n")
942 repo.ui.debug(b" checking for directory renames\n")
918
943
919 dirmove1, movewithdir2 = _dir_renames(repo, c1, copy1, copies1, u2)
944 dirmove1, movewithdir2 = _dir_renames(repo, c1, copy1, copies1, u2)
920 dirmove2, movewithdir1 = _dir_renames(repo, c2, copy2, copies2, u1)
945 dirmove2, movewithdir1 = _dir_renames(repo, c2, copy2, copies2, u1)
921
946
922 branch_copies1 = branch_copies(copy1, renamedelete1, dirmove1, movewithdir1)
947 branch_copies1 = branch_copies(copy1, renamedelete1, dirmove1, movewithdir1)
923 branch_copies2 = branch_copies(copy2, renamedelete2, dirmove2, movewithdir2)
948 branch_copies2 = branch_copies(copy2, renamedelete2, dirmove2, movewithdir2)
924
949
925 return branch_copies1, branch_copies2, diverge
950 return branch_copies1, branch_copies2, diverge
926
951
927
952
928 def _dir_renames(repo, ctx, copy, fullcopy, addedfiles):
953 def _dir_renames(repo, ctx, copy, fullcopy, addedfiles):
929 """Finds moved directories and files that should move with them.
954 """Finds moved directories and files that should move with them.
930
955
931 ctx: the context for one of the sides
956 ctx: the context for one of the sides
932 copy: files copied on the same side (as ctx)
957 copy: files copied on the same side (as ctx)
933 fullcopy: files copied on the same side (as ctx), including those that
958 fullcopy: files copied on the same side (as ctx), including those that
934 merge.manifestmerge() won't care about
959 merge.manifestmerge() won't care about
935 addedfiles: added files on the other side (compared to ctx)
960 addedfiles: added files on the other side (compared to ctx)
936 """
961 """
937 # generate a directory move map
962 # generate a directory move map
938 invalid = set()
963 invalid = set()
939 dirmove = {}
964 dirmove = {}
940
965
941 # examine each file copy for a potential directory move, which is
966 # examine each file copy for a potential directory move, which is
942 # when all the files in a directory are moved to a new directory
967 # when all the files in a directory are moved to a new directory
943 for dst, src in pycompat.iteritems(fullcopy):
968 for dst, src in pycompat.iteritems(fullcopy):
944 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
969 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
945 if dsrc in invalid:
970 if dsrc in invalid:
946 # already seen to be uninteresting
971 # already seen to be uninteresting
947 continue
972 continue
948 elif ctx.hasdir(dsrc) and ctx.hasdir(ddst):
973 elif ctx.hasdir(dsrc) and ctx.hasdir(ddst):
949 # directory wasn't entirely moved locally
974 # directory wasn't entirely moved locally
950 invalid.add(dsrc)
975 invalid.add(dsrc)
951 elif dsrc in dirmove and dirmove[dsrc] != ddst:
976 elif dsrc in dirmove and dirmove[dsrc] != ddst:
952 # files from the same directory moved to two different places
977 # files from the same directory moved to two different places
953 invalid.add(dsrc)
978 invalid.add(dsrc)
954 else:
979 else:
955 # looks good so far
980 # looks good so far
956 dirmove[dsrc] = ddst
981 dirmove[dsrc] = ddst
957
982
958 for i in invalid:
983 for i in invalid:
959 if i in dirmove:
984 if i in dirmove:
960 del dirmove[i]
985 del dirmove[i]
961 del invalid
986 del invalid
962
987
963 if not dirmove:
988 if not dirmove:
964 return {}, {}
989 return {}, {}
965
990
966 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
991 dirmove = {k + b"/": v + b"/" for k, v in pycompat.iteritems(dirmove)}
967
992
968 for d in dirmove:
993 for d in dirmove:
969 repo.ui.debug(
994 repo.ui.debug(
970 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
995 b" discovered dir src: '%s' -> dst: '%s'\n" % (d, dirmove[d])
971 )
996 )
972
997
973 movewithdir = {}
998 movewithdir = {}
974 # check unaccounted nonoverlapping files against directory moves
999 # check unaccounted nonoverlapping files against directory moves
975 for f in addedfiles:
1000 for f in addedfiles:
976 if f not in fullcopy:
1001 if f not in fullcopy:
977 for d in dirmove:
1002 for d in dirmove:
978 if f.startswith(d):
1003 if f.startswith(d):
979 # new file added in a directory that was moved, move it
1004 # new file added in a directory that was moved, move it
980 df = dirmove[d] + f[len(d) :]
1005 df = dirmove[d] + f[len(d) :]
981 if df not in copy:
1006 if df not in copy:
982 movewithdir[f] = df
1007 movewithdir[f] = df
983 repo.ui.debug(
1008 repo.ui.debug(
984 b" pending file src: '%s' -> dst: '%s'\n"
1009 b" pending file src: '%s' -> dst: '%s'\n"
985 % (f, df)
1010 % (f, df)
986 )
1011 )
987 break
1012 break
988
1013
989 return dirmove, movewithdir
1014 return dirmove, movewithdir
990
1015
991
1016
992 def _heuristicscopytracing(repo, c1, c2, base):
1017 def _heuristicscopytracing(repo, c1, c2, base):
993 """Fast copytracing using filename heuristics
1018 """Fast copytracing using filename heuristics
994
1019
995 Assumes that moves or renames are of following two types:
1020 Assumes that moves or renames are of following two types:
996
1021
997 1) Inside a directory only (same directory name but different filenames)
1022 1) Inside a directory only (same directory name but different filenames)
998 2) Move from one directory to another
1023 2) Move from one directory to another
999 (same filenames but different directory names)
1024 (same filenames but different directory names)
1000
1025
1001 Works only when there are no merge commits in the "source branch".
1026 Works only when there are no merge commits in the "source branch".
1002 Source branch is commits from base up to c2 not including base.
1027 Source branch is commits from base up to c2 not including base.
1003
1028
1004 If merge is involved it fallbacks to _fullcopytracing().
1029 If merge is involved it fallbacks to _fullcopytracing().
1005
1030
1006 Can be used by setting the following config:
1031 Can be used by setting the following config:
1007
1032
1008 [experimental]
1033 [experimental]
1009 copytrace = heuristics
1034 copytrace = heuristics
1010
1035
1011 In some cases the copy/move candidates found by heuristics can be very large
1036 In some cases the copy/move candidates found by heuristics can be very large
1012 in number and that will make the algorithm slow. The number of possible
1037 in number and that will make the algorithm slow. The number of possible
1013 candidates to check can be limited by using the config
1038 candidates to check can be limited by using the config
1014 `experimental.copytrace.movecandidateslimit` which defaults to 100.
1039 `experimental.copytrace.movecandidateslimit` which defaults to 100.
1015 """
1040 """
1016
1041
1017 if c1.rev() is None:
1042 if c1.rev() is None:
1018 c1 = c1.p1()
1043 c1 = c1.p1()
1019 if c2.rev() is None:
1044 if c2.rev() is None:
1020 c2 = c2.p1()
1045 c2 = c2.p1()
1021
1046
1022 changedfiles = set()
1047 changedfiles = set()
1023 m1 = c1.manifest()
1048 m1 = c1.manifest()
1024 if not repo.revs(b'%d::%d', base.rev(), c2.rev()):
1049 if not repo.revs(b'%d::%d', base.rev(), c2.rev()):
1025 # If base is not in c2 branch, we switch to fullcopytracing
1050 # If base is not in c2 branch, we switch to fullcopytracing
1026 repo.ui.debug(
1051 repo.ui.debug(
1027 b"switching to full copytracing as base is not "
1052 b"switching to full copytracing as base is not "
1028 b"an ancestor of c2\n"
1053 b"an ancestor of c2\n"
1029 )
1054 )
1030 return _fullcopytracing(repo, c1, c2, base)
1055 return _fullcopytracing(repo, c1, c2, base)
1031
1056
1032 ctx = c2
1057 ctx = c2
1033 while ctx != base:
1058 while ctx != base:
1034 if len(ctx.parents()) == 2:
1059 if len(ctx.parents()) == 2:
1035 # To keep things simple let's not handle merges
1060 # To keep things simple let's not handle merges
1036 repo.ui.debug(b"switching to full copytracing because of merges\n")
1061 repo.ui.debug(b"switching to full copytracing because of merges\n")
1037 return _fullcopytracing(repo, c1, c2, base)
1062 return _fullcopytracing(repo, c1, c2, base)
1038 changedfiles.update(ctx.files())
1063 changedfiles.update(ctx.files())
1039 ctx = ctx.p1()
1064 ctx = ctx.p1()
1040
1065
1041 copies2 = {}
1066 copies2 = {}
1042 cp = _forwardcopies(base, c2)
1067 cp = _forwardcopies(base, c2)
1043 for dst, src in pycompat.iteritems(cp):
1068 for dst, src in pycompat.iteritems(cp):
1044 if src in m1:
1069 if src in m1:
1045 copies2[dst] = src
1070 copies2[dst] = src
1046
1071
1047 # file is missing if it isn't present in the destination, but is present in
1072 # file is missing if it isn't present in the destination, but is present in
1048 # the base and present in the source.
1073 # the base and present in the source.
1049 # Presence in the base is important to exclude added files, presence in the
1074 # Presence in the base is important to exclude added files, presence in the
1050 # source is important to exclude removed files.
1075 # source is important to exclude removed files.
1051 filt = lambda f: f not in m1 and f in base and f in c2
1076 filt = lambda f: f not in m1 and f in base and f in c2
1052 missingfiles = [f for f in changedfiles if filt(f)]
1077 missingfiles = [f for f in changedfiles if filt(f)]
1053
1078
1054 copies1 = {}
1079 copies1 = {}
1055 if missingfiles:
1080 if missingfiles:
1056 basenametofilename = collections.defaultdict(list)
1081 basenametofilename = collections.defaultdict(list)
1057 dirnametofilename = collections.defaultdict(list)
1082 dirnametofilename = collections.defaultdict(list)
1058
1083
1059 for f in m1.filesnotin(base.manifest()):
1084 for f in m1.filesnotin(base.manifest()):
1060 basename = os.path.basename(f)
1085 basename = os.path.basename(f)
1061 dirname = os.path.dirname(f)
1086 dirname = os.path.dirname(f)
1062 basenametofilename[basename].append(f)
1087 basenametofilename[basename].append(f)
1063 dirnametofilename[dirname].append(f)
1088 dirnametofilename[dirname].append(f)
1064
1089
1065 for f in missingfiles:
1090 for f in missingfiles:
1066 basename = os.path.basename(f)
1091 basename = os.path.basename(f)
1067 dirname = os.path.dirname(f)
1092 dirname = os.path.dirname(f)
1068 samebasename = basenametofilename[basename]
1093 samebasename = basenametofilename[basename]
1069 samedirname = dirnametofilename[dirname]
1094 samedirname = dirnametofilename[dirname]
1070 movecandidates = samebasename + samedirname
1095 movecandidates = samebasename + samedirname
1071 # f is guaranteed to be present in c2, that's why
1096 # f is guaranteed to be present in c2, that's why
1072 # c2.filectx(f) won't fail
1097 # c2.filectx(f) won't fail
1073 f2 = c2.filectx(f)
1098 f2 = c2.filectx(f)
1074 # we can have a lot of candidates which can slow down the heuristics
1099 # we can have a lot of candidates which can slow down the heuristics
1075 # config value to limit the number of candidates moves to check
1100 # config value to limit the number of candidates moves to check
1076 maxcandidates = repo.ui.configint(
1101 maxcandidates = repo.ui.configint(
1077 b'experimental', b'copytrace.movecandidateslimit'
1102 b'experimental', b'copytrace.movecandidateslimit'
1078 )
1103 )
1079
1104
1080 if len(movecandidates) > maxcandidates:
1105 if len(movecandidates) > maxcandidates:
1081 repo.ui.status(
1106 repo.ui.status(
1082 _(
1107 _(
1083 b"skipping copytracing for '%s', more "
1108 b"skipping copytracing for '%s', more "
1084 b"candidates than the limit: %d\n"
1109 b"candidates than the limit: %d\n"
1085 )
1110 )
1086 % (f, len(movecandidates))
1111 % (f, len(movecandidates))
1087 )
1112 )
1088 continue
1113 continue
1089
1114
1090 for candidate in movecandidates:
1115 for candidate in movecandidates:
1091 f1 = c1.filectx(candidate)
1116 f1 = c1.filectx(candidate)
1092 if _related(f1, f2):
1117 if _related(f1, f2):
1093 # if there are a few related copies then we'll merge
1118 # if there are a few related copies then we'll merge
1094 # changes into all of them. This matches the behaviour
1119 # changes into all of them. This matches the behaviour
1095 # of upstream copytracing
1120 # of upstream copytracing
1096 copies1[candidate] = f
1121 copies1[candidate] = f
1097
1122
1098 return branch_copies(copies1), branch_copies(copies2), {}
1123 return branch_copies(copies1), branch_copies(copies2), {}
1099
1124
1100
1125
1101 def _related(f1, f2):
1126 def _related(f1, f2):
1102 """return True if f1 and f2 filectx have a common ancestor
1127 """return True if f1 and f2 filectx have a common ancestor
1103
1128
1104 Walk back to common ancestor to see if the two files originate
1129 Walk back to common ancestor to see if the two files originate
1105 from the same file. Since workingfilectx's rev() is None it messes
1130 from the same file. Since workingfilectx's rev() is None it messes
1106 up the integer comparison logic, hence the pre-step check for
1131 up the integer comparison logic, hence the pre-step check for
1107 None (f1 and f2 can only be workingfilectx's initially).
1132 None (f1 and f2 can only be workingfilectx's initially).
1108 """
1133 """
1109
1134
1110 if f1 == f2:
1135 if f1 == f2:
1111 return True # a match
1136 return True # a match
1112
1137
1113 g1, g2 = f1.ancestors(), f2.ancestors()
1138 g1, g2 = f1.ancestors(), f2.ancestors()
1114 try:
1139 try:
1115 f1r, f2r = f1.linkrev(), f2.linkrev()
1140 f1r, f2r = f1.linkrev(), f2.linkrev()
1116
1141
1117 if f1r is None:
1142 if f1r is None:
1118 f1 = next(g1)
1143 f1 = next(g1)
1119 if f2r is None:
1144 if f2r is None:
1120 f2 = next(g2)
1145 f2 = next(g2)
1121
1146
1122 while True:
1147 while True:
1123 f1r, f2r = f1.linkrev(), f2.linkrev()
1148 f1r, f2r = f1.linkrev(), f2.linkrev()
1124 if f1r > f2r:
1149 if f1r > f2r:
1125 f1 = next(g1)
1150 f1 = next(g1)
1126 elif f2r > f1r:
1151 elif f2r > f1r:
1127 f2 = next(g2)
1152 f2 = next(g2)
1128 else: # f1 and f2 point to files in the same linkrev
1153 else: # f1 and f2 point to files in the same linkrev
1129 return f1 == f2 # true if they point to the same file
1154 return f1 == f2 # true if they point to the same file
1130 except StopIteration:
1155 except StopIteration:
1131 return False
1156 return False
1132
1157
1133
1158
1134 def graftcopies(wctx, ctx, base):
1159 def graftcopies(wctx, ctx, base):
1135 """reproduce copies between base and ctx in the wctx
1160 """reproduce copies between base and ctx in the wctx
1136
1161
1137 Unlike mergecopies(), this function will only consider copies between base
1162 Unlike mergecopies(), this function will only consider copies between base
1138 and ctx; it will ignore copies between base and wctx. Also unlike
1163 and ctx; it will ignore copies between base and wctx. Also unlike
1139 mergecopies(), this function will apply copies to the working copy (instead
1164 mergecopies(), this function will apply copies to the working copy (instead
1140 of just returning information about the copies). That makes it cheaper
1165 of just returning information about the copies). That makes it cheaper
1141 (especially in the common case of base==ctx.p1()) and useful also when
1166 (especially in the common case of base==ctx.p1()) and useful also when
1142 experimental.copytrace=off.
1167 experimental.copytrace=off.
1143
1168
1144 merge.update() will have already marked most copies, but it will only
1169 merge.update() will have already marked most copies, but it will only
1145 mark copies if it thinks the source files are related (see
1170 mark copies if it thinks the source files are related (see
1146 merge._related()). It will also not mark copies if the file wasn't modified
1171 merge._related()). It will also not mark copies if the file wasn't modified
1147 on the local side. This function adds the copies that were "missed"
1172 on the local side. This function adds the copies that were "missed"
1148 by merge.update().
1173 by merge.update().
1149 """
1174 """
1150 new_copies = pathcopies(base, ctx)
1175 new_copies = pathcopies(base, ctx)
1151 _filter(wctx.p1(), wctx, new_copies)
1176 _filter(wctx.p1(), wctx, new_copies)
1152 for dst, src in pycompat.iteritems(new_copies):
1177 for dst, src in pycompat.iteritems(new_copies):
1153 wctx[dst].markcopied(src)
1178 wctx[dst].markcopied(src)
@@ -1,415 +1,567 b''
1 use crate::utils::hg_path::HgPath;
1 use crate::utils::hg_path::HgPath;
2 use crate::utils::hg_path::HgPathBuf;
2 use crate::utils::hg_path::HgPathBuf;
3 use crate::Revision;
3 use crate::Revision;
4
4
5 use im_rc::ordmap::DiffItem;
5 use im_rc::ordmap::DiffItem;
6 use im_rc::ordmap::OrdMap;
6 use im_rc::ordmap::OrdMap;
7
7
8 use std::cmp::Ordering;
8 use std::collections::HashMap;
9 use std::collections::HashMap;
9 use std::collections::HashSet;
10 use std::convert::TryInto;
10
11
11 pub type PathCopies = HashMap<HgPathBuf, HgPathBuf>;
12 pub type PathCopies = HashMap<HgPathBuf, HgPathBuf>;
12
13
13 #[derive(Clone, Debug, PartialEq)]
14 #[derive(Clone, Debug, PartialEq)]
14 struct TimeStampedPathCopy {
15 struct TimeStampedPathCopy {
15 /// revision at which the copy information was added
16 /// revision at which the copy information was added
16 rev: Revision,
17 rev: Revision,
17 /// the copy source, (Set to None in case of deletion of the associated
18 /// the copy source, (Set to None in case of deletion of the associated
18 /// key)
19 /// key)
19 path: Option<HgPathBuf>,
20 path: Option<HgPathBuf>,
20 }
21 }
21
22
22 /// maps CopyDestination to Copy Source (+ a "timestamp" for the operation)
23 /// maps CopyDestination to Copy Source (+ a "timestamp" for the operation)
23 type TimeStampedPathCopies = OrdMap<HgPathBuf, TimeStampedPathCopy>;
24 type TimeStampedPathCopies = OrdMap<HgPathBuf, TimeStampedPathCopy>;
24
25
25 /// hold parent 1, parent 2 and relevant files actions.
26 /// hold parent 1, parent 2 and relevant files actions.
26 pub type RevInfo = (Revision, Revision, ChangedFiles);
27 pub type RevInfo<'a> = (Revision, Revision, ChangedFiles<'a>);
27
28
28 /// represent the files affected by a changesets
29 /// represent the files affected by a changesets
29 ///
30 ///
30 /// This hold a subset of mercurial.metadata.ChangingFiles as we do not need
31 /// This hold a subset of mercurial.metadata.ChangingFiles as we do not need
31 /// all the data categories tracked by it.
32 /// all the data categories tracked by it.
32 pub struct ChangedFiles {
33 /// This hold a subset of mercurial.metadata.ChangingFiles as we do not need
33 removed: HashSet<HgPathBuf>,
34 /// all the data categories tracked by it.
34 merged: HashSet<HgPathBuf>,
35 pub struct ChangedFiles<'a> {
35 salvaged: HashSet<HgPathBuf>,
36 nb_items: u32,
36 copied_from_p1: PathCopies,
37 index: &'a [u8],
37 copied_from_p2: PathCopies,
38 data: &'a [u8],
38 }
39 }
39
40
40 /// Represent active changes that affect the copy tracing.
41 /// Represent active changes that affect the copy tracing.
41 enum Action<'a> {
42 enum Action<'a> {
42 /// The parent ? children edge is removing a file
43 /// The parent ? children edge is removing a file
43 ///
44 ///
44 /// (actually, this could be the edge from the other parent, but it does
45 /// (actually, this could be the edge from the other parent, but it does
45 /// not matters)
46 /// not matters)
46 Removed(&'a HgPath),
47 Removed(&'a HgPath),
47 /// The parent ? children edge introduce copy information between (dest,
48 /// The parent ? children edge introduce copy information between (dest,
48 /// source)
49 /// source)
49 Copied(&'a HgPath, &'a HgPath),
50 Copied(&'a HgPath, &'a HgPath),
50 }
51 }
51
52
52 /// This express the possible "special" case we can get in a merge
53 /// This express the possible "special" case we can get in a merge
53 ///
54 ///
54 /// See mercurial/metadata.py for details on these values.
55 /// See mercurial/metadata.py for details on these values.
55 #[derive(PartialEq)]
56 #[derive(PartialEq)]
56 enum MergeCase {
57 enum MergeCase {
57 /// Merged: file had history on both side that needed to be merged
58 /// Merged: file had history on both side that needed to be merged
58 Merged,
59 Merged,
59 /// Salvaged: file was candidate for deletion, but survived the merge
60 /// Salvaged: file was candidate for deletion, but survived the merge
60 Salvaged,
61 Salvaged,
61 /// Normal: Not one of the two cases above
62 /// Normal: Not one of the two cases above
62 Normal,
63 Normal,
63 }
64 }
64
65
65 impl ChangedFiles {
66 type FileChange<'a> = (u8, &'a HgPath, &'a HgPath);
66 pub fn new(
67
67 removed: HashSet<HgPathBuf>,
68 const EMPTY: &[u8] = b"";
68 merged: HashSet<HgPathBuf>,
69 const COPY_MASK: u8 = 3;
69 salvaged: HashSet<HgPathBuf>,
70 const P1_COPY: u8 = 2;
70 copied_from_p1: PathCopies,
71 const P2_COPY: u8 = 3;
71 copied_from_p2: PathCopies,
72 const ACTION_MASK: u8 = 28;
72 ) -> Self {
73 const REMOVED: u8 = 12;
73 ChangedFiles {
74 const MERGED: u8 = 8;
74 removed,
75 const SALVAGED: u8 = 16;
75 merged,
76
76 salvaged,
77 impl<'a> ChangedFiles<'a> {
77 copied_from_p1,
78 const INDEX_START: usize = 4;
78 copied_from_p2,
79 const ENTRY_SIZE: u32 = 9;
79 }
80 const FILENAME_START: u32 = 1;
81 const COPY_SOURCE_START: u32 = 5;
82
83 pub fn new(data: &'a [u8]) -> Self {
84 assert!(
85 data.len() >= 4,
86 "data size ({}) is too small to contain the header (4)",
87 data.len()
88 );
89 let nb_items_raw: [u8; 4] = (&data[0..=3])
90 .try_into()
91 .expect("failed to turn 4 bytes into 4 bytes");
92 let nb_items = u32::from_be_bytes(nb_items_raw);
93
94 let index_size = (nb_items * Self::ENTRY_SIZE) as usize;
95 let index_end = Self::INDEX_START + index_size;
96
97 assert!(
98 data.len() >= index_end,
99 "data size ({}) is too small to fit the index_data ({})",
100 data.len(),
101 index_end
102 );
103
104 let ret = ChangedFiles {
105 nb_items,
106 index: &data[Self::INDEX_START..index_end],
107 data: &data[index_end..],
108 };
109 let max_data = ret.filename_end(nb_items - 1) as usize;
110 assert!(
111 ret.data.len() >= max_data,
112 "data size ({}) is too small to fit all data ({})",
113 data.len(),
114 index_end + max_data
115 );
116 ret
80 }
117 }
81
118
82 pub fn new_empty() -> Self {
119 pub fn new_empty() -> Self {
83 ChangedFiles {
120 ChangedFiles {
84 removed: HashSet::new(),
121 nb_items: 0,
85 merged: HashSet::new(),
122 index: EMPTY,
86 salvaged: HashSet::new(),
123 data: EMPTY,
87 copied_from_p1: PathCopies::new(),
124 }
88 copied_from_p2: PathCopies::new(),
125 }
126
127 /// internal function to return an individual entry at a given index
128 fn entry(&'a self, idx: u32) -> FileChange<'a> {
129 if idx >= self.nb_items {
130 panic!(
131 "index for entry is higher that the number of file {} >= {}",
132 idx, self.nb_items
133 )
134 }
135 let flags = self.flags(idx);
136 let filename = self.filename(idx);
137 let copy_idx = self.copy_idx(idx);
138 let copy_source = self.filename(copy_idx);
139 (flags, filename, copy_source)
140 }
141
142 /// internal function to return the filename of the entry at a given index
143 fn filename(&self, idx: u32) -> &HgPath {
144 let filename_start;
145 if idx == 0 {
146 filename_start = 0;
147 } else {
148 filename_start = self.filename_end(idx - 1)
89 }
149 }
150 let filename_end = self.filename_end(idx);
151 let filename_start = filename_start as usize;
152 let filename_end = filename_end as usize;
153 HgPath::new(&self.data[filename_start..filename_end])
154 }
155
156 /// internal function to return the flag field of the entry at a given
157 /// index
158 fn flags(&self, idx: u32) -> u8 {
159 let idx = idx as usize;
160 self.index[idx * (Self::ENTRY_SIZE as usize)]
161 }
162
163 /// internal function to return the end of a filename part at a given index
164 fn filename_end(&self, idx: u32) -> u32 {
165 let start = (idx * Self::ENTRY_SIZE) + Self::FILENAME_START;
166 let end = (idx * Self::ENTRY_SIZE) + Self::COPY_SOURCE_START;
167 let start = start as usize;
168 let end = end as usize;
169 let raw = (&self.index[start..end])
170 .try_into()
171 .expect("failed to turn 4 bytes into 4 bytes");
172 u32::from_be_bytes(raw)
173 }
174
175 /// internal function to return index of the copy source of the entry at a
176 /// given index
177 fn copy_idx(&self, idx: u32) -> u32 {
178 let start = (idx * Self::ENTRY_SIZE) + Self::COPY_SOURCE_START;
179 let end = (idx + 1) * Self::ENTRY_SIZE;
180 let start = start as usize;
181 let end = end as usize;
182 let raw = (&self.index[start..end])
183 .try_into()
184 .expect("failed to turn 4 bytes into 4 bytes");
185 u32::from_be_bytes(raw)
90 }
186 }
91
187
92 /// Return an iterator over all the `Action` in this instance.
188 /// Return an iterator over all the `Action` in this instance.
93 fn iter_actions(&self, parent: usize) -> impl Iterator<Item = Action> {
189 fn iter_actions(&self, parent: usize) -> ActionsIterator {
94 let copies_iter = match parent {
190 ActionsIterator {
95 1 => self.copied_from_p1.iter(),
191 changes: &self,
96 2 => self.copied_from_p2.iter(),
192 parent: parent,
97 _ => unreachable!(),
193 current: 0,
98 };
194 }
99 let remove_iter = self.removed.iter();
100 let copies_iter = copies_iter.map(|(x, y)| Action::Copied(x, y));
101 let remove_iter = remove_iter.map(|x| Action::Removed(x));
102 copies_iter.chain(remove_iter)
103 }
195 }
104
196
105 /// return the MergeCase value associated with a filename
197 /// return the MergeCase value associated with a filename
106 fn get_merge_case(&self, path: &HgPath) -> MergeCase {
198 fn get_merge_case(&self, path: &HgPath) -> MergeCase {
107 if self.salvaged.contains(path) {
199 if self.nb_items == 0 {
108 return MergeCase::Salvaged;
109 } else if self.merged.contains(path) {
110 return MergeCase::Merged;
111 } else {
112 return MergeCase::Normal;
200 return MergeCase::Normal;
113 }
201 }
202 let mut low_part = 0;
203 let mut high_part = self.nb_items;
204
205 while low_part < high_part {
206 let cursor = (low_part + high_part - 1) / 2;
207 let (flags, filename, _source) = self.entry(cursor);
208 match path.cmp(filename) {
209 Ordering::Less => low_part = cursor + 1,
210 Ordering::Greater => high_part = cursor,
211 Ordering::Equal => {
212 return match flags & ACTION_MASK {
213 MERGED => MergeCase::Merged,
214 SALVAGED => MergeCase::Salvaged,
215 _ => MergeCase::Normal,
216 };
217 }
218 }
219 }
220 MergeCase::Normal
114 }
221 }
115 }
222 }
116
223
117 /// A struct responsible for answering "is X ancestors of Y" quickly
224 /// A struct responsible for answering "is X ancestors of Y" quickly
118 ///
225 ///
119 /// The structure will delegate ancestors call to a callback, and cache the
226 /// The structure will delegate ancestors call to a callback, and cache the
120 /// result.
227 /// result.
121 #[derive(Debug)]
228 #[derive(Debug)]
122 struct AncestorOracle<'a, A: Fn(Revision, Revision) -> bool> {
229 struct AncestorOracle<'a, A: Fn(Revision, Revision) -> bool> {
123 inner: &'a A,
230 inner: &'a A,
124 pairs: HashMap<(Revision, Revision), bool>,
231 pairs: HashMap<(Revision, Revision), bool>,
125 }
232 }
126
233
127 impl<'a, A: Fn(Revision, Revision) -> bool> AncestorOracle<'a, A> {
234 impl<'a, A: Fn(Revision, Revision) -> bool> AncestorOracle<'a, A> {
128 fn new(func: &'a A) -> Self {
235 fn new(func: &'a A) -> Self {
129 Self {
236 Self {
130 inner: func,
237 inner: func,
131 pairs: HashMap::default(),
238 pairs: HashMap::default(),
132 }
239 }
133 }
240 }
134
241
135 /// returns `true` if `anc` is an ancestors of `desc`, `false` otherwise
242 /// returns `true` if `anc` is an ancestors of `desc`, `false` otherwise
136 fn is_ancestor(&mut self, anc: Revision, desc: Revision) -> bool {
243 fn is_ancestor(&mut self, anc: Revision, desc: Revision) -> bool {
137 if anc > desc {
244 if anc > desc {
138 false
245 false
139 } else if anc == desc {
246 } else if anc == desc {
140 true
247 true
141 } else {
248 } else {
142 if let Some(b) = self.pairs.get(&(anc, desc)) {
249 if let Some(b) = self.pairs.get(&(anc, desc)) {
143 *b
250 *b
144 } else {
251 } else {
145 let b = (self.inner)(anc, desc);
252 let b = (self.inner)(anc, desc);
146 self.pairs.insert((anc, desc), b);
253 self.pairs.insert((anc, desc), b);
147 b
254 b
148 }
255 }
149 }
256 }
150 }
257 }
151 }
258 }
152
259
260 struct ActionsIterator<'a> {
261 changes: &'a ChangedFiles<'a>,
262 parent: usize,
263 current: u32,
264 }
265
266 impl<'a> Iterator for ActionsIterator<'a> {
267 type Item = Action<'a>;
268
269 fn next(&mut self) -> Option<Action<'a>> {
270 while self.current < self.changes.nb_items {
271 let (flags, file, source) = self.changes.entry(self.current);
272 self.current += 1;
273 if (flags & ACTION_MASK) == REMOVED {
274 return Some(Action::Removed(file));
275 }
276 let copy = flags & COPY_MASK;
277 if self.parent == 1 && copy == P1_COPY {
278 return Some(Action::Copied(file, source));
279 }
280 if self.parent == 2 && copy == P2_COPY {
281 return Some(Action::Copied(file, source));
282 }
283 }
284 return None;
285 }
286 }
287
288 /// A small struct whose purpose is to ensure lifetime of bytes referenced in
289 /// ChangedFiles
290 ///
291 /// It is passed to the RevInfoMaker callback who can assign any necessary
292 /// content to the `data` attribute. The copy tracing code is responsible for
293 /// keeping the DataHolder alive at least as long as the ChangedFiles object.
294 pub struct DataHolder<D> {
295 /// RevInfoMaker callback should assign data referenced by the
296 /// ChangedFiles struct it return to this attribute. The DataHolder
297 /// lifetime will be at least as long as the ChangedFiles one.
298 pub data: Option<D>,
299 }
300
301 pub type RevInfoMaker<'a, D> =
302 Box<dyn for<'r> Fn(Revision, &'r mut DataHolder<D>) -> RevInfo<'r> + 'a>;
303
153 /// Same as mercurial.copies._combine_changeset_copies, but in Rust.
304 /// Same as mercurial.copies._combine_changeset_copies, but in Rust.
154 ///
305 ///
155 /// Arguments are:
306 /// Arguments are:
156 ///
307 ///
157 /// revs: all revisions to be considered
308 /// revs: all revisions to be considered
158 /// children: a {parent ? [childrens]} mapping
309 /// children: a {parent ? [childrens]} mapping
159 /// target_rev: the final revision we are combining copies to
310 /// target_rev: the final revision we are combining copies to
160 /// rev_info(rev): callback to get revision information:
311 /// rev_info(rev): callback to get revision information:
161 /// * first parent
312 /// * first parent
162 /// * second parent
313 /// * second parent
163 /// * ChangedFiles
314 /// * ChangedFiles
164 /// isancestors(low_rev, high_rev): callback to check if a revision is an
315 /// isancestors(low_rev, high_rev): callback to check if a revision is an
165 /// ancestor of another
316 /// ancestor of another
166 pub fn combine_changeset_copies<A: Fn(Revision, Revision) -> bool>(
317 pub fn combine_changeset_copies<A: Fn(Revision, Revision) -> bool, D>(
167 revs: Vec<Revision>,
318 revs: Vec<Revision>,
168 children: HashMap<Revision, Vec<Revision>>,
319 children: HashMap<Revision, Vec<Revision>>,
169 target_rev: Revision,
320 target_rev: Revision,
170 rev_info: &impl Fn(Revision) -> RevInfo,
321 rev_info: RevInfoMaker<D>,
171 is_ancestor: &A,
322 is_ancestor: &A,
172 ) -> PathCopies {
323 ) -> PathCopies {
173 let mut all_copies = HashMap::new();
324 let mut all_copies = HashMap::new();
174 let mut oracle = AncestorOracle::new(is_ancestor);
325 let mut oracle = AncestorOracle::new(is_ancestor);
175
326
176 for rev in revs {
327 for rev in revs {
177 // Retrieve data computed in a previous iteration
328 // Retrieve data computed in a previous iteration
178 let copies = all_copies.remove(&rev);
329 let copies = all_copies.remove(&rev);
179 let copies = match copies {
330 let copies = match copies {
180 Some(c) => c,
331 Some(c) => c,
181 None => TimeStampedPathCopies::default(), // root of the walked set
332 None => TimeStampedPathCopies::default(), // root of the walked set
182 };
333 };
183
334
184 let current_children = match children.get(&rev) {
335 let current_children = match children.get(&rev) {
185 Some(c) => c,
336 Some(c) => c,
186 None => panic!("inconsistent `revs` and `children`"),
337 None => panic!("inconsistent `revs` and `children`"),
187 };
338 };
188
339
189 for child in current_children {
340 for child in current_children {
190 // We will chain the copies information accumulated for `rev` with
341 // We will chain the copies information accumulated for `rev` with
191 // the individual copies information for each of its children.
342 // the individual copies information for each of its children.
192 // Creating a new PathCopies for each `rev` ? `children` vertex.
343 // Creating a new PathCopies for each `rev` `children` vertex.
193 let (p1, p2, changes) = rev_info(*child);
344 let mut d: DataHolder<D> = DataHolder { data: None };
345 let (p1, p2, changes) = rev_info(*child, &mut d);
194
346
195 let parent = if rev == p1 {
347 let parent = if rev == p1 {
196 1
348 1
197 } else {
349 } else {
198 assert_eq!(rev, p2);
350 assert_eq!(rev, p2);
199 2
351 2
200 };
352 };
201 let mut new_copies = copies.clone();
353 let mut new_copies = copies.clone();
202
354
203 for action in changes.iter_actions(parent) {
355 for action in changes.iter_actions(parent) {
204 match action {
356 match action {
205 Action::Copied(dest, source) => {
357 Action::Copied(dest, source) => {
206 let entry;
358 let entry;
207 if let Some(v) = copies.get(source) {
359 if let Some(v) = copies.get(source) {
208 entry = match &v.path {
360 entry = match &v.path {
209 Some(path) => Some((*(path)).to_owned()),
361 Some(path) => Some((*(path)).to_owned()),
210 None => Some(source.to_owned()),
362 None => Some(source.to_owned()),
211 }
363 }
212 } else {
364 } else {
213 entry = Some(source.to_owned());
365 entry = Some(source.to_owned());
214 }
366 }
215 // Each new entry is introduced by the children, we
367 // Each new entry is introduced by the children, we
216 // record this information as we will need it to take
368 // record this information as we will need it to take
217 // the right decision when merging conflicting copy
369 // the right decision when merging conflicting copy
218 // information. See merge_copies_dict for details.
370 // information. See merge_copies_dict for details.
219 let ttpc = TimeStampedPathCopy {
371 let ttpc = TimeStampedPathCopy {
220 rev: *child,
372 rev: *child,
221 path: entry,
373 path: entry,
222 };
374 };
223 new_copies.insert(dest.to_owned(), ttpc);
375 new_copies.insert(dest.to_owned(), ttpc);
224 }
376 }
225 Action::Removed(f) => {
377 Action::Removed(f) => {
226 // We must drop copy information for removed file.
378 // We must drop copy information for removed file.
227 //
379 //
228 // We need to explicitly record them as dropped to
380 // We need to explicitly record them as dropped to
229 // propagate this information when merging two
381 // propagate this information when merging two
230 // TimeStampedPathCopies object.
382 // TimeStampedPathCopies object.
231 if new_copies.contains_key(f.as_ref()) {
383 if new_copies.contains_key(f.as_ref()) {
232 let ttpc = TimeStampedPathCopy {
384 let ttpc = TimeStampedPathCopy {
233 rev: *child,
385 rev: *child,
234 path: None,
386 path: None,
235 };
387 };
236 new_copies.insert(f.to_owned(), ttpc);
388 new_copies.insert(f.to_owned(), ttpc);
237 }
389 }
238 }
390 }
239 }
391 }
240 }
392 }
241
393
242 // Merge has two parents needs to combines their copy information.
394 // Merge has two parents needs to combines their copy information.
243 //
395 //
244 // If the vertex from the other parent was already processed, we
396 // If the vertex from the other parent was already processed, we
245 // will have a value for the child ready to be used. We need to
397 // will have a value for the child ready to be used. We need to
246 // grab it and combine it with the one we already
398 // grab it and combine it with the one we already
247 // computed. If not we can simply store the newly
399 // computed. If not we can simply store the newly
248 // computed data. The processing happening at
400 // computed data. The processing happening at
249 // the time of the second parent will take care of combining the
401 // the time of the second parent will take care of combining the
250 // two TimeStampedPathCopies instance.
402 // two TimeStampedPathCopies instance.
251 match all_copies.remove(child) {
403 match all_copies.remove(child) {
252 None => {
404 None => {
253 all_copies.insert(child, new_copies);
405 all_copies.insert(child, new_copies);
254 }
406 }
255 Some(other_copies) => {
407 Some(other_copies) => {
256 let (minor, major) = match parent {
408 let (minor, major) = match parent {
257 1 => (other_copies, new_copies),
409 1 => (other_copies, new_copies),
258 2 => (new_copies, other_copies),
410 2 => (new_copies, other_copies),
259 _ => unreachable!(),
411 _ => unreachable!(),
260 };
412 };
261 let merged_copies =
413 let merged_copies =
262 merge_copies_dict(minor, major, &changes, &mut oracle);
414 merge_copies_dict(minor, major, &changes, &mut oracle);
263 all_copies.insert(child, merged_copies);
415 all_copies.insert(child, merged_copies);
264 }
416 }
265 };
417 };
266 }
418 }
267 }
419 }
268
420
269 // Drop internal information (like the timestamp) and return the final
421 // Drop internal information (like the timestamp) and return the final
270 // mapping.
422 // mapping.
271 let tt_result = all_copies
423 let tt_result = all_copies
272 .remove(&target_rev)
424 .remove(&target_rev)
273 .expect("target revision was not processed");
425 .expect("target revision was not processed");
274 let mut result = PathCopies::default();
426 let mut result = PathCopies::default();
275 for (dest, tt_source) in tt_result {
427 for (dest, tt_source) in tt_result {
276 if let Some(path) = tt_source.path {
428 if let Some(path) = tt_source.path {
277 result.insert(dest, path);
429 result.insert(dest, path);
278 }
430 }
279 }
431 }
280 result
432 result
281 }
433 }
282
434
283 /// merge two copies-mapping together, minor and major
435 /// merge two copies-mapping together, minor and major
284 ///
436 ///
285 /// In case of conflict, value from "major" will be picked, unless in some
437 /// In case of conflict, value from "major" will be picked, unless in some
286 /// cases. See inline documentation for details.
438 /// cases. See inline documentation for details.
287 #[allow(clippy::if_same_then_else)]
439 #[allow(clippy::if_same_then_else)]
288 fn merge_copies_dict<A: Fn(Revision, Revision) -> bool>(
440 fn merge_copies_dict<A: Fn(Revision, Revision) -> bool>(
289 minor: TimeStampedPathCopies,
441 minor: TimeStampedPathCopies,
290 major: TimeStampedPathCopies,
442 major: TimeStampedPathCopies,
291 changes: &ChangedFiles,
443 changes: &ChangedFiles,
292 oracle: &mut AncestorOracle<A>,
444 oracle: &mut AncestorOracle<A>,
293 ) -> TimeStampedPathCopies {
445 ) -> TimeStampedPathCopies {
294 if minor.is_empty() {
446 if minor.is_empty() {
295 return major;
447 return major;
296 } else if major.is_empty() {
448 } else if major.is_empty() {
297 return minor;
449 return minor;
298 }
450 }
299 let mut override_minor = Vec::new();
451 let mut override_minor = Vec::new();
300 let mut override_major = Vec::new();
452 let mut override_major = Vec::new();
301
453
302 let mut to_major = |k: &HgPathBuf, v: &TimeStampedPathCopy| {
454 let mut to_major = |k: &HgPathBuf, v: &TimeStampedPathCopy| {
303 override_major.push((k.clone(), v.clone()))
455 override_major.push((k.clone(), v.clone()))
304 };
456 };
305 let mut to_minor = |k: &HgPathBuf, v: &TimeStampedPathCopy| {
457 let mut to_minor = |k: &HgPathBuf, v: &TimeStampedPathCopy| {
306 override_minor.push((k.clone(), v.clone()))
458 override_minor.push((k.clone(), v.clone()))
307 };
459 };
308
460
309 // The diff function leverage detection of the identical subpart if minor
461 // The diff function leverage detection of the identical subpart if minor
310 // and major has some common ancestors. This make it very fast is most
462 // and major has some common ancestors. This make it very fast is most
311 // case.
463 // case.
312 //
464 //
313 // In case where the two map are vastly different in size, the current
465 // In case where the two map are vastly different in size, the current
314 // approach is still slowish because the iteration will iterate over
466 // approach is still slowish because the iteration will iterate over
315 // all the "exclusive" content of the larger on. This situation can be
467 // all the "exclusive" content of the larger on. This situation can be
316 // frequent when the subgraph of revision we are processing has a lot
468 // frequent when the subgraph of revision we are processing has a lot
317 // of roots. Each roots adding they own fully new map to the mix (and
469 // of roots. Each roots adding they own fully new map to the mix (and
318 // likely a small map, if the path from the root to the "main path" is
470 // likely a small map, if the path from the root to the "main path" is
319 // small.
471 // small.
320 //
472 //
321 // We could do better by detecting such situation and processing them
473 // We could do better by detecting such situation and processing them
322 // differently.
474 // differently.
323 for d in minor.diff(&major) {
475 for d in minor.diff(&major) {
324 match d {
476 match d {
325 DiffItem::Add(k, v) => to_minor(k, v),
477 DiffItem::Add(k, v) => to_minor(k, v),
326 DiffItem::Remove(k, v) => to_major(k, v),
478 DiffItem::Remove(k, v) => to_major(k, v),
327 DiffItem::Update { old, new } => {
479 DiffItem::Update { old, new } => {
328 let (dest, src_major) = new;
480 let (dest, src_major) = new;
329 let (_, src_minor) = old;
481 let (_, src_minor) = old;
330 let mut pick_minor = || (to_major(dest, src_minor));
482 let mut pick_minor = || (to_major(dest, src_minor));
331 let mut pick_major = || (to_minor(dest, src_major));
483 let mut pick_major = || (to_minor(dest, src_major));
332 if src_major.path == src_minor.path {
484 if src_major.path == src_minor.path {
333 // we have the same value, but from other source;
485 // we have the same value, but from other source;
334 if src_major.rev == src_minor.rev {
486 if src_major.rev == src_minor.rev {
335 // If the two entry are identical, no need to do
487 // If the two entry are identical, no need to do
336 // anything (but diff should not have yield them)
488 // anything (but diff should not have yield them)
337 unreachable!();
489 unreachable!();
338 } else if oracle.is_ancestor(src_major.rev, src_minor.rev)
490 } else if oracle.is_ancestor(src_major.rev, src_minor.rev)
339 {
491 {
340 pick_minor();
492 pick_minor();
341 } else {
493 } else {
342 pick_major();
494 pick_major();
343 }
495 }
344 } else if src_major.rev == src_minor.rev {
496 } else if src_major.rev == src_minor.rev {
345 // We cannot get copy information for both p1 and p2 in the
497 // We cannot get copy information for both p1 and p2 in the
346 // same rev. So this is the same value.
498 // same rev. So this is the same value.
347 unreachable!();
499 unreachable!();
348 } else {
500 } else {
349 let action = changes.get_merge_case(&dest);
501 let action = changes.get_merge_case(&dest);
350 if src_major.path.is_none()
502 if src_major.path.is_none()
351 && action == MergeCase::Salvaged
503 && action == MergeCase::Salvaged
352 {
504 {
353 // If the file is "deleted" in the major side but was
505 // If the file is "deleted" in the major side but was
354 // salvaged by the merge, we keep the minor side alive
506 // salvaged by the merge, we keep the minor side alive
355 pick_minor();
507 pick_minor();
356 } else if src_minor.path.is_none()
508 } else if src_minor.path.is_none()
357 && action == MergeCase::Salvaged
509 && action == MergeCase::Salvaged
358 {
510 {
359 // If the file is "deleted" in the minor side but was
511 // If the file is "deleted" in the minor side but was
360 // salvaged by the merge, unconditionnaly preserve the
512 // salvaged by the merge, unconditionnaly preserve the
361 // major side.
513 // major side.
362 pick_major();
514 pick_major();
363 } else if action == MergeCase::Merged {
515 } else if action == MergeCase::Merged {
364 // If the file was actively merged, copy information
516 // If the file was actively merged, copy information
365 // from each side might conflict. The major side will
517 // from each side might conflict. The major side will
366 // win such conflict.
518 // win such conflict.
367 pick_major();
519 pick_major();
368 } else if oracle.is_ancestor(src_major.rev, src_minor.rev)
520 } else if oracle.is_ancestor(src_major.rev, src_minor.rev)
369 {
521 {
370 // If the minor side is strictly newer than the major
522 // If the minor side is strictly newer than the major
371 // side, it should be kept.
523 // side, it should be kept.
372 pick_minor();
524 pick_minor();
373 } else if src_major.path.is_some() {
525 } else if src_major.path.is_some() {
374 // without any special case, the "major" value win
526 // without any special case, the "major" value win
375 // other the "minor" one.
527 // other the "minor" one.
376 pick_major();
528 pick_major();
377 } else if oracle.is_ancestor(src_minor.rev, src_major.rev)
529 } else if oracle.is_ancestor(src_minor.rev, src_major.rev)
378 {
530 {
379 // the "major" rev is a direct ancestors of "minor",
531 // the "major" rev is a direct ancestors of "minor",
380 // any different value should
532 // any different value should
381 // overwrite
533 // overwrite
382 pick_major();
534 pick_major();
383 } else {
535 } else {
384 // major version is None (so the file was deleted on
536 // major version is None (so the file was deleted on
385 // that branch) and that branch is independant (neither
537 // that branch) and that branch is independant (neither
386 // minor nor major is an ancestors of the other one.)
538 // minor nor major is an ancestors of the other one.)
387 // We preserve the new
539 // We preserve the new
388 // information about the new file.
540 // information about the new file.
389 pick_minor();
541 pick_minor();
390 }
542 }
391 }
543 }
392 }
544 }
393 };
545 };
394 }
546 }
395
547
396 let updates;
548 let updates;
397 let mut result;
549 let mut result;
398 if override_major.is_empty() {
550 if override_major.is_empty() {
399 result = major
551 result = major
400 } else if override_minor.is_empty() {
552 } else if override_minor.is_empty() {
401 result = minor
553 result = minor
402 } else {
554 } else {
403 if override_minor.len() < override_major.len() {
555 if override_minor.len() < override_major.len() {
404 updates = override_minor;
556 updates = override_minor;
405 result = minor;
557 result = minor;
406 } else {
558 } else {
407 updates = override_major;
559 updates = override_major;
408 result = major;
560 result = major;
409 }
561 }
410 for (k, v) in updates {
562 for (k, v) in updates {
411 result.insert(k, v);
563 result.insert(k, v);
412 }
564 }
413 }
565 }
414 result
566 result
415 }
567 }
@@ -1,295 +1,153 b''
1 use cpython::ObjectProtocol;
1 use cpython::ObjectProtocol;
2 use cpython::PyBool;
2 use cpython::PyBool;
3 use cpython::PyBytes;
3 use cpython::PyBytes;
4 use cpython::PyDict;
4 use cpython::PyDict;
5 use cpython::PyList;
5 use cpython::PyList;
6 use cpython::PyModule;
6 use cpython::PyModule;
7 use cpython::PyObject;
7 use cpython::PyObject;
8 use cpython::PyResult;
8 use cpython::PyResult;
9 use cpython::PyTuple;
9 use cpython::PyTuple;
10 use cpython::Python;
10 use cpython::Python;
11
11
12 use hg::copy_tracing::combine_changeset_copies;
12 use hg::copy_tracing::combine_changeset_copies;
13 use hg::copy_tracing::ChangedFiles;
13 use hg::copy_tracing::ChangedFiles;
14 use hg::copy_tracing::DataHolder;
14 use hg::copy_tracing::RevInfo;
15 use hg::copy_tracing::RevInfo;
15 use hg::utils::hg_path::HgPathBuf;
16 use hg::copy_tracing::RevInfoMaker;
16 use hg::Revision;
17 use hg::Revision;
17
18
18 /// Combines copies information contained into revision `revs` to build a copy
19 /// Combines copies information contained into revision `revs` to build a copy
19 /// map.
20 /// map.
20 ///
21 ///
21 /// See mercurial/copies.py for details
22 /// See mercurial/copies.py for details
22 pub fn combine_changeset_copies_wrapper(
23 pub fn combine_changeset_copies_wrapper(
23 py: Python,
24 py: Python,
24 revs: PyList,
25 revs: PyList,
25 children: PyDict,
26 children: PyDict,
26 target_rev: Revision,
27 target_rev: Revision,
27 rev_info: PyObject,
28 rev_info: PyObject,
28 is_ancestor: PyObject,
29 is_ancestor: PyObject,
29 ) -> PyResult<PyDict> {
30 ) -> PyResult<PyDict> {
30 let revs: PyResult<_> =
31 let revs: PyResult<_> =
31 revs.iter(py).map(|r| Ok(r.extract(py)?)).collect();
32 revs.iter(py).map(|r| Ok(r.extract(py)?)).collect();
32
33
33 // Wrap the `is_ancestor` python callback as a Rust closure
34 // Wrap the `is_ancestor` python callback as a Rust closure
34 //
35 //
35 // No errors are expected from the Python side, and they will should only
36 // No errors are expected from the Python side, and they will should only
36 // happens in case of programing error or severe data corruption. Such
37 // happens in case of programing error or severe data corruption. Such
37 // errors will raise panic and the rust-cpython harness will turn them into
38 // errors will raise panic and the rust-cpython harness will turn them into
38 // Python exception.
39 // Python exception.
39 let is_ancestor_wrap = |anc: Revision, desc: Revision| -> bool {
40 let is_ancestor_wrap = |anc: Revision, desc: Revision| -> bool {
40 is_ancestor
41 is_ancestor
41 .call(py, (anc, desc), None)
42 .call(py, (anc, desc), None)
42 .expect(
43 .expect(
43 "rust-copy-tracing: python call to `is_ancestor` \
44 "rust-copy-tracing: python call to `is_ancestor` \
44 failed",
45 failed",
45 )
46 )
46 .cast_into::<PyBool>(py)
47 .cast_into::<PyBool>(py)
47 .expect(
48 .expect(
48 "rust-copy-tracing: python call to `is_ancestor` \
49 "rust-copy-tracing: python call to `is_ancestor` \
49 returned unexpected non-Bool value",
50 returned unexpected non-Bool value",
50 )
51 )
51 .is_true()
52 .is_true()
52 };
53 };
53
54
54 // Wrap the `rev_info_maker` python callback as a Rust closure
55 // Wrap the `rev_info_maker` python callback as a Rust closure
55 //
56 //
56 // No errors are expected from the Python side, and they will should only
57 // No errors are expected from the Python side, and they will should only
57 // happens in case of programing error or severe data corruption. Such
58 // happens in case of programing error or severe data corruption. Such
58 // errors will raise panic and the rust-cpython harness will turn them into
59 // errors will raise panic and the rust-cpython harness will turn them into
59 // Python exception.
60 // Python exception.
60 let rev_info_maker = |rev: Revision| -> RevInfo {
61 let rev_info_maker: RevInfoMaker<PyBytes> =
62 Box::new(|rev: Revision, d: &mut DataHolder<PyBytes>| -> RevInfo {
61 let res: PyTuple = rev_info
63 let res: PyTuple = rev_info
62 .call(py, (rev,), None)
64 .call(py, (rev,), None)
63 .expect("rust-copy-tracing: python call to `rev_info` failed")
65 .expect("rust-copy-tracing: python call to `rev_info` failed")
64 .cast_into(py)
66 .cast_into(py)
65 .expect(
67 .expect(
66 "rust-copy_tracing: python call to `rev_info` returned \
68 "rust-copy_tracing: python call to `rev_info` returned \
67 unexpected non-Tuple value",
69 unexpected non-Tuple value",
68 );
70 );
69 let p1 = res.get_item(py, 0).extract(py).expect(
71 let p1 = res.get_item(py, 0).extract(py).expect(
70 "rust-copy-tracing: \
72 "rust-copy-tracing: rev_info return is invalid, first item \
71 rev_info return is invalid, first item is a not a revision",
73 is a not a revision",
72 );
74 );
73 let p2 = res.get_item(py, 1).extract(py).expect(
75 let p2 = res.get_item(py, 1).extract(py).expect(
74 "rust-copy-tracing: \
76 "rust-copy-tracing: rev_info return is invalid, first item \
75 rev_info return is invalid, second item is a not a revision",
77 is a not a revision",
76 );
78 );
77
79
78 let changes = res.get_item(py, 2);
80 let files = match res.get_item(py, 2).extract::<PyBytes>(py) {
79
81 Ok(raw) => {
80 let files;
82 // Give responsability for the raw bytes lifetime to
81 if !changes
83 // hg-core
82 .hasattr(py, "copied_from_p1")
84 d.data = Some(raw);
83 .expect("rust-copy-tracing: python call to `hasattr` failed")
85 let addrs = d.data.as_ref().expect(
84 {
86 "rust-copy-tracing: failed to get a reference to the \
85 files = ChangedFiles::new_empty();
87 raw bytes for copy data").data(py);
86 } else {
88 ChangedFiles::new(addrs)
87 let p1_copies: PyDict = changes
88 .getattr(py, "copied_from_p1")
89 .expect(
90 "rust-copy-tracing: retrieval of python attribute \
91 `copied_from_p1` failed",
92 )
93 .cast_into(py)
94 .expect(
95 "rust-copy-tracing: failed to convert `copied_from_p1` \
96 to PyDict",
97 );
98 let p1_copies: PyResult<_> = p1_copies
99 .items(py)
100 .iter()
101 .map(|(key, value)| {
102 let key = key.extract::<PyBytes>(py).expect(
103 "rust-copy-tracing: conversion of copy destination to\
104 PyBytes failed",
105 );
106 let key = key.data(py);
107 let value = value.extract::<PyBytes>(py).expect(
108 "rust-copy-tracing: conversion of copy source to \
109 PyBytes failed",
110 );
111 let value = value.data(py);
112 Ok((
113 HgPathBuf::from_bytes(key),
114 HgPathBuf::from_bytes(value),
115 ))
116 })
117 .collect();
118
119 let p2_copies: PyDict = changes
120 .getattr(py, "copied_from_p2")
121 .expect(
122 "rust-copy-tracing: retrieval of python attribute \
123 `copied_from_p2` failed",
124 )
125 .cast_into(py)
126 .expect(
127 "rust-copy-tracing: failed to convert `copied_from_p2` \
128 to PyDict",
129 );
130 let p2_copies: PyResult<_> = p2_copies
131 .items(py)
132 .iter()
133 .map(|(key, value)| {
134 let key = key.extract::<PyBytes>(py).expect(
135 "rust-copy-tracing: conversion of copy destination to \
136 PyBytes failed");
137 let key = key.data(py);
138 let value = value.extract::<PyBytes>(py).expect(
139 "rust-copy-tracing: conversion of copy source to \
140 PyBytes failed",
141 );
142 let value = value.data(py);
143 Ok((
144 HgPathBuf::from_bytes(key),
145 HgPathBuf::from_bytes(value),
146 ))
147 })
148 .collect();
149
150 let removed: PyObject = changes.getattr(py, "removed").expect(
151 "rust-copy-tracing: retrieval of python attribute \
152 `removed` failed",
153 );
154 let removed: PyResult<_> = removed
155 .iter(py)
156 .expect(
157 "rust-copy-tracing: getting a python iterator over the \
158 `removed` set failed",
159 )
160 .map(|filename| {
161 let filename = filename
162 .expect(
163 "rust-copy-tracing: python iteration over the \
164 `removed` set failed",
165 )
166 .extract::<PyBytes>(py)
167 .expect(
168 "rust-copy-tracing: \
169 conversion of `removed` item to PyBytes failed",
170 );
171 let filename = filename.data(py);
172 Ok(HgPathBuf::from_bytes(filename))
173 })
174 .collect();
175
176 let merged: PyObject = changes.getattr(py, "merged").expect(
177 "rust-copy-tracing: retrieval of python attribute \
178 `merged` failed",
179 );
180 let merged: PyResult<_> = merged
181 .iter(py)
182 .expect(
183 "rust-copy-tracing: getting a python iterator over the \
184 `merged` set failed",
185 )
186 .map(|filename| {
187 let filename = filename
188 .expect(
189 "rust-copy-tracing: python iteration over the \
190 `merged` set failed",
191 )
192 .extract::<PyBytes>(py)
193 .expect(
194 "rust-copy-tracing: \
195 conversion of `merged` item to PyBytes failed",
196 );
197 let filename = filename.data(py);
198 Ok(HgPathBuf::from_bytes(filename))
199 })
200 .collect();
201
202 let salvaged: PyObject = changes.getattr(py, "salvaged").expect(
203 "rust-copy-tracing: retrieval of python attribute \
204 `salvaged` failed",
205 );
206 let salvaged: PyResult<_> = salvaged
207 .iter(py)
208 .expect(
209 "rust-copy-tracing: getting a python iterator over the \
210 `salvaged` set failed",
211 )
212 .map(|filename| {
213 let filename = filename
214 .expect(
215 "rust-copy-tracing: python iteration over the \
216 `salvaged` set failed",
217 )
218 .extract::<PyBytes>(py)
219 .expect(
220 "rust-copy-tracing: \
221 conversion of `salvaged` item to PyBytes failed",
222 );
223 let filename = filename.data(py);
224 Ok(HgPathBuf::from_bytes(filename))
225 })
226 .collect();
227 files = ChangedFiles::new(
228 removed.unwrap(),
229 merged.unwrap(),
230 salvaged.unwrap(),
231 p1_copies.unwrap(),
232 p2_copies.unwrap(),
233 );
234 }
89 }
90 // value was presumably None, meaning they was no copy data.
91 Err(_) => ChangedFiles::new_empty(),
92 };
235
93
236 (p1, p2, files)
94 (p1, p2, files)
237 };
95 });
238 let children: PyResult<_> = children
96 let children: PyResult<_> = children
239 .items(py)
97 .items(py)
240 .iter()
98 .iter()
241 .map(|(k, v)| {
99 .map(|(k, v)| {
242 let v: &PyList = v.cast_as(py)?;
100 let v: &PyList = v.cast_as(py)?;
243 let v: PyResult<_> =
101 let v: PyResult<_> =
244 v.iter(py).map(|child| Ok(child.extract(py)?)).collect();
102 v.iter(py).map(|child| Ok(child.extract(py)?)).collect();
245 Ok((k.extract(py)?, v?))
103 Ok((k.extract(py)?, v?))
246 })
104 })
247 .collect();
105 .collect();
248
106
249 let res = combine_changeset_copies(
107 let res = combine_changeset_copies(
250 revs?,
108 revs?,
251 children?,
109 children?,
252 target_rev,
110 target_rev,
253 &rev_info_maker,
111 rev_info_maker,
254 &is_ancestor_wrap,
112 &is_ancestor_wrap,
255 );
113 );
256 let out = PyDict::new(py);
114 let out = PyDict::new(py);
257 for (dest, source) in res.into_iter() {
115 for (dest, source) in res.into_iter() {
258 out.set_item(
116 out.set_item(
259 py,
117 py,
260 PyBytes::new(py, &dest.into_vec()),
118 PyBytes::new(py, &dest.into_vec()),
261 PyBytes::new(py, &source.into_vec()),
119 PyBytes::new(py, &source.into_vec()),
262 )?;
120 )?;
263 }
121 }
264 Ok(out)
122 Ok(out)
265 }
123 }
266
124
267 /// Create the module, with `__package__` given from parent
125 /// Create the module, with `__package__` given from parent
268 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
126 pub fn init_module(py: Python, package: &str) -> PyResult<PyModule> {
269 let dotted_name = &format!("{}.copy_tracing", package);
127 let dotted_name = &format!("{}.copy_tracing", package);
270 let m = PyModule::new(py, dotted_name)?;
128 let m = PyModule::new(py, dotted_name)?;
271
129
272 m.add(py, "__package__", package)?;
130 m.add(py, "__package__", package)?;
273 m.add(py, "__doc__", "Copy tracing - Rust implementation")?;
131 m.add(py, "__doc__", "Copy tracing - Rust implementation")?;
274
132
275 m.add(
133 m.add(
276 py,
134 py,
277 "combine_changeset_copies",
135 "combine_changeset_copies",
278 py_fn!(
136 py_fn!(
279 py,
137 py,
280 combine_changeset_copies_wrapper(
138 combine_changeset_copies_wrapper(
281 revs: PyList,
139 revs: PyList,
282 children: PyDict,
140 children: PyDict,
283 target_rev: Revision,
141 target_rev: Revision,
284 rev_info: PyObject,
142 rev_info: PyObject,
285 is_ancestor: PyObject
143 is_ancestor: PyObject
286 )
144 )
287 ),
145 ),
288 )?;
146 )?;
289
147
290 let sys = PyModule::import(py, "sys")?;
148 let sys = PyModule::import(py, "sys")?;
291 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
149 let sys_modules: PyDict = sys.get(py, "modules")?.extract(py)?;
292 sys_modules.set_item(py, dotted_name, &m)?;
150 sys_modules.set_item(py, dotted_name, &m)?;
293
151
294 Ok(m)
152 Ok(m)
295 }
153 }
General Comments 0
You need to be logged in to leave comments. Login now