##// END OF EJS Templates
Merge: refactor err and failedmerge -> unresolved
Matt Mackall -
r2813:56f99f5a default
parent child Browse files
Show More
@@ -1,347 +1,344
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(), "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 update(repo, node, branchmerge=False, force=False, partial=None,
51 51 forcemerge=False, wlock=None, show_stats=True, remind=True):
52 52
53 53 if not wlock:
54 54 wlock = repo.wlock()
55 55
56 56 pl = repo.dirstate.parents()
57 57 if not force and pl[1] != nullid:
58 58 raise util.Abort(_("outstanding uncommitted merges"))
59 59
60 err = False
61
62 60 p1, p2 = pl[0], node
63 61 pa = repo.changelog.ancestor(p1, p2)
64 62 m1n = repo.changelog.read(p1)[0]
65 63 m2n = repo.changelog.read(p2)[0]
66 64 man = repo.manifest.ancestor(m1n, m2n)
67 65 m1 = repo.manifest.read(m1n)
68 66 mf1 = repo.manifest.readflags(m1n)
69 67 m2 = repo.manifest.read(m2n).copy()
70 68 mf2 = repo.manifest.readflags(m2n)
71 69 ma = repo.manifest.read(man)
72 70 mfa = repo.manifest.readflags(man)
73 71
74 72 modified, added, removed, deleted, unknown = repo.changes()
75 73
76 74 # is this a jump, or a merge? i.e. is there a linear path
77 75 # from p1 to p2?
78 76 linear_path = (pa == p1 or pa == p2)
79 77
80 78 if branchmerge and linear_path:
81 79 raise util.Abort(_("there is nothing to merge, just use "
82 80 "'hg update' or look at 'hg heads'"))
83 81 if branchmerge and not forcemerge:
84 82 if modified or added or removed:
85 83 raise util.Abort(_("outstanding uncommitted changes"))
86 84
87 85 if not forcemerge and not force:
88 86 for f in unknown:
89 87 if f in m2:
90 88 t1 = repo.wread(f)
91 89 t2 = repo.file(f).read(m2[f])
92 90 if cmp(t1, t2) != 0:
93 91 raise util.Abort(_("'%s' already exists in the working"
94 92 " dir and differs from remote") % f)
95 93
96 94 # resolve the manifest to determine which files
97 95 # we care about merging
98 96 repo.ui.note(_("resolving manifests\n"))
99 97 repo.ui.debug(_(" force %s branchmerge %s partial %s linear %s\n") %
100 98 (force, branchmerge, partial and True or False, linear_path))
101 99 repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
102 100 (short(man), short(m1n), short(m2n)))
103 101
104 102 merge = {}
105 103 get = {}
106 104 remove = []
107 105
108 106 # construct a working dir manifest
109 107 mw = m1.copy()
110 108 mfw = mf1.copy()
111 109 umap = dict.fromkeys(unknown)
112 110
113 111 for f in added + modified + unknown:
114 112 mw[f] = ""
115 113 mfw[f] = util.is_exec(repo.wjoin(f), mfw.get(f, False))
116 114
117 115 for f in deleted + removed:
118 116 if f in mw:
119 117 del mw[f]
120 118
121 119 # If we're jumping between revisions (as opposed to merging),
122 120 # and if neither the working directory nor the target rev has
123 121 # the file, then we need to remove it from the dirstate, to
124 122 # prevent the dirstate from listing the file when it is no
125 123 # longer in the manifest.
126 124 if not partial and linear_path and f not in m2:
127 125 repo.dirstate.forget((f,))
128 126
129 127 # Compare manifests
130 128 for f, n in mw.iteritems():
131 129 if partial and not partial(f):
132 130 continue
133 131 if f in m2:
134 132 s = 0
135 133
136 134 # is the wfile new since m1, and match m2?
137 135 if f not in m1:
138 136 t1 = repo.wread(f)
139 137 t2 = repo.file(f).read(m2[f])
140 138 if cmp(t1, t2) == 0:
141 139 n = m2[f]
142 140 del t1, t2
143 141
144 142 # are files different?
145 143 if n != m2[f]:
146 144 a = ma.get(f, nullid)
147 145 # are both different from the ancestor?
148 146 if n != a and m2[f] != a:
149 147 repo.ui.debug(_(" %s versions differ, resolve\n") % f)
150 148 # merge executable bits
151 149 # "if we changed or they changed, change in merge"
152 150 a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
153 151 mode = ((a^b) | (a^c)) ^ a
154 152 merge[f] = (m1.get(f, nullid), m2[f], mode)
155 153 s = 1
156 154 # are we clobbering?
157 155 # is remote's version newer?
158 156 # or are we going back in time?
159 157 elif force or m2[f] != a or (p2 == pa and mw[f] == m1[f]):
160 158 repo.ui.debug(_(" remote %s is newer, get\n") % f)
161 159 get[f] = m2[f]
162 160 s = 1
163 161 elif f in umap or f in added:
164 162 # this unknown file is the same as the checkout
165 163 # we need to reset the dirstate if the file was added
166 164 get[f] = m2[f]
167 165
168 166 if not s and mfw[f] != mf2[f]:
169 167 if force:
170 168 repo.ui.debug(_(" updating permissions for %s\n") % f)
171 169 util.set_exec(repo.wjoin(f), mf2[f])
172 170 else:
173 171 a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
174 172 mode = ((a^b) | (a^c)) ^ a
175 173 if mode != b:
176 174 repo.ui.debug(_(" updating permissions for %s\n")
177 175 % f)
178 176 util.set_exec(repo.wjoin(f), mode)
179 177 del m2[f]
180 178 elif f in ma:
181 179 if n != ma[f]:
182 180 r = _("d")
183 181 if not force and (linear_path or branchmerge):
184 182 r = repo.ui.prompt(
185 183 (_(" local changed %s which remote deleted\n") % f) +
186 184 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
187 185 if r == _("d"):
188 186 remove.append(f)
189 187 else:
190 188 repo.ui.debug(_("other deleted %s\n") % f)
191 189 remove.append(f) # other deleted it
192 190 else:
193 191 # file is created on branch or in working directory
194 192 if force and f not in umap:
195 193 repo.ui.debug(_("remote deleted %s, clobbering\n") % f)
196 194 remove.append(f)
197 195 elif n == m1.get(f, nullid): # same as parent
198 196 if p2 == pa: # going backwards?
199 197 repo.ui.debug(_("remote deleted %s\n") % f)
200 198 remove.append(f)
201 199 else:
202 200 repo.ui.debug(_("local modified %s, keeping\n") % f)
203 201 else:
204 202 repo.ui.debug(_("working dir created %s, keeping\n") % f)
205 203
206 204 for f, n in m2.iteritems():
207 205 if partial and not partial(f):
208 206 continue
209 207 if f[0] == "/":
210 208 continue
211 209 if f in ma and n != ma[f]:
212 210 r = _("k")
213 211 if not force and (linear_path or branchmerge):
214 212 r = repo.ui.prompt(
215 213 (_("remote changed %s which local deleted\n") % f) +
216 214 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
217 215 if r == _("k"):
218 216 get[f] = n
219 217 elif f not in ma:
220 218 repo.ui.debug(_("remote created %s\n") % f)
221 219 get[f] = n
222 220 else:
223 221 if force or p2 == pa: # going backwards?
224 222 repo.ui.debug(_("local deleted %s, recreating\n") % f)
225 223 get[f] = n
226 224 else:
227 225 repo.ui.debug(_("local deleted %s\n") % f)
228 226
229 227 del mw, m1, m2, ma
230 228
231 229 if force:
232 230 for f in merge:
233 231 get[f] = merge[f][1]
234 232 merge = {}
235 233
236 234 if linear_path or force:
237 235 # we don't need to do any magic, just jump to the new rev
238 236 p1, p2 = p2, nullid
239 237 else:
240 238 if not branchmerge:
241 239 repo.ui.status(_("this update spans a branch"
242 240 " affecting the following files:\n"))
243 241 fl = merge.keys() + get.keys()
244 242 fl.sort()
245 243 for f in fl:
246 244 cf = ""
247 245 if f in merge:
248 246 cf = _(" (resolve)")
249 247 repo.ui.status(" %s%s\n" % (f, cf))
250 248 repo.ui.warn(_("aborting update spanning branches!\n"))
251 249 repo.ui.status(_("(use 'hg merge' to merge across branches"
252 250 " or 'hg update -C' to lose changes)\n"))
253 251 return 1
254 252
255 253 xp1 = hex(p1)
256 254 xp2 = hex(p2)
257 255 if p2 == nullid: xxp2 = ''
258 256 else: xxp2 = xp2
259 257
260 258 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
261 259
262 260 # get the files we don't need to change
263 261 files = get.keys()
264 262 files.sort()
265 263 for f in files:
266 264 if f[0] == "/":
267 265 continue
268 266 repo.ui.note(_("getting %s\n") % f)
269 267 t = repo.file(f).read(get[f])
270 268 repo.wwrite(f, t)
271 269 util.set_exec(repo.wjoin(f), mf2[f])
272 270 if not partial:
273 271 if branchmerge:
274 272 repo.dirstate.update([f], 'n', st_mtime=-1)
275 273 else:
276 274 repo.dirstate.update([f], 'n')
277 275
278 276 # merge the tricky bits
279 failedmerge = []
277 unresolved = []
280 278 files = merge.keys()
281 279 files.sort()
282 280 for f in files:
283 281 repo.ui.status(_("merging %s\n") % f)
284 282 my, other, flag = merge[f]
285 283 ret = merge3(repo, f, my, other, xp1, xp2)
286 284 if ret:
287 err = True
288 failedmerge.append(f)
285 unresolved.append(f)
289 286 util.set_exec(repo.wjoin(f), flag)
290 287 if not partial:
291 288 if branchmerge:
292 289 # We've done a branch merge, mark this file as merged
293 290 # so that we properly record the merger later
294 291 repo.dirstate.update([f], 'm')
295 292 else:
296 293 # We've update-merged a locally modified file, so
297 294 # we set the dirstate to emulate a normal checkout
298 295 # of that file some time in the past. Thus our
299 296 # merge will appear as a normal local file
300 297 # modification.
301 298 f_len = len(repo.file(f).read(other))
302 299 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
303 300
304 301 remove.sort()
305 302 for f in remove:
306 303 repo.ui.note(_("removing %s\n") % f)
307 304 util.audit_path(f)
308 305 try:
309 306 util.unlink(repo.wjoin(f))
310 307 except OSError, inst:
311 308 if inst.errno != errno.ENOENT:
312 309 repo.ui.warn(_("update failed to remove %s: %s!\n") %
313 310 (f, inst.strerror))
314 311 if not partial:
315 312 if branchmerge:
316 313 repo.dirstate.update(remove, 'r')
317 314 else:
318 315 repo.dirstate.forget(remove)
319 316
320 317 if not partial:
321 318 repo.dirstate.setparents(p1, p2)
322 319
323 320 if show_stats:
324 321 stats = ((len(get), _("updated")),
325 (len(merge) - len(failedmerge), _("merged")),
322 (len(merge) - len(unresolved), _("merged")),
326 323 (len(remove), _("removed")),
327 (len(failedmerge), _("unresolved")))
324 (len(unresolved), _("unresolved")))
328 325 note = ", ".join([_("%d files %s") % s for s in stats])
329 326 repo.ui.status("%s\n" % note)
330 327 if not partial:
331 328 if branchmerge:
332 if failedmerge:
329 if unresolved:
333 330 repo.ui.status(_("There are unresolved merges,"
334 331 " you can redo the full merge using:\n"
335 332 " hg update -C %s\n"
336 333 " hg merge %s\n"
337 334 % (repo.changelog.rev(p1),
338 335 repo.changelog.rev(p2))))
339 336 elif remind:
340 337 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
341 elif failedmerge:
338 elif unresolved:
342 339 repo.ui.status(_("There are unresolved merges with"
343 340 " locally modified files.\n"))
344 341
345 repo.hook('update', parent1=xp1, parent2=xxp2, error=int(err))
346 return err
342 repo.hook('update', parent1=xp1, parent2=xxp2, error=len(unresolved))
343 return len(unresolved)
347 344
General Comments 0
You need to be logged in to leave comments. Login now