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