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