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