comparison mercurial/merge.py @ 2799:b550cd82f92a

Move merge code to its own module Pull update and merge3 out of localrepo into merge.py s/self/repo/ Add temporary API function in hg.py Convert all users
author Matt Mackall <mpm@selenic.com>
date Thu, 03 Aug 2006 15:24:41 -0500
parents
children 987c31e2a08c
comparison
equal deleted inserted replaced
2798:8cd3e19bf4a5 2799:b550cd82f92a
1 # merge.py - directory-level update/merge handling for Mercurial
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
7
8 from node import *
9 from i18n import gettext as _
10 from demandload import *
11 demandload(globals(), "util os tempfile")
12
13 def merge3(repo, fn, my, other, p1, p2):
14 """perform a 3-way merge in the working directory"""
15
16 def temp(prefix, node):
17 pre = "%s~%s." % (os.path.basename(fn), prefix)
18 (fd, name) = tempfile.mkstemp(prefix=pre)
19 f = os.fdopen(fd, "wb")
20 repo.wwrite(fn, fl.read(node), f)
21 f.close()
22 return name
23
24 fl = repo.file(fn)
25 base = fl.ancestor(my, other)
26 a = repo.wjoin(fn)
27 b = temp("base", base)
28 c = temp("other", other)
29
30 repo.ui.note(_("resolving %s\n") % fn)
31 repo.ui.debug(_("file %s: my %s other %s ancestor %s\n") %
32 (fn, short(my), short(other), short(base)))
33
34 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
35 or "hgmerge")
36 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
37 environ={'HG_FILE': fn,
38 'HG_MY_NODE': p1,
39 'HG_OTHER_NODE': p2,
40 'HG_FILE_MY_NODE': hex(my),
41 'HG_FILE_OTHER_NODE': hex(other),
42 'HG_FILE_BASE_NODE': hex(base)})
43 if r:
44 repo.ui.warn(_("merging %s failed!\n") % fn)
45
46 os.unlink(b)
47 os.unlink(c)
48 return r
49
50 def update(repo, node, allow=False, force=False, choose=None,
51 moddirstate=True, forcemerge=False, wlock=None, show_stats=True):
52 pl = repo.dirstate.parents()
53 if not force and pl[1] != nullid:
54 raise util.Abort(_("outstanding uncommitted merges"))
55
56 err = False
57
58 p1, p2 = pl[0], node
59 pa = repo.changelog.ancestor(p1, p2)
60 m1n = repo.changelog.read(p1)[0]
61 m2n = repo.changelog.read(p2)[0]
62 man = repo.manifest.ancestor(m1n, m2n)
63 m1 = repo.manifest.read(m1n)
64 mf1 = repo.manifest.readflags(m1n)
65 m2 = repo.manifest.read(m2n).copy()
66 mf2 = repo.manifest.readflags(m2n)
67 ma = repo.manifest.read(man)
68 mfa = repo.manifest.readflags(man)
69
70 modified, added, removed, deleted, unknown = repo.changes()
71
72 # is this a jump, or a merge? i.e. is there a linear path
73 # from p1 to p2?
74 linear_path = (pa == p1 or pa == p2)
75
76 if allow and linear_path:
77 raise util.Abort(_("there is nothing to merge, just use "
78 "'hg update' or look at 'hg heads'"))
79 if allow and not forcemerge:
80 if modified or added or removed:
81 raise util.Abort(_("outstanding uncommitted changes"))
82
83 if not forcemerge and not force:
84 for f in unknown:
85 if f in m2:
86 t1 = repo.wread(f)
87 t2 = repo.file(f).read(m2[f])
88 if cmp(t1, t2) != 0:
89 raise util.Abort(_("'%s' already exists in the working"
90 " dir and differs from remote") % f)
91
92 # resolve the manifest to determine which files
93 # we care about merging
94 repo.ui.note(_("resolving manifests\n"))
95 repo.ui.debug(_(" force %s allow %s moddirstate %s linear %s\n") %
96 (force, allow, moddirstate, linear_path))
97 repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
98 (short(man), short(m1n), short(m2n)))
99
100 merge = {}
101 get = {}
102 remove = []
103
104 # construct a working dir manifest
105 mw = m1.copy()
106 mfw = mf1.copy()
107 umap = dict.fromkeys(unknown)
108
109 for f in added + modified + unknown:
110 mw[f] = ""
111 mfw[f] = util.is_exec(repo.wjoin(f), mfw.get(f, False))
112
113 if moddirstate and not wlock:
114 wlock = repo.wlock()
115
116 for f in deleted + removed:
117 if f in mw:
118 del mw[f]
119
120 # If we're jumping between revisions (as opposed to merging),
121 # and if neither the working directory nor the target rev has
122 # the file, then we need to remove it from the dirstate, to
123 # prevent the dirstate from listing the file when it is no
124 # longer in the manifest.
125 if moddirstate and linear_path and f not in m2:
126 repo.dirstate.forget((f,))
127
128 # Compare manifests
129 for f, n in mw.iteritems():
130 if choose and not choose(f):
131 continue
132 if f in m2:
133 s = 0
134
135 # is the wfile new since m1, and match m2?
136 if f not in m1:
137 t1 = repo.wread(f)
138 t2 = repo.file(f).read(m2[f])
139 if cmp(t1, t2) == 0:
140 n = m2[f]
141 del t1, t2
142
143 # are files different?
144 if n != m2[f]:
145 a = ma.get(f, nullid)
146 # are both different from the ancestor?
147 if n != a and m2[f] != a:
148 repo.ui.debug(_(" %s versions differ, resolve\n") % f)
149 # merge executable bits
150 # "if we changed or they changed, change in merge"
151 a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
152 mode = ((a^b) | (a^c)) ^ a
153 merge[f] = (m1.get(f, nullid), m2[f], mode)
154 s = 1
155 # are we clobbering?
156 # is remote's version newer?
157 # or are we going back in time?
158 elif force or m2[f] != a or (p2 == pa and mw[f] == m1[f]):
159 repo.ui.debug(_(" remote %s is newer, get\n") % f)
160 get[f] = m2[f]
161 s = 1
162 elif f in umap or f in added:
163 # this unknown file is the same as the checkout
164 # we need to reset the dirstate if the file was added
165 get[f] = m2[f]
166
167 if not s and mfw[f] != mf2[f]:
168 if force:
169 repo.ui.debug(_(" updating permissions for %s\n") % f)
170 util.set_exec(repo.wjoin(f), mf2[f])
171 else:
172 a, b, c = mfa.get(f, 0), mfw[f], mf2[f]
173 mode = ((a^b) | (a^c)) ^ a
174 if mode != b:
175 repo.ui.debug(_(" updating permissions for %s\n")
176 % f)
177 util.set_exec(repo.wjoin(f), mode)
178 del m2[f]
179 elif f in ma:
180 if n != ma[f]:
181 r = _("d")
182 if not force and (linear_path or allow):
183 r = repo.ui.prompt(
184 (_(" local changed %s which remote deleted\n") % f) +
185 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
186 if r == _("d"):
187 remove.append(f)
188 else:
189 repo.ui.debug(_("other deleted %s\n") % f)
190 remove.append(f) # other deleted it
191 else:
192 # file is created on branch or in working directory
193 if force and f not in umap:
194 repo.ui.debug(_("remote deleted %s, clobbering\n") % f)
195 remove.append(f)
196 elif n == m1.get(f, nullid): # same as parent
197 if p2 == pa: # going backwards?
198 repo.ui.debug(_("remote deleted %s\n") % f)
199 remove.append(f)
200 else:
201 repo.ui.debug(_("local modified %s, keeping\n") % f)
202 else:
203 repo.ui.debug(_("working dir created %s, keeping\n") % f)
204
205 for f, n in m2.iteritems():
206 if choose and not choose(f):
207 continue
208 if f[0] == "/":
209 continue
210 if f in ma and n != ma[f]:
211 r = _("k")
212 if not force and (linear_path or allow):
213 r = repo.ui.prompt(
214 (_("remote changed %s which local deleted\n") % f) +
215 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
216 if r == _("k"):
217 get[f] = n
218 elif f not in ma:
219 repo.ui.debug(_("remote created %s\n") % f)
220 get[f] = n
221 else:
222 if force or p2 == pa: # going backwards?
223 repo.ui.debug(_("local deleted %s, recreating\n") % f)
224 get[f] = n
225 else:
226 repo.ui.debug(_("local deleted %s\n") % f)
227
228 del mw, m1, m2, ma
229
230 if force:
231 for f in merge:
232 get[f] = merge[f][1]
233 merge = {}
234
235 if linear_path or force:
236 # we don't need to do any magic, just jump to the new rev
237 branch_merge = False
238 p1, p2 = p2, nullid
239 else:
240 if not allow:
241 repo.ui.status(_("this update spans a branch"
242 " affecting the following files:\n"))
243 fl = merge.keys() + get.keys()
244 fl.sort()
245 for f in fl:
246 cf = ""
247 if f in merge:
248 cf = _(" (resolve)")
249 repo.ui.status(" %s%s\n" % (f, cf))
250 repo.ui.warn(_("aborting update spanning branches!\n"))
251 repo.ui.status(_("(use 'hg merge' to merge across branches"
252 " or 'hg update -C' to lose changes)\n"))
253 return 1
254 branch_merge = True
255
256 xp1 = hex(p1)
257 xp2 = hex(p2)
258 if p2 == nullid: xxp2 = ''
259 else: xxp2 = xp2
260
261 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
262
263 # get the files we don't need to change
264 files = get.keys()
265 files.sort()
266 for f in files:
267 if f[0] == "/":
268 continue
269 repo.ui.note(_("getting %s\n") % f)
270 t = repo.file(f).read(get[f])
271 repo.wwrite(f, t)
272 util.set_exec(repo.wjoin(f), mf2[f])
273 if moddirstate:
274 if branch_merge:
275 repo.dirstate.update([f], 'n', st_mtime=-1)
276 else:
277 repo.dirstate.update([f], 'n')
278
279 # merge the tricky bits
280 failedmerge = []
281 files = merge.keys()
282 files.sort()
283 for f in files:
284 repo.ui.status(_("merging %s\n") % f)
285 my, other, flag = merge[f]
286 ret = merge3(repo, f, my, other, xp1, xp2)
287 if ret:
288 err = True
289 failedmerge.append(f)
290 util.set_exec(repo.wjoin(f), flag)
291 if moddirstate:
292 if branch_merge:
293 # We've done a branch merge, mark this file as merged
294 # so that we properly record the merger later
295 repo.dirstate.update([f], 'm')
296 else:
297 # We've update-merged a locally modified file, so
298 # we set the dirstate to emulate a normal checkout
299 # of that file some time in the past. Thus our
300 # merge will appear as a normal local file
301 # modification.
302 f_len = len(repo.file(f).read(other))
303 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
304
305 remove.sort()
306 for f in remove:
307 repo.ui.note(_("removing %s\n") % f)
308 util.audit_path(f)
309 try:
310 util.unlink(repo.wjoin(f))
311 except OSError, inst:
312 if inst.errno != errno.ENOENT:
313 repo.ui.warn(_("update failed to remove %s: %s!\n") %
314 (f, inst.strerror))
315 if moddirstate:
316 if branch_merge:
317 repo.dirstate.update(remove, 'r')
318 else:
319 repo.dirstate.forget(remove)
320
321 if moddirstate:
322 repo.dirstate.setparents(p1, p2)
323
324 if show_stats:
325 stats = ((len(get), _("updated")),
326 (len(merge) - len(failedmerge), _("merged")),
327 (len(remove), _("removed")),
328 (len(failedmerge), _("unresolved")))
329 note = ", ".join([_("%d files %s") % s for s in stats])
330 repo.ui.status("%s\n" % note)
331 if moddirstate:
332 if branch_merge:
333 if failedmerge:
334 repo.ui.status(_("There are unresolved merges,"
335 " you can redo the full merge using:\n"
336 " hg update -C %s\n"
337 " hg merge %s\n"
338 % (repo.changelog.rev(p1),
339 repo.changelog.rev(p2))))
340 else:
341 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
342 elif failedmerge:
343 repo.ui.status(_("There are unresolved merges with"
344 " locally modified files.\n"))
345
346 repo.hook('update', parent1=xp1, parent2=xxp2, error=int(err))
347 return err
348