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