##// END OF EJS Templates
merge: simplify hook code
Matt Mackall -
r3109:62044d16 default
parent child Browse files
Show More
@@ -1,366 +1,364 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 queued = 0
120 120
121 121 # are files different?
122 122 if n != m2[f]:
123 123 a = ma.get(f, nullid)
124 124 # are both different from the ancestor?
125 125 if not overwrite and n != a and m2[f] != a:
126 126 ui.debug(_(" %s versions differ, resolve\n") % f)
127 127 action.append((f, "m", fmerge(f, m1, m2, ma), n[:20], m2[f]))
128 128 queued = 1
129 129 # are we clobbering?
130 130 # is remote's version newer?
131 131 # or are we going back in time and clean?
132 132 elif overwrite or m2[f] != a or (backwards and not n[20:]):
133 133 ui.debug(_(" remote %s is newer, get\n") % f)
134 134 action.append((f, "g", m2.execf(f), m2[f]))
135 135 queued = 1
136 136 elif n[20:] in ("u","a"):
137 137 # this unknown file is the same as the checkout
138 138 # we need to reset the dirstate if the file was added
139 139 action.append((f, "g", m2.execf(f), m2[f]))
140 140
141 141 # do we still need to look at mode bits?
142 142 if not queued and m1.execf(f) != m2.execf(f):
143 143 if overwrite:
144 144 ui.debug(_(" updating permissions for %s\n") % f)
145 145 action.append((f, "e", m2.execf(f)))
146 146 else:
147 147 mode = fmerge(f, m1, m2, ma)
148 148 if mode != m1.execf(f):
149 149 ui.debug(_(" updating permissions for %s\n")
150 150 % f)
151 151 action.append((f, "e", m2.execf(f)))
152 152 del m2[f]
153 153 elif f in ma:
154 154 if n != ma[f]:
155 155 r = _("d")
156 156 if not overwrite:
157 157 r = ui.prompt(
158 158 (_(" local changed %s which remote deleted\n") % f) +
159 159 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
160 160 if r == _("d"):
161 161 action.append((f, "r"))
162 162 else:
163 163 ui.debug(_("other deleted %s\n") % f)
164 164 action.append((f, "r"))
165 165 else:
166 166 # file is created on branch or in working directory
167 167 if overwrite and n[20:] != "u":
168 168 ui.debug(_("remote deleted %s, clobbering\n") % f)
169 169 action.append((f, "r"))
170 170 elif not n[20:]: # same as parent
171 171 if backwards:
172 172 ui.debug(_("remote deleted %s\n") % f)
173 173 action.append((f, "r"))
174 174 else:
175 175 ui.debug(_("local modified %s, keeping\n") % f)
176 176 else:
177 177 ui.debug(_("working dir created %s, keeping\n") % f)
178 178
179 179 for f, n in m2.iteritems():
180 180 if f[0] == "/":
181 181 continue
182 182 if f in ma and n != ma[f]:
183 183 r = _("k")
184 184 if not overwrite:
185 185 r = ui.prompt(
186 186 (_("remote changed %s which local deleted\n") % f) +
187 187 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
188 188 if r == _("k"):
189 189 action.append((f, "g", m2.execf(f), n))
190 190 elif f not in ma:
191 191 ui.debug(_("remote created %s\n") % f)
192 192 action.append((f, "g", m2.execf(f), n))
193 193 else:
194 194 if overwrite or backwards:
195 195 ui.debug(_("local deleted %s, recreating\n") % f)
196 196 action.append((f, "g", m2.execf(f), n))
197 197 else:
198 198 ui.debug(_("local deleted %s\n") % f)
199 199
200 200 return action
201 201
202 202 def update(repo, node, branchmerge=False, force=False, partial=None,
203 203 wlock=None, show_stats=True, remind=True):
204 204
205 205 overwrite = force and not branchmerge
206 206 forcemerge = force and branchmerge
207 207
208 208 if not wlock:
209 209 wlock = repo.wlock()
210 210
211 211 ### check phase
212 212
213 213 pl = repo.dirstate.parents()
214 214 if not overwrite and pl[1] != nullid:
215 215 raise util.Abort(_("outstanding uncommitted merges"))
216 216
217 217 p1, p2 = pl[0], node
218 218 pa = repo.changelog.ancestor(p1, p2)
219 219
220 220 # are we going backwards?
221 221 backwards = (pa == p2)
222 222
223 223 # is there a linear path from p1 to p2?
224 224 linear = (pa == p1 or pa == p2)
225 225 if branchmerge and linear:
226 226 raise util.Abort(_("there is nothing to merge, just use "
227 227 "'hg update' or look at 'hg heads'"))
228 228
229 229 if not linear and not (overwrite or branchmerge):
230 230 raise util.Abort(_("update spans branches, use 'hg merge' "
231 231 "or 'hg update -C' to lose changes"))
232 232
233 233 status = repo.status()
234 234 modified, added, removed, deleted, unknown = status[:5]
235 235 if branchmerge and not forcemerge:
236 236 if modified or added or removed:
237 237 raise util.Abort(_("outstanding uncommitted changes"))
238 238
239 239 m1 = repo.changectx(p1).manifest().copy()
240 240 m2 = repo.changectx(p2).manifest().copy()
241 241 ma = repo.changectx(pa).manifest()
242 242
243 243 # resolve the manifest to determine which files
244 244 # we care about merging
245 245 repo.ui.note(_("resolving manifests\n"))
246 246 repo.ui.debug(_(" overwrite %s branchmerge %s partial %s linear %s\n") %
247 247 (overwrite, branchmerge, bool(partial), linear))
248 248 repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
249 249 (short(p1), short(p2), short(pa)))
250 250
251 251 action = []
252 252 m1 = workingmanifest(repo, m1, status)
253 253
254 254 if not force:
255 255 checkunknown(repo, m2, status)
256 256 if linear:
257 257 action += forgetremoved(m2, status)
258 258 action += manifestmerge(repo.ui, m1, m2, ma, overwrite, backwards, partial)
259 259 del m1, m2, ma
260 260
261 261 ### apply phase
262 262
263 263 if linear or overwrite:
264 264 # we don't need to do any magic, just jump to the new rev
265 265 p1, p2 = p2, nullid
266 266
267 xp1 = hex(p1)
268 xp2 = hex(p2)
269 if p2 == nullid: xxp2 = ''
270 else: xxp2 = xp2
267 xp1, xp2 = hex(p1), hex(p2)
268 if p2 == nullid: xp2 = ''
271 269
272 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
270 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
273 271
274 272 # update files
275 273 updated, merged, removed, unresolved = 0, 0, 0, 0
276 274 action.sort()
277 275 for a in action:
278 276 f, m = a[:2]
279 277 if f[0] == "/":
280 278 continue
281 279 if m == "r": # remove
282 280 repo.ui.note(_("removing %s\n") % f)
283 281 util.audit_path(f)
284 282 try:
285 283 util.unlink(repo.wjoin(f))
286 284 except OSError, inst:
287 285 if inst.errno != errno.ENOENT:
288 286 repo.ui.warn(_("update failed to remove %s: %s!\n") %
289 287 (f, inst.strerror))
290 288 removed +=1
291 289 elif m == "m": # merge
292 290 flag, my, other = a[2:]
293 291 repo.ui.status(_("merging %s\n") % f)
294 292 if merge3(repo, f, my, other, xp1, xp2):
295 293 unresolved += 1
296 294 util.set_exec(repo.wjoin(f), flag)
297 295 merged += 1
298 296 elif m == "g": # get
299 297 flag, node = a[2:]
300 298 repo.ui.note(_("getting %s\n") % f)
301 299 t = repo.file(f).read(node)
302 300 repo.wwrite(f, t)
303 301 util.set_exec(repo.wjoin(f), flag)
304 302 updated += 1
305 303 elif m == "e": # exec
306 304 flag = a[2:]
307 305 util.set_exec(repo.wjoin(f), flag)
308 306
309 307 # update dirstate
310 308 if not partial:
311 309 repo.dirstate.setparents(p1, p2)
312 310 for a in action:
313 311 f, m = a[:2]
314 312 if m == "r": # remove
315 313 if branchmerge:
316 314 repo.dirstate.update([f], 'r')
317 315 else:
318 316 repo.dirstate.forget([f])
319 317 elif m == "f": # forget
320 318 repo.dirstate.forget([f])
321 319 elif m == "g": # get
322 320 if branchmerge:
323 321 repo.dirstate.update([f], 'n', st_mtime=-1)
324 322 else:
325 323 repo.dirstate.update([f], 'n')
326 324 elif m == "m": # merge
327 325 flag, my, other = a[2:]
328 326 if branchmerge:
329 327 # We've done a branch merge, mark this file as merged
330 328 # so that we properly record the merger later
331 329 repo.dirstate.update([f], 'm')
332 330 else:
333 331 # We've update-merged a locally modified file, so
334 332 # we set the dirstate to emulate a normal checkout
335 333 # of that file some time in the past. Thus our
336 334 # merge will appear as a normal local file
337 335 # modification.
338 336 fl = repo.file(f)
339 337 f_len = fl.size(fl.rev(other))
340 338 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
341 339
342 340 if show_stats:
343 341 stats = ((updated, _("updated")),
344 342 (merged - unresolved, _("merged")),
345 343 (removed, _("removed")),
346 344 (unresolved, _("unresolved")))
347 345 note = ", ".join([_("%d files %s") % s for s in stats])
348 346 repo.ui.status("%s\n" % note)
349 347 if not partial:
350 348 if branchmerge:
351 349 if unresolved:
352 350 repo.ui.status(_("There are unresolved merges,"
353 351 " you can redo the full merge using:\n"
354 352 " hg update -C %s\n"
355 353 " hg merge %s\n"
356 354 % (repo.changelog.rev(p1),
357 355 repo.changelog.rev(p2))))
358 356 elif remind:
359 357 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
360 358 elif unresolved:
361 359 repo.ui.status(_("There are unresolved merges with"
362 360 " locally modified files.\n"))
363 361
364 repo.hook('update', parent1=xp1, parent2=xxp2, error=unresolved)
362 repo.hook('update', parent1=xp1, parent2=xp2, error=unresolved)
365 363 return unresolved
366 364
General Comments 0
You need to be logged in to leave comments. Login now