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