##// END OF EJS Templates
merge: simplify merge tests, fix exec flag bug...
Matt Mackall -
r5700:9cedc3fb default
parent child Browse files
Show More
@@ -1,671 +1,675 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, 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, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
360 if ((a^b) | (a^c)) ^ a:
360 if ((a^b) | (a^c)) ^ a:
361 return 'x'
361 return 'x'
362 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
362 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
363 if ((a^b) | (a^c)) ^ a:
363 if ((a^b) | (a^c)) ^ a:
364 return 'l'
364 return 'l'
365 return ''
365 return ''
366
366
367 def act(msg, m, f, *args):
367 def act(msg, m, f, *args):
368 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
368 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
369 action.append((f, m) + args)
369 action.append((f, m) + args)
370
370
371 if not (backwards or overwrite):
371 if not (backwards or overwrite):
372 rev1 = p1.rev()
372 rev1 = p1.rev()
373 if rev1 is None:
373 if rev1 is None:
374 # p1 is a workingctx
374 # p1 is a workingctx
375 rev1 = p1.parents()[0].rev()
375 rev1 = p1.parents()[0].rev()
376 limit = min(symmetricdifference(repo, rev1, p2.rev()))
376 limit = min(symmetricdifference(repo, rev1, p2.rev()))
377 copy, diverge = findcopies(repo, m1, m2, ma, limit)
377 copy, diverge = findcopies(repo, m1, m2, ma, limit)
378
378
379 for of, fl in diverge.items():
379 for of, fl in diverge.items():
380 act("divergent renames", "dr", of, fl)
380 act("divergent renames", "dr", of, fl)
381
381
382 copied = dict.fromkeys(copy.values())
382 copied = dict.fromkeys(copy.values())
383
383
384 # Compare manifests
384 # Compare manifests
385 for f, n in m1.iteritems():
385 for f, n in m1.iteritems():
386 if partial and not partial(f):
386 if partial and not partial(f):
387 continue
387 continue
388 if f in m2:
388 if f in m2:
389 # are files different?
389 # are files different?
390 if n != m2[f]:
390 if n != m2[f]:
391 a = ma.get(f, nullid)
391 a = ma.get(f, nullid)
392 # are we clobbering?
393 if overwrite:
394 act("clobbering", "g", f, m2.flags(f))
395 # or are we going back in time and clean?
396 elif backwards and not n[20:]:
397 act("reverting", "g", f, m2.flags(f))
392 # are both different from the ancestor?
398 # are both different from the ancestor?
393 if not overwrite and n != a and m2[f] != a:
399 elif n != a and m2[f] != a:
394 act("versions differ", "m", f, f, f, fmerge(f), False)
400 act("versions differ", "m", f, f, f, fmerge(f), False)
395 # are we clobbering?
396 # is remote's version newer?
401 # is remote's version newer?
397 # or are we going back in time and clean?
402 elif m2[f] != a:
398 elif overwrite or m2[f] != a or (backwards and not n[20:]):
403 act("remote is newer", "g", f, fmerge(f))
399 act("remote is newer", "g", f, m2.flags(f))
400 # local is newer, not overwrite, check mode bits
404 # local is newer, not overwrite, check mode bits
401 elif fmerge(f) != m1.flags(f):
405 elif fmerge(f) != m1.flags(f):
402 act("update permissions", "e", f, m2.flags(f))
406 act("update permissions", "e", f, m2.flags(f))
403 # contents same, check mode bits
407 # contents same, check mode bits
404 elif m1.flags(f) != m2.flags(f):
408 elif m1.flags(f) != m2.flags(f):
405 # are we clobbering?
409 # are we clobbering?
406 # is remote's version newer?
410 # is remote's version newer?
407 # or are we going back?
411 # or are we going back?
408 if overwrite or fmerge(f) != m1.flags(f) or backwards:
412 if overwrite or fmerge(f) != m1.flags(f) or backwards:
409 act("update permissions", "e", f, m2.flags(f))
413 act("update permissions", "e", f, m2.flags(f))
410 elif f in copied:
414 elif f in copied:
411 continue
415 continue
412 elif f in copy:
416 elif f in copy:
413 f2 = copy[f]
417 f2 = copy[f]
414 if f2 not in m2: # directory rename
418 if f2 not in m2: # directory rename
415 act("remote renamed directory to " + f2, "d",
419 act("remote renamed directory to " + f2, "d",
416 f, None, f2, m1.flags(f))
420 f, None, f2, m1.flags(f))
417 elif f2 in m1: # case 2 A,B/B/B
421 elif f2 in m1: # case 2 A,B/B/B
418 act("local copied to " + f2, "m",
422 act("local copied to " + f2, "m",
419 f, f2, f, fmerge(f, f2, f2), False)
423 f, f2, f, fmerge(f, f2, f2), False)
420 else: # case 4,21 A/B/B
424 else: # case 4,21 A/B/B
421 act("local moved to " + f2, "m",
425 act("local moved to " + f2, "m",
422 f, f2, f, fmerge(f, f2, f2), False)
426 f, f2, f, fmerge(f, f2, f2), False)
423 elif f in ma:
427 elif f in ma:
424 if n != ma[f] and not overwrite:
428 if n != ma[f] and not overwrite:
425 if repo.ui.prompt(
429 if repo.ui.prompt(
426 _(" local changed %s which remote deleted\n"
430 _(" local changed %s which remote deleted\n"
427 "use (c)hanged version or (d)elete?") % f,
431 "use (c)hanged version or (d)elete?") % f,
428 _("[cd]"), _("c")) == _("d"):
432 _("[cd]"), _("c")) == _("d"):
429 act("prompt delete", "r", f)
433 act("prompt delete", "r", f)
430 else:
434 else:
431 act("other deleted", "r", f)
435 act("other deleted", "r", f)
432 else:
436 else:
433 # file is created on branch or in working directory
437 # file is created on branch or in working directory
434 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
438 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
435 act("remote deleted", "r", f)
439 act("remote deleted", "r", f)
436
440
437 for f, n in m2.iteritems():
441 for f, n in m2.iteritems():
438 if partial and not partial(f):
442 if partial and not partial(f):
439 continue
443 continue
440 if f in m1:
444 if f in m1:
441 continue
445 continue
442 if f in copied:
446 if f in copied:
443 continue
447 continue
444 if f in copy:
448 if f in copy:
445 f2 = copy[f]
449 f2 = copy[f]
446 if f2 not in m1: # directory rename
450 if f2 not in m1: # directory rename
447 act("local renamed directory to " + f2, "d",
451 act("local renamed directory to " + f2, "d",
448 None, f, f2, m2.flags(f))
452 None, f, f2, m2.flags(f))
449 elif f2 in m2: # rename case 1, A/A,B/A
453 elif f2 in m2: # rename case 1, A/A,B/A
450 act("remote copied to " + f, "m",
454 act("remote copied to " + f, "m",
451 f2, f, f, fmerge(f2, f, f2), False)
455 f2, f, f, fmerge(f2, f, f2), False)
452 else: # case 3,20 A/B/A
456 else: # case 3,20 A/B/A
453 act("remote moved to " + f, "m",
457 act("remote moved to " + f, "m",
454 f2, f, f, fmerge(f2, f, f2), True)
458 f2, f, f, fmerge(f2, f, f2), True)
455 elif f in ma:
459 elif f in ma:
456 if overwrite or backwards:
460 if overwrite or backwards:
457 act("recreating", "g", f, m2.flags(f))
461 act("recreating", "g", f, m2.flags(f))
458 elif n != ma[f]:
462 elif n != ma[f]:
459 if repo.ui.prompt(
463 if repo.ui.prompt(
460 _("remote changed %s which local deleted\n"
464 _("remote changed %s which local deleted\n"
461 "use (c)hanged version or leave (d)eleted?") % f,
465 "use (c)hanged version or leave (d)eleted?") % f,
462 _("[cd]"), _("c")) == _("c"):
466 _("[cd]"), _("c")) == _("c"):
463 act("prompt recreating", "g", f, m2.flags(f))
467 act("prompt recreating", "g", f, m2.flags(f))
464 else:
468 else:
465 act("remote created", "g", f, m2.flags(f))
469 act("remote created", "g", f, m2.flags(f))
466
470
467 return action
471 return action
468
472
469 def applyupdates(repo, action, wctx, mctx):
473 def applyupdates(repo, action, wctx, mctx):
470 "apply the merge action list to the working directory"
474 "apply the merge action list to the working directory"
471
475
472 updated, merged, removed, unresolved = 0, 0, 0, 0
476 updated, merged, removed, unresolved = 0, 0, 0, 0
473 action.sort()
477 action.sort()
474 # prescan for copy/renames
478 # prescan for copy/renames
475 for a in action:
479 for a in action:
476 f, m = a[:2]
480 f, m = a[:2]
477 if m == 'm': # merge
481 if m == 'm': # merge
478 f2, fd, flags, move = a[2:]
482 f2, fd, flags, move = a[2:]
479 if f != fd:
483 if f != fd:
480 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
484 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
481 repo.wwrite(fd, repo.wread(f), flags)
485 repo.wwrite(fd, repo.wread(f), flags)
482
486
483 audit_path = util.path_auditor(repo.root)
487 audit_path = util.path_auditor(repo.root)
484
488
485 for a in action:
489 for a in action:
486 f, m = a[:2]
490 f, m = a[:2]
487 if f and f[0] == "/":
491 if f and f[0] == "/":
488 continue
492 continue
489 if m == "r": # remove
493 if m == "r": # remove
490 repo.ui.note(_("removing %s\n") % f)
494 repo.ui.note(_("removing %s\n") % f)
491 audit_path(f)
495 audit_path(f)
492 try:
496 try:
493 util.unlink(repo.wjoin(f))
497 util.unlink(repo.wjoin(f))
494 except OSError, inst:
498 except OSError, inst:
495 if inst.errno != errno.ENOENT:
499 if inst.errno != errno.ENOENT:
496 repo.ui.warn(_("update failed to remove %s: %s!\n") %
500 repo.ui.warn(_("update failed to remove %s: %s!\n") %
497 (f, inst.strerror))
501 (f, inst.strerror))
498 removed += 1
502 removed += 1
499 elif m == "m": # merge
503 elif m == "m": # merge
500 f2, fd, flags, move = a[2:]
504 f2, fd, flags, move = a[2:]
501 r = filemerge(repo, f, fd, f2, wctx, mctx)
505 r = filemerge(repo, f, fd, f2, wctx, mctx)
502 if r > 0:
506 if r > 0:
503 unresolved += 1
507 unresolved += 1
504 else:
508 else:
505 if r is None:
509 if r is None:
506 updated += 1
510 updated += 1
507 else:
511 else:
508 merged += 1
512 merged += 1
509 util.set_exec(repo.wjoin(fd), "x" in flags)
513 util.set_exec(repo.wjoin(fd), "x" in flags)
510 if f != fd and move and util.lexists(repo.wjoin(f)):
514 if f != fd and move and util.lexists(repo.wjoin(f)):
511 repo.ui.debug(_("removing %s\n") % f)
515 repo.ui.debug(_("removing %s\n") % f)
512 os.unlink(repo.wjoin(f))
516 os.unlink(repo.wjoin(f))
513 elif m == "g": # get
517 elif m == "g": # get
514 flags = a[2]
518 flags = a[2]
515 repo.ui.note(_("getting %s\n") % f)
519 repo.ui.note(_("getting %s\n") % f)
516 t = mctx.filectx(f).data()
520 t = mctx.filectx(f).data()
517 repo.wwrite(f, t, flags)
521 repo.wwrite(f, t, flags)
518 updated += 1
522 updated += 1
519 elif m == "d": # directory rename
523 elif m == "d": # directory rename
520 f2, fd, flags = a[2:]
524 f2, fd, flags = a[2:]
521 if f:
525 if f:
522 repo.ui.note(_("moving %s to %s\n") % (f, fd))
526 repo.ui.note(_("moving %s to %s\n") % (f, fd))
523 t = wctx.filectx(f).data()
527 t = wctx.filectx(f).data()
524 repo.wwrite(fd, t, flags)
528 repo.wwrite(fd, t, flags)
525 util.unlink(repo.wjoin(f))
529 util.unlink(repo.wjoin(f))
526 if f2:
530 if f2:
527 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
531 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
528 t = mctx.filectx(f2).data()
532 t = mctx.filectx(f2).data()
529 repo.wwrite(fd, t, flags)
533 repo.wwrite(fd, t, flags)
530 updated += 1
534 updated += 1
531 elif m == "dr": # divergent renames
535 elif m == "dr": # divergent renames
532 fl = a[2]
536 fl = a[2]
533 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
537 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
534 for nf in fl:
538 for nf in fl:
535 repo.ui.warn(" %s\n" % nf)
539 repo.ui.warn(" %s\n" % nf)
536 elif m == "e": # exec
540 elif m == "e": # exec
537 flags = a[2]
541 flags = a[2]
538 util.set_exec(repo.wjoin(f), flags)
542 util.set_exec(repo.wjoin(f), flags)
539
543
540 return updated, merged, removed, unresolved
544 return updated, merged, removed, unresolved
541
545
542 def recordupdates(repo, action, branchmerge):
546 def recordupdates(repo, action, branchmerge):
543 "record merge actions to the dirstate"
547 "record merge actions to the dirstate"
544
548
545 for a in action:
549 for a in action:
546 f, m = a[:2]
550 f, m = a[:2]
547 if m == "r": # remove
551 if m == "r": # remove
548 if branchmerge:
552 if branchmerge:
549 repo.dirstate.remove(f)
553 repo.dirstate.remove(f)
550 else:
554 else:
551 repo.dirstate.forget(f)
555 repo.dirstate.forget(f)
552 elif m == "f": # forget
556 elif m == "f": # forget
553 repo.dirstate.forget(f)
557 repo.dirstate.forget(f)
554 elif m in "ge": # get or exec change
558 elif m in "ge": # get or exec change
555 if branchmerge:
559 if branchmerge:
556 repo.dirstate.normaldirty(f)
560 repo.dirstate.normaldirty(f)
557 else:
561 else:
558 repo.dirstate.normal(f)
562 repo.dirstate.normal(f)
559 elif m == "m": # merge
563 elif m == "m": # merge
560 f2, fd, flag, move = a[2:]
564 f2, fd, flag, move = a[2:]
561 if branchmerge:
565 if branchmerge:
562 # We've done a branch merge, mark this file as merged
566 # We've done a branch merge, mark this file as merged
563 # so that we properly record the merger later
567 # so that we properly record the merger later
564 repo.dirstate.merge(fd)
568 repo.dirstate.merge(fd)
565 if f != f2: # copy/rename
569 if f != f2: # copy/rename
566 if move:
570 if move:
567 repo.dirstate.remove(f)
571 repo.dirstate.remove(f)
568 if f != fd:
572 if f != fd:
569 repo.dirstate.copy(f, fd)
573 repo.dirstate.copy(f, fd)
570 else:
574 else:
571 repo.dirstate.copy(f2, fd)
575 repo.dirstate.copy(f2, fd)
572 else:
576 else:
573 # We've update-merged a locally modified file, so
577 # We've update-merged a locally modified file, so
574 # we set the dirstate to emulate a normal checkout
578 # we set the dirstate to emulate a normal checkout
575 # of that file some time in the past. Thus our
579 # of that file some time in the past. Thus our
576 # merge will appear as a normal local file
580 # merge will appear as a normal local file
577 # modification.
581 # modification.
578 repo.dirstate.normallookup(fd)
582 repo.dirstate.normallookup(fd)
579 if move:
583 if move:
580 repo.dirstate.forget(f)
584 repo.dirstate.forget(f)
581 elif m == "d": # directory rename
585 elif m == "d": # directory rename
582 f2, fd, flag = a[2:]
586 f2, fd, flag = a[2:]
583 if not f2 and f not in repo.dirstate:
587 if not f2 and f not in repo.dirstate:
584 # untracked file moved
588 # untracked file moved
585 continue
589 continue
586 if branchmerge:
590 if branchmerge:
587 repo.dirstate.add(fd)
591 repo.dirstate.add(fd)
588 if f:
592 if f:
589 repo.dirstate.remove(f)
593 repo.dirstate.remove(f)
590 repo.dirstate.copy(f, fd)
594 repo.dirstate.copy(f, fd)
591 if f2:
595 if f2:
592 repo.dirstate.copy(f2, fd)
596 repo.dirstate.copy(f2, fd)
593 else:
597 else:
594 repo.dirstate.normal(fd)
598 repo.dirstate.normal(fd)
595 if f:
599 if f:
596 repo.dirstate.forget(f)
600 repo.dirstate.forget(f)
597
601
598 def update(repo, node, branchmerge, force, partial):
602 def update(repo, node, branchmerge, force, partial):
599 """
603 """
600 Perform a merge between the working directory and the given node
604 Perform a merge between the working directory and the given node
601
605
602 branchmerge = whether to merge between branches
606 branchmerge = whether to merge between branches
603 force = whether to force branch merging or file overwriting
607 force = whether to force branch merging or file overwriting
604 partial = a function to filter file lists (dirstate not updated)
608 partial = a function to filter file lists (dirstate not updated)
605 """
609 """
606
610
607 wlock = repo.wlock()
611 wlock = repo.wlock()
608 try:
612 try:
609 wc = repo.workingctx()
613 wc = repo.workingctx()
610 if node is None:
614 if node is None:
611 # tip of current branch
615 # tip of current branch
612 try:
616 try:
613 node = repo.branchtags()[wc.branch()]
617 node = repo.branchtags()[wc.branch()]
614 except KeyError:
618 except KeyError:
615 if wc.branch() == "default": # no default branch!
619 if wc.branch() == "default": # no default branch!
616 node = repo.lookup("tip") # update to tip
620 node = repo.lookup("tip") # update to tip
617 else:
621 else:
618 raise util.Abort(_("branch %s not found") % wc.branch())
622 raise util.Abort(_("branch %s not found") % wc.branch())
619 overwrite = force and not branchmerge
623 overwrite = force and not branchmerge
620 forcemerge = force and branchmerge
624 forcemerge = force and branchmerge
621 pl = wc.parents()
625 pl = wc.parents()
622 p1, p2 = pl[0], repo.changectx(node)
626 p1, p2 = pl[0], repo.changectx(node)
623 pa = p1.ancestor(p2)
627 pa = p1.ancestor(p2)
624 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
628 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
625 fastforward = False
629 fastforward = False
626
630
627 ### check phase
631 ### check phase
628 if not overwrite and len(pl) > 1:
632 if not overwrite and len(pl) > 1:
629 raise util.Abort(_("outstanding uncommitted merges"))
633 raise util.Abort(_("outstanding uncommitted merges"))
630 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
634 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
631 if branchmerge:
635 if branchmerge:
632 if p1.branch() != p2.branch() and pa != p2:
636 if p1.branch() != p2.branch() and pa != p2:
633 fastforward = True
637 fastforward = True
634 else:
638 else:
635 raise util.Abort(_("there is nothing to merge, just use "
639 raise util.Abort(_("there is nothing to merge, just use "
636 "'hg update' or look at 'hg heads'"))
640 "'hg update' or look at 'hg heads'"))
637 elif not (overwrite or branchmerge):
641 elif not (overwrite or branchmerge):
638 raise util.Abort(_("update spans branches, use 'hg merge' "
642 raise util.Abort(_("update spans branches, use 'hg merge' "
639 "or 'hg update -C' to lose changes"))
643 "or 'hg update -C' to lose changes"))
640 if branchmerge and not forcemerge:
644 if branchmerge and not forcemerge:
641 if wc.files():
645 if wc.files():
642 raise util.Abort(_("outstanding uncommitted changes"))
646 raise util.Abort(_("outstanding uncommitted changes"))
643
647
644 ### calculate phase
648 ### calculate phase
645 action = []
649 action = []
646 if not force:
650 if not force:
647 checkunknown(wc, p2)
651 checkunknown(wc, p2)
648 if not util.checkfolding(repo.path):
652 if not util.checkfolding(repo.path):
649 checkcollision(p2)
653 checkcollision(p2)
650 if not branchmerge:
654 if not branchmerge:
651 action += forgetremoved(wc, p2)
655 action += forgetremoved(wc, p2)
652 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
656 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
653
657
654 ### apply phase
658 ### apply phase
655 if not branchmerge: # just jump to the new rev
659 if not branchmerge: # just jump to the new rev
656 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
660 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
657 if not partial:
661 if not partial:
658 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
662 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
659
663
660 stats = applyupdates(repo, action, wc, p2)
664 stats = applyupdates(repo, action, wc, p2)
661
665
662 if not partial:
666 if not partial:
663 recordupdates(repo, action, branchmerge)
667 recordupdates(repo, action, branchmerge)
664 repo.dirstate.setparents(fp1, fp2)
668 repo.dirstate.setparents(fp1, fp2)
665 if not branchmerge and not fastforward:
669 if not branchmerge and not fastforward:
666 repo.dirstate.setbranch(p2.branch())
670 repo.dirstate.setbranch(p2.branch())
667 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
671 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
668
672
669 return stats
673 return stats
670 finally:
674 finally:
671 del wlock
675 del wlock
General Comments 0
You need to be logged in to leave comments. Login now