##// END OF EJS Templates
copies: only calculate 'addedinm[12]' sets once...
Martin von Zweigbergk -
r24187:30219bd4 default
parent child Browse files
Show More
@@ -1,485 +1,483 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 _dirname(f):
11 def _dirname(f):
12 s = f.rfind("/")
12 s = f.rfind("/")
13 if s == -1:
13 if s == -1:
14 return ""
14 return ""
15 return f[:s]
15 return f[:s]
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):
143 def _computeforwardmissing(a, b):
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 return b.manifest().filesnotin(a.manifest())
148 return b.manifest().filesnotin(a.manifest())
149
149
150 def _forwardcopies(a, b):
150 def _forwardcopies(a, b):
151 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
151 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
152
152
153 # check for working copy
153 # check for working copy
154 w = None
154 w = None
155 if b.rev() is None:
155 if b.rev() is None:
156 w = b
156 w = b
157 b = w.p1()
157 b = w.p1()
158 if a == b:
158 if a == b:
159 # short-circuit to avoid issues with merge states
159 # short-circuit to avoid issues with merge states
160 return _dirstatecopies(w)
160 return _dirstatecopies(w)
161
161
162 # files might have to be traced back to the fctx parent of the last
162 # files might have to be traced back to the fctx parent of the last
163 # one-side-only changeset, but not further back than that
163 # one-side-only changeset, but not further back than that
164 limit = _findlimit(a._repo, a.rev(), b.rev())
164 limit = _findlimit(a._repo, a.rev(), b.rev())
165 if limit is None:
165 if limit is None:
166 limit = -1
166 limit = -1
167 am = a.manifest()
167 am = a.manifest()
168
168
169 # find where new files came from
169 # find where new files came from
170 # we currently don't try to find where old files went, too expensive
170 # we currently don't try to find where old files went, too expensive
171 # this means we can miss a case like 'hg rm b; hg cp a b'
171 # this means we can miss a case like 'hg rm b; hg cp a b'
172 cm = {}
172 cm = {}
173 missing = _computeforwardmissing(a, b)
173 missing = _computeforwardmissing(a, b)
174 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
174 ancestrycontext = a._repo.changelog.ancestors([b.rev()], inclusive=True)
175 for f in missing:
175 for f in missing:
176 fctx = b[f]
176 fctx = b[f]
177 fctx._ancestrycontext = ancestrycontext
177 fctx._ancestrycontext = ancestrycontext
178 ofctx = _tracefile(fctx, am, limit)
178 ofctx = _tracefile(fctx, am, limit)
179 if ofctx:
179 if ofctx:
180 cm[f] = ofctx.path()
180 cm[f] = ofctx.path()
181
181
182 # combine copies from dirstate if necessary
182 # combine copies from dirstate if necessary
183 if w is not None:
183 if w is not None:
184 cm = _chain(a, w, cm, _dirstatecopies(w))
184 cm = _chain(a, w, cm, _dirstatecopies(w))
185
185
186 return cm
186 return cm
187
187
188 def _backwardrenames(a, b):
188 def _backwardrenames(a, b):
189 # Even though we're not taking copies into account, 1:n rename situations
189 # Even though we're not taking copies into account, 1:n rename situations
190 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
190 # can still exist (e.g. hg cp a b; hg mv a c). In those cases we
191 # arbitrarily pick one of the renames.
191 # arbitrarily pick one of the renames.
192 f = _forwardcopies(b, a)
192 f = _forwardcopies(b, a)
193 r = {}
193 r = {}
194 for k, v in sorted(f.iteritems()):
194 for k, v in sorted(f.iteritems()):
195 # remove copies
195 # remove copies
196 if v in a:
196 if v in a:
197 continue
197 continue
198 r[v] = k
198 r[v] = k
199 return r
199 return r
200
200
201 def pathcopies(x, y):
201 def pathcopies(x, y):
202 '''find {dst@y: src@x} copy mapping for directed compare'''
202 '''find {dst@y: src@x} copy mapping for directed compare'''
203 if x == y or not x or not y:
203 if x == y or not x or not y:
204 return {}
204 return {}
205 a = y.ancestor(x)
205 a = y.ancestor(x)
206 if a == x:
206 if a == x:
207 return _forwardcopies(x, y)
207 return _forwardcopies(x, y)
208 if a == y:
208 if a == y:
209 return _backwardrenames(x, y)
209 return _backwardrenames(x, y)
210 return _chain(x, y, _backwardrenames(x, a), _forwardcopies(a, y))
210 return _chain(x, y, _backwardrenames(x, a), _forwardcopies(a, y))
211
211
212 def _computenonoverlap(repo, m1, m2, ma):
212 def _computenonoverlap(repo, addedinm1, addedinm2):
213 """Computes the files exclusive to m1 and m2.
213 """Computes, based on addedinm1 and addedinm2, the files exclusive to m1
214 This is its own function so extensions can easily wrap this call to see what
214 and m2. This is its own function so extensions can easily wrap this call
215 files mergecopies is about to process.
215 to see what files mergecopies is about to process.
216 """
216 """
217 addedinm1 = m1.filesnotin(ma)
218 addedinm2 = m2.filesnotin(ma)
219 u1 = sorted(addedinm1 - addedinm2)
217 u1 = sorted(addedinm1 - addedinm2)
220 u2 = sorted(addedinm2 - addedinm1)
218 u2 = sorted(addedinm2 - addedinm1)
221
219
222 if u1:
220 if u1:
223 repo.ui.debug(" unmatched files in local:\n %s\n"
221 repo.ui.debug(" unmatched files in local:\n %s\n"
224 % "\n ".join(u1))
222 % "\n ".join(u1))
225 if u2:
223 if u2:
226 repo.ui.debug(" unmatched files in other:\n %s\n"
224 repo.ui.debug(" unmatched files in other:\n %s\n"
227 % "\n ".join(u2))
225 % "\n ".join(u2))
228 return u1, u2
226 return u1, u2
229
227
230 def mergecopies(repo, c1, c2, ca):
228 def mergecopies(repo, c1, c2, ca):
231 """
229 """
232 Find moves and copies between context c1 and c2 that are relevant
230 Find moves and copies between context c1 and c2 that are relevant
233 for merging.
231 for merging.
234
232
235 Returns four dicts: "copy", "movewithdir", "diverge", and
233 Returns four dicts: "copy", "movewithdir", "diverge", and
236 "renamedelete".
234 "renamedelete".
237
235
238 "copy" is a mapping from destination name -> source name,
236 "copy" is a mapping from destination name -> source name,
239 where source is in c1 and destination is in c2 or vice-versa.
237 where source is in c1 and destination is in c2 or vice-versa.
240
238
241 "movewithdir" is a mapping from source name -> destination name,
239 "movewithdir" is a mapping from source name -> destination name,
242 where the file at source present in one context but not the other
240 where the file at source present in one context but not the other
243 needs to be moved to destination by the merge process, because the
241 needs to be moved to destination by the merge process, because the
244 other context moved the directory it is in.
242 other context moved the directory it is in.
245
243
246 "diverge" is a mapping of source name -> list of destination names
244 "diverge" is a mapping of source name -> list of destination names
247 for divergent renames.
245 for divergent renames.
248
246
249 "renamedelete" is a mapping of source name -> list of destination
247 "renamedelete" is a mapping of source name -> list of destination
250 names for files deleted in c1 that were renamed in c2 or vice-versa.
248 names for files deleted in c1 that were renamed in c2 or vice-versa.
251 """
249 """
252 # avoid silly behavior for update from empty dir
250 # avoid silly behavior for update from empty dir
253 if not c1 or not c2 or c1 == c2:
251 if not c1 or not c2 or c1 == c2:
254 return {}, {}, {}, {}
252 return {}, {}, {}, {}
255
253
256 # avoid silly behavior for parent -> working dir
254 # avoid silly behavior for parent -> working dir
257 if c2.node() is None and c1.node() == repo.dirstate.p1():
255 if c2.node() is None and c1.node() == repo.dirstate.p1():
258 return repo.dirstate.copies(), {}, {}, {}
256 return repo.dirstate.copies(), {}, {}, {}
259
257
260 limit = _findlimit(repo, c1.rev(), c2.rev())
258 limit = _findlimit(repo, c1.rev(), c2.rev())
261 if limit is None:
259 if limit is None:
262 # no common ancestor, no copies
260 # no common ancestor, no copies
263 return {}, {}, {}, {}
261 return {}, {}, {}, {}
264 m1 = c1.manifest()
262 m1 = c1.manifest()
265 m2 = c2.manifest()
263 m2 = c2.manifest()
266 ma = ca.manifest()
264 ma = ca.manifest()
267
265
268 def makectx(f, n):
266 def makectx(f, n):
269 if len(n) != 20: # in a working context?
267 if len(n) != 20: # in a working context?
270 if c1.rev() is None:
268 if c1.rev() is None:
271 return c1.filectx(f)
269 return c1.filectx(f)
272 return c2.filectx(f)
270 return c2.filectx(f)
273 return repo.filectx(f, fileid=n)
271 return repo.filectx(f, fileid=n)
274
272
275 ctx = util.lrucachefunc(makectx)
273 ctx = util.lrucachefunc(makectx)
276 copy = {}
274 copy = {}
277 movewithdir = {}
275 movewithdir = {}
278 fullcopy = {}
276 fullcopy = {}
279 diverge = {}
277 diverge = {}
280
278
281 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
279 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
282
280
283 u1, u2 = _computenonoverlap(repo, m1, m2, ma)
281 addedinm1 = m1.filesnotin(ma)
282 addedinm2 = m2.filesnotin(ma)
283 u1, u2 = _computenonoverlap(repo, addedinm1, addedinm2)
284
284
285 for f in u1:
285 for f in u1:
286 checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy)
286 checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy)
287
287
288 for f in u2:
288 for f in u2:
289 checkcopies(ctx, f, m2, m1, ca, limit, diverge, copy, fullcopy)
289 checkcopies(ctx, f, m2, m1, ca, limit, diverge, copy, fullcopy)
290
290
291 renamedelete = {}
291 renamedelete = {}
292 renamedelete2 = set()
292 renamedelete2 = set()
293 diverge2 = set()
293 diverge2 = set()
294 for of, fl in diverge.items():
294 for of, fl in diverge.items():
295 if len(fl) == 1 or of in c1 or of in c2:
295 if len(fl) == 1 or of in c1 or of in c2:
296 del diverge[of] # not actually divergent, or not a rename
296 del diverge[of] # not actually divergent, or not a rename
297 if of not in c1 and of not in c2:
297 if of not in c1 and of not in c2:
298 # renamed on one side, deleted on the other side, but filter
298 # renamed on one side, deleted on the other side, but filter
299 # out files that have been renamed and then deleted
299 # out files that have been renamed and then deleted
300 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
300 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
301 renamedelete2.update(fl) # reverse map for below
301 renamedelete2.update(fl) # reverse map for below
302 else:
302 else:
303 diverge2.update(fl) # reverse map for below
303 diverge2.update(fl) # reverse map for below
304
304
305 addedinm1 = m1.filesnotin(ma)
306 addedinm2 = m2.filesnotin(ma)
307 bothnew = sorted(addedinm1 & addedinm2)
305 bothnew = sorted(addedinm1 & addedinm2)
308 if bothnew:
306 if bothnew:
309 repo.ui.debug(" unmatched files new in both:\n %s\n"
307 repo.ui.debug(" unmatched files new in both:\n %s\n"
310 % "\n ".join(bothnew))
308 % "\n ".join(bothnew))
311 bothdiverge, _copy, _fullcopy = {}, {}, {}
309 bothdiverge, _copy, _fullcopy = {}, {}, {}
312 for f in bothnew:
310 for f in bothnew:
313 checkcopies(ctx, f, m1, m2, ca, limit, bothdiverge, _copy, _fullcopy)
311 checkcopies(ctx, f, m1, m2, ca, limit, bothdiverge, _copy, _fullcopy)
314 checkcopies(ctx, f, m2, m1, ca, limit, bothdiverge, _copy, _fullcopy)
312 checkcopies(ctx, f, m2, m1, ca, limit, bothdiverge, _copy, _fullcopy)
315 for of, fl in bothdiverge.items():
313 for of, fl in bothdiverge.items():
316 if len(fl) == 2 and fl[0] == fl[1]:
314 if len(fl) == 2 and fl[0] == fl[1]:
317 copy[fl[0]] = of # not actually divergent, just matching renames
315 copy[fl[0]] = of # not actually divergent, just matching renames
318
316
319 if fullcopy and repo.ui.debugflag:
317 if fullcopy and repo.ui.debugflag:
320 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
318 repo.ui.debug(" all copies found (* = to merge, ! = divergent, "
321 "% = renamed and deleted):\n")
319 "% = renamed and deleted):\n")
322 for f in sorted(fullcopy):
320 for f in sorted(fullcopy):
323 note = ""
321 note = ""
324 if f in copy:
322 if f in copy:
325 note += "*"
323 note += "*"
326 if f in diverge2:
324 if f in diverge2:
327 note += "!"
325 note += "!"
328 if f in renamedelete2:
326 if f in renamedelete2:
329 note += "%"
327 note += "%"
330 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
328 repo.ui.debug(" src: '%s' -> dst: '%s' %s\n" % (fullcopy[f], f,
331 note))
329 note))
332 del diverge2
330 del diverge2
333
331
334 if not fullcopy:
332 if not fullcopy:
335 return copy, movewithdir, diverge, renamedelete
333 return copy, movewithdir, diverge, renamedelete
336
334
337 repo.ui.debug(" checking for directory renames\n")
335 repo.ui.debug(" checking for directory renames\n")
338
336
339 # generate a directory move map
337 # generate a directory move map
340 d1, d2 = c1.dirs(), c2.dirs()
338 d1, d2 = c1.dirs(), c2.dirs()
341 d1.addpath('/')
339 d1.addpath('/')
342 d2.addpath('/')
340 d2.addpath('/')
343 invalid = set()
341 invalid = set()
344 dirmove = {}
342 dirmove = {}
345
343
346 # examine each file copy for a potential directory move, which is
344 # examine each file copy for a potential directory move, which is
347 # when all the files in a directory are moved to a new directory
345 # when all the files in a directory are moved to a new directory
348 for dst, src in fullcopy.iteritems():
346 for dst, src in fullcopy.iteritems():
349 dsrc, ddst = _dirname(src), _dirname(dst)
347 dsrc, ddst = _dirname(src), _dirname(dst)
350 if dsrc in invalid:
348 if dsrc in invalid:
351 # already seen to be uninteresting
349 # already seen to be uninteresting
352 continue
350 continue
353 elif dsrc in d1 and ddst in d1:
351 elif dsrc in d1 and ddst in d1:
354 # directory wasn't entirely moved locally
352 # directory wasn't entirely moved locally
355 invalid.add(dsrc)
353 invalid.add(dsrc)
356 elif dsrc in d2 and ddst in d2:
354 elif dsrc in d2 and ddst in d2:
357 # directory wasn't entirely moved remotely
355 # directory wasn't entirely moved remotely
358 invalid.add(dsrc)
356 invalid.add(dsrc)
359 elif dsrc in dirmove and dirmove[dsrc] != ddst:
357 elif dsrc in dirmove and dirmove[dsrc] != ddst:
360 # files from the same directory moved to two different places
358 # files from the same directory moved to two different places
361 invalid.add(dsrc)
359 invalid.add(dsrc)
362 else:
360 else:
363 # looks good so far
361 # looks good so far
364 dirmove[dsrc + "/"] = ddst + "/"
362 dirmove[dsrc + "/"] = ddst + "/"
365
363
366 for i in invalid:
364 for i in invalid:
367 if i in dirmove:
365 if i in dirmove:
368 del dirmove[i]
366 del dirmove[i]
369 del d1, d2, invalid
367 del d1, d2, invalid
370
368
371 if not dirmove:
369 if not dirmove:
372 return copy, movewithdir, diverge, renamedelete
370 return copy, movewithdir, diverge, renamedelete
373
371
374 for d in dirmove:
372 for d in dirmove:
375 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
373 repo.ui.debug(" discovered dir src: '%s' -> dst: '%s'\n" %
376 (d, dirmove[d]))
374 (d, dirmove[d]))
377
375
378 # check unaccounted nonoverlapping files against directory moves
376 # check unaccounted nonoverlapping files against directory moves
379 for f in u1 + u2:
377 for f in u1 + u2:
380 if f not in fullcopy:
378 if f not in fullcopy:
381 for d in dirmove:
379 for d in dirmove:
382 if f.startswith(d):
380 if f.startswith(d):
383 # new file added in a directory that was moved, move it
381 # new file added in a directory that was moved, move it
384 df = dirmove[d] + f[len(d):]
382 df = dirmove[d] + f[len(d):]
385 if df not in copy:
383 if df not in copy:
386 movewithdir[f] = df
384 movewithdir[f] = df
387 repo.ui.debug((" pending file src: '%s' -> "
385 repo.ui.debug((" pending file src: '%s' -> "
388 "dst: '%s'\n") % (f, df))
386 "dst: '%s'\n") % (f, df))
389 break
387 break
390
388
391 return copy, movewithdir, diverge, renamedelete
389 return copy, movewithdir, diverge, renamedelete
392
390
393 def checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy):
391 def checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy):
394 """
392 """
395 check possible copies of f from m1 to m2
393 check possible copies of f from m1 to m2
396
394
397 ctx = function accepting (filename, node) that returns a filectx.
395 ctx = function accepting (filename, node) that returns a filectx.
398 f = the filename to check
396 f = the filename to check
399 m1 = the source manifest
397 m1 = the source manifest
400 m2 = the destination manifest
398 m2 = the destination manifest
401 ca = the changectx of the common ancestor
399 ca = the changectx of the common ancestor
402 limit = the rev number to not search beyond
400 limit = the rev number to not search beyond
403 diverge = record all diverges in this dict
401 diverge = record all diverges in this dict
404 copy = record all non-divergent copies in this dict
402 copy = record all non-divergent copies in this dict
405 fullcopy = record all copies in this dict
403 fullcopy = record all copies in this dict
406 """
404 """
407
405
408 ma = ca.manifest()
406 ma = ca.manifest()
409
407
410 def _related(f1, f2, limit):
408 def _related(f1, f2, limit):
411 # Walk back to common ancestor to see if the two files originate
409 # Walk back to common ancestor to see if the two files originate
412 # from the same file. Since workingfilectx's rev() is None it messes
410 # from the same file. Since workingfilectx's rev() is None it messes
413 # up the integer comparison logic, hence the pre-step check for
411 # up the integer comparison logic, hence the pre-step check for
414 # None (f1 and f2 can only be workingfilectx's initially).
412 # None (f1 and f2 can only be workingfilectx's initially).
415
413
416 if f1 == f2:
414 if f1 == f2:
417 return f1 # a match
415 return f1 # a match
418
416
419 g1, g2 = f1.ancestors(), f2.ancestors()
417 g1, g2 = f1.ancestors(), f2.ancestors()
420 try:
418 try:
421 f1r, f2r = f1.rev(), f2.rev()
419 f1r, f2r = f1.rev(), f2.rev()
422
420
423 if f1r is None:
421 if f1r is None:
424 f1 = g1.next()
422 f1 = g1.next()
425 if f2r is None:
423 if f2r is None:
426 f2 = g2.next()
424 f2 = g2.next()
427
425
428 while True:
426 while True:
429 f1r, f2r = f1.rev(), f2.rev()
427 f1r, f2r = f1.rev(), f2.rev()
430 if f1r > f2r:
428 if f1r > f2r:
431 f1 = g1.next()
429 f1 = g1.next()
432 elif f2r > f1r:
430 elif f2r > f1r:
433 f2 = g2.next()
431 f2 = g2.next()
434 elif f1 == f2:
432 elif f1 == f2:
435 return f1 # a match
433 return f1 # a match
436 elif f1r == f2r or f1r < limit or f2r < limit:
434 elif f1r == f2r or f1r < limit or f2r < limit:
437 return False # copy no longer relevant
435 return False # copy no longer relevant
438 except StopIteration:
436 except StopIteration:
439 return False
437 return False
440
438
441 of = None
439 of = None
442 seen = set([f])
440 seen = set([f])
443 for oc in ctx(f, m1[f]).ancestors():
441 for oc in ctx(f, m1[f]).ancestors():
444 ocr = oc.rev()
442 ocr = oc.rev()
445 of = oc.path()
443 of = oc.path()
446 if of in seen:
444 if of in seen:
447 # check limit late - grab last rename before
445 # check limit late - grab last rename before
448 if ocr < limit:
446 if ocr < limit:
449 break
447 break
450 continue
448 continue
451 seen.add(of)
449 seen.add(of)
452
450
453 fullcopy[f] = of # remember for dir rename detection
451 fullcopy[f] = of # remember for dir rename detection
454 if of not in m2:
452 if of not in m2:
455 continue # no match, keep looking
453 continue # no match, keep looking
456 if m2[of] == ma.get(of):
454 if m2[of] == ma.get(of):
457 break # no merge needed, quit early
455 break # no merge needed, quit early
458 c2 = ctx(of, m2[of])
456 c2 = ctx(of, m2[of])
459 cr = _related(oc, c2, ca.rev())
457 cr = _related(oc, c2, ca.rev())
460 if cr and (of == f or of == c2.path()): # non-divergent
458 if cr and (of == f or of == c2.path()): # non-divergent
461 copy[f] = of
459 copy[f] = of
462 of = None
460 of = None
463 break
461 break
464
462
465 if of in ma:
463 if of in ma:
466 diverge.setdefault(of, []).append(f)
464 diverge.setdefault(of, []).append(f)
467
465
468 def duplicatecopies(repo, rev, fromrev, skiprev=None):
466 def duplicatecopies(repo, rev, fromrev, skiprev=None):
469 '''reproduce copies from fromrev to rev in the dirstate
467 '''reproduce copies from fromrev to rev in the dirstate
470
468
471 If skiprev is specified, it's a revision that should be used to
469 If skiprev is specified, it's a revision that should be used to
472 filter copy records. Any copies that occur between fromrev and
470 filter copy records. Any copies that occur between fromrev and
473 skiprev will not be duplicated, even if they appear in the set of
471 skiprev will not be duplicated, even if they appear in the set of
474 copies between fromrev and rev.
472 copies between fromrev and rev.
475 '''
473 '''
476 exclude = {}
474 exclude = {}
477 if skiprev is not None:
475 if skiprev is not None:
478 exclude = pathcopies(repo[fromrev], repo[skiprev])
476 exclude = pathcopies(repo[fromrev], repo[skiprev])
479 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
477 for dst, src in pathcopies(repo[fromrev], repo[rev]).iteritems():
480 # copies.pathcopies returns backward renames, so dst might not
478 # copies.pathcopies returns backward renames, so dst might not
481 # actually be in the dirstate
479 # actually be in the dirstate
482 if dst in exclude:
480 if dst in exclude:
483 continue
481 continue
484 if repo.dirstate[dst] in "nma":
482 if repo.dirstate[dst] in "nma":
485 repo.dirstate.copy(src, dst)
483 repo.dirstate.copy(src, dst)
General Comments 0
You need to be logged in to leave comments. Login now