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