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