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