##// END OF EJS Templates
copies: begin separating mergecopies sides
Matt Mackall -
r26316:d5618e21 default
parent child Browse files
Show More
@@ -1,540 +1,544 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 heapq
10 import heapq
11
11
12 from . import (
12 from . import (
13 pathutil,
13 pathutil,
14 util,
14 util,
15 )
15 )
16
16
17 def _findlimit(repo, a, b):
17 def _findlimit(repo, a, b):
18 """
18 """
19 Find the last revision that needs to be checked to ensure that a full
19 Find the last revision that needs to be checked to ensure that a full
20 transitive closure for file copies can be properly calculated.
20 transitive closure for file copies can be properly calculated.
21 Generally, this means finding the earliest revision number that's an
21 Generally, this means finding the earliest revision number that's an
22 ancestor of a or b but not both, except when a or b is a direct descendent
22 ancestor of a or b but not both, except when a or b is a direct descendent
23 of the other, in which case we can return the minimum revnum of a and b.
23 of the other, in which case we can return the minimum revnum of a and b.
24 None if no such revision exists.
24 None if no such revision exists.
25 """
25 """
26
26
27 # basic idea:
27 # basic idea:
28 # - mark a and b with different sides
28 # - mark a and b with different sides
29 # - if a parent's children are all on the same side, the parent is
29 # - if a parent's children are all on the same side, the parent is
30 # on that side, otherwise it is on no side
30 # on that side, otherwise it is on no side
31 # - walk the graph in topological order with the help of a heap;
31 # - walk the graph in topological order with the help of a heap;
32 # - add unseen parents to side map
32 # - add unseen parents to side map
33 # - clear side of any parent that has children on different sides
33 # - clear side of any parent that has children on different sides
34 # - track number of interesting revs that might still be on a side
34 # - track number of interesting revs that might still be on a side
35 # - track the lowest interesting rev seen
35 # - track the lowest interesting rev seen
36 # - quit when interesting revs is zero
36 # - quit when interesting revs is zero
37
37
38 cl = repo.changelog
38 cl = repo.changelog
39 working = len(cl) # pseudo rev for the working directory
39 working = len(cl) # pseudo rev for the working directory
40 if a is None:
40 if a is None:
41 a = working
41 a = working
42 if b is None:
42 if b is None:
43 b = working
43 b = working
44
44
45 side = {a: -1, b: 1}
45 side = {a: -1, b: 1}
46 visit = [-a, -b]
46 visit = [-a, -b]
47 heapq.heapify(visit)
47 heapq.heapify(visit)
48 interesting = len(visit)
48 interesting = len(visit)
49 hascommonancestor = False
49 hascommonancestor = False
50 limit = working
50 limit = working
51
51
52 while interesting:
52 while interesting:
53 r = -heapq.heappop(visit)
53 r = -heapq.heappop(visit)
54 if r == working:
54 if r == working:
55 parents = [cl.rev(p) for p in repo.dirstate.parents()]
55 parents = [cl.rev(p) for p in repo.dirstate.parents()]
56 else:
56 else:
57 parents = cl.parentrevs(r)
57 parents = cl.parentrevs(r)
58 for p in parents:
58 for p in parents:
59 if p < 0:
59 if p < 0:
60 continue
60 continue
61 if p not in side:
61 if p not in side:
62 # first time we see p; add it to visit
62 # first time we see p; add it to visit
63 side[p] = side[r]
63 side[p] = side[r]
64 if side[p]:
64 if side[p]:
65 interesting += 1
65 interesting += 1
66 heapq.heappush(visit, -p)
66 heapq.heappush(visit, -p)
67 elif side[p] and side[p] != side[r]:
67 elif side[p] and side[p] != side[r]:
68 # p was interesting but now we know better
68 # p was interesting but now we know better
69 side[p] = 0
69 side[p] = 0
70 interesting -= 1
70 interesting -= 1
71 hascommonancestor = True
71 hascommonancestor = True
72 if side[r]:
72 if side[r]:
73 limit = r # lowest rev visited
73 limit = r # lowest rev visited
74 interesting -= 1
74 interesting -= 1
75
75
76 if not hascommonancestor:
76 if not hascommonancestor:
77 return None
77 return None
78
78
79 # Consider the following flow (see test-commit-amend.t under issue4405):
79 # Consider the following flow (see test-commit-amend.t under issue4405):
80 # 1/ File 'a0' committed
80 # 1/ File 'a0' committed
81 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
81 # 2/ File renamed from 'a0' to 'a1' in a new commit (call it 'a1')
82 # 3/ Move back to first commit
82 # 3/ Move back to first commit
83 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
83 # 4/ Create a new commit via revert to contents of 'a1' (call it 'a1-amend')
84 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
84 # 5/ Rename file from 'a1' to 'a2' and commit --amend 'a1-msg'
85 #
85 #
86 # During the amend in step five, we will be in this state:
86 # During the amend in step five, we will be in this state:
87 #
87 #
88 # @ 3 temporary amend commit for a1-amend
88 # @ 3 temporary amend commit for a1-amend
89 # |
89 # |
90 # o 2 a1-amend
90 # o 2 a1-amend
91 # |
91 # |
92 # | o 1 a1
92 # | o 1 a1
93 # |/
93 # |/
94 # o 0 a0
94 # o 0 a0
95 #
95 #
96 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
96 # When _findlimit is called, a and b are revs 3 and 0, so limit will be 2,
97 # yet the filelog has the copy information in rev 1 and we will not look
97 # yet the filelog has the copy information in rev 1 and we will not look
98 # back far enough unless we also look at the a and b as candidates.
98 # back far enough unless we also look at the a and b as candidates.
99 # This only occurs when a is a descendent of b or visa-versa.
99 # This only occurs when a is a descendent of b or visa-versa.
100 return min(limit, a, b)
100 return min(limit, a, b)
101
101
102 def _chain(src, dst, a, b):
102 def _chain(src, dst, a, b):
103 '''chain two sets of copies a->b'''
103 '''chain two sets of copies a->b'''
104 t = a.copy()
104 t = a.copy()
105 for k, v in b.iteritems():
105 for k, v in b.iteritems():
106 if v in t:
106 if v in t:
107 # found a chain
107 # found a chain
108 if t[v] != k:
108 if t[v] != k:
109 # file wasn't renamed back to itself
109 # file wasn't renamed back to itself
110 t[k] = t[v]
110 t[k] = t[v]
111 if v not in dst:
111 if v not in dst:
112 # chain was a rename, not a copy
112 # chain was a rename, not a copy
113 del t[v]
113 del t[v]
114 if v in src:
114 if v in src:
115 # file is a copy of an existing file
115 # file is a copy of an existing file
116 t[k] = v
116 t[k] = v
117
117
118 # remove criss-crossed copies
118 # remove criss-crossed copies
119 for k, v in t.items():
119 for k, v in t.items():
120 if k in src and v in dst:
120 if k in src and v in dst:
121 del t[k]
121 del t[k]
122
122
123 return t
123 return t
124
124
125 def _tracefile(fctx, am, limit=-1):
125 def _tracefile(fctx, am, limit=-1):
126 '''return file context that is the ancestor of fctx present in ancestor
126 '''return file context that is the ancestor of fctx present in ancestor
127 manifest am, stopping after the first ancestor lower than limit'''
127 manifest am, stopping after the first ancestor lower than limit'''
128
128
129 for f in fctx.ancestors():
129 for f in fctx.ancestors():
130 if am.get(f.path(), None) == f.filenode():
130 if am.get(f.path(), None) == f.filenode():
131 return f
131 return f
132 if limit >= 0 and f.linkrev() < limit and f.rev() < limit:
132 if limit >= 0 and f.linkrev() < limit and f.rev() < limit:
133 return None
133 return None
134
134
135 def _dirstatecopies(d):
135 def _dirstatecopies(d):
136 ds = d._repo.dirstate
136 ds = d._repo.dirstate
137 c = ds.copies().copy()
137 c = ds.copies().copy()
138 for k in c.keys():
138 for k in c.keys():
139 if ds[k] not in 'anm':
139 if ds[k] not in 'anm':
140 del c[k]
140 del c[k]
141 return c
141 return c
142
142
143 def _computeforwardmissing(a, b, match=None):
143 def _computeforwardmissing(a, b, match=None):
144 """Computes which files are in b but not a.
144 """Computes which files are in b but not a.
145 This is its own function so extensions can easily wrap this call to see what
145 This is its own function so extensions can easily wrap this call to see what
146 files _forwardcopies is about to process.
146 files _forwardcopies is about to process.
147 """
147 """
148 ma = a.manifest()
148 ma = a.manifest()
149 mb = b.manifest()
149 mb = b.manifest()
150 if match:
150 if match:
151 ma = ma.matches(match)
151 ma = ma.matches(match)
152 mb = mb.matches(match)
152 mb = mb.matches(match)
153 return mb.filesnotin(ma)
153 return mb.filesnotin(ma)
154
154
155 def _forwardcopies(a, b, match=None):
155 def _forwardcopies(a, b, match=None):
156 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
156 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
157
157
158 # check for working copy
158 # check for working copy
159 w = None
159 w = None
160 if b.rev() is None:
160 if b.rev() is None:
161 w = b
161 w = b
162 b = w.p1()
162 b = w.p1()
163 if a == b:
163 if a == b:
164 # short-circuit to avoid issues with merge states
164 # short-circuit to avoid issues with merge states
165 return _dirstatecopies(w)
165 return _dirstatecopies(w)
166
166
167 # files might have to be traced back to the fctx parent of the last
167 # files might have to be traced back to the fctx parent of the last
168 # one-side-only changeset, but not further back than that
168 # one-side-only changeset, but not further back than that
169 limit = _findlimit(a._repo, a.rev(), b.rev())
169 limit = _findlimit(a._repo, a.rev(), b.rev())
170 if limit is None:
170 if limit is None:
171 limit = -1
171 limit = -1
172 am = a.manifest()
172 am = a.manifest()
173
173
174 # find where new files came from
174 # find where new files came from
175 # we currently don't try to find where old files went, too expensive
175 # we currently don't try to find where old files went, too expensive
176 # this means we can miss a case like 'hg rm b; hg cp a b'
176 # this means we can miss a case like 'hg rm b; hg cp a b'
177 cm = {}
177 cm = {}
178 missing = _computeforwardmissing(a, b, match=match)
178 missing = _computeforwardmissing(a, b, match=match)
179 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
179 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
180 for f in missing:
180 for f in missing:
181 fctx = b[f]
181 fctx = b[f]
182 fctx._ancestrycontext = ancestrycontext
182 fctx._ancestrycontext = ancestrycontext
183 ofctx = _tracefile(fctx, am, limit)
183 ofctx = _tracefile(fctx, am, limit)
184 if ofctx:
184 if ofctx:
185 cm[f] = ofctx.path()
185 cm[f] = ofctx.path()
186
186
187 # combine copies from dirstate if necessary
187 # combine copies from dirstate if necessary
188 if w is not None:
188 if w is not None:
189 cm = _chain(a, w, cm, _dirstatecopies(w))
189 cm = _chain(a, w, cm, _dirstatecopies(w))
190
190
191 return cm
191 return cm
192
192
193 def _backwardrenames(a, b):
193 def _backwardrenames(a, b):
194 if a._repo.ui.configbool('experimental', 'disablecopytrace'):
194 if a._repo.ui.configbool('experimental', 'disablecopytrace'):
195 return {}
195 return {}
196
196
197 # Even though we're not taking copies into account, 1:n rename situations
197 # Even though we're not taking copies into account, 1:n rename situations
198 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
198 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
199 # arbitrarily pick one of the renames.
199 # arbitrarily pick one of the renames.
200 f = _forwardcopies(b, a)
200 f = _forwardcopies(b, a)
201 r = {}
201 r = {}
202 for k, v in sorted(f.iteritems()):
202 for k, v in sorted(f.iteritems()):
203 # remove copies
203 # remove copies
204 if v in a:
204 if v in a:
205 continue
205 continue
206 r[v] = k
206 r[v] = k
207 return r
207 return r
208
208
209 def pathcopies(x, y, match=None):
209 def pathcopies(x, y, match=None):
210 '''find {dst@y: src@x} copy mapping for directed compare'''
210 '''find {dst@y: src@x} copy mapping for directed compare'''
211 if x == y or not x or not y:
211 if x == y or not x or not y:
212 return {}
212 return {}
213 a = y.ancestor(x)
213 a = y.ancestor(x)
214 if a == x:
214 if a == x:
215 return _forwardcopies(x, y, match=match)
215 return _forwardcopies(x, y, match=match)
216 if a == y:
216 if a == y:
217 return _backwardrenames(x, y)
217 return _backwardrenames(x, y)
218 return _chain(x, y, _backwardrenames(x, a),
218 return _chain(x, y, _backwardrenames(x, a),
219 _forwardcopies(a, y, match=match))
219 _forwardcopies(a, y, match=match))
220
220
221 def _computenonoverlap(repo, c1, c2, addedinm1, addedinm2):
221 def _computenonoverlap(repo, c1, c2, addedinm1, addedinm2):
222 """Computes, based on addedinm1 and addedinm2, the files exclusive to c1
222 """Computes, based on addedinm1 and addedinm2, the files exclusive to c1
223 and c2. This is its own function so extensions can easily wrap this call
223 and c2. This is its own function so extensions can easily wrap this call
224 to see what files mergecopies is about to process.
224 to see what files mergecopies is about to process.
225
225
226 Even though c1 and c2 are not used in this function, they are useful in
226 Even though c1 and c2 are not used in this function, they are useful in
227 other extensions for being able to read the file nodes of the changed files.
227 other extensions for being able to read the file nodes of the changed files.
228 """
228 """
229 u1 = sorted(addedinm1 - addedinm2)
229 u1 = sorted(addedinm1 - addedinm2)
230 u2 = sorted(addedinm2 - addedinm1)
230 u2 = sorted(addedinm2 - addedinm1)
231
231
232 if u1:
232 if u1:
233 repo.ui.debug(" unmatched files in local:\n %s\n"
233 repo.ui.debug(" unmatched files in local:\n %s\n"
234 % "\n ".join(u1))
234 % "\n ".join(u1))
235 if u2:
235 if u2:
236 repo.ui.debug(" unmatched files in other:\n %s\n"
236 repo.ui.debug(" unmatched files in other:\n %s\n"
237 % "\n ".join(u2))
237 % "\n ".join(u2))
238 return u1, u2
238 return u1, u2
239
239
240 def mergecopies(repo, c1, c2, ca):
240 def mergecopies(repo, c1, c2, ca):
241 """
241 """
242 Find moves and copies between context c1 and c2 that are relevant
242 Find moves and copies between context c1 and c2 that are relevant
243 for merging.
243 for merging.
244
244
245 Returns four dicts: "copy", "movewithdir", "diverge", and
245 Returns four dicts: "copy", "movewithdir", "diverge", and
246 "renamedelete".
246 "renamedelete".
247
247
248 "copy" is a mapping from destination name -> source name,
248 "copy" is a mapping from destination name -> source name,
249 where source is in c1 and destination is in c2 or vice-versa.
249 where source is in c1 and destination is in c2 or vice-versa.
250
250
251 "movewithdir" is a mapping from source name -> destination name,
251 "movewithdir" is a mapping from source name -> destination name,
252 where the file at source present in one context but not the other
252 where the file at source present in one context but not the other
253 needs to be moved to destination by the merge process, because the
253 needs to be moved to destination by the merge process, because the
254 other context moved the directory it is in.
254 other context moved the directory it is in.
255
255
256 "diverge" is a mapping of source name -> list of destination names
256 "diverge" is a mapping of source name -> list of destination names
257 for divergent renames.
257 for divergent renames.
258
258
259 "renamedelete" is a mapping of source name -> list of destination
259 "renamedelete" is a mapping of source name -> list of destination
260 names for files deleted in c1 that were renamed in c2 or vice-versa.
260 names for files deleted in c1 that were renamed in c2 or vice-versa.
261 """
261 """
262 # avoid silly behavior for update from empty dir
262 # avoid silly behavior for update from empty dir
263 if not c1 or not c2 or c1 == c2:
263 if not c1 or not c2 or c1 == c2:
264 return {}, {}, {}, {}
264 return {}, {}, {}, {}
265
265
266 # avoid silly behavior for parent -> working dir
266 # avoid silly behavior for parent -> working dir
267 if c2.node() is None and c1.node() == repo.dirstate.p1():
267 if c2.node() is None and c1.node() == repo.dirstate.p1():
268 return repo.dirstate.copies(), {}, {}, {}
268 return repo.dirstate.copies(), {}, {}, {}
269
269
270 # Copy trace disabling is explicitly below the node == p1 logic above
270 # Copy trace disabling is explicitly below the node == p1 logic above
271 # because the logic above is required for a simple copy to be kept across a
271 # because the logic above is required for a simple copy to be kept across a
272 # rebase.
272 # rebase.
273 if repo.ui.configbool('experimental', 'disablecopytrace'):
273 if repo.ui.configbool('experimental', 'disablecopytrace'):
274 return {}, {}, {}, {}
274 return {}, {}, {}, {}
275
275
276 limit = _findlimit(repo, c1.rev(), c2.rev())
276 limit = _findlimit(repo, c1.rev(), c2.rev())
277 if limit is None:
277 if limit is None:
278 # no common ancestor, no copies
278 # no common ancestor, no copies
279 return {}, {}, {}, {}
279 return {}, {}, {}, {}
280 m1 = c1.manifest()
280 m1 = c1.manifest()
281 m2 = c2.manifest()
281 m2 = c2.manifest()
282 ma = ca.manifest()
282 ma = ca.manifest()
283
283
284
284
285 def setupctx(ctx):
285 def setupctx(ctx):
286 """return a 'getfctx' function suitable for checkcopies usage
286 """return a 'getfctx' function suitable for checkcopies usage
287
287
288 We have to re-setup the function building 'filectx' for each
288 We have to re-setup the function building 'filectx' for each
289 'checkcopies' to ensure the linkrev adjustement is properly setup for
289 'checkcopies' to ensure the linkrev adjustement is properly setup for
290 each. Linkrev adjustment is important to avoid bug in rename
290 each. Linkrev adjustment is important to avoid bug in rename
291 detection. Moreover, having a proper '_ancestrycontext' setup ensures
291 detection. Moreover, having a proper '_ancestrycontext' setup ensures
292 the performance impact of this adjustment is kept limited. Without it,
292 the performance impact of this adjustment is kept limited. Without it,
293 each file could do a full dag traversal making the time complexity of
293 each file could do a full dag traversal making the time complexity of
294 the operation explode (see issue4537).
294 the operation explode (see issue4537).
295
295
296 This function exists here mostly to limit the impact on stable. Feel
296 This function exists here mostly to limit the impact on stable. Feel
297 free to refactor on default.
297 free to refactor on default.
298 """
298 """
299 rev = ctx.rev()
299 rev = ctx.rev()
300 ac = getattr(ctx, '_ancestrycontext', None)
300 ac = getattr(ctx, '_ancestrycontext', None)
301 if ac is None:
301 if ac is None:
302 revs = [rev]
302 revs = [rev]
303 if rev is None:
303 if rev is None:
304 revs = [p.rev() for p in ctx.parents()]
304 revs = [p.rev() for p in ctx.parents()]
305 ac = ctx._repo.changelog.ancestors(revs, inclusive=True)
305 ac = ctx._repo.changelog.ancestors(revs, inclusive=True)
306 ctx._ancestrycontext = ac
306 ctx._ancestrycontext = ac
307 def makectx(f, n):
307 def makectx(f, n):
308 if len(n) != 20: # in a working context?
308 if len(n) != 20: # in a working context?
309 if c1.rev() is None:
309 if c1.rev() is None:
310 return c1.filectx(f)
310 return c1.filectx(f)
311 return c2.filectx(f)
311 return c2.filectx(f)
312 fctx = repo.filectx(f, fileid=n)
312 fctx = repo.filectx(f, fileid=n)
313 # setup only needed for filectx not create from a changectx
313 # setup only needed for filectx not create from a changectx
314 fctx._ancestrycontext = ac
314 fctx._ancestrycontext = ac
315 fctx._descendantrev = rev
315 fctx._descendantrev = rev
316 return fctx
316 return fctx
317 return util.lrucachefunc(makectx)
317 return util.lrucachefunc(makectx)
318
318
319 copy = {}
319 copy1, copy2, = {}, {}
320 movewithdir = {}
320 movewithdir1, movewithdir2 = {}, {}
321 fullcopy = {}
321 fullcopy1, fullcopy2 = {}, {}
322 diverge = {}
322 diverge = {}
323
323
324 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
324 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
325
325
326 addedinm1 = m1.filesnotin(ma)
326 addedinm1 = m1.filesnotin(ma)
327 addedinm2 = m2.filesnotin(ma)
327 addedinm2 = m2.filesnotin(ma)
328 u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
328 u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
329
329
330 for f in u1:
330 for f in u1:
331 getfctx = setupctx(c1)
331 getfctx = setupctx(c1)
332 checkcopies(getfctx, f, m1, m2, ca, limit, diverge, copy, fullcopy)
332 checkcopies(getfctx, f, m1, m2, ca, limit, diverge, copy1, fullcopy1)
333
333
334 for f in u2:
334 for f in u2:
335 getfctx = setupctx(c2)
335 getfctx = setupctx(c2)
336 checkcopies(getfctx, f, m2, m1, ca, limit, diverge, copy, fullcopy)
336 checkcopies(getfctx, f, m2, m1, ca, limit, diverge, copy2, fullcopy2)
337
338 copy = dict(copy1.items() + copy2.items())
339 movewithdir = dict(movewithdir1.items() + movewithdir2.items())
340 fullcopy = dict(fullcopy1.items() + fullcopy2.items())
337
341
338 renamedelete = {}
342 renamedelete = {}
339 renamedelete2 = set()
343 renamedelete2 = set()
340 diverge2 = set()
344 diverge2 = set()
341 for of, fl in diverge.items():
345 for of, fl in diverge.items():
342 if len(fl) == 1 or of in c1 or of in c2:
346 if len(fl) == 1 or of in c1 or of in c2:
343 del diverge[of] # not actually divergent, or not a rename
347 del diverge[of] # not actually divergent, or not a rename
344 if of not in c1 and of not in c2:
348 if of not in c1 and of not in c2:
345 # renamed on one side, deleted on the other side, but filter
349 # renamed on one side, deleted on the other side, but filter
346 # out files that have been renamed and then deleted
350 # out files that have been renamed and then deleted
347 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
351 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
348 renamedelete2.update(fl) # reverse map for below
352 renamedelete2.update(fl) # reverse map for below
349 else:
353 else:
350 diverge2.update(fl) # reverse map for below
354 diverge2.update(fl) # reverse map for below
351
355
352 bothnew = sorted(addedinm1 & addedinm2)
356 bothnew = sorted(addedinm1 & addedinm2)
353 if bothnew:
357 if bothnew:
354 repo.ui.debug(" unmatched files new in both:\n %s\n"
358 repo.ui.debug(" unmatched files new in both:\n %s\n"
355 % "\n ".join(bothnew))
359 % "\n ".join(bothnew))
356 bothdiverge, _copy, _fullcopy = {}, {}, {}
360 bothdiverge, _copy, _fullcopy = {}, {}, {}
357 for f in bothnew:
361 for f in bothnew:
358 getfctx = setupctx(c1)
362 getfctx = setupctx(c1)
359 checkcopies(getfctx, f, m1, m2, ca, limit, bothdiverge,
363 checkcopies(getfctx, f, m1, m2, ca, limit, bothdiverge,
360 _copy, _fullcopy)
364 _copy, _fullcopy)
361 getfctx = setupctx(c2)
365 getfctx = setupctx(c2)
362 checkcopies(getfctx, f, m2, m1, ca, limit, bothdiverge,
366 checkcopies(getfctx, f, m2, m1, ca, limit, bothdiverge,
363 _copy, _fullcopy)
367 _copy, _fullcopy)
364 for of, fl in bothdiverge.items():
368 for of, fl in bothdiverge.items():
365 if len(fl) == 2 and fl[0] == fl[1]:
369 if len(fl) == 2 and fl[0] == fl[1]:
366 copy[fl[0]] = of # not actually divergent, just matching renames
370 copy[fl[0]] = of # not actually divergent, just matching renames
367
371
368 if fullcopy and repo.ui.debugflag:
372 if fullcopy and repo.ui.debugflag:
369 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
373 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
370 "% = renamed and deleted):\n")
374 "% = renamed and deleted):\n")
371 for f in sorted(fullcopy):
375 for f in sorted(fullcopy):
372 note = ""
376 note = ""
373 if f in copy:
377 if f in copy:
374 note += "*"
378 note += "*"
375 if f in diverge2:
379 if f in diverge2:
376 note += "!"
380 note += "!"
377 if f in renamedelete2:
381 if f in renamedelete2:
378 note += "%"
382 note += "%"
379 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
383 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
380 note))
384 note))
381 del diverge2
385 del diverge2
382
386
383 if not fullcopy:
387 if not fullcopy:
384 return copy, movewithdir, diverge, renamedelete
388 return copy, movewithdir, diverge, renamedelete
385
389
386 repo.ui.debug(" checking for directory renames\n")
390 repo.ui.debug(" checking for directory renames\n")
387
391
388 # generate a directory move map
392 # generate a directory move map
389 d1, d2 = c1.dirs(), c2.dirs()
393 d1, d2 = c1.dirs(), c2.dirs()
390 # Hack for adding '', which is not otherwise added, to d1 and d2
394 # Hack for adding '', which is not otherwise added, to d1 and d2
391 d1.addpath('/')
395 d1.addpath('/')
392 d2.addpath('/')
396 d2.addpath('/')
393 invalid = set()
397 invalid = set()
394 dirmove = {}
398 dirmove = {}
395
399
396 # examine each file copy for a potential directory move, which is
400 # examine each file copy for a potential directory move, which is
397 # when all the files in a directory are moved to a new directory
401 # when all the files in a directory are moved to a new directory
398 for dst, src in fullcopy.iteritems():
402 for dst, src in fullcopy.iteritems():
399 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
403 dsrc, ddst = pathutil.dirname(src), pathutil.dirname(dst)
400 if dsrc in invalid:
404 if dsrc in invalid:
401 # already seen to be uninteresting
405 # already seen to be uninteresting
402 continue
406 continue
403 elif dsrc in d1 and ddst in d1:
407 elif dsrc in d1 and ddst in d1:
404 # directory wasn't entirely moved locally
408 # directory wasn't entirely moved locally
405 invalid.add(dsrc)
409 invalid.add(dsrc)
406 elif dsrc in d2 and ddst in d2:
410 elif dsrc in d2 and ddst in d2:
407 # directory wasn't entirely moved remotely
411 # directory wasn't entirely moved remotely
408 invalid.add(dsrc)
412 invalid.add(dsrc)
409 elif dsrc in dirmove and dirmove[dsrc] != ddst:
413 elif dsrc in dirmove and dirmove[dsrc] != ddst:
410 # files from the same directory moved to two different places
414 # files from the same directory moved to two different places
411 invalid.add(dsrc)
415 invalid.add(dsrc)
412 else:
416 else:
413 # looks good so far
417 # looks good so far
414 dirmove[dsrc + "/"] = ddst + "/"
418 dirmove[dsrc + "/"] = ddst + "/"
415
419
416 for i in invalid:
420 for i in invalid:
417 if i in dirmove:
421 if i in dirmove:
418 del dirmove[i]
422 del dirmove[i]
419 del d1, d2, invalid
423 del d1, d2, invalid
420
424
421 if not dirmove:
425 if not dirmove:
422 return copy, movewithdir, diverge, renamedelete
426 return copy, movewithdir, diverge, renamedelete
423
427
424 for d in dirmove:
428 for d in dirmove:
425 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
429 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
426 (d, dirmove[d]))
430 (d, dirmove[d]))
427
431
428 # check unaccounted nonoverlapping files against directory moves
432 # check unaccounted nonoverlapping files against directory moves
429 for f in u1 + u2:
433 for f in u1 + u2:
430 if f not in fullcopy:
434 if f not in fullcopy:
431 for d in dirmove:
435 for d in dirmove:
432 if f.startswith(d):
436 if f.startswith(d):
433 # new file added in a directory that was moved, move it
437 # new file added in a directory that was moved, move it
434 df = dirmove[d] + f[len(d):]
438 df = dirmove[d] + f[len(d):]
435 if df not in copy:
439 if df not in copy:
436 movewithdir[f] = df
440 movewithdir[f] = df
437 repo.ui.debug((" pending file src: '%s' -> "
441 repo.ui.debug((" pending file src: '%s' -> "
438 "dst: '%s'\n") % (f, df))
442 "dst: '%s'\n") % (f, df))
439 break
443 break
440
444
441 return copy, movewithdir, diverge, renamedelete
445 return copy, movewithdir, diverge, renamedelete
442
446
443 def checkcopies(getfctx, f, m1, m2, ca, limit, diverge, copy, fullcopy):
447 def checkcopies(getfctx, f, m1, m2, ca, limit, diverge, copy, fullcopy):
444 """
448 """
445 check possible copies of f from m1 to m2
449 check possible copies of f from m1 to m2
446
450
447 getfctx = function accepting (filename, node) that returns a filectx.
451 getfctx = function accepting (filename, node) that returns a filectx.
448 f = the filename to check
452 f = the filename to check
449 m1 = the source manifest
453 m1 = the source manifest
450 m2 = the destination manifest
454 m2 = the destination manifest
451 ca = the changectx of the common ancestor
455 ca = the changectx of the common ancestor
452 limit = the rev number to not search beyond
456 limit = the rev number to not search beyond
453 diverge = record all diverges in this dict
457 diverge = record all diverges in this dict
454 copy = record all non-divergent copies in this dict
458 copy = record all non-divergent copies in this dict
455 fullcopy = record all copies in this dict
459 fullcopy = record all copies in this dict
456 """
460 """
457
461
458 ma = ca.manifest()
462 ma = ca.manifest()
459
463
460 def _related(f1, f2, limit):
464 def _related(f1, f2, limit):
461 # Walk back to common ancestor to see if the two files originate
465 # Walk back to common ancestor to see if the two files originate
462 # from the same file. Since workingfilectx's rev() is None it messes
466 # from the same file. Since workingfilectx's rev() is None it messes
463 # up the integer comparison logic, hence the pre-step check for
467 # up the integer comparison logic, hence the pre-step check for
464 # None (f1 and f2 can only be workingfilectx's initially).
468 # None (f1 and f2 can only be workingfilectx's initially).
465
469
466 if f1 == f2:
470 if f1 == f2:
467 return f1 # a match
471 return f1 # a match
468
472
469 g1, g2 = f1.ancestors(), f2.ancestors()
473 g1, g2 = f1.ancestors(), f2.ancestors()
470 try:
474 try:
471 f1r, f2r = f1.linkrev(), f2.linkrev()
475 f1r, f2r = f1.linkrev(), f2.linkrev()
472
476
473 if f1r is None:
477 if f1r is None:
474 f1 = g1.next()
478 f1 = g1.next()
475 if f2r is None:
479 if f2r is None:
476 f2 = g2.next()
480 f2 = g2.next()
477
481
478 while True:
482 while True:
479 f1r, f2r = f1.linkrev(), f2.linkrev()
483 f1r, f2r = f1.linkrev(), f2.linkrev()
480 if f1r > f2r:
484 if f1r > f2r:
481 f1 = g1.next()
485 f1 = g1.next()
482 elif f2r > f1r:
486 elif f2r > f1r:
483 f2 = g2.next()
487 f2 = g2.next()
484 elif f1 == f2:
488 elif f1 == f2:
485 return f1 # a match
489 return f1 # a match
486 elif f1r == f2r or f1r < limit or f2r < limit:
490 elif f1r == f2r or f1r < limit or f2r < limit:
487 return False # copy no longer relevant
491 return False # copy no longer relevant
488 except StopIteration:
492 except StopIteration:
489 return False
493 return False
490
494
491 of = None
495 of = None
492 seen = set([f])
496 seen = set([f])
493 for oc in getfctx(f, m1[f]).ancestors():
497 for oc in getfctx(f, m1[f]).ancestors():
494 ocr = oc.linkrev()
498 ocr = oc.linkrev()
495 of = oc.path()
499 of = oc.path()
496 if of in seen:
500 if of in seen:
497 # check limit late - grab last rename before
501 # check limit late - grab last rename before
498 if ocr < limit:
502 if ocr < limit:
499 break
503 break
500 continue
504 continue
501 seen.add(of)
505 seen.add(of)
502
506
503 fullcopy[f] = of # remember for dir rename detection
507 fullcopy[f] = of # remember for dir rename detection
504 if of not in m2:
508 if of not in m2:
505 continue # no match, keep looking
509 continue # no match, keep looking
506 if m2[of] == ma.get(of):
510 if m2[of] == ma.get(of):
507 break # no merge needed, quit early
511 break # no merge needed, quit early
508 c2 = getfctx(of, m2[of])
512 c2 = getfctx(of, m2[of])
509 cr = _related(oc, c2, ca.rev())
513 cr = _related(oc, c2, ca.rev())
510 if cr and (of == f or of == c2.path()): # non-divergent
514 if cr and (of == f or of == c2.path()): # non-divergent
511 copy[f] = of
515 copy[f] = of
512 of = None
516 of = None
513 break
517 break
514
518
515 if of in ma:
519 if of in ma:
516 diverge.setdefault(of, []).append(f)
520 diverge.setdefault(of, []).append(f)
517
521
518 def duplicatecopies(repo, rev, fromrev, skiprev=None):
522 def duplicatecopies(repo, rev, fromrev, skiprev=None):
519 '''reproduce copies from fromrev to rev in the dirstate
523 '''reproduce copies from fromrev to rev in the dirstate
520
524
521 If skiprev is specified, it's a revision that should be used to
525 If skiprev is specified, it's a revision that should be used to
522 filter copy records. Any copies that occur between fromrev and
526 filter copy records. Any copies that occur between fromrev and
523 skiprev will not be duplicated, even if they appear in the set of
527 skiprev will not be duplicated, even if they appear in the set of
524 copies between fromrev and rev.
528 copies between fromrev and rev.
525 '''
529 '''
526 exclude = {}
530 exclude = {}
527 if (skiprev is not None and
531 if (skiprev is not None and
528 not repo.ui.configbool('experimental', 'disablecopytrace')):
532 not repo.ui.configbool('experimental', 'disablecopytrace')):
529 # disablecopytrace skips this line, but not the entire function because
533 # disablecopytrace skips this line, but not the entire function because
530 # the line below is O(size of the repo) during a rebase, while the rest
534 # the line below is O(size of the repo) during a rebase, while the rest
531 # of the function is much faster (and is required for carrying copy
535 # of the function is much faster (and is required for carrying copy
532 # metadata across the rebase anyway).
536 # metadata across the rebase anyway).
533 exclude = pathcopies(repo[fromrev], repo[skiprev])
537 exclude = pathcopies(repo[fromrev], repo[skiprev])
534 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
538 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
535 # copies.pathcopies returns backward renames, so dst might not
539 # copies.pathcopies returns backward renames, so dst might not
536 # actually be in the dirstate
540 # actually be in the dirstate
537 if dst in exclude:
541 if dst in exclude:
538 continue
542 continue
539 if repo.dirstate[dst] in "nma":
543 if repo.dirstate[dst] in "nma":
540 repo.dirstate.copy(src, dst)
544 repo.dirstate.copy(src, dst)
General Comments 0
You need to be logged in to leave comments. Login now