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