##// END OF EJS Templates
merge: add flag merging intelligence...
Matt Mackall -
r5708:f6bf8972 default
parent child Browse files
Show More
@@ -1,675 +1,685
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, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 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 _
9 from i18n import _
10 import errno, util, os, tempfile, context, heapq
10 import errno, util, os, tempfile, context, heapq
11
11
12 def filemerge(repo, fw, fd, fo, wctx, mctx):
12 def filemerge(repo, fw, fd, fo, wctx, mctx):
13 """perform a 3-way merge in the working directory
13 """perform a 3-way merge in the working directory
14
14
15 fw = original filename in the working directory
15 fw = original filename in the working directory
16 fd = destination filename in the working directory
16 fd = destination filename in the working directory
17 fo = filename in other parent
17 fo = filename in other parent
18 wctx, mctx = working and merge changecontexts
18 wctx, mctx = working and merge changecontexts
19 """
19 """
20
20
21 def temp(prefix, ctx):
21 def temp(prefix, ctx):
22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 (fd, name) = tempfile.mkstemp(prefix=pre)
23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 data = repo.wwritedata(ctx.path(), ctx.data())
24 data = repo.wwritedata(ctx.path(), ctx.data())
25 f = os.fdopen(fd, "wb")
25 f = os.fdopen(fd, "wb")
26 f.write(data)
26 f.write(data)
27 f.close()
27 f.close()
28 return name
28 return name
29
29
30 fcm = wctx.filectx(fw)
30 fcm = wctx.filectx(fw)
31 fcmdata = wctx.filectx(fd).data()
31 fcmdata = wctx.filectx(fd).data()
32 fco = mctx.filectx(fo)
32 fco = mctx.filectx(fo)
33
33
34 if not fco.cmp(fcmdata): # files identical?
34 if not fco.cmp(fcmdata): # files identical?
35 return None
35 return None
36
36
37 fca = fcm.ancestor(fco)
37 fca = fcm.ancestor(fco)
38 if not fca:
38 if not fca:
39 fca = repo.filectx(fw, fileid=nullrev)
39 fca = repo.filectx(fw, fileid=nullrev)
40 a = repo.wjoin(fd)
40 a = repo.wjoin(fd)
41 b = temp("base", fca)
41 b = temp("base", fca)
42 c = temp("other", fco)
42 c = temp("other", fco)
43
43
44 if fw != fo:
44 if fw != fo:
45 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
45 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
46 else:
46 else:
47 repo.ui.status(_("merging %s\n") % fw)
47 repo.ui.status(_("merging %s\n") % fw)
48
48
49 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
49 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
50
50
51 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
51 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
52 or "hgmerge")
52 or "hgmerge")
53 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
53 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
54 environ={'HG_FILE': fd,
54 environ={'HG_FILE': fd,
55 'HG_MY_NODE': str(wctx.parents()[0]),
55 'HG_MY_NODE': str(wctx.parents()[0]),
56 'HG_OTHER_NODE': str(mctx),
56 'HG_OTHER_NODE': str(mctx),
57 'HG_MY_ISLINK': fcm.islink(),
57 'HG_MY_ISLINK': fcm.islink(),
58 'HG_OTHER_ISLINK': fco.islink(),
58 'HG_OTHER_ISLINK': fco.islink(),
59 'HG_BASE_ISLINK': fca.islink(),})
59 'HG_BASE_ISLINK': fca.islink(),})
60 if r:
60 if r:
61 repo.ui.warn(_("merging %s failed!\n") % fd)
61 repo.ui.warn(_("merging %s failed!\n") % fd)
62
62
63 os.unlink(b)
63 os.unlink(b)
64 os.unlink(c)
64 os.unlink(c)
65 return r
65 return r
66
66
67 def checkunknown(wctx, mctx):
67 def checkunknown(wctx, mctx):
68 "check for collisions between unknown files and files in mctx"
68 "check for collisions between unknown files and files in mctx"
69 man = mctx.manifest()
69 man = mctx.manifest()
70 for f in wctx.unknown():
70 for f in wctx.unknown():
71 if f in man:
71 if f in man:
72 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
72 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
73 raise util.Abort(_("untracked file in working directory differs"
73 raise util.Abort(_("untracked file in working directory differs"
74 " from file in requested revision: '%s'")
74 " from file in requested revision: '%s'")
75 % f)
75 % f)
76
76
77 def checkcollision(mctx):
77 def checkcollision(mctx):
78 "check for case folding collisions in the destination context"
78 "check for case folding collisions in the destination context"
79 folded = {}
79 folded = {}
80 for fn in mctx.manifest():
80 for fn in mctx.manifest():
81 fold = fn.lower()
81 fold = fn.lower()
82 if fold in folded:
82 if fold in folded:
83 raise util.Abort(_("case-folding collision between %s and %s")
83 raise util.Abort(_("case-folding collision between %s and %s")
84 % (fn, folded[fold]))
84 % (fn, folded[fold]))
85 folded[fold] = fn
85 folded[fold] = fn
86
86
87 def forgetremoved(wctx, mctx):
87 def forgetremoved(wctx, mctx):
88 """
88 """
89 Forget removed files
89 Forget removed files
90
90
91 If we're jumping between revisions (as opposed to merging), and if
91 If we're jumping between revisions (as opposed to merging), and if
92 neither the working directory nor the target rev has the file,
92 neither the working directory nor the target rev has the file,
93 then we need to remove it from the dirstate, to prevent the
93 then we need to remove it from the dirstate, to prevent the
94 dirstate from listing the file when it is no longer in the
94 dirstate from listing the file when it is no longer in the
95 manifest.
95 manifest.
96 """
96 """
97
97
98 action = []
98 action = []
99 man = mctx.manifest()
99 man = mctx.manifest()
100 for f in wctx.deleted() + wctx.removed():
100 for f in wctx.deleted() + wctx.removed():
101 if f not in man:
101 if f not in man:
102 action.append((f, "f"))
102 action.append((f, "f"))
103
103
104 return action
104 return action
105
105
106 def findcopies(repo, m1, m2, ma, limit):
106 def findcopies(repo, m1, m2, ma, limit):
107 """
107 """
108 Find moves and copies between m1 and m2 back to limit linkrev
108 Find moves and copies between m1 and m2 back to limit linkrev
109 """
109 """
110
110
111 def nonoverlap(d1, d2, d3):
111 def nonoverlap(d1, d2, d3):
112 "Return list of elements in d1 not in d2 or d3"
112 "Return list of elements in d1 not in d2 or d3"
113 l = [d for d in d1 if d not in d3 and d not in d2]
113 l = [d for d in d1 if d not in d3 and d not in d2]
114 l.sort()
114 l.sort()
115 return l
115 return l
116
116
117 def dirname(f):
117 def dirname(f):
118 s = f.rfind("/")
118 s = f.rfind("/")
119 if s == -1:
119 if s == -1:
120 return ""
120 return ""
121 return f[:s]
121 return f[:s]
122
122
123 def dirs(files):
123 def dirs(files):
124 d = {}
124 d = {}
125 for f in files:
125 for f in files:
126 f = dirname(f)
126 f = dirname(f)
127 while f not in d:
127 while f not in d:
128 d[f] = True
128 d[f] = True
129 f = dirname(f)
129 f = dirname(f)
130 return d
130 return d
131
131
132 wctx = repo.workingctx()
132 wctx = repo.workingctx()
133
133
134 def makectx(f, n):
134 def makectx(f, n):
135 if len(n) == 20:
135 if len(n) == 20:
136 return repo.filectx(f, fileid=n)
136 return repo.filectx(f, fileid=n)
137 return wctx.filectx(f)
137 return wctx.filectx(f)
138 ctx = util.cachefunc(makectx)
138 ctx = util.cachefunc(makectx)
139
139
140 def findold(fctx):
140 def findold(fctx):
141 "find files that path was copied from, back to linkrev limit"
141 "find files that path was copied from, back to linkrev limit"
142 old = {}
142 old = {}
143 seen = {}
143 seen = {}
144 orig = fctx.path()
144 orig = fctx.path()
145 visit = [fctx]
145 visit = [fctx]
146 while visit:
146 while visit:
147 fc = visit.pop()
147 fc = visit.pop()
148 s = str(fc)
148 s = str(fc)
149 if s in seen:
149 if s in seen:
150 continue
150 continue
151 seen[s] = 1
151 seen[s] = 1
152 if fc.path() != orig and fc.path() not in old:
152 if fc.path() != orig and fc.path() not in old:
153 old[fc.path()] = 1
153 old[fc.path()] = 1
154 if fc.rev() < limit:
154 if fc.rev() < limit:
155 continue
155 continue
156 visit += fc.parents()
156 visit += fc.parents()
157
157
158 old = old.keys()
158 old = old.keys()
159 old.sort()
159 old.sort()
160 return old
160 return old
161
161
162 copy = {}
162 copy = {}
163 fullcopy = {}
163 fullcopy = {}
164 diverge = {}
164 diverge = {}
165
165
166 def checkcopies(c, man, aman):
166 def checkcopies(c, man, aman):
167 '''check possible copies for filectx c'''
167 '''check possible copies for filectx c'''
168 for of in findold(c):
168 for of in findold(c):
169 fullcopy[c.path()] = of # remember for dir rename detection
169 fullcopy[c.path()] = of # remember for dir rename detection
170 if of not in man: # original file not in other manifest?
170 if of not in man: # original file not in other manifest?
171 if of in ma:
171 if of in ma:
172 diverge.setdefault(of, []).append(c.path())
172 diverge.setdefault(of, []).append(c.path())
173 continue
173 continue
174 # if the original file is unchanged on the other branch,
174 # if the original file is unchanged on the other branch,
175 # no merge needed
175 # no merge needed
176 if man[of] == aman.get(of):
176 if man[of] == aman.get(of):
177 continue
177 continue
178 c2 = ctx(of, man[of])
178 c2 = ctx(of, man[of])
179 ca = c.ancestor(c2)
179 ca = c.ancestor(c2)
180 if not ca: # unrelated?
180 if not ca: # unrelated?
181 continue
181 continue
182 # named changed on only one side?
182 # named changed on only one side?
183 if ca.path() == c.path() or ca.path() == c2.path():
183 if ca.path() == c.path() or ca.path() == c2.path():
184 if c == ca and c2 == ca: # no merge needed, ignore copy
184 if c == ca and c2 == ca: # no merge needed, ignore copy
185 continue
185 continue
186 copy[c.path()] = of
186 copy[c.path()] = of
187
187
188 if not repo.ui.configbool("merge", "followcopies", True):
188 if not repo.ui.configbool("merge", "followcopies", True):
189 return {}, {}
189 return {}, {}
190
190
191 # avoid silly behavior for update from empty dir
191 # avoid silly behavior for update from empty dir
192 if not m1 or not m2 or not ma:
192 if not m1 or not m2 or not ma:
193 return {}, {}
193 return {}, {}
194
194
195 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit)
195 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit)
196
196
197 u1 = nonoverlap(m1, m2, ma)
197 u1 = nonoverlap(m1, m2, ma)
198 u2 = nonoverlap(m2, m1, ma)
198 u2 = nonoverlap(m2, m1, ma)
199
199
200 if u1:
200 if u1:
201 repo.ui.debug(_(" unmatched files in local:\n %s\n")
201 repo.ui.debug(_(" unmatched files in local:\n %s\n")
202 % "\n ".join(u1))
202 % "\n ".join(u1))
203 if u2:
203 if u2:
204 repo.ui.debug(_(" unmatched files in other:\n %s\n")
204 repo.ui.debug(_(" unmatched files in other:\n %s\n")
205 % "\n ".join(u2))
205 % "\n ".join(u2))
206
206
207 for f in u1:
207 for f in u1:
208 checkcopies(ctx(f, m1[f]), m2, ma)
208 checkcopies(ctx(f, m1[f]), m2, ma)
209
209
210 for f in u2:
210 for f in u2:
211 checkcopies(ctx(f, m2[f]), m1, ma)
211 checkcopies(ctx(f, m2[f]), m1, ma)
212
212
213 d2 = {}
213 d2 = {}
214 for of, fl in diverge.items():
214 for of, fl in diverge.items():
215 for f in fl:
215 for f in fl:
216 fo = list(fl)
216 fo = list(fl)
217 fo.remove(f)
217 fo.remove(f)
218 d2[f] = (of, fo)
218 d2[f] = (of, fo)
219
219
220 if fullcopy:
220 if fullcopy:
221 repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n"))
221 repo.ui.debug(_(" all copies found (* = to merge, ! = divergent):\n"))
222 for f in fullcopy:
222 for f in fullcopy:
223 note = ""
223 note = ""
224 if f in copy: note += "*"
224 if f in copy: note += "*"
225 if f in diverge: note += "!"
225 if f in diverge: note += "!"
226 repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note))
226 repo.ui.debug(_(" %s -> %s %s\n") % (f, fullcopy[f], note))
227
227
228 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
228 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
229 return copy, diverge
229 return copy, diverge
230
230
231 repo.ui.debug(_(" checking for directory renames\n"))
231 repo.ui.debug(_(" checking for directory renames\n"))
232
232
233 # generate a directory move map
233 # generate a directory move map
234 d1, d2 = dirs(m1), dirs(m2)
234 d1, d2 = dirs(m1), dirs(m2)
235 invalid = {}
235 invalid = {}
236 dirmove = {}
236 dirmove = {}
237
237
238 # examine each file copy for a potential directory move, which is
238 # examine each file copy for a potential directory move, which is
239 # when all the files in a directory are moved to a new directory
239 # when all the files in a directory are moved to a new directory
240 for dst, src in fullcopy.items():
240 for dst, src in fullcopy.items():
241 dsrc, ddst = dirname(src), dirname(dst)
241 dsrc, ddst = dirname(src), dirname(dst)
242 if dsrc in invalid:
242 if dsrc in invalid:
243 # already seen to be uninteresting
243 # already seen to be uninteresting
244 continue
244 continue
245 elif dsrc in d1 and ddst in d1:
245 elif dsrc in d1 and ddst in d1:
246 # directory wasn't entirely moved locally
246 # directory wasn't entirely moved locally
247 invalid[dsrc] = True
247 invalid[dsrc] = True
248 elif dsrc in d2 and ddst in d2:
248 elif dsrc in d2 and ddst in d2:
249 # directory wasn't entirely moved remotely
249 # directory wasn't entirely moved remotely
250 invalid[dsrc] = True
250 invalid[dsrc] = True
251 elif dsrc in dirmove and dirmove[dsrc] != ddst:
251 elif dsrc in dirmove and dirmove[dsrc] != ddst:
252 # files from the same directory moved to two different places
252 # files from the same directory moved to two different places
253 invalid[dsrc] = True
253 invalid[dsrc] = True
254 else:
254 else:
255 # looks good so far
255 # looks good so far
256 dirmove[dsrc + "/"] = ddst + "/"
256 dirmove[dsrc + "/"] = ddst + "/"
257
257
258 for i in invalid:
258 for i in invalid:
259 if i in dirmove:
259 if i in dirmove:
260 del dirmove[i]
260 del dirmove[i]
261
261
262 del d1, d2, invalid
262 del d1, d2, invalid
263
263
264 if not dirmove:
264 if not dirmove:
265 return copy, diverge
265 return copy, diverge
266
266
267 for d in dirmove:
267 for d in dirmove:
268 repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d]))
268 repo.ui.debug(_(" dir %s -> %s\n") % (d, dirmove[d]))
269
269
270 # check unaccounted nonoverlapping files against directory moves
270 # check unaccounted nonoverlapping files against directory moves
271 for f in u1 + u2:
271 for f in u1 + u2:
272 if f not in fullcopy:
272 if f not in fullcopy:
273 for d in dirmove:
273 for d in dirmove:
274 if f.startswith(d):
274 if f.startswith(d):
275 # new file added in a directory that was moved, move it
275 # new file added in a directory that was moved, move it
276 copy[f] = dirmove[d] + f[len(d):]
276 copy[f] = dirmove[d] + f[len(d):]
277 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f]))
277 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f]))
278 break
278 break
279
279
280 return copy, diverge
280 return copy, diverge
281
281
282 def symmetricdifference(repo, rev1, rev2):
282 def symmetricdifference(repo, rev1, rev2):
283 """symmetric difference of the sets of ancestors of rev1 and rev2
283 """symmetric difference of the sets of ancestors of rev1 and rev2
284
284
285 I.e. revisions that are ancestors of rev1 or rev2, but not both.
285 I.e. revisions that are ancestors of rev1 or rev2, but not both.
286 """
286 """
287 # basic idea:
287 # basic idea:
288 # - mark rev1 and rev2 with different colors
288 # - mark rev1 and rev2 with different colors
289 # - walk the graph in topological order with the help of a heap;
289 # - walk the graph in topological order with the help of a heap;
290 # for each revision r:
290 # for each revision r:
291 # - if r has only one color, we want to return it
291 # - if r has only one color, we want to return it
292 # - add colors[r] to its parents
292 # - add colors[r] to its parents
293 #
293 #
294 # We keep track of the number of revisions in the heap that
294 # We keep track of the number of revisions in the heap that
295 # we may be interested in. We stop walking the graph as soon
295 # we may be interested in. We stop walking the graph as soon
296 # as this number reaches 0.
296 # as this number reaches 0.
297 WHITE = 1
297 WHITE = 1
298 BLACK = 2
298 BLACK = 2
299 ALLCOLORS = WHITE | BLACK
299 ALLCOLORS = WHITE | BLACK
300 colors = {rev1: WHITE, rev2: BLACK}
300 colors = {rev1: WHITE, rev2: BLACK}
301
301
302 cl = repo.changelog
302 cl = repo.changelog
303
303
304 visit = [-rev1, -rev2]
304 visit = [-rev1, -rev2]
305 heapq.heapify(visit)
305 heapq.heapify(visit)
306 n_wanted = len(visit)
306 n_wanted = len(visit)
307 ret = []
307 ret = []
308
308
309 while n_wanted:
309 while n_wanted:
310 r = -heapq.heappop(visit)
310 r = -heapq.heappop(visit)
311 wanted = colors[r] != ALLCOLORS
311 wanted = colors[r] != ALLCOLORS
312 n_wanted -= wanted
312 n_wanted -= wanted
313 if wanted:
313 if wanted:
314 ret.append(r)
314 ret.append(r)
315
315
316 for p in cl.parentrevs(r):
316 for p in cl.parentrevs(r):
317 if p == nullrev:
317 if p == nullrev:
318 continue
318 continue
319 if p not in colors:
319 if p not in colors:
320 # first time we see p; add it to visit
320 # first time we see p; add it to visit
321 n_wanted += wanted
321 n_wanted += wanted
322 colors[p] = colors[r]
322 colors[p] = colors[r]
323 heapq.heappush(visit, -p)
323 heapq.heappush(visit, -p)
324 elif colors[p] != ALLCOLORS and colors[p] != colors[r]:
324 elif colors[p] != ALLCOLORS and colors[p] != colors[r]:
325 # at first we thought we wanted p, but now
325 # at first we thought we wanted p, but now
326 # we know we don't really want it
326 # we know we don't really want it
327 n_wanted -= 1
327 n_wanted -= 1
328 colors[p] |= colors[r]
328 colors[p] |= colors[r]
329
329
330 del colors[r]
330 del colors[r]
331
331
332 return ret
332 return ret
333
333
334 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
334 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
335 """
335 """
336 Merge p1 and p2 with ancestor ma and generate merge action list
336 Merge p1 and p2 with ancestor ma and generate merge action list
337
337
338 overwrite = whether we clobber working files
338 overwrite = whether we clobber working files
339 partial = function to filter file lists
339 partial = function to filter file lists
340 """
340 """
341
341
342 repo.ui.note(_("resolving manifests\n"))
342 repo.ui.note(_("resolving manifests\n"))
343 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
343 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
344 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
344 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
345
345
346 m1 = p1.manifest()
346 m1 = p1.manifest()
347 m2 = p2.manifest()
347 m2 = p2.manifest()
348 ma = pa.manifest()
348 ma = pa.manifest()
349 backwards = (pa == p2)
349 backwards = (pa == p2)
350 action = []
350 action = []
351 copy = {}
351 copy = {}
352 diverge = {}
352 diverge = {}
353
353
354 def fmerge(f, f2=None, fa=None):
354 def fmerge(f, f2=None, fa=None):
355 """merge flags"""
355 """merge flags"""
356 if not f2:
356 if not f2:
357 f2 = f
357 f2 = f
358 fa = f
358 fa = f
359 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
359 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
360 if ((a^b) | (a^c)) ^ a:
360 if m == n: # flags agree
361 return 'x'
361 return m # unchanged
362 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
362 if m and n: # flags are set but don't agree
363 if ((a^b) | (a^c)) ^ a:
363 if not a: # both differ from parent
364 return 'l'
364 r = repo.ui.prompt(
365 return ''
365 _(" conflicting flags for %s\n"
366 "(n)one, e(x)ec or sym(l)ink?") % f, "[nxl]", "n")
367 return r != "n" and r or ''
368 if m == a:
369 return n # changed from m to n
370 return m # changed from n to m
371 if m and m != a: # changed from a to m
372 return m
373 if n and n != a: # changed from a to n
374 return n
375 return '' # flag was cleared
366
376
367 def act(msg, m, f, *args):
377 def act(msg, m, f, *args):
368 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
378 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
369 action.append((f, m) + args)
379 action.append((f, m) + args)
370
380
371 if not (backwards or overwrite):
381 if not (backwards or overwrite):
372 rev1 = p1.rev()
382 rev1 = p1.rev()
373 if rev1 is None:
383 if rev1 is None:
374 # p1 is a workingctx
384 # p1 is a workingctx
375 rev1 = p1.parents()[0].rev()
385 rev1 = p1.parents()[0].rev()
376 limit = min(symmetricdifference(repo, rev1, p2.rev()))
386 limit = min(symmetricdifference(repo, rev1, p2.rev()))
377 copy, diverge = findcopies(repo, m1, m2, ma, limit)
387 copy, diverge = findcopies(repo, m1, m2, ma, limit)
378
388
379 for of, fl in diverge.items():
389 for of, fl in diverge.items():
380 act("divergent renames", "dr", of, fl)
390 act("divergent renames", "dr", of, fl)
381
391
382 copied = dict.fromkeys(copy.values())
392 copied = dict.fromkeys(copy.values())
383
393
384 # Compare manifests
394 # Compare manifests
385 for f, n in m1.iteritems():
395 for f, n in m1.iteritems():
386 if partial and not partial(f):
396 if partial and not partial(f):
387 continue
397 continue
388 if f in m2:
398 if f in m2:
399 if overwrite or backwards:
400 rflags = m2.flags(f)
401 else:
402 rflags = fmerge(f)
389 # are files different?
403 # are files different?
390 if n != m2[f]:
404 if n != m2[f]:
391 a = ma.get(f, nullid)
405 a = ma.get(f, nullid)
392 # are we clobbering?
406 # are we clobbering?
393 if overwrite:
407 if overwrite:
394 act("clobbering", "g", f, m2.flags(f))
408 act("clobbering", "g", f, rflags)
395 # or are we going back in time and clean?
409 # or are we going back in time and clean?
396 elif backwards and not n[20:]:
410 elif backwards and not n[20:]:
397 act("reverting", "g", f, m2.flags(f))
411 act("reverting", "g", f, rflags)
398 # are both different from the ancestor?
412 # are both different from the ancestor?
399 elif n != a and m2[f] != a:
413 elif n != a and m2[f] != a:
400 act("versions differ", "m", f, f, f, fmerge(f), False)
414 act("versions differ", "m", f, f, f, rflags, False)
401 # is remote's version newer?
415 # is remote's version newer?
402 elif m2[f] != a:
416 elif m2[f] != a:
403 act("remote is newer", "g", f, fmerge(f))
417 act("remote is newer", "g", f, rflags)
404 # local is newer, not overwrite, check mode bits
418 # local is newer, not overwrite, check mode bits
405 elif fmerge(f) != m1.flags(f):
419 elif m1.flags(f) != rflags:
406 act("update permissions", "e", f, m2.flags(f))
420 act("update permissions", "e", f, rflags)
407 # contents same, check mode bits
421 # contents same, check mode bits
408 elif m1.flags(f) != m2.flags(f):
422 elif m1.flags(f) != rflags:
409 # are we clobbering?
423 act("update permissions", "e", f, rflags)
410 # is remote's version newer?
411 # or are we going back?
412 if overwrite or fmerge(f) != m1.flags(f) or backwards:
413 act("update permissions", "e", f, m2.flags(f))
414 elif f in copied:
424 elif f in copied:
415 continue
425 continue
416 elif f in copy:
426 elif f in copy:
417 f2 = copy[f]
427 f2 = copy[f]
418 if f2 not in m2: # directory rename
428 if f2 not in m2: # directory rename
419 act("remote renamed directory to " + f2, "d",
429 act("remote renamed directory to " + f2, "d",
420 f, None, f2, m1.flags(f))
430 f, None, f2, m1.flags(f))
421 elif f2 in m1: # case 2 A,B/B/B
431 elif f2 in m1: # case 2 A,B/B/B
422 act("local copied to " + f2, "m",
432 act("local copied to " + f2, "m",
423 f, f2, f, fmerge(f, f2, f2), False)
433 f, f2, f, fmerge(f, f2, f2), False)
424 else: # case 4,21 A/B/B
434 else: # case 4,21 A/B/B
425 act("local moved to " + f2, "m",
435 act("local moved to " + f2, "m",
426 f, f2, f, fmerge(f, f2, f2), False)
436 f, f2, f, fmerge(f, f2, f2), False)
427 elif f in ma:
437 elif f in ma:
428 if n != ma[f] and not overwrite:
438 if n != ma[f] and not overwrite:
429 if repo.ui.prompt(
439 if repo.ui.prompt(
430 _(" local changed %s which remote deleted\n"
440 _(" local changed %s which remote deleted\n"
431 "use (c)hanged version or (d)elete?") % f,
441 "use (c)hanged version or (d)elete?") % f,
432 _("[cd]"), _("c")) == _("d"):
442 _("[cd]"), _("c")) == _("d"):
433 act("prompt delete", "r", f)
443 act("prompt delete", "r", f)
434 else:
444 else:
435 act("other deleted", "r", f)
445 act("other deleted", "r", f)
436 else:
446 else:
437 # file is created on branch or in working directory
447 # file is created on branch or in working directory
438 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
448 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
439 act("remote deleted", "r", f)
449 act("remote deleted", "r", f)
440
450
441 for f, n in m2.iteritems():
451 for f, n in m2.iteritems():
442 if partial and not partial(f):
452 if partial and not partial(f):
443 continue
453 continue
444 if f in m1:
454 if f in m1:
445 continue
455 continue
446 if f in copied:
456 if f in copied:
447 continue
457 continue
448 if f in copy:
458 if f in copy:
449 f2 = copy[f]
459 f2 = copy[f]
450 if f2 not in m1: # directory rename
460 if f2 not in m1: # directory rename
451 act("local renamed directory to " + f2, "d",
461 act("local renamed directory to " + f2, "d",
452 None, f, f2, m2.flags(f))
462 None, f, f2, m2.flags(f))
453 elif f2 in m2: # rename case 1, A/A,B/A
463 elif f2 in m2: # rename case 1, A/A,B/A
454 act("remote copied to " + f, "m",
464 act("remote copied to " + f, "m",
455 f2, f, f, fmerge(f2, f, f2), False)
465 f2, f, f, fmerge(f2, f, f2), False)
456 else: # case 3,20 A/B/A
466 else: # case 3,20 A/B/A
457 act("remote moved to " + f, "m",
467 act("remote moved to " + f, "m",
458 f2, f, f, fmerge(f2, f, f2), True)
468 f2, f, f, fmerge(f2, f, f2), True)
459 elif f in ma:
469 elif f in ma:
460 if overwrite or backwards:
470 if overwrite or backwards:
461 act("recreating", "g", f, m2.flags(f))
471 act("recreating", "g", f, m2.flags(f))
462 elif n != ma[f]:
472 elif n != ma[f]:
463 if repo.ui.prompt(
473 if repo.ui.prompt(
464 _("remote changed %s which local deleted\n"
474 _("remote changed %s which local deleted\n"
465 "use (c)hanged version or leave (d)eleted?") % f,
475 "use (c)hanged version or leave (d)eleted?") % f,
466 _("[cd]"), _("c")) == _("c"):
476 _("[cd]"), _("c")) == _("c"):
467 act("prompt recreating", "g", f, m2.flags(f))
477 act("prompt recreating", "g", f, m2.flags(f))
468 else:
478 else:
469 act("remote created", "g", f, m2.flags(f))
479 act("remote created", "g", f, m2.flags(f))
470
480
471 return action
481 return action
472
482
473 def applyupdates(repo, action, wctx, mctx):
483 def applyupdates(repo, action, wctx, mctx):
474 "apply the merge action list to the working directory"
484 "apply the merge action list to the working directory"
475
485
476 updated, merged, removed, unresolved = 0, 0, 0, 0
486 updated, merged, removed, unresolved = 0, 0, 0, 0
477 action.sort()
487 action.sort()
478 # prescan for copy/renames
488 # prescan for copy/renames
479 for a in action:
489 for a in action:
480 f, m = a[:2]
490 f, m = a[:2]
481 if m == 'm': # merge
491 if m == 'm': # merge
482 f2, fd, flags, move = a[2:]
492 f2, fd, flags, move = a[2:]
483 if f != fd:
493 if f != fd:
484 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
494 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
485 repo.wwrite(fd, repo.wread(f), flags)
495 repo.wwrite(fd, repo.wread(f), flags)
486
496
487 audit_path = util.path_auditor(repo.root)
497 audit_path = util.path_auditor(repo.root)
488
498
489 for a in action:
499 for a in action:
490 f, m = a[:2]
500 f, m = a[:2]
491 if f and f[0] == "/":
501 if f and f[0] == "/":
492 continue
502 continue
493 if m == "r": # remove
503 if m == "r": # remove
494 repo.ui.note(_("removing %s\n") % f)
504 repo.ui.note(_("removing %s\n") % f)
495 audit_path(f)
505 audit_path(f)
496 try:
506 try:
497 util.unlink(repo.wjoin(f))
507 util.unlink(repo.wjoin(f))
498 except OSError, inst:
508 except OSError, inst:
499 if inst.errno != errno.ENOENT:
509 if inst.errno != errno.ENOENT:
500 repo.ui.warn(_("update failed to remove %s: %s!\n") %
510 repo.ui.warn(_("update failed to remove %s: %s!\n") %
501 (f, inst.strerror))
511 (f, inst.strerror))
502 removed += 1
512 removed += 1
503 elif m == "m": # merge
513 elif m == "m": # merge
504 f2, fd, flags, move = a[2:]
514 f2, fd, flags, move = a[2:]
505 r = filemerge(repo, f, fd, f2, wctx, mctx)
515 r = filemerge(repo, f, fd, f2, wctx, mctx)
506 if r > 0:
516 if r > 0:
507 unresolved += 1
517 unresolved += 1
508 else:
518 else:
509 if r is None:
519 if r is None:
510 updated += 1
520 updated += 1
511 else:
521 else:
512 merged += 1
522 merged += 1
513 util.set_flags(repo.wjoin(fd), flags)
523 util.set_flags(repo.wjoin(fd), flags)
514 if f != fd and move and util.lexists(repo.wjoin(f)):
524 if f != fd and move and util.lexists(repo.wjoin(f)):
515 repo.ui.debug(_("removing %s\n") % f)
525 repo.ui.debug(_("removing %s\n") % f)
516 os.unlink(repo.wjoin(f))
526 os.unlink(repo.wjoin(f))
517 elif m == "g": # get
527 elif m == "g": # get
518 flags = a[2]
528 flags = a[2]
519 repo.ui.note(_("getting %s\n") % f)
529 repo.ui.note(_("getting %s\n") % f)
520 t = mctx.filectx(f).data()
530 t = mctx.filectx(f).data()
521 repo.wwrite(f, t, flags)
531 repo.wwrite(f, t, flags)
522 updated += 1
532 updated += 1
523 elif m == "d": # directory rename
533 elif m == "d": # directory rename
524 f2, fd, flags = a[2:]
534 f2, fd, flags = a[2:]
525 if f:
535 if f:
526 repo.ui.note(_("moving %s to %s\n") % (f, fd))
536 repo.ui.note(_("moving %s to %s\n") % (f, fd))
527 t = wctx.filectx(f).data()
537 t = wctx.filectx(f).data()
528 repo.wwrite(fd, t, flags)
538 repo.wwrite(fd, t, flags)
529 util.unlink(repo.wjoin(f))
539 util.unlink(repo.wjoin(f))
530 if f2:
540 if f2:
531 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
541 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
532 t = mctx.filectx(f2).data()
542 t = mctx.filectx(f2).data()
533 repo.wwrite(fd, t, flags)
543 repo.wwrite(fd, t, flags)
534 updated += 1
544 updated += 1
535 elif m == "dr": # divergent renames
545 elif m == "dr": # divergent renames
536 fl = a[2]
546 fl = a[2]
537 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
547 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
538 for nf in fl:
548 for nf in fl:
539 repo.ui.warn(" %s\n" % nf)
549 repo.ui.warn(" %s\n" % nf)
540 elif m == "e": # exec
550 elif m == "e": # exec
541 flags = a[2]
551 flags = a[2]
542 util.set_flags(repo.wjoin(f), flags)
552 util.set_flags(repo.wjoin(f), flags)
543
553
544 return updated, merged, removed, unresolved
554 return updated, merged, removed, unresolved
545
555
546 def recordupdates(repo, action, branchmerge):
556 def recordupdates(repo, action, branchmerge):
547 "record merge actions to the dirstate"
557 "record merge actions to the dirstate"
548
558
549 for a in action:
559 for a in action:
550 f, m = a[:2]
560 f, m = a[:2]
551 if m == "r": # remove
561 if m == "r": # remove
552 if branchmerge:
562 if branchmerge:
553 repo.dirstate.remove(f)
563 repo.dirstate.remove(f)
554 else:
564 else:
555 repo.dirstate.forget(f)
565 repo.dirstate.forget(f)
556 elif m == "f": # forget
566 elif m == "f": # forget
557 repo.dirstate.forget(f)
567 repo.dirstate.forget(f)
558 elif m in "ge": # get or exec change
568 elif m in "ge": # get or exec change
559 if branchmerge:
569 if branchmerge:
560 repo.dirstate.normaldirty(f)
570 repo.dirstate.normaldirty(f)
561 else:
571 else:
562 repo.dirstate.normal(f)
572 repo.dirstate.normal(f)
563 elif m == "m": # merge
573 elif m == "m": # merge
564 f2, fd, flag, move = a[2:]
574 f2, fd, flag, move = a[2:]
565 if branchmerge:
575 if branchmerge:
566 # We've done a branch merge, mark this file as merged
576 # We've done a branch merge, mark this file as merged
567 # so that we properly record the merger later
577 # so that we properly record the merger later
568 repo.dirstate.merge(fd)
578 repo.dirstate.merge(fd)
569 if f != f2: # copy/rename
579 if f != f2: # copy/rename
570 if move:
580 if move:
571 repo.dirstate.remove(f)
581 repo.dirstate.remove(f)
572 if f != fd:
582 if f != fd:
573 repo.dirstate.copy(f, fd)
583 repo.dirstate.copy(f, fd)
574 else:
584 else:
575 repo.dirstate.copy(f2, fd)
585 repo.dirstate.copy(f2, fd)
576 else:
586 else:
577 # We've update-merged a locally modified file, so
587 # We've update-merged a locally modified file, so
578 # we set the dirstate to emulate a normal checkout
588 # we set the dirstate to emulate a normal checkout
579 # of that file some time in the past. Thus our
589 # of that file some time in the past. Thus our
580 # merge will appear as a normal local file
590 # merge will appear as a normal local file
581 # modification.
591 # modification.
582 repo.dirstate.normallookup(fd)
592 repo.dirstate.normallookup(fd)
583 if move:
593 if move:
584 repo.dirstate.forget(f)
594 repo.dirstate.forget(f)
585 elif m == "d": # directory rename
595 elif m == "d": # directory rename
586 f2, fd, flag = a[2:]
596 f2, fd, flag = a[2:]
587 if not f2 and f not in repo.dirstate:
597 if not f2 and f not in repo.dirstate:
588 # untracked file moved
598 # untracked file moved
589 continue
599 continue
590 if branchmerge:
600 if branchmerge:
591 repo.dirstate.add(fd)
601 repo.dirstate.add(fd)
592 if f:
602 if f:
593 repo.dirstate.remove(f)
603 repo.dirstate.remove(f)
594 repo.dirstate.copy(f, fd)
604 repo.dirstate.copy(f, fd)
595 if f2:
605 if f2:
596 repo.dirstate.copy(f2, fd)
606 repo.dirstate.copy(f2, fd)
597 else:
607 else:
598 repo.dirstate.normal(fd)
608 repo.dirstate.normal(fd)
599 if f:
609 if f:
600 repo.dirstate.forget(f)
610 repo.dirstate.forget(f)
601
611
602 def update(repo, node, branchmerge, force, partial):
612 def update(repo, node, branchmerge, force, partial):
603 """
613 """
604 Perform a merge between the working directory and the given node
614 Perform a merge between the working directory and the given node
605
615
606 branchmerge = whether to merge between branches
616 branchmerge = whether to merge between branches
607 force = whether to force branch merging or file overwriting
617 force = whether to force branch merging or file overwriting
608 partial = a function to filter file lists (dirstate not updated)
618 partial = a function to filter file lists (dirstate not updated)
609 """
619 """
610
620
611 wlock = repo.wlock()
621 wlock = repo.wlock()
612 try:
622 try:
613 wc = repo.workingctx()
623 wc = repo.workingctx()
614 if node is None:
624 if node is None:
615 # tip of current branch
625 # tip of current branch
616 try:
626 try:
617 node = repo.branchtags()[wc.branch()]
627 node = repo.branchtags()[wc.branch()]
618 except KeyError:
628 except KeyError:
619 if wc.branch() == "default": # no default branch!
629 if wc.branch() == "default": # no default branch!
620 node = repo.lookup("tip") # update to tip
630 node = repo.lookup("tip") # update to tip
621 else:
631 else:
622 raise util.Abort(_("branch %s not found") % wc.branch())
632 raise util.Abort(_("branch %s not found") % wc.branch())
623 overwrite = force and not branchmerge
633 overwrite = force and not branchmerge
624 forcemerge = force and branchmerge
634 forcemerge = force and branchmerge
625 pl = wc.parents()
635 pl = wc.parents()
626 p1, p2 = pl[0], repo.changectx(node)
636 p1, p2 = pl[0], repo.changectx(node)
627 pa = p1.ancestor(p2)
637 pa = p1.ancestor(p2)
628 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
638 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
629 fastforward = False
639 fastforward = False
630
640
631 ### check phase
641 ### check phase
632 if not overwrite and len(pl) > 1:
642 if not overwrite and len(pl) > 1:
633 raise util.Abort(_("outstanding uncommitted merges"))
643 raise util.Abort(_("outstanding uncommitted merges"))
634 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
644 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
635 if branchmerge:
645 if branchmerge:
636 if p1.branch() != p2.branch() and pa != p2:
646 if p1.branch() != p2.branch() and pa != p2:
637 fastforward = True
647 fastforward = True
638 else:
648 else:
639 raise util.Abort(_("there is nothing to merge, just use "
649 raise util.Abort(_("there is nothing to merge, just use "
640 "'hg update' or look at 'hg heads'"))
650 "'hg update' or look at 'hg heads'"))
641 elif not (overwrite or branchmerge):
651 elif not (overwrite or branchmerge):
642 raise util.Abort(_("update spans branches, use 'hg merge' "
652 raise util.Abort(_("update spans branches, use 'hg merge' "
643 "or 'hg update -C' to lose changes"))
653 "or 'hg update -C' to lose changes"))
644 if branchmerge and not forcemerge:
654 if branchmerge and not forcemerge:
645 if wc.files():
655 if wc.files():
646 raise util.Abort(_("outstanding uncommitted changes"))
656 raise util.Abort(_("outstanding uncommitted changes"))
647
657
648 ### calculate phase
658 ### calculate phase
649 action = []
659 action = []
650 if not force:
660 if not force:
651 checkunknown(wc, p2)
661 checkunknown(wc, p2)
652 if not util.checkfolding(repo.path):
662 if not util.checkfolding(repo.path):
653 checkcollision(p2)
663 checkcollision(p2)
654 if not branchmerge:
664 if not branchmerge:
655 action += forgetremoved(wc, p2)
665 action += forgetremoved(wc, p2)
656 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
666 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
657
667
658 ### apply phase
668 ### apply phase
659 if not branchmerge: # just jump to the new rev
669 if not branchmerge: # just jump to the new rev
660 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
670 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
661 if not partial:
671 if not partial:
662 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
672 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
663
673
664 stats = applyupdates(repo, action, wc, p2)
674 stats = applyupdates(repo, action, wc, p2)
665
675
666 if not partial:
676 if not partial:
667 recordupdates(repo, action, branchmerge)
677 recordupdates(repo, action, branchmerge)
668 repo.dirstate.setparents(fp1, fp2)
678 repo.dirstate.setparents(fp1, fp2)
669 if not branchmerge and not fastforward:
679 if not branchmerge and not fastforward:
670 repo.dirstate.setbranch(p2.branch())
680 repo.dirstate.setbranch(p2.branch())
671 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
681 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
672
682
673 return stats
683 return stats
674 finally:
684 finally:
675 del wlock
685 del wlock
General Comments 0
You need to be logged in to leave comments. Login now