Mercurial > public > mercurial-scm > hg
comparison mercurial/merge.py @ 2775: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
2774:8cd3e19bf4a5 | 2775: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 |