comparison mercurial/merge.py @ 6274:f3f383efbeae

copies: move findcopies code to its own module - pass in contexts - fold symmetricdifference check into copies.copies
author Matt Mackall <mpm@selenic.com>
date Sat, 15 Mar 2008 10:02:31 -0500
parents 20aa460a52b6
children fda369b5779c
comparison
equal deleted inserted replaced
6273:20aa460a52b6 6274:f3f383efbeae
5 # This software may be used and distributed according to the terms 5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference. 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 from node import nullid, nullrev 8 from node import nullid, nullrev
9 from i18n import _ 9 from i18n import _
10 import errno, util, os, heapq, filemerge, ancestor 10 import errno, util, os, filemerge, copies
11 11
12 def _checkunknown(wctx, mctx): 12 def _checkunknown(wctx, mctx):
13 "check for collisions between unknown files and files in mctx" 13 "check for collisions between unknown files and files in mctx"
14 for f in wctx.unknown(): 14 for f in wctx.unknown():
15 if f in mctx and mctx[f].cmp(wctx[f].data()): 15 if f in mctx and mctx[f].cmp(wctx[f].data()):
52 if f not in mctx: 52 if f not in mctx:
53 action.append((f, "f")) 53 action.append((f, "f"))
54 54
55 return action 55 return action
56 56
57 def _nonoverlap(d1, d2, d3):
58 "Return list of elements in d1 not in d2 or d3"
59 l = [d for d in d1 if d not in d3 and d not in d2]
60 l.sort()
61 return l
62
63 def _dirname(f):
64 s = f.rfind("/")
65 if s == -1:
66 return ""
67 return f[:s]
68
69 def _dirs(files):
70 d = {}
71 for f in files:
72 f = _dirname(f)
73 while f not in d:
74 d[f] = True
75 f = _dirname(f)
76 return d
77
78 def _findoldnames(fctx, limit):
79 "find files that path was copied from, back to linkrev limit"
80 old = {}
81 seen = {}
82 orig = fctx.path()
83 visit = [fctx]
84 while visit:
85 fc = visit.pop()
86 s = str(fc)
87 if s in seen:
88 continue
89 seen[s] = 1
90 if fc.path() != orig and fc.path() not in old:
91 old[fc.path()] = 1
92 if fc.rev() < limit:
93 continue
94 visit += fc.parents()
95
96 old = old.keys()
97 old.sort()
98 return old
99
100 def findcopies(repo, m1, m2, ma, limit):
101 """
102 Find moves and copies between m1 and m2 back to limit linkrev
103 """
104
105 wctx = repo.workingctx()
106
107 def makectx(f, n):
108 if len(n) == 20:
109 return repo.filectx(f, fileid=n)
110 return wctx.filectx(f)
111 ctx = util.cachefunc(makectx)
112
113 copy = {}
114 fullcopy = {}
115 diverge = {}
116
117 def checkcopies(f, m1, m2):
118 '''check possible copies of f from m1 to m2'''
119 c1 = ctx(f, m1[f])
120 for of in _findoldnames(c1, limit):
121 fullcopy[f] = of # remember for dir rename detection
122 if of in m2: # original file not in other manifest?
123 # if the original file is unchanged on the other branch,
124 # no merge needed
125 if m2[of] != ma.get(of):
126 c2 = ctx(of, m2[of])
127 ca = c1.ancestor(c2)
128 # related and named changed on only one side?
129 if ca and ca.path() == f or ca.path() == c2.path():
130 if c1 != ca or c2 != ca: # merge needed?
131 copy[f] = of
132 elif of in ma:
133 diverge.setdefault(of, []).append(f)
134
135 if not repo.ui.configbool("merge", "followcopies", True):
136 return {}, {}
137
138 # avoid silly behavior for update from empty dir
139 if not m1 or not m2 or not ma:
140 return {}, {}
141
142 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit)
143
144 u1 = _nonoverlap(m1, m2, ma)
145 u2 = _nonoverlap(m2, m1, ma)
146
147 if u1:
148 repo.ui.debug(_(" unmatched files in local:\n %s\n")
149 % "\n ".join(u1))
150 if u2:
151 repo.ui.debug(_(" unmatched files in other:\n %s\n")
152 % "\n ".join(u2))
153
154 for f in u1:
155 checkcopies(f, m1, m2)
156
157 for f in u2:
158 checkcopies(f, m2, m1)
159
160 diverge2 = {}
161 for of, fl in diverge.items():
162 if len(fl) == 1:
163 del diverge[of] # not actually divergent
164 else:
165 diverge2.update(dict.fromkeys(fl)) # reverse map for below
166
167 if fullcopy:
168 repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n"))
169 for f in fullcopy:
170 note = ""
171 if f in copy: note += "*"
172 if f in diverge2: note += "!"
173 repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note))
174
175 del diverge2
176
177 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
178 return copy, diverge
179
180 repo.ui.debug(_(" checking for directory renames\n"))
181
182 # generate a directory move map
183 d1, d2 = _dirs(m1), _dirs(m2)
184 invalid = {}
185 dirmove = {}
186
187 # examine each file copy for a potential directory move, which is
188 # when all the files in a directory are moved to a new directory
189 for dst, src in fullcopy.items():
190 dsrc, ddst = _dirname(src), _dirname(dst)
191 if dsrc in invalid:
192 # already seen to be uninteresting
193 continue
194 elif dsrc in d1 and ddst in d1:
195 # directory wasn't entirely moved locally
196 invalid[dsrc] = True
197 elif dsrc in d2 and ddst in d2:
198 # directory wasn't entirely moved remotely
199 invalid[dsrc] = True
200 elif dsrc in dirmove and dirmove[dsrc] != ddst:
201 # files from the same directory moved to two different places
202 invalid[dsrc] = True
203 else:
204 # looks good so far
205 dirmove[dsrc + "/"] = ddst + "/"
206
207 for i in invalid:
208 if i in dirmove:
209 del dirmove[i]
210
211 del d1, d2, invalid
212
213 if not dirmove:
214 return copy, diverge
215
216 for d in dirmove:
217 repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d]))
218
219 # check unaccounted nonoverlapping files against directory moves
220 for f in u1 + u2:
221 if f not in fullcopy:
222 for d in dirmove:
223 if f.startswith(d):
224 # new file added in a directory that was moved, move it
225 copy[f] = dirmove[d] + f[len(d):]
226 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f]))
227 break
228
229 return copy, diverge
230
231 def manifestmerge(repo, p1, p2, pa, overwrite, partial): 57 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
232 """ 58 """
233 Merge p1 and p2 with ancestor ma and generate merge action list 59 Merge p1 and p2 with ancestor ma and generate merge action list
234 60
235 overwrite = whether we clobber working files 61 overwrite = whether we clobber working files
243 m1 = p1.manifest() 69 m1 = p1.manifest()
244 m2 = p2.manifest() 70 m2 = p2.manifest()
245 ma = pa.manifest() 71 ma = pa.manifest()
246 backwards = (pa == p2) 72 backwards = (pa == p2)
247 action = [] 73 action = []
248 copy = {} 74 copy, copied, diverge = {}, {}, {}
249 diverge = {}
250 75
251 def fmerge(f, f2=None, fa=None): 76 def fmerge(f, f2=None, fa=None):
252 """merge flags""" 77 """merge flags"""
253 if not f2: 78 if not f2:
254 f2 = f 79 f2 = f
274 def act(msg, m, f, *args): 99 def act(msg, m, f, *args):
275 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m)) 100 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
276 action.append((f, m) + args) 101 action.append((f, m) + args)
277 102
278 if not (backwards or overwrite): 103 if not (backwards or overwrite):
279 rev1 = p1.rev() 104 copy, diverge = copies.copies(repo, p1, p2, pa)
280 if rev1 is None: 105 copied = dict.fromkeys(copy.values())
281 # p1 is a workingctx 106 for of, fl in diverge.items():
282 rev1 = p1.parents()[0].rev() 107 act("divergent renames", "dr", of, fl)
283 pr = repo.changelog.parentrevs
284 def parents(rev):
285 return [p for p in pr(rev) if p != nullrev]
286 limit = min(ancestor.symmetricdifference(rev1, p2.rev(), parents))
287 copy, diverge = findcopies(repo, m1, m2, ma, limit)
288
289 for of, fl in diverge.items():
290 act("divergent renames", "dr", of, fl)
291
292 copied = dict.fromkeys(copy.values())
293 108
294 # Compare manifests 109 # Compare manifests
295 for f, n in m1.iteritems(): 110 for f, n in m1.iteritems():
296 if partial and not partial(f): 111 if partial and not partial(f):
297 continue 112 continue