##// END OF EJS Templates
merge: move check for empty ancestor into findcopies
Matt Mackall -
r3731:b4af5f92 default
parent child Browse files
Show More
@@ -1,417 +1,417 b''
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 90 def nonoverlap(d1, d2, d3):
91 91 "Return list of elements in d1 not in d2 or d3"
92 92
93 93 l = []
94 94 for d in d1:
95 95 if d not in d3 and d not in d2:
96 96 l.append(d)
97 97
98 98 l.sort()
99 99 return l
100 100
101 101 def findold(fctx, limit):
102 102 "find files that path was copied from, back to linkrev limit"
103 103
104 104 old = {}
105 105 orig = fctx.path()
106 106 visit = [fctx]
107 107 while visit:
108 108 fc = visit.pop()
109 109 if fc.rev() < limit:
110 110 continue
111 111 if fc.path() != orig and fc.path() not in old:
112 112 old[fc.path()] = 1
113 113 visit += fc.parents()
114 114
115 115 old = old.keys()
116 116 old.sort()
117 117 return old
118 118
119 119 def findcopies(repo, m1, m2, ma, limit):
120 120 """
121 121 Find moves and copies between m1 and m2 back to limit linkrev
122 122 """
123 123
124 124 if not repo.ui.configbool("merge", "followcopies", True):
125 125 return {}
126 126
127 127 # avoid silly behavior for update from empty dir
128 if not m1:
128 if not m1 or not m2 or not ma:
129 129 return {}
130 130
131 131 dcopies = repo.dirstate.copies()
132 132 copy = {}
133 133 u1 = nonoverlap(m1, m2, ma)
134 134 u2 = nonoverlap(m2, m1, ma)
135 135 ctx = util.cachefunc(lambda f, n: repo.filectx(f, fileid=n[:20]))
136 136
137 137 def checkpair(c, f2, man):
138 138 ''' check if an apparent pair actually matches '''
139 139 if f2 not in man:
140 140 return
141 141 c2 = ctx(f2, man[f2])
142 142 ca = c.ancestor(c2)
143 143 if not ca or c == ca or c2 == ca:
144 144 return
145 145 if ca.path() == c.path() or ca.path() == c2.path():
146 146 copy[c.path()] = f2
147 147
148 148 for f in u1:
149 149 c = ctx(dcopies.get(f, f), m1[f])
150 150 for of in findold(c, limit):
151 151 checkpair(c, of, m2)
152 152
153 153 for f in u2:
154 154 c = ctx(f, m2[f])
155 155 for of in findold(c, limit):
156 156 checkpair(c, of, m1)
157 157
158 158 return copy
159 159
160 160 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
161 161 """
162 162 Merge p1 and p2 with ancestor ma and generate merge action list
163 163
164 164 overwrite = whether we clobber working files
165 165 partial = function to filter file lists
166 166 """
167 167
168 168 repo.ui.note(_("resolving manifests\n"))
169 169 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
170 170 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
171 171
172 172 m1 = p1.manifest()
173 173 m2 = p2.manifest()
174 174 ma = pa.manifest()
175 175 backwards = (pa == p2)
176 176 action = []
177 177 copy = {}
178 178
179 179 def fmerge(f, f2=None, fa=None):
180 180 """merge executable flags"""
181 181 if not f2:
182 182 f2 = f
183 183 fa = f
184 184 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
185 185 return ((a^b) | (a^c)) ^ a
186 186
187 187 def act(msg, m, f, *args):
188 188 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
189 189 action.append((f, m) + args)
190 190
191 if pa and not (backwards or overwrite):
191 if not (backwards or overwrite):
192 192 copy = findcopies(repo, m1, m2, ma, pa.rev())
193 193 copied = dict.fromkeys(copy.values())
194 194
195 195 # Compare manifests
196 196 for f, n in m1.iteritems():
197 197 if partial and not partial(f):
198 198 continue
199 199 if f in m2:
200 200 # are files different?
201 201 if n != m2[f]:
202 202 a = ma.get(f, nullid)
203 203 # are both different from the ancestor?
204 204 if not overwrite and n != a and m2[f] != a:
205 205 act("versions differ", "m", f, f, f, fmerge(f), False)
206 206 # are we clobbering?
207 207 # is remote's version newer?
208 208 # or are we going back in time and clean?
209 209 elif overwrite or m2[f] != a or (backwards and not n[20:]):
210 210 act("remote is newer", "g", f, m2.execf(f))
211 211 # local is newer, not overwrite, check mode bits
212 212 elif fmerge(f) != m1.execf(f):
213 213 act("update permissions", "e", f, m2.execf(f))
214 214 # contents same, check mode bits
215 215 elif m1.execf(f) != m2.execf(f):
216 216 if overwrite or fmerge(f) != m1.execf(f):
217 217 act("update permissions", "e", f, m2.execf(f))
218 218 elif f in copied:
219 219 continue
220 220 elif f in copy:
221 221 f2 = copy[f]
222 222 if f2 in m1: # case 2 A,B/B/B
223 223 act("local copied to " + f2, "m",
224 224 f, f2, f, fmerge(f, f2, f2), False)
225 225 else: # case 4,21 A/B/B
226 226 act("local moved to " + f2, "m",
227 227 f, f2, f, fmerge(f, f2, f2), False)
228 228 elif f in ma:
229 229 if n != ma[f] and not overwrite:
230 230 if repo.ui.prompt(
231 231 (_(" local changed %s which remote deleted\n") % f) +
232 232 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
233 233 act("prompt delete", "r", f)
234 234 else:
235 235 act("other deleted", "r", f)
236 236 else:
237 237 # file is created on branch or in working directory
238 238 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
239 239 act("remote deleted", "r", f)
240 240
241 241 for f, n in m2.iteritems():
242 242 if partial and not partial(f):
243 243 continue
244 244 if f in m1:
245 245 continue
246 246 if f in copied:
247 247 continue
248 248 if f in copy:
249 249 f2 = copy[f]
250 250 if f2 in m2: # rename case 1, A/A,B/A
251 251 act("remote copied to " + f, "m",
252 252 f2, f, f, fmerge(f2, f, f2), False)
253 253 else: # case 3,20 A/B/A
254 254 act("remote moved to " + f, "m",
255 255 f2, f, f, fmerge(f2, f, f2), True)
256 256 elif f in ma:
257 257 if overwrite or backwards:
258 258 act("recreating", "g", f, m2.execf(f))
259 259 elif n != ma[f]:
260 260 if repo.ui.prompt(
261 261 (_("remote changed %s which local deleted\n") % f) +
262 262 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
263 263 act("prompt recreating", "g", f, m2.execf(f))
264 264 else:
265 265 act("remote created", "g", f, m2.execf(f))
266 266
267 267 return action
268 268
269 269 def applyupdates(repo, action, wctx, mctx):
270 270 "apply the merge action list to the working directory"
271 271
272 272 updated, merged, removed, unresolved = 0, 0, 0, 0
273 273 action.sort()
274 274 for a in action:
275 275 f, m = a[:2]
276 276 if f[0] == "/":
277 277 continue
278 278 if m == "r": # remove
279 279 repo.ui.note(_("removing %s\n") % f)
280 280 util.audit_path(f)
281 281 try:
282 282 util.unlink(repo.wjoin(f))
283 283 except OSError, inst:
284 284 if inst.errno != errno.ENOENT:
285 285 repo.ui.warn(_("update failed to remove %s: %s!\n") %
286 286 (f, inst.strerror))
287 287 removed += 1
288 288 elif m == "m": # merge
289 289 f2, fd, flag, move = a[2:]
290 290 r = filemerge(repo, f, f2, wctx, mctx)
291 291 if r > 0:
292 292 unresolved += 1
293 293 else:
294 294 if r is None:
295 295 updated += 1
296 296 else:
297 297 merged += 1
298 298 if f != fd:
299 299 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
300 300 repo.wwrite(fd, repo.wread(f))
301 301 if move:
302 302 repo.ui.debug(_("removing %s\n") % f)
303 303 os.unlink(repo.wjoin(f))
304 304 util.set_exec(repo.wjoin(fd), flag)
305 305 elif m == "g": # get
306 306 flag = a[2]
307 307 repo.ui.note(_("getting %s\n") % f)
308 308 t = mctx.filectx(f).data()
309 309 repo.wwrite(f, t)
310 310 util.set_exec(repo.wjoin(f), flag)
311 311 updated += 1
312 312 elif m == "e": # exec
313 313 flag = a[2]
314 314 util.set_exec(repo.wjoin(f), flag)
315 315
316 316 return updated, merged, removed, unresolved
317 317
318 318 def recordupdates(repo, action, branchmerge):
319 319 "record merge actions to the dirstate"
320 320
321 321 for a in action:
322 322 f, m = a[:2]
323 323 if m == "r": # remove
324 324 if branchmerge:
325 325 repo.dirstate.update([f], 'r')
326 326 else:
327 327 repo.dirstate.forget([f])
328 328 elif m == "f": # forget
329 329 repo.dirstate.forget([f])
330 330 elif m == "g": # get
331 331 if branchmerge:
332 332 repo.dirstate.update([f], 'n', st_mtime=-1)
333 333 else:
334 334 repo.dirstate.update([f], 'n')
335 335 elif m == "m": # merge
336 336 f2, fd, flag, move = a[2:]
337 337 if branchmerge:
338 338 # We've done a branch merge, mark this file as merged
339 339 # so that we properly record the merger later
340 340 repo.dirstate.update([fd], 'm')
341 341 if f != f2: # copy/rename
342 342 if move:
343 343 repo.dirstate.update([f], 'r')
344 344 if f != fd:
345 345 repo.dirstate.copy(f, fd)
346 346 else:
347 347 repo.dirstate.copy(f2, fd)
348 348 else:
349 349 # We've update-merged a locally modified file, so
350 350 # we set the dirstate to emulate a normal checkout
351 351 # of that file some time in the past. Thus our
352 352 # merge will appear as a normal local file
353 353 # modification.
354 354 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
355 355 if move:
356 356 repo.dirstate.forget([f])
357 357
358 358 def update(repo, node, branchmerge, force, partial, wlock):
359 359 """
360 360 Perform a merge between the working directory and the given node
361 361
362 362 branchmerge = whether to merge between branches
363 363 force = whether to force branch merging or file overwriting
364 364 partial = a function to filter file lists (dirstate not updated)
365 365 wlock = working dir lock, if already held
366 366 """
367 367
368 368 if not wlock:
369 369 wlock = repo.wlock()
370 370
371 371 overwrite = force and not branchmerge
372 372 forcemerge = force and branchmerge
373 373 wc = repo.workingctx()
374 374 pl = wc.parents()
375 375 p1, p2 = pl[0], repo.changectx(node)
376 376 pa = p1.ancestor(p2)
377 377 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
378 378
379 379 ### check phase
380 380 if not overwrite and len(pl) > 1:
381 381 raise util.Abort(_("outstanding uncommitted merges"))
382 382 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
383 383 if branchmerge:
384 384 raise util.Abort(_("there is nothing to merge, just use "
385 385 "'hg update' or look at 'hg heads'"))
386 386 elif not (overwrite or branchmerge):
387 387 raise util.Abort(_("update spans branches, use 'hg merge' "
388 388 "or 'hg update -C' to lose changes"))
389 389 if branchmerge and not forcemerge:
390 390 if wc.files():
391 391 raise util.Abort(_("outstanding uncommitted changes"))
392 392
393 393 ### calculate phase
394 394 action = []
395 395 if not force:
396 396 checkunknown(wc, p2)
397 397 if not branchmerge:
398 398 action += forgetremoved(wc, p2)
399 399 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
400 400
401 401 ### apply phase
402 402 if not branchmerge: # just jump to the new rev
403 403 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
404 404 if not partial:
405 405 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
406 406
407 407 stats = applyupdates(repo, action, wc, p2)
408 408
409 409 if not partial:
410 410 recordupdates(repo, action, branchmerge)
411 411 repo.dirstate.setparents(fp1, fp2)
412 412 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
413 413 if not branchmerge:
414 414 repo.opener("branch", "w").write(p2.branch() + "\n")
415 415
416 416 return stats
417 417
General Comments 0
You need to be logged in to leave comments. Login now