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