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