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