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