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