##// END OF EJS Templates
merge: move partial filtering out of manifest merge
Matt Mackall -
r3162:a9e75b37 default
parent child Browse files
Show More
@@ -1,419 +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 def manifestmerge(ui, m1, m2, ma, overwrite, backwards, partial):
174 def filtermanifest(man, partial):
175 if partial:
176 for k in man.keys():
177 if not partial(k): del man[k]
178
179 def manifestmerge(ui, m1, m2, ma, overwrite, backwards):
175 180 """
176 181 Merge manifest m1 with m2 using ancestor ma and generate merge action list
177 182 """
178 183
179 184 def fmerge(f):
180 185 """merge executable flags"""
181 186 a, b, c = ma.execf(f), m1.execf(f), m2.execf(f)
182 187 return ((a^b) | (a^c)) ^ a
183 188
184 189 action = []
185 190
186 191 def act(msg, f, m, *args):
187 192 ui.debug(" %s: %s -> %s\n" % (f, msg, m))
188 193 action.append((f, m) + args)
189 194
190 # Filter manifests
191 if partial:
192 for f in m1.keys():
193 if not partial(f): del m1[f]
194 for f in m2.keys():
195 if not partial(f): del m2[f]
196
197 195 # Compare manifests
198 196 for f, n in m1.iteritems():
199 197 if f in m2:
200 198 # are files different?
201 199 if n != m2[f]:
202 200 a = ma.get(f, nullid)
203 201 # are both different from the ancestor?
204 202 if not overwrite and n != a and m2[f] != a:
205 203 act("versions differ", f, "m", fmerge(f), n[:20], m2[f])
206 204 # are we clobbering?
207 205 # is remote's version newer?
208 206 # or are we going back in time and clean?
209 207 elif overwrite or m2[f] != a or (backwards and not n[20:]):
210 208 act("remote is newer", f, "g", m2.execf(f), m2[f])
211 209 # local is newer, not overwrite, check mode bits
212 210 elif fmerge(f) != m1.execf(f):
213 211 act("update permissions", f, "e", m2.execf(f))
214 212 # contents same, check mode bits
215 213 elif m1.execf(f) != m2.execf(f):
216 214 if overwrite or fmerge(f) != m1.execf(f):
217 215 act("update permissions", f, "e", m2.execf(f))
218 216 del m2[f]
219 217 elif f in ma:
220 218 if n != ma[f] and not overwrite:
221 219 if ui.prompt(
222 220 (_(" local changed %s which remote deleted\n") % f) +
223 221 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
224 222 act("prompt delete", f, "r")
225 223 else:
226 224 act("other deleted", f, "r")
227 225 else:
228 226 # file is created on branch or in working directory
229 227 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
230 228 act("remote deleted", f, "r")
231 229
232 230 for f, n in m2.iteritems():
233 231 if f in ma:
234 232 if overwrite or backwards:
235 233 act("recreating", f, "g", m2.execf(f), n)
236 234 elif n != ma[f]:
237 235 if ui.prompt(
238 236 (_("remote changed %s which local deleted\n") % f) +
239 237 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
240 238 act("prompt recreating", f, "g", m2.execf(f), n)
241 239 else:
242 240 act("remote created", f, "g", m2.execf(f), n)
243 241
244 242 return action
245 243
246 244 def applyupdates(repo, action, xp1, xp2):
247 245 updated, merged, removed, unresolved = 0, 0, 0, 0
248 246 action.sort()
249 247 for a in action:
250 248 f, m = a[:2]
251 249 if f[0] == "/":
252 250 continue
253 251 if m == "r": # remove
254 252 repo.ui.note(_("removing %s\n") % f)
255 253 util.audit_path(f)
256 254 try:
257 255 util.unlink(repo.wjoin(f))
258 256 except OSError, inst:
259 257 if inst.errno != errno.ENOENT:
260 258 repo.ui.warn(_("update failed to remove %s: %s!\n") %
261 259 (f, inst.strerror))
262 260 removed +=1
263 261 elif m == "m": # merge
264 262 flag, my, other = a[2:]
265 263 repo.ui.status(_("merging %s\n") % f)
266 264 if merge3(repo, f, my, other, xp1, xp2):
267 265 unresolved += 1
268 266 util.set_exec(repo.wjoin(f), flag)
269 267 merged += 1
270 268 elif m == "g": # get
271 269 flag, node = a[2:]
272 270 repo.ui.note(_("getting %s\n") % f)
273 271 t = repo.file(f).read(node)
274 272 repo.wwrite(f, t)
275 273 util.set_exec(repo.wjoin(f), flag)
276 274 updated += 1
277 275 elif m == "e": # exec
278 276 flag = a[2:]
279 277 util.set_exec(repo.wjoin(f), flag)
280 278
281 279 return updated, merged, removed, unresolved
282 280
283 281 def recordupdates(repo, action, branchmerge):
284 282 for a in action:
285 283 f, m = a[:2]
286 284 if m == "r": # remove
287 285 if branchmerge:
288 286 repo.dirstate.update([f], 'r')
289 287 else:
290 288 repo.dirstate.forget([f])
291 289 elif m == "f": # forget
292 290 repo.dirstate.forget([f])
293 291 elif m == "g": # get
294 292 if branchmerge:
295 293 repo.dirstate.update([f], 'n', st_mtime=-1)
296 294 else:
297 295 repo.dirstate.update([f], 'n')
298 296 elif m == "m": # merge
299 297 flag, my, other = a[2:]
300 298 if branchmerge:
301 299 # We've done a branch merge, mark this file as merged
302 300 # so that we properly record the merger later
303 301 repo.dirstate.update([f], 'm')
304 302 else:
305 303 # We've update-merged a locally modified file, so
306 304 # we set the dirstate to emulate a normal checkout
307 305 # of that file some time in the past. Thus our
308 306 # merge will appear as a normal local file
309 307 # modification.
310 308 fl = repo.file(f)
311 309 f_len = fl.size(fl.rev(other))
312 310 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
313 311
314 312 def update(repo, node, branchmerge=False, force=False, partial=None,
315 313 wlock=None, show_stats=True, remind=True):
316 314
317 315 overwrite = force and not branchmerge
318 316 forcemerge = force and branchmerge
319 317
320 318 if not wlock:
321 319 wlock = repo.wlock()
322 320
323 321 ### check phase
324 322
325 323 pl = repo.dirstate.parents()
326 324 if not overwrite and pl[1] != nullid:
327 325 raise util.Abort(_("outstanding uncommitted merges"))
328 326
329 327 p1, p2 = pl[0], node
330 328 pa = repo.changelog.ancestor(p1, p2)
331 329
332 330 # are we going backwards?
333 331 backwards = (pa == p2)
334 332
335 333 # is there a linear path from p1 to p2?
336 334 if pa == p1 or pa == p2:
337 335 if branchmerge:
338 336 raise util.Abort(_("there is nothing to merge, just use "
339 337 "'hg update' or look at 'hg heads'"))
340 338 elif not (overwrite or branchmerge):
341 339 raise util.Abort(_("update spans branches, use 'hg merge' "
342 340 "or 'hg update -C' to lose changes"))
343 341
344 342 status = repo.status()
345 343 modified, added, removed, deleted, unknown = status[:5]
346 344 if branchmerge and not forcemerge:
347 345 if modified or added or removed:
348 346 raise util.Abort(_("outstanding uncommitted changes"))
349 347
350 348 m1 = repo.changectx(p1).manifest().copy()
351 349 m2 = repo.changectx(p2).manifest().copy()
352 350 ma = repo.changectx(pa).manifest()
353 351
354 352 # resolve the manifest to determine which files
355 353 # we care about merging
356 354 repo.ui.note(_("resolving manifests\n"))
357 355 repo.ui.debug(_(" overwrite %s branchmerge %s partial %s\n") %
358 356 (overwrite, branchmerge, bool(partial)))
359 357 repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
360 358 (short(p1), short(p2), short(pa)))
361 359
362 360 action = []
363 361 copy = {}
364 362
365 363 m1 = workingmanifest(repo, m1, status)
364 filtermanifest(m1, partial)
365 filtermanifest(m2, partial)
366 366
367 367 if not force:
368 368 checkunknown(repo, m2, status)
369 369 if not branchmerge:
370 370 action += forgetremoved(m2, status)
371 371 if not (backwards or overwrite):
372 372 copy = findcopies(repo, m1, m2, repo.changelog.rev(pa))
373 373
374 action += manifestmerge(repo.ui, m1, m2, ma, overwrite, backwards, partial)
374 action += manifestmerge(repo.ui, m1, m2, ma, overwrite, backwards)
375 375 del m1, m2, ma
376 376
377 377 ### apply phase
378 378
379 379 if not branchmerge:
380 380 # we don't need to do any magic, just jump to the new rev
381 381 p1, p2 = p2, nullid
382 382
383 383 xp1, xp2 = hex(p1), hex(p2)
384 384 if p2 == nullid: xp2 = ''
385 385
386 386 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
387 387
388 388 updated, merged, removed, unresolved = applyupdates(repo, action, xp1, xp2)
389 389
390 390 # update dirstate
391 391 if not partial:
392 392 repo.dirstate.setparents(p1, p2)
393 393 recordupdates(repo, action, branchmerge)
394 394
395 395 if show_stats:
396 396 stats = ((updated, _("updated")),
397 397 (merged - unresolved, _("merged")),
398 398 (removed, _("removed")),
399 399 (unresolved, _("unresolved")))
400 400 note = ", ".join([_("%d files %s") % s for s in stats])
401 401 repo.ui.status("%s\n" % note)
402 402 if not partial:
403 403 if branchmerge:
404 404 if unresolved:
405 405 repo.ui.status(_("There are unresolved merges,"
406 406 " you can redo the full merge using:\n"
407 407 " hg update -C %s\n"
408 408 " hg merge %s\n"
409 409 % (repo.changelog.rev(p1),
410 410 repo.changelog.rev(p2))))
411 411 elif remind:
412 412 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
413 413 elif unresolved:
414 414 repo.ui.status(_("There are unresolved merges with"
415 415 " locally modified files.\n"))
416 416
417 417 repo.hook('update', parent1=xp1, parent2=xp2, error=unresolved)
418 418 return unresolved
419 419
General Comments 0
You need to be logged in to leave comments. Login now