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