##// END OF EJS Templates
merge: pull findcopies helpers inside, refactor checkpair to checkcopies
Matt Mackall -
r3732:ffe9fef8 default
parent child Browse files
Show More
@@ -1,417 +1,408
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import gettext as _
10 10 from demandload import *
11 11 demandload(globals(), "errno util os tempfile")
12 12
13 13 def filemerge(repo, fw, fo, wctx, mctx):
14 14 """perform a 3-way merge in the working directory
15 15
16 16 fw = filename in the working directory
17 17 fo = filename in other parent
18 18 wctx, mctx = working and merge changecontexts
19 19 """
20 20
21 21 def temp(prefix, ctx):
22 22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 24 f = os.fdopen(fd, "wb")
25 25 repo.wwrite(ctx.path(), ctx.data(), f)
26 26 f.close()
27 27 return name
28 28
29 29 fcm = wctx.filectx(fw)
30 30 fco = mctx.filectx(fo)
31 31
32 32 if not fco.cmp(fcm.data()): # files identical?
33 33 return None
34 34
35 35 fca = fcm.ancestor(fco)
36 36 if not fca:
37 37 fca = repo.filectx(fw, fileid=nullrev)
38 38 a = repo.wjoin(fw)
39 39 b = temp("base", fca)
40 40 c = temp("other", fco)
41 41
42 42 if fw != fo:
43 43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
44 44 else:
45 45 repo.ui.status(_("merging %s\n") % fw)
46 46
47 47 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
48 48
49 49 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
50 50 or "hgmerge")
51 51 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
52 52 environ={'HG_FILE': fw,
53 53 'HG_MY_NODE': str(wctx.parents()[0]),
54 54 'HG_OTHER_NODE': str(mctx)})
55 55 if r:
56 56 repo.ui.warn(_("merging %s failed!\n") % fw)
57 57
58 58 os.unlink(b)
59 59 os.unlink(c)
60 60 return r
61 61
62 62 def checkunknown(wctx, mctx):
63 63 "check for collisions between unknown files and files in mctx"
64 64 man = mctx.manifest()
65 65 for f in wctx.unknown():
66 66 if f in man:
67 67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
68 68 raise util.Abort(_("untracked local file '%s' differs"\
69 69 " from remote version") % f)
70 70
71 71 def forgetremoved(wctx, mctx):
72 72 """
73 73 Forget removed files
74 74
75 75 If we're jumping between revisions (as opposed to merging), and if
76 76 neither the working directory nor the target rev has the file,
77 77 then we need to remove it from the dirstate, to prevent the
78 78 dirstate from listing the file when it is no longer in the
79 79 manifest.
80 80 """
81 81
82 82 action = []
83 83 man = mctx.manifest()
84 84 for f in wctx.deleted() + wctx.removed():
85 85 if f not in man:
86 86 action.append((f, "f"))
87 87
88 88 return action
89 89
90 def nonoverlap(d1, d2, d3):
91 "Return list of elements in d1 not in d2 or d3"
92
93 l = []
94 for d in d1:
95 if d not in d3 and d not in d2:
96 l.append(d)
97
98 l.sort()
99 return l
100
101 def findold(fctx, limit):
102 "find files that path was copied from, back to linkrev limit"
103
104 old = {}
105 orig = fctx.path()
106 visit = [fctx]
107 while visit:
108 fc = visit.pop()
109 if fc.rev() < limit:
110 continue
111 if fc.path() != orig and fc.path() not in old:
112 old[fc.path()] = 1
113 visit += fc.parents()
114
115 old = old.keys()
116 old.sort()
117 return old
118
119 90 def findcopies(repo, m1, m2, ma, limit):
120 91 """
121 92 Find moves and copies between m1 and m2 back to limit linkrev
122 93 """
123 94
95 def findold(fctx):
96 "find files that path was copied from, back to linkrev limit"
97 old = {}
98 orig = fctx.path()
99 visit = [fctx]
100 while visit:
101 fc = visit.pop()
102 if fc.rev() < limit:
103 continue
104 if fc.path() != orig and fc.path() not in old:
105 old[fc.path()] = 1
106 visit += fc.parents()
107
108 old = old.keys()
109 old.sort()
110 return old
111
112 def nonoverlap(d1, d2, d3):
113 "Return list of elements in d1 not in d2 or d3"
114 l = [d for d in d1 if d not in d3 and d not in d2]
115 l.sort()
116 return l
117
118 def checkcopies(c, man):
119 '''check possible copies for filectx c'''
120 for of in findold(c):
121 if of not in man:
122 return
123 c2 = ctx(of, man[of])
124 ca = c.ancestor(c2)
125 if not ca or c == ca or c2 == ca:
126 return
127 if ca.path() == c.path() or ca.path() == c2.path():
128 copy[c.path()] = of
129
124 130 if not repo.ui.configbool("merge", "followcopies", True):
125 131 return {}
126 132
127 133 # avoid silly behavior for update from empty dir
128 134 if not m1 or not m2 or not ma:
129 135 return {}
130 136
131 137 dcopies = repo.dirstate.copies()
132 138 copy = {}
133 139 u1 = nonoverlap(m1, m2, ma)
134 140 u2 = nonoverlap(m2, m1, ma)
135 141 ctx = util.cachefunc(lambda f, n: repo.filectx(f, fileid=n[:20]))
136 142
137 def checkpair(c, f2, man):
138 ''' check if an apparent pair actually matches '''
139 if f2 not in man:
140 return
141 c2 = ctx(f2, man[f2])
142 ca = c.ancestor(c2)
143 if not ca or c == ca or c2 == ca:
144 return
145 if ca.path() == c.path() or ca.path() == c2.path():
146 copy[c.path()] = f2
147
148 143 for f in u1:
149 c = ctx(dcopies.get(f, f), m1[f])
150 for of in findold(c, limit):
151 checkpair(c, of, m2)
144 checkcopies(ctx(dcopies.get(f, f), m1[f]), m2)
152 145
153 146 for f in u2:
154 c = ctx(f, m2[f])
155 for of in findold(c, limit):
156 checkpair(c, of, m1)
147 checkcopies(ctx(f, m2[f]), m1)
157 148
158 149 return copy
159 150
160 151 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
161 152 """
162 153 Merge p1 and p2 with ancestor ma and generate merge action list
163 154
164 155 overwrite = whether we clobber working files
165 156 partial = function to filter file lists
166 157 """
167 158
168 159 repo.ui.note(_("resolving manifests\n"))
169 160 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
170 161 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
171 162
172 163 m1 = p1.manifest()
173 164 m2 = p2.manifest()
174 165 ma = pa.manifest()
175 166 backwards = (pa == p2)
176 167 action = []
177 168 copy = {}
178 169
179 170 def fmerge(f, f2=None, fa=None):
180 171 """merge executable flags"""
181 172 if not f2:
182 173 f2 = f
183 174 fa = f
184 175 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
185 176 return ((a^b) | (a^c)) ^ a
186 177
187 178 def act(msg, m, f, *args):
188 179 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
189 180 action.append((f, m) + args)
190 181
191 182 if not (backwards or overwrite):
192 183 copy = findcopies(repo, m1, m2, ma, pa.rev())
193 184 copied = dict.fromkeys(copy.values())
194 185
195 186 # Compare manifests
196 187 for f, n in m1.iteritems():
197 188 if partial and not partial(f):
198 189 continue
199 190 if f in m2:
200 191 # are files different?
201 192 if n != m2[f]:
202 193 a = ma.get(f, nullid)
203 194 # are both different from the ancestor?
204 195 if not overwrite and n != a and m2[f] != a:
205 196 act("versions differ", "m", f, f, f, fmerge(f), False)
206 197 # are we clobbering?
207 198 # is remote's version newer?
208 199 # or are we going back in time and clean?
209 200 elif overwrite or m2[f] != a or (backwards and not n[20:]):
210 201 act("remote is newer", "g", f, m2.execf(f))
211 202 # local is newer, not overwrite, check mode bits
212 203 elif fmerge(f) != m1.execf(f):
213 204 act("update permissions", "e", f, m2.execf(f))
214 205 # contents same, check mode bits
215 206 elif m1.execf(f) != m2.execf(f):
216 207 if overwrite or fmerge(f) != m1.execf(f):
217 208 act("update permissions", "e", f, m2.execf(f))
218 209 elif f in copied:
219 210 continue
220 211 elif f in copy:
221 212 f2 = copy[f]
222 213 if f2 in m1: # case 2 A,B/B/B
223 214 act("local copied to " + f2, "m",
224 215 f, f2, f, fmerge(f, f2, f2), False)
225 216 else: # case 4,21 A/B/B
226 217 act("local moved to " + f2, "m",
227 218 f, f2, f, fmerge(f, f2, f2), False)
228 219 elif f in ma:
229 220 if n != ma[f] and not overwrite:
230 221 if repo.ui.prompt(
231 222 (_(" local changed %s which remote deleted\n") % f) +
232 223 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
233 224 act("prompt delete", "r", f)
234 225 else:
235 226 act("other deleted", "r", f)
236 227 else:
237 228 # file is created on branch or in working directory
238 229 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
239 230 act("remote deleted", "r", f)
240 231
241 232 for f, n in m2.iteritems():
242 233 if partial and not partial(f):
243 234 continue
244 235 if f in m1:
245 236 continue
246 237 if f in copied:
247 238 continue
248 239 if f in copy:
249 240 f2 = copy[f]
250 241 if f2 in m2: # rename case 1, A/A,B/A
251 242 act("remote copied to " + f, "m",
252 243 f2, f, f, fmerge(f2, f, f2), False)
253 244 else: # case 3,20 A/B/A
254 245 act("remote moved to " + f, "m",
255 246 f2, f, f, fmerge(f2, f, f2), True)
256 247 elif f in ma:
257 248 if overwrite or backwards:
258 249 act("recreating", "g", f, m2.execf(f))
259 250 elif n != ma[f]:
260 251 if repo.ui.prompt(
261 252 (_("remote changed %s which local deleted\n") % f) +
262 253 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
263 254 act("prompt recreating", "g", f, m2.execf(f))
264 255 else:
265 256 act("remote created", "g", f, m2.execf(f))
266 257
267 258 return action
268 259
269 260 def applyupdates(repo, action, wctx, mctx):
270 261 "apply the merge action list to the working directory"
271 262
272 263 updated, merged, removed, unresolved = 0, 0, 0, 0
273 264 action.sort()
274 265 for a in action:
275 266 f, m = a[:2]
276 267 if f[0] == "/":
277 268 continue
278 269 if m == "r": # remove
279 270 repo.ui.note(_("removing %s\n") % f)
280 271 util.audit_path(f)
281 272 try:
282 273 util.unlink(repo.wjoin(f))
283 274 except OSError, inst:
284 275 if inst.errno != errno.ENOENT:
285 276 repo.ui.warn(_("update failed to remove %s: %s!\n") %
286 277 (f, inst.strerror))
287 278 removed += 1
288 279 elif m == "m": # merge
289 280 f2, fd, flag, move = a[2:]
290 281 r = filemerge(repo, f, f2, wctx, mctx)
291 282 if r > 0:
292 283 unresolved += 1
293 284 else:
294 285 if r is None:
295 286 updated += 1
296 287 else:
297 288 merged += 1
298 289 if f != fd:
299 290 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
300 291 repo.wwrite(fd, repo.wread(f))
301 292 if move:
302 293 repo.ui.debug(_("removing %s\n") % f)
303 294 os.unlink(repo.wjoin(f))
304 295 util.set_exec(repo.wjoin(fd), flag)
305 296 elif m == "g": # get
306 297 flag = a[2]
307 298 repo.ui.note(_("getting %s\n") % f)
308 299 t = mctx.filectx(f).data()
309 300 repo.wwrite(f, t)
310 301 util.set_exec(repo.wjoin(f), flag)
311 302 updated += 1
312 303 elif m == "e": # exec
313 304 flag = a[2]
314 305 util.set_exec(repo.wjoin(f), flag)
315 306
316 307 return updated, merged, removed, unresolved
317 308
318 309 def recordupdates(repo, action, branchmerge):
319 310 "record merge actions to the dirstate"
320 311
321 312 for a in action:
322 313 f, m = a[:2]
323 314 if m == "r": # remove
324 315 if branchmerge:
325 316 repo.dirstate.update([f], 'r')
326 317 else:
327 318 repo.dirstate.forget([f])
328 319 elif m == "f": # forget
329 320 repo.dirstate.forget([f])
330 321 elif m == "g": # get
331 322 if branchmerge:
332 323 repo.dirstate.update([f], 'n', st_mtime=-1)
333 324 else:
334 325 repo.dirstate.update([f], 'n')
335 326 elif m == "m": # merge
336 327 f2, fd, flag, move = a[2:]
337 328 if branchmerge:
338 329 # We've done a branch merge, mark this file as merged
339 330 # so that we properly record the merger later
340 331 repo.dirstate.update([fd], 'm')
341 332 if f != f2: # copy/rename
342 333 if move:
343 334 repo.dirstate.update([f], 'r')
344 335 if f != fd:
345 336 repo.dirstate.copy(f, fd)
346 337 else:
347 338 repo.dirstate.copy(f2, fd)
348 339 else:
349 340 # We've update-merged a locally modified file, so
350 341 # we set the dirstate to emulate a normal checkout
351 342 # of that file some time in the past. Thus our
352 343 # merge will appear as a normal local file
353 344 # modification.
354 345 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
355 346 if move:
356 347 repo.dirstate.forget([f])
357 348
358 349 def update(repo, node, branchmerge, force, partial, wlock):
359 350 """
360 351 Perform a merge between the working directory and the given node
361 352
362 353 branchmerge = whether to merge between branches
363 354 force = whether to force branch merging or file overwriting
364 355 partial = a function to filter file lists (dirstate not updated)
365 356 wlock = working dir lock, if already held
366 357 """
367 358
368 359 if not wlock:
369 360 wlock = repo.wlock()
370 361
371 362 overwrite = force and not branchmerge
372 363 forcemerge = force and branchmerge
373 364 wc = repo.workingctx()
374 365 pl = wc.parents()
375 366 p1, p2 = pl[0], repo.changectx(node)
376 367 pa = p1.ancestor(p2)
377 368 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
378 369
379 370 ### check phase
380 371 if not overwrite and len(pl) > 1:
381 372 raise util.Abort(_("outstanding uncommitted merges"))
382 373 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
383 374 if branchmerge:
384 375 raise util.Abort(_("there is nothing to merge, just use "
385 376 "'hg update' or look at 'hg heads'"))
386 377 elif not (overwrite or branchmerge):
387 378 raise util.Abort(_("update spans branches, use 'hg merge' "
388 379 "or 'hg update -C' to lose changes"))
389 380 if branchmerge and not forcemerge:
390 381 if wc.files():
391 382 raise util.Abort(_("outstanding uncommitted changes"))
392 383
393 384 ### calculate phase
394 385 action = []
395 386 if not force:
396 387 checkunknown(wc, p2)
397 388 if not branchmerge:
398 389 action += forgetremoved(wc, p2)
399 390 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
400 391
401 392 ### apply phase
402 393 if not branchmerge: # just jump to the new rev
403 394 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
404 395 if not partial:
405 396 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
406 397
407 398 stats = applyupdates(repo, action, wc, p2)
408 399
409 400 if not partial:
410 401 recordupdates(repo, action, branchmerge)
411 402 repo.dirstate.setparents(fp1, fp2)
412 403 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
413 404 if not branchmerge:
414 405 repo.opener("branch", "w").write(p2.branch() + "\n")
415 406
416 407 return stats
417 408
General Comments 0
You need to be logged in to leave comments. Login now