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