##// END OF EJS Templates
merge: warn about file deleted in one branch and renamed in other (issue3074)...
Thomas Arendsen Hein -
r16794:98687cdd default
parent child Browse files
Show More
@@ -1,357 +1,365 b''
1 # copies.py - copy detection for Mercurial
1 # copies.py - copy detection for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import util
8 import util
9 import heapq
9 import heapq
10
10
11 def _nonoverlap(d1, d2, d3):
11 def _nonoverlap(d1, d2, d3):
12 "Return list of elements in d1 not in d2 or d3"
12 "Return list of elements in d1 not in d2 or d3"
13 return sorted([d for d in d1 if d not in d3 and d not in d2])
13 return sorted([d for d in d1 if d not in d3 and d not in d2])
14
14
15 def _dirname(f):
15 def _dirname(f):
16 s = f.rfind("/")
16 s = f.rfind("/")
17 if s == -1:
17 if s == -1:
18 return ""
18 return ""
19 return f[:s]
19 return f[:s]
20
20
21 def _findlimit(repo, a, b):
21 def _findlimit(repo, a, b):
22 """Find the earliest revision that's an ancestor of a or b but not both,
22 """Find the earliest revision that's an ancestor of a or b but not both,
23 None if no such revision exists.
23 None if no such revision exists.
24 """
24 """
25 # basic idea:
25 # basic idea:
26 # - mark a and b with different sides
26 # - mark a and b with different sides
27 # - if a parent's children are all on the same side, the parent is
27 # - if a parent's children are all on the same side, the parent is
28 # on that side, otherwise it is on no side
28 # on that side, otherwise it is on no side
29 # - walk the graph in topological order with the help of a heap;
29 # - walk the graph in topological order with the help of a heap;
30 # - add unseen parents to side map
30 # - add unseen parents to side map
31 # - clear side of any parent that has children on different sides
31 # - clear side of any parent that has children on different sides
32 # - track number of interesting revs that might still be on a side
32 # - track number of interesting revs that might still be on a side
33 # - track the lowest interesting rev seen
33 # - track the lowest interesting rev seen
34 # - quit when interesting revs is zero
34 # - quit when interesting revs is zero
35
35
36 cl = repo.changelog
36 cl = repo.changelog
37 working = len(cl) # pseudo rev for the working directory
37 working = len(cl) # pseudo rev for the working directory
38 if a is None:
38 if a is None:
39 a = working
39 a = working
40 if b is None:
40 if b is None:
41 b = working
41 b = working
42
42
43 side = {a: -1, b: 1}
43 side = {a: -1, b: 1}
44 visit = [-a, -b]
44 visit = [-a, -b]
45 heapq.heapify(visit)
45 heapq.heapify(visit)
46 interesting = len(visit)
46 interesting = len(visit)
47 hascommonancestor = False
47 hascommonancestor = False
48 limit = working
48 limit = working
49
49
50 while interesting:
50 while interesting:
51 r = -heapq.heappop(visit)
51 r = -heapq.heappop(visit)
52 if r == working:
52 if r == working:
53 parents = [cl.rev(p) for p in repo.dirstate.parents()]
53 parents = [cl.rev(p) for p in repo.dirstate.parents()]
54 else:
54 else:
55 parents = cl.parentrevs(r)
55 parents = cl.parentrevs(r)
56 for p in parents:
56 for p in parents:
57 if p < 0:
57 if p < 0:
58 continue
58 continue
59 if p not in side:
59 if p not in side:
60 # first time we see p; add it to visit
60 # first time we see p; add it to visit
61 side[p] = side[r]
61 side[p] = side[r]
62 if side[p]:
62 if side[p]:
63 interesting += 1
63 interesting += 1
64 heapq.heappush(visit, -p)
64 heapq.heappush(visit, -p)
65 elif side[p] and side[p] != side[r]:
65 elif side[p] and side[p] != side[r]:
66 # p was interesting but now we know better
66 # p was interesting but now we know better
67 side[p] = 0
67 side[p] = 0
68 interesting -= 1
68 interesting -= 1
69 hascommonancestor = True
69 hascommonancestor = True
70 if side[r]:
70 if side[r]:
71 limit = r # lowest rev visited
71 limit = r # lowest rev visited
72 interesting -= 1
72 interesting -= 1
73
73
74 if not hascommonancestor:
74 if not hascommonancestor:
75 return None
75 return None
76 return limit
76 return limit
77
77
78 def _chain(src, dst, a, b):
78 def _chain(src, dst, a, b):
79 '''chain two sets of copies a->b'''
79 '''chain two sets of copies a->b'''
80 t = a.copy()
80 t = a.copy()
81 for k, v in b.iteritems():
81 for k, v in b.iteritems():
82 if v in t:
82 if v in t:
83 # found a chain
83 # found a chain
84 if t[v] != k:
84 if t[v] != k:
85 # file wasn't renamed back to itself
85 # file wasn't renamed back to itself
86 t[k] = t[v]
86 t[k] = t[v]
87 if v not in dst:
87 if v not in dst:
88 # chain was a rename, not a copy
88 # chain was a rename, not a copy
89 del t[v]
89 del t[v]
90 if v in src:
90 if v in src:
91 # file is a copy of an existing file
91 # file is a copy of an existing file
92 t[k] = v
92 t[k] = v
93
93
94 # remove criss-crossed copies
94 # remove criss-crossed copies
95 for k, v in t.items():
95 for k, v in t.items():
96 if k in src and v in dst:
96 if k in src and v in dst:
97 del t[k]
97 del t[k]
98
98
99 return t
99 return t
100
100
101 def _tracefile(fctx, actx):
101 def _tracefile(fctx, actx):
102 '''return file context that is the ancestor of fctx present in actx'''
102 '''return file context that is the ancestor of fctx present in actx'''
103 stop = actx.rev()
103 stop = actx.rev()
104 am = actx.manifest()
104 am = actx.manifest()
105
105
106 for f in fctx.ancestors():
106 for f in fctx.ancestors():
107 if am.get(f.path(), None) == f.filenode():
107 if am.get(f.path(), None) == f.filenode():
108 return f
108 return f
109 if f.rev() < stop:
109 if f.rev() < stop:
110 return None
110 return None
111
111
112 def _dirstatecopies(d):
112 def _dirstatecopies(d):
113 ds = d._repo.dirstate
113 ds = d._repo.dirstate
114 c = ds.copies().copy()
114 c = ds.copies().copy()
115 for k in c.keys():
115 for k in c.keys():
116 if ds[k] not in 'anm':
116 if ds[k] not in 'anm':
117 del c[k]
117 del c[k]
118 return c
118 return c
119
119
120 def _forwardcopies(a, b):
120 def _forwardcopies(a, b):
121 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
121 '''find {dst@b: src@a} copy mapping where a is an ancestor of b'''
122
122
123 # check for working copy
123 # check for working copy
124 w = None
124 w = None
125 if b.rev() is None:
125 if b.rev() is None:
126 w = b
126 w = b
127 b = w.p1()
127 b = w.p1()
128 if a == b:
128 if a == b:
129 # short-circuit to avoid issues with merge states
129 # short-circuit to avoid issues with merge states
130 return _dirstatecopies(w)
130 return _dirstatecopies(w)
131
131
132 # find where new files came from
132 # find where new files came from
133 # we currently don't try to find where old files went, too expensive
133 # we currently don't try to find where old files went, too expensive
134 # this means we can miss a case like 'hg rm b; hg cp a b'
134 # this means we can miss a case like 'hg rm b; hg cp a b'
135 cm = {}
135 cm = {}
136 for f in b:
136 for f in b:
137 if f not in a:
137 if f not in a:
138 ofctx = _tracefile(b[f], a)
138 ofctx = _tracefile(b[f], a)
139 if ofctx:
139 if ofctx:
140 cm[f] = ofctx.path()
140 cm[f] = ofctx.path()
141
141
142 # combine copies from dirstate if necessary
142 # combine copies from dirstate if necessary
143 if w is not None:
143 if w is not None:
144 cm = _chain(a, w, cm, _dirstatecopies(w))
144 cm = _chain(a, w, cm, _dirstatecopies(w))
145
145
146 return cm
146 return cm
147
147
148 def _backwardcopies(a, b):
148 def _backwardcopies(a, b):
149 # because the forward mapping is 1:n, we can lose renames here
149 # because the forward mapping is 1:n, we can lose renames here
150 # in particular, we find renames better than copies
150 # in particular, we find renames better than copies
151 f = _forwardcopies(b, a)
151 f = _forwardcopies(b, a)
152 r = {}
152 r = {}
153 for k, v in f.iteritems():
153 for k, v in f.iteritems():
154 r[v] = k
154 r[v] = k
155 return r
155 return r
156
156
157 def pathcopies(x, y):
157 def pathcopies(x, y):
158 '''find {dst@y: src@x} copy mapping for directed compare'''
158 '''find {dst@y: src@x} copy mapping for directed compare'''
159 if x == y or not x or not y:
159 if x == y or not x or not y:
160 return {}
160 return {}
161 a = y.ancestor(x)
161 a = y.ancestor(x)
162 if a == x:
162 if a == x:
163 return _forwardcopies(x, y)
163 return _forwardcopies(x, y)
164 if a == y:
164 if a == y:
165 return _backwardcopies(x, y)
165 return _backwardcopies(x, y)
166 return _chain(x, y, _backwardcopies(x, a), _forwardcopies(a, y))
166 return _chain(x, y, _backwardcopies(x, a), _forwardcopies(a, y))
167
167
168 def mergecopies(repo, c1, c2, ca):
168 def mergecopies(repo, c1, c2, ca):
169 """
169 """
170 Find moves and copies between context c1 and c2 that are relevant
170 Find moves and copies between context c1 and c2 that are relevant
171 for merging.
171 for merging.
172
172
173 Returns two dicts, "copy" and "diverge".
173 Returns two dicts, "copy" and "diverge".
174
174
175 "copy" is a mapping from destination name -> source name,
175 "copy" is a mapping from destination name -> source name,
176 where source is in c1 and destination is in c2 or vice-versa.
176 where source is in c1 and destination is in c2 or vice-versa.
177
177
178 "diverge" is a mapping of source name -> list of destination names
178 "diverge" is a mapping of source name -> list of destination names
179 for divergent renames.
179 for divergent renames.
180
181 "renamedelete" is a mapping of source name -> list of destination
182 names for files deleted in c1 that were renamed in c2 or vice-versa.
180 """
183 """
181 # avoid silly behavior for update from empty dir
184 # avoid silly behavior for update from empty dir
182 if not c1 or not c2 or c1 == c2:
185 if not c1 or not c2 or c1 == c2:
183 return {}, {}
186 return {}, {}, {}
184
187
185 # avoid silly behavior for parent -> working dir
188 # avoid silly behavior for parent -> working dir
186 if c2.node() is None and c1.node() == repo.dirstate.p1():
189 if c2.node() is None and c1.node() == repo.dirstate.p1():
187 return repo.dirstate.copies(), {}
190 return repo.dirstate.copies(), {}, {}
188
191
189 limit = _findlimit(repo, c1.rev(), c2.rev())
192 limit = _findlimit(repo, c1.rev(), c2.rev())
190 if limit is None:
193 if limit is None:
191 # no common ancestor, no copies
194 # no common ancestor, no copies
192 return {}, {}
195 return {}, {}, {}
193 m1 = c1.manifest()
196 m1 = c1.manifest()
194 m2 = c2.manifest()
197 m2 = c2.manifest()
195 ma = ca.manifest()
198 ma = ca.manifest()
196
199
197 def makectx(f, n):
200 def makectx(f, n):
198 if len(n) != 20: # in a working context?
201 if len(n) != 20: # in a working context?
199 if c1.rev() is None:
202 if c1.rev() is None:
200 return c1.filectx(f)
203 return c1.filectx(f)
201 return c2.filectx(f)
204 return c2.filectx(f)
202 return repo.filectx(f, fileid=n)
205 return repo.filectx(f, fileid=n)
203
206
204 ctx = util.lrucachefunc(makectx)
207 ctx = util.lrucachefunc(makectx)
205 copy = {}
208 copy = {}
206 fullcopy = {}
209 fullcopy = {}
207 diverge = {}
210 diverge = {}
208
211
209 def related(f1, f2, limit):
212 def related(f1, f2, limit):
210 # Walk back to common ancestor to see if the two files originate
213 # Walk back to common ancestor to see if the two files originate
211 # from the same file. Since workingfilectx's rev() is None it messes
214 # from the same file. Since workingfilectx's rev() is None it messes
212 # up the integer comparison logic, hence the pre-step check for
215 # up the integer comparison logic, hence the pre-step check for
213 # None (f1 and f2 can only be workingfilectx's initially).
216 # None (f1 and f2 can only be workingfilectx's initially).
214
217
215 if f1 == f2:
218 if f1 == f2:
216 return f1 # a match
219 return f1 # a match
217
220
218 g1, g2 = f1.ancestors(), f2.ancestors()
221 g1, g2 = f1.ancestors(), f2.ancestors()
219 try:
222 try:
220 f1r, f2r = f1.rev(), f2.rev()
223 f1r, f2r = f1.rev(), f2.rev()
221
224
222 if f1r is None:
225 if f1r is None:
223 f1 = g1.next()
226 f1 = g1.next()
224 if f2r is None:
227 if f2r is None:
225 f2 = g2.next()
228 f2 = g2.next()
226
229
227 while True:
230 while True:
228 f1r, f2r = f1.rev(), f2.rev()
231 f1r, f2r = f1.rev(), f2.rev()
229 if f1r > f2r:
232 if f1r > f2r:
230 f1 = g1.next()
233 f1 = g1.next()
231 elif f2r > f1r:
234 elif f2r > f1r:
232 f2 = g2.next()
235 f2 = g2.next()
233 elif f1 == f2:
236 elif f1 == f2:
234 return f1 # a match
237 return f1 # a match
235 elif f1r == f2r or f1r < limit or f2r < limit:
238 elif f1r == f2r or f1r < limit or f2r < limit:
236 return False # copy no longer relevant
239 return False # copy no longer relevant
237 except StopIteration:
240 except StopIteration:
238 return False
241 return False
239
242
240 def checkcopies(f, m1, m2):
243 def checkcopies(f, m1, m2):
241 '''check possible copies of f from m1 to m2'''
244 '''check possible copies of f from m1 to m2'''
242 of = None
245 of = None
243 seen = set([f])
246 seen = set([f])
244 for oc in ctx(f, m1[f]).ancestors():
247 for oc in ctx(f, m1[f]).ancestors():
245 ocr = oc.rev()
248 ocr = oc.rev()
246 of = oc.path()
249 of = oc.path()
247 if of in seen:
250 if of in seen:
248 # check limit late - grab last rename before
251 # check limit late - grab last rename before
249 if ocr < limit:
252 if ocr < limit:
250 break
253 break
251 continue
254 continue
252 seen.add(of)
255 seen.add(of)
253
256
254 fullcopy[f] = of # remember for dir rename detection
257 fullcopy[f] = of # remember for dir rename detection
255 if of not in m2:
258 if of not in m2:
256 continue # no match, keep looking
259 continue # no match, keep looking
257 if m2[of] == ma.get(of):
260 if m2[of] == ma.get(of):
258 break # no merge needed, quit early
261 break # no merge needed, quit early
259 c2 = ctx(of, m2[of])
262 c2 = ctx(of, m2[of])
260 cr = related(oc, c2, ca.rev())
263 cr = related(oc, c2, ca.rev())
261 if cr and (of == f or of == c2.path()): # non-divergent
264 if cr and (of == f or of == c2.path()): # non-divergent
262 copy[f] = of
265 copy[f] = of
263 of = None
266 of = None
264 break
267 break
265
268
266 if of in ma:
269 if of in ma:
267 diverge.setdefault(of, []).append(f)
270 diverge.setdefault(of, []).append(f)
268
271
269 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
272 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
270
273
271 u1 = _nonoverlap(m1, m2, ma)
274 u1 = _nonoverlap(m1, m2, ma)
272 u2 = _nonoverlap(m2, m1, ma)
275 u2 = _nonoverlap(m2, m1, ma)
273
276
274 if u1:
277 if u1:
275 repo.ui.debug(" unmatched files in local:\n %s\n"
278 repo.ui.debug(" unmatched files in local:\n %s\n"
276 % "\n ".join(u1))
279 % "\n ".join(u1))
277 if u2:
280 if u2:
278 repo.ui.debug(" unmatched files in other:\n %s\n"
281 repo.ui.debug(" unmatched files in other:\n %s\n"
279 % "\n ".join(u2))
282 % "\n ".join(u2))
280
283
281 for f in u1:
284 for f in u1:
282 checkcopies(f, m1, m2)
285 checkcopies(f, m1, m2)
283 for f in u2:
286 for f in u2:
284 checkcopies(f, m2, m1)
287 checkcopies(f, m2, m1)
285
288
289 renamedelete = {}
286 diverge2 = set()
290 diverge2 = set()
287 for of, fl in diverge.items():
291 for of, fl in diverge.items():
288 if len(fl) == 1 or of in c1 or of in c2:
292 if len(fl) == 1 or of in c1 or of in c2:
289 del diverge[of] # not actually divergent, or not a rename
293 del diverge[of] # not actually divergent, or not a rename
294 if of not in c1 and of not in c2:
295 # renamed on one side, deleted on the other side, but filter
296 # out files that have been renamed and then deleted
297 renamedelete[of] = [f for f in fl if f in c1 or f in c2]
290 else:
298 else:
291 diverge2.update(fl) # reverse map for below
299 diverge2.update(fl) # reverse map for below
292
300
293 if fullcopy:
301 if fullcopy:
294 repo.ui.debug(" all copies found (* = to merge, ! = divergent):\n")
302 repo.ui.debug(" all copies found (* = to merge, ! = divergent):\n")
295 for f in fullcopy:
303 for f in fullcopy:
296 note = ""
304 note = ""
297 if f in copy:
305 if f in copy:
298 note += "*"
306 note += "*"
299 if f in diverge2:
307 if f in diverge2:
300 note += "!"
308 note += "!"
301 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
309 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
302 del diverge2
310 del diverge2
303
311
304 if not fullcopy:
312 if not fullcopy:
305 return copy, diverge
313 return copy, diverge, renamedelete
306
314
307 repo.ui.debug(" checking for directory renames\n")
315 repo.ui.debug(" checking for directory renames\n")
308
316
309 # generate a directory move map
317 # generate a directory move map
310 d1, d2 = c1.dirs(), c2.dirs()
318 d1, d2 = c1.dirs(), c2.dirs()
311 invalid = set([""])
319 invalid = set([""])
312 dirmove = {}
320 dirmove = {}
313
321
314 # examine each file copy for a potential directory move, which is
322 # examine each file copy for a potential directory move, which is
315 # when all the files in a directory are moved to a new directory
323 # when all the files in a directory are moved to a new directory
316 for dst, src in fullcopy.iteritems():
324 for dst, src in fullcopy.iteritems():
317 dsrc, ddst = _dirname(src), _dirname(dst)
325 dsrc, ddst = _dirname(src), _dirname(dst)
318 if dsrc in invalid:
326 if dsrc in invalid:
319 # already seen to be uninteresting
327 # already seen to be uninteresting
320 continue
328 continue
321 elif dsrc in d1 and ddst in d1:
329 elif dsrc in d1 and ddst in d1:
322 # directory wasn't entirely moved locally
330 # directory wasn't entirely moved locally
323 invalid.add(dsrc)
331 invalid.add(dsrc)
324 elif dsrc in d2 and ddst in d2:
332 elif dsrc in d2 and ddst in d2:
325 # directory wasn't entirely moved remotely
333 # directory wasn't entirely moved remotely
326 invalid.add(dsrc)
334 invalid.add(dsrc)
327 elif dsrc in dirmove and dirmove[dsrc] != ddst:
335 elif dsrc in dirmove and dirmove[dsrc] != ddst:
328 # files from the same directory moved to two different places
336 # files from the same directory moved to two different places
329 invalid.add(dsrc)
337 invalid.add(dsrc)
330 else:
338 else:
331 # looks good so far
339 # looks good so far
332 dirmove[dsrc + "/"] = ddst + "/"
340 dirmove[dsrc + "/"] = ddst + "/"
333
341
334 for i in invalid:
342 for i in invalid:
335 if i in dirmove:
343 if i in dirmove:
336 del dirmove[i]
344 del dirmove[i]
337 del d1, d2, invalid
345 del d1, d2, invalid
338
346
339 if not dirmove:
347 if not dirmove:
340 return copy, diverge
348 return copy, diverge, renamedelete
341
349
342 for d in dirmove:
350 for d in dirmove:
343 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
351 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
344
352
345 # check unaccounted nonoverlapping files against directory moves
353 # check unaccounted nonoverlapping files against directory moves
346 for f in u1 + u2:
354 for f in u1 + u2:
347 if f not in fullcopy:
355 if f not in fullcopy:
348 for d in dirmove:
356 for d in dirmove:
349 if f.startswith(d):
357 if f.startswith(d):
350 # new file added in a directory that was moved, move it
358 # new file added in a directory that was moved, move it
351 df = dirmove[d] + f[len(d):]
359 df = dirmove[d] + f[len(d):]
352 if df not in copy:
360 if df not in copy:
353 copy[f] = df
361 copy[f] = df
354 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
362 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
355 break
363 break
356
364
357 return copy, diverge
365 return copy, diverge, renamedelete
@@ -1,614 +1,622 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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from node import nullid, nullrev, hex, bin
8 from node import nullid, nullrev, hex, bin
9 from i18n import _
9 from i18n import _
10 import error, scmutil, util, filemerge, copies, subrepo
10 import error, scmutil, util, filemerge, copies, subrepo
11 import errno, os, shutil
11 import errno, os, shutil
12
12
13 class mergestate(object):
13 class mergestate(object):
14 '''track 3-way merge state of individual files'''
14 '''track 3-way merge state of individual files'''
15 def __init__(self, repo):
15 def __init__(self, repo):
16 self._repo = repo
16 self._repo = repo
17 self._dirty = False
17 self._dirty = False
18 self._read()
18 self._read()
19 def reset(self, node=None):
19 def reset(self, node=None):
20 self._state = {}
20 self._state = {}
21 if node:
21 if node:
22 self._local = node
22 self._local = node
23 shutil.rmtree(self._repo.join("merge"), True)
23 shutil.rmtree(self._repo.join("merge"), True)
24 self._dirty = False
24 self._dirty = False
25 def _read(self):
25 def _read(self):
26 self._state = {}
26 self._state = {}
27 try:
27 try:
28 f = self._repo.opener("merge/state")
28 f = self._repo.opener("merge/state")
29 for i, l in enumerate(f):
29 for i, l in enumerate(f):
30 if i == 0:
30 if i == 0:
31 self._local = bin(l[:-1])
31 self._local = bin(l[:-1])
32 else:
32 else:
33 bits = l[:-1].split("\0")
33 bits = l[:-1].split("\0")
34 self._state[bits[0]] = bits[1:]
34 self._state[bits[0]] = bits[1:]
35 f.close()
35 f.close()
36 except IOError, err:
36 except IOError, err:
37 if err.errno != errno.ENOENT:
37 if err.errno != errno.ENOENT:
38 raise
38 raise
39 self._dirty = False
39 self._dirty = False
40 def commit(self):
40 def commit(self):
41 if self._dirty:
41 if self._dirty:
42 f = self._repo.opener("merge/state", "w")
42 f = self._repo.opener("merge/state", "w")
43 f.write(hex(self._local) + "\n")
43 f.write(hex(self._local) + "\n")
44 for d, v in self._state.iteritems():
44 for d, v in self._state.iteritems():
45 f.write("\0".join([d] + v) + "\n")
45 f.write("\0".join([d] + v) + "\n")
46 f.close()
46 f.close()
47 self._dirty = False
47 self._dirty = False
48 def add(self, fcl, fco, fca, fd, flags):
48 def add(self, fcl, fco, fca, fd, flags):
49 hash = util.sha1(fcl.path()).hexdigest()
49 hash = util.sha1(fcl.path()).hexdigest()
50 self._repo.opener.write("merge/" + hash, fcl.data())
50 self._repo.opener.write("merge/" + hash, fcl.data())
51 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
51 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
52 hex(fca.filenode()), fco.path(), flags]
52 hex(fca.filenode()), fco.path(), flags]
53 self._dirty = True
53 self._dirty = True
54 def __contains__(self, dfile):
54 def __contains__(self, dfile):
55 return dfile in self._state
55 return dfile in self._state
56 def __getitem__(self, dfile):
56 def __getitem__(self, dfile):
57 return self._state[dfile][0]
57 return self._state[dfile][0]
58 def __iter__(self):
58 def __iter__(self):
59 l = self._state.keys()
59 l = self._state.keys()
60 l.sort()
60 l.sort()
61 for f in l:
61 for f in l:
62 yield f
62 yield f
63 def mark(self, dfile, state):
63 def mark(self, dfile, state):
64 self._state[dfile][0] = state
64 self._state[dfile][0] = state
65 self._dirty = True
65 self._dirty = True
66 def resolve(self, dfile, wctx, octx):
66 def resolve(self, dfile, wctx, octx):
67 if self[dfile] == 'r':
67 if self[dfile] == 'r':
68 return 0
68 return 0
69 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
69 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
70 f = self._repo.opener("merge/" + hash)
70 f = self._repo.opener("merge/" + hash)
71 self._repo.wwrite(dfile, f.read(), flags)
71 self._repo.wwrite(dfile, f.read(), flags)
72 f.close()
72 f.close()
73 fcd = wctx[dfile]
73 fcd = wctx[dfile]
74 fco = octx[ofile]
74 fco = octx[ofile]
75 fca = self._repo.filectx(afile, fileid=anode)
75 fca = self._repo.filectx(afile, fileid=anode)
76 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
76 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
77 if r is None:
77 if r is None:
78 # no real conflict
78 # no real conflict
79 del self._state[dfile]
79 del self._state[dfile]
80 elif not r:
80 elif not r:
81 self.mark(dfile, 'r')
81 self.mark(dfile, 'r')
82 return r
82 return r
83
83
84 def _checkunknownfile(repo, wctx, mctx, f):
84 def _checkunknownfile(repo, wctx, mctx, f):
85 return (not repo.dirstate._ignore(f)
85 return (not repo.dirstate._ignore(f)
86 and os.path.isfile(repo.wjoin(f))
86 and os.path.isfile(repo.wjoin(f))
87 and repo.dirstate.normalize(f) not in repo.dirstate
87 and repo.dirstate.normalize(f) not in repo.dirstate
88 and mctx[f].cmp(wctx[f]))
88 and mctx[f].cmp(wctx[f]))
89
89
90 def _checkunknown(repo, wctx, mctx):
90 def _checkunknown(repo, wctx, mctx):
91 "check for collisions between unknown files and files in mctx"
91 "check for collisions between unknown files and files in mctx"
92
92
93 error = False
93 error = False
94 for f in mctx:
94 for f in mctx:
95 if f not in wctx and _checkunknownfile(repo, wctx, mctx, f):
95 if f not in wctx and _checkunknownfile(repo, wctx, mctx, f):
96 error = True
96 error = True
97 wctx._repo.ui.warn(_("%s: untracked file differs\n") % f)
97 wctx._repo.ui.warn(_("%s: untracked file differs\n") % f)
98 if error:
98 if error:
99 raise util.Abort(_("untracked files in working directory differ "
99 raise util.Abort(_("untracked files in working directory differ "
100 "from files in requested revision"))
100 "from files in requested revision"))
101
101
102 def _checkcollision(mctx, wctx):
102 def _checkcollision(mctx, wctx):
103 "check for case folding collisions in the destination context"
103 "check for case folding collisions in the destination context"
104 folded = {}
104 folded = {}
105 for fn in mctx:
105 for fn in mctx:
106 fold = util.normcase(fn)
106 fold = util.normcase(fn)
107 if fold in folded:
107 if fold in folded:
108 raise util.Abort(_("case-folding collision between %s and %s")
108 raise util.Abort(_("case-folding collision between %s and %s")
109 % (fn, folded[fold]))
109 % (fn, folded[fold]))
110 folded[fold] = fn
110 folded[fold] = fn
111
111
112 if wctx:
112 if wctx:
113 # class to delay looking up copy mapping
113 # class to delay looking up copy mapping
114 class pathcopies(object):
114 class pathcopies(object):
115 @util.propertycache
115 @util.propertycache
116 def map(self):
116 def map(self):
117 # {dst@mctx: src@wctx} copy mapping
117 # {dst@mctx: src@wctx} copy mapping
118 return copies.pathcopies(wctx, mctx)
118 return copies.pathcopies(wctx, mctx)
119 pc = pathcopies()
119 pc = pathcopies()
120
120
121 for fn in wctx:
121 for fn in wctx:
122 fold = util.normcase(fn)
122 fold = util.normcase(fn)
123 mfn = folded.get(fold, None)
123 mfn = folded.get(fold, None)
124 if mfn and mfn != fn and pc.map.get(mfn) != fn:
124 if mfn and mfn != fn and pc.map.get(mfn) != fn:
125 raise util.Abort(_("case-folding collision between %s and %s")
125 raise util.Abort(_("case-folding collision between %s and %s")
126 % (mfn, fn))
126 % (mfn, fn))
127
127
128 def _forgetremoved(wctx, mctx, branchmerge):
128 def _forgetremoved(wctx, mctx, branchmerge):
129 """
129 """
130 Forget removed files
130 Forget removed files
131
131
132 If we're jumping between revisions (as opposed to merging), and if
132 If we're jumping between revisions (as opposed to merging), and if
133 neither the working directory nor the target rev has the file,
133 neither the working directory nor the target rev has the file,
134 then we need to remove it from the dirstate, to prevent the
134 then we need to remove it from the dirstate, to prevent the
135 dirstate from listing the file when it is no longer in the
135 dirstate from listing the file when it is no longer in the
136 manifest.
136 manifest.
137
137
138 If we're merging, and the other revision has removed a file
138 If we're merging, and the other revision has removed a file
139 that is not present in the working directory, we need to mark it
139 that is not present in the working directory, we need to mark it
140 as removed.
140 as removed.
141 """
141 """
142
142
143 action = []
143 action = []
144 state = branchmerge and 'r' or 'f'
144 state = branchmerge and 'r' or 'f'
145 for f in wctx.deleted():
145 for f in wctx.deleted():
146 if f not in mctx:
146 if f not in mctx:
147 action.append((f, state))
147 action.append((f, state))
148
148
149 if not branchmerge:
149 if not branchmerge:
150 for f in wctx.removed():
150 for f in wctx.removed():
151 if f not in mctx:
151 if f not in mctx:
152 action.append((f, "f"))
152 action.append((f, "f"))
153
153
154 return action
154 return action
155
155
156 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
156 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
157 """
157 """
158 Merge p1 and p2 with ancestor pa and generate merge action list
158 Merge p1 and p2 with ancestor pa and generate merge action list
159
159
160 overwrite = whether we clobber working files
160 overwrite = whether we clobber working files
161 partial = function to filter file lists
161 partial = function to filter file lists
162 """
162 """
163
163
164 def fmerge(f, f2, fa):
164 def fmerge(f, f2, fa):
165 """merge flags"""
165 """merge flags"""
166 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
166 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
167 if m == n: # flags agree
167 if m == n: # flags agree
168 return m # unchanged
168 return m # unchanged
169 if m and n and not a: # flags set, don't agree, differ from parent
169 if m and n and not a: # flags set, don't agree, differ from parent
170 r = repo.ui.promptchoice(
170 r = repo.ui.promptchoice(
171 _(" conflicting flags for %s\n"
171 _(" conflicting flags for %s\n"
172 "(n)one, e(x)ec or sym(l)ink?") % f,
172 "(n)one, e(x)ec or sym(l)ink?") % f,
173 (_("&None"), _("E&xec"), _("Sym&link")), 0)
173 (_("&None"), _("E&xec"), _("Sym&link")), 0)
174 if r == 1:
174 if r == 1:
175 return "x" # Exec
175 return "x" # Exec
176 if r == 2:
176 if r == 2:
177 return "l" # Symlink
177 return "l" # Symlink
178 return ""
178 return ""
179 if m and m != a: # changed from a to m
179 if m and m != a: # changed from a to m
180 return m
180 return m
181 if n and n != a: # changed from a to n
181 if n and n != a: # changed from a to n
182 if (n == 'l' or a == 'l') and m1.get(f) != ma.get(f):
182 if (n == 'l' or a == 'l') and m1.get(f) != ma.get(f):
183 # can't automatically merge symlink flag when there
183 # can't automatically merge symlink flag when there
184 # are file-level conflicts here, let filemerge take
184 # are file-level conflicts here, let filemerge take
185 # care of it
185 # care of it
186 return m
186 return m
187 return n
187 return n
188 return '' # flag was cleared
188 return '' # flag was cleared
189
189
190 def act(msg, m, f, *args):
190 def act(msg, m, f, *args):
191 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
191 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
192 action.append((f, m) + args)
192 action.append((f, m) + args)
193
193
194 action, copy = [], {}
194 action, copy = [], {}
195
195
196 if overwrite:
196 if overwrite:
197 pa = p1
197 pa = p1
198 elif pa == p2: # backwards
198 elif pa == p2: # backwards
199 pa = p1.p1()
199 pa = p1.p1()
200 elif pa and repo.ui.configbool("merge", "followcopies", True):
200 elif pa and repo.ui.configbool("merge", "followcopies", True):
201 copy, diverge = copies.mergecopies(repo, p1, p2, pa)
201 copy, diverge, renamedelete = copies.mergecopies(repo, p1, p2, pa)
202 for of, fl in diverge.iteritems():
202 for of, fl in diverge.iteritems():
203 act("divergent renames", "dr", of, fl)
203 act("divergent renames", "dr", of, fl)
204 for of, fl in renamedelete.iteritems():
205 act("rename and delete", "rd", of, fl)
204
206
205 repo.ui.note(_("resolving manifests\n"))
207 repo.ui.note(_("resolving manifests\n"))
206 repo.ui.debug(" overwrite: %s, partial: %s\n"
208 repo.ui.debug(" overwrite: %s, partial: %s\n"
207 % (bool(overwrite), bool(partial)))
209 % (bool(overwrite), bool(partial)))
208 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, p1, p2))
210 repo.ui.debug(" ancestor: %s, local: %s, remote: %s\n" % (pa, p1, p2))
209
211
210 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
212 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
211 copied = set(copy.values())
213 copied = set(copy.values())
212
214
213 if '.hgsubstate' in m1:
215 if '.hgsubstate' in m1:
214 # check whether sub state is modified
216 # check whether sub state is modified
215 for s in p1.substate:
217 for s in p1.substate:
216 if p1.sub(s).dirty():
218 if p1.sub(s).dirty():
217 m1['.hgsubstate'] += "+"
219 m1['.hgsubstate'] += "+"
218 break
220 break
219
221
220 # Compare manifests
222 # Compare manifests
221 for f, n in m1.iteritems():
223 for f, n in m1.iteritems():
222 if partial and not partial(f):
224 if partial and not partial(f):
223 continue
225 continue
224 if f in m2:
226 if f in m2:
225 rflags = fmerge(f, f, f)
227 rflags = fmerge(f, f, f)
226 a = ma.get(f, nullid)
228 a = ma.get(f, nullid)
227 if n == m2[f] or m2[f] == a: # same or local newer
229 if n == m2[f] or m2[f] == a: # same or local newer
228 # is file locally modified or flags need changing?
230 # is file locally modified or flags need changing?
229 # dirstate flags may need to be made current
231 # dirstate flags may need to be made current
230 if m1.flags(f) != rflags or n[20:]:
232 if m1.flags(f) != rflags or n[20:]:
231 act("update permissions", "e", f, rflags)
233 act("update permissions", "e", f, rflags)
232 elif n == a: # remote newer
234 elif n == a: # remote newer
233 act("remote is newer", "g", f, rflags)
235 act("remote is newer", "g", f, rflags)
234 else: # both changed
236 else: # both changed
235 act("versions differ", "m", f, f, f, rflags, False)
237 act("versions differ", "m", f, f, f, rflags, False)
236 elif f in copied: # files we'll deal with on m2 side
238 elif f in copied: # files we'll deal with on m2 side
237 pass
239 pass
238 elif f in copy:
240 elif f in copy:
239 f2 = copy[f]
241 f2 = copy[f]
240 if f2 not in m2: # directory rename
242 if f2 not in m2: # directory rename
241 act("remote renamed directory to " + f2, "d",
243 act("remote renamed directory to " + f2, "d",
242 f, None, f2, m1.flags(f))
244 f, None, f2, m1.flags(f))
243 else: # case 2 A,B/B/B or case 4,21 A/B/B
245 else: # case 2 A,B/B/B or case 4,21 A/B/B
244 act("local copied/moved to " + f2, "m",
246 act("local copied/moved to " + f2, "m",
245 f, f2, f, fmerge(f, f2, f2), False)
247 f, f2, f, fmerge(f, f2, f2), False)
246 elif f in ma: # clean, a different, no remote
248 elif f in ma: # clean, a different, no remote
247 if n != ma[f]:
249 if n != ma[f]:
248 if repo.ui.promptchoice(
250 if repo.ui.promptchoice(
249 _(" local changed %s which remote deleted\n"
251 _(" local changed %s which remote deleted\n"
250 "use (c)hanged version or (d)elete?") % f,
252 "use (c)hanged version or (d)elete?") % f,
251 (_("&Changed"), _("&Delete")), 0):
253 (_("&Changed"), _("&Delete")), 0):
252 act("prompt delete", "r", f)
254 act("prompt delete", "r", f)
253 else:
255 else:
254 act("prompt keep", "a", f)
256 act("prompt keep", "a", f)
255 elif n[20:] == "a": # added, no remote
257 elif n[20:] == "a": # added, no remote
256 act("remote deleted", "f", f)
258 act("remote deleted", "f", f)
257 else:
259 else:
258 act("other deleted", "r", f)
260 act("other deleted", "r", f)
259
261
260 for f, n in m2.iteritems():
262 for f, n in m2.iteritems():
261 if partial and not partial(f):
263 if partial and not partial(f):
262 continue
264 continue
263 if f in m1 or f in copied: # files already visited
265 if f in m1 or f in copied: # files already visited
264 continue
266 continue
265 if f in copy:
267 if f in copy:
266 f2 = copy[f]
268 f2 = copy[f]
267 if f2 not in m1: # directory rename
269 if f2 not in m1: # directory rename
268 act("local renamed directory to " + f2, "d",
270 act("local renamed directory to " + f2, "d",
269 None, f, f2, m2.flags(f))
271 None, f, f2, m2.flags(f))
270 elif f2 in m2: # rename case 1, A/A,B/A
272 elif f2 in m2: # rename case 1, A/A,B/A
271 act("remote copied to " + f, "m",
273 act("remote copied to " + f, "m",
272 f2, f, f, fmerge(f2, f, f2), False)
274 f2, f, f, fmerge(f2, f, f2), False)
273 else: # case 3,20 A/B/A
275 else: # case 3,20 A/B/A
274 act("remote moved to " + f, "m",
276 act("remote moved to " + f, "m",
275 f2, f, f, fmerge(f2, f, f2), True)
277 f2, f, f, fmerge(f2, f, f2), True)
276 elif f not in ma:
278 elif f not in ma:
277 if (not overwrite
279 if (not overwrite
278 and _checkunknownfile(repo, p1, p2, f)):
280 and _checkunknownfile(repo, p1, p2, f)):
279 rflags = fmerge(f, f, f)
281 rflags = fmerge(f, f, f)
280 act("remote differs from untracked local",
282 act("remote differs from untracked local",
281 "m", f, f, f, rflags, False)
283 "m", f, f, f, rflags, False)
282 else:
284 else:
283 act("remote created", "g", f, m2.flags(f))
285 act("remote created", "g", f, m2.flags(f))
284 elif n != ma[f]:
286 elif n != ma[f]:
285 if repo.ui.promptchoice(
287 if repo.ui.promptchoice(
286 _("remote changed %s which local deleted\n"
288 _("remote changed %s which local deleted\n"
287 "use (c)hanged version or leave (d)eleted?") % f,
289 "use (c)hanged version or leave (d)eleted?") % f,
288 (_("&Changed"), _("&Deleted")), 0) == 0:
290 (_("&Changed"), _("&Deleted")), 0) == 0:
289 act("prompt recreating", "g", f, m2.flags(f))
291 act("prompt recreating", "g", f, m2.flags(f))
290
292
291 return action
293 return action
292
294
293 def actionkey(a):
295 def actionkey(a):
294 return a[1] == 'r' and -1 or 0, a
296 return a[1] == 'r' and -1 or 0, a
295
297
296 def applyupdates(repo, action, wctx, mctx, actx, overwrite):
298 def applyupdates(repo, action, wctx, mctx, actx, overwrite):
297 """apply the merge action list to the working directory
299 """apply the merge action list to the working directory
298
300
299 wctx is the working copy context
301 wctx is the working copy context
300 mctx is the context to be merged into the working copy
302 mctx is the context to be merged into the working copy
301 actx is the context of the common ancestor
303 actx is the context of the common ancestor
302
304
303 Return a tuple of counts (updated, merged, removed, unresolved) that
305 Return a tuple of counts (updated, merged, removed, unresolved) that
304 describes how many files were affected by the update.
306 describes how many files were affected by the update.
305 """
307 """
306
308
307 updated, merged, removed, unresolved = 0, 0, 0, 0
309 updated, merged, removed, unresolved = 0, 0, 0, 0
308 ms = mergestate(repo)
310 ms = mergestate(repo)
309 ms.reset(wctx.p1().node())
311 ms.reset(wctx.p1().node())
310 moves = []
312 moves = []
311 action.sort(key=actionkey)
313 action.sort(key=actionkey)
312
314
313 # prescan for merges
315 # prescan for merges
314 for a in action:
316 for a in action:
315 f, m = a[:2]
317 f, m = a[:2]
316 if m == 'm': # merge
318 if m == 'm': # merge
317 f2, fd, flags, move = a[2:]
319 f2, fd, flags, move = a[2:]
318 if f == '.hgsubstate': # merged internally
320 if f == '.hgsubstate': # merged internally
319 continue
321 continue
320 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
322 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
321 fcl = wctx[f]
323 fcl = wctx[f]
322 fco = mctx[f2]
324 fco = mctx[f2]
323 if mctx == actx: # backwards, use working dir parent as ancestor
325 if mctx == actx: # backwards, use working dir parent as ancestor
324 if fcl.parents():
326 if fcl.parents():
325 fca = fcl.p1()
327 fca = fcl.p1()
326 else:
328 else:
327 fca = repo.filectx(f, fileid=nullrev)
329 fca = repo.filectx(f, fileid=nullrev)
328 else:
330 else:
329 fca = fcl.ancestor(fco, actx)
331 fca = fcl.ancestor(fco, actx)
330 if not fca:
332 if not fca:
331 fca = repo.filectx(f, fileid=nullrev)
333 fca = repo.filectx(f, fileid=nullrev)
332 ms.add(fcl, fco, fca, fd, flags)
334 ms.add(fcl, fco, fca, fd, flags)
333 if f != fd and move:
335 if f != fd and move:
334 moves.append(f)
336 moves.append(f)
335
337
336 audit = scmutil.pathauditor(repo.root)
338 audit = scmutil.pathauditor(repo.root)
337
339
338 # remove renamed files after safely stored
340 # remove renamed files after safely stored
339 for f in moves:
341 for f in moves:
340 if os.path.lexists(repo.wjoin(f)):
342 if os.path.lexists(repo.wjoin(f)):
341 repo.ui.debug("removing %s\n" % f)
343 repo.ui.debug("removing %s\n" % f)
342 audit(f)
344 audit(f)
343 os.unlink(repo.wjoin(f))
345 os.unlink(repo.wjoin(f))
344
346
345 numupdates = len(action)
347 numupdates = len(action)
346 for i, a in enumerate(action):
348 for i, a in enumerate(action):
347 f, m = a[:2]
349 f, m = a[:2]
348 repo.ui.progress(_('updating'), i + 1, item=f, total=numupdates,
350 repo.ui.progress(_('updating'), i + 1, item=f, total=numupdates,
349 unit=_('files'))
351 unit=_('files'))
350 if f and f[0] == "/":
352 if f and f[0] == "/":
351 continue
353 continue
352 if m == "r": # remove
354 if m == "r": # remove
353 repo.ui.note(_("removing %s\n") % f)
355 repo.ui.note(_("removing %s\n") % f)
354 audit(f)
356 audit(f)
355 if f == '.hgsubstate': # subrepo states need updating
357 if f == '.hgsubstate': # subrepo states need updating
356 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
358 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
357 try:
359 try:
358 util.unlinkpath(repo.wjoin(f))
360 util.unlinkpath(repo.wjoin(f))
359 except OSError, inst:
361 except OSError, inst:
360 if inst.errno != errno.ENOENT:
362 if inst.errno != errno.ENOENT:
361 repo.ui.warn(_("update failed to remove %s: %s!\n") %
363 repo.ui.warn(_("update failed to remove %s: %s!\n") %
362 (f, inst.strerror))
364 (f, inst.strerror))
363 removed += 1
365 removed += 1
364 elif m == "m": # merge
366 elif m == "m": # merge
365 if f == '.hgsubstate': # subrepo states need updating
367 if f == '.hgsubstate': # subrepo states need updating
366 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
368 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
367 overwrite)
369 overwrite)
368 continue
370 continue
369 f2, fd, flags, move = a[2:]
371 f2, fd, flags, move = a[2:]
370 repo.wopener.audit(fd)
372 repo.wopener.audit(fd)
371 r = ms.resolve(fd, wctx, mctx)
373 r = ms.resolve(fd, wctx, mctx)
372 if r is not None and r > 0:
374 if r is not None and r > 0:
373 unresolved += 1
375 unresolved += 1
374 else:
376 else:
375 if r is None:
377 if r is None:
376 updated += 1
378 updated += 1
377 else:
379 else:
378 merged += 1
380 merged += 1
379 if (move and repo.dirstate.normalize(fd) != f
381 if (move and repo.dirstate.normalize(fd) != f
380 and os.path.lexists(repo.wjoin(f))):
382 and os.path.lexists(repo.wjoin(f))):
381 repo.ui.debug("removing %s\n" % f)
383 repo.ui.debug("removing %s\n" % f)
382 audit(f)
384 audit(f)
383 os.unlink(repo.wjoin(f))
385 os.unlink(repo.wjoin(f))
384 elif m == "g": # get
386 elif m == "g": # get
385 flags = a[2]
387 flags = a[2]
386 repo.ui.note(_("getting %s\n") % f)
388 repo.ui.note(_("getting %s\n") % f)
387 t = mctx.filectx(f).data()
389 t = mctx.filectx(f).data()
388 repo.wwrite(f, t, flags)
390 repo.wwrite(f, t, flags)
389 t = None
391 t = None
390 updated += 1
392 updated += 1
391 if f == '.hgsubstate': # subrepo states need updating
393 if f == '.hgsubstate': # subrepo states need updating
392 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
394 subrepo.submerge(repo, wctx, mctx, wctx, overwrite)
393 elif m == "d": # directory rename
395 elif m == "d": # directory rename
394 f2, fd, flags = a[2:]
396 f2, fd, flags = a[2:]
395 if f:
397 if f:
396 repo.ui.note(_("moving %s to %s\n") % (f, fd))
398 repo.ui.note(_("moving %s to %s\n") % (f, fd))
397 audit(f)
399 audit(f)
398 t = wctx.filectx(f).data()
400 t = wctx.filectx(f).data()
399 repo.wwrite(fd, t, flags)
401 repo.wwrite(fd, t, flags)
400 util.unlinkpath(repo.wjoin(f))
402 util.unlinkpath(repo.wjoin(f))
401 if f2:
403 if f2:
402 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
404 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
403 t = mctx.filectx(f2).data()
405 t = mctx.filectx(f2).data()
404 repo.wwrite(fd, t, flags)
406 repo.wwrite(fd, t, flags)
405 updated += 1
407 updated += 1
406 elif m == "dr": # divergent renames
408 elif m == "dr": # divergent renames
407 fl = a[2]
409 fl = a[2]
408 repo.ui.warn(_("note: possible conflict - %s was renamed "
410 repo.ui.warn(_("note: possible conflict - %s was renamed "
409 "multiple times to:\n") % f)
411 "multiple times to:\n") % f)
410 for nf in fl:
412 for nf in fl:
411 repo.ui.warn(" %s\n" % nf)
413 repo.ui.warn(" %s\n" % nf)
414 elif m == "rd": # rename and delete
415 fl = a[2]
416 repo.ui.warn(_("note: possible conflict - %s was deleted "
417 "and renamed to:\n") % f)
418 for nf in fl:
419 repo.ui.warn(" %s\n" % nf)
412 elif m == "e": # exec
420 elif m == "e": # exec
413 flags = a[2]
421 flags = a[2]
414 repo.wopener.audit(f)
422 repo.wopener.audit(f)
415 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
423 util.setflags(repo.wjoin(f), 'l' in flags, 'x' in flags)
416 ms.commit()
424 ms.commit()
417 repo.ui.progress(_('updating'), None, total=numupdates, unit=_('files'))
425 repo.ui.progress(_('updating'), None, total=numupdates, unit=_('files'))
418
426
419 return updated, merged, removed, unresolved
427 return updated, merged, removed, unresolved
420
428
421 def recordupdates(repo, action, branchmerge):
429 def recordupdates(repo, action, branchmerge):
422 "record merge actions to the dirstate"
430 "record merge actions to the dirstate"
423
431
424 for a in action:
432 for a in action:
425 f, m = a[:2]
433 f, m = a[:2]
426 if m == "r": # remove
434 if m == "r": # remove
427 if branchmerge:
435 if branchmerge:
428 repo.dirstate.remove(f)
436 repo.dirstate.remove(f)
429 else:
437 else:
430 repo.dirstate.drop(f)
438 repo.dirstate.drop(f)
431 elif m == "a": # re-add
439 elif m == "a": # re-add
432 if not branchmerge:
440 if not branchmerge:
433 repo.dirstate.add(f)
441 repo.dirstate.add(f)
434 elif m == "f": # forget
442 elif m == "f": # forget
435 repo.dirstate.drop(f)
443 repo.dirstate.drop(f)
436 elif m == "e": # exec change
444 elif m == "e": # exec change
437 repo.dirstate.normallookup(f)
445 repo.dirstate.normallookup(f)
438 elif m == "g": # get
446 elif m == "g": # get
439 if branchmerge:
447 if branchmerge:
440 repo.dirstate.otherparent(f)
448 repo.dirstate.otherparent(f)
441 else:
449 else:
442 repo.dirstate.normal(f)
450 repo.dirstate.normal(f)
443 elif m == "m": # merge
451 elif m == "m": # merge
444 f2, fd, flag, move = a[2:]
452 f2, fd, flag, move = a[2:]
445 if branchmerge:
453 if branchmerge:
446 # We've done a branch merge, mark this file as merged
454 # We've done a branch merge, mark this file as merged
447 # so that we properly record the merger later
455 # so that we properly record the merger later
448 repo.dirstate.merge(fd)
456 repo.dirstate.merge(fd)
449 if f != f2: # copy/rename
457 if f != f2: # copy/rename
450 if move:
458 if move:
451 repo.dirstate.remove(f)
459 repo.dirstate.remove(f)
452 if f != fd:
460 if f != fd:
453 repo.dirstate.copy(f, fd)
461 repo.dirstate.copy(f, fd)
454 else:
462 else:
455 repo.dirstate.copy(f2, fd)
463 repo.dirstate.copy(f2, fd)
456 else:
464 else:
457 # We've update-merged a locally modified file, so
465 # We've update-merged a locally modified file, so
458 # we set the dirstate to emulate a normal checkout
466 # we set the dirstate to emulate a normal checkout
459 # of that file some time in the past. Thus our
467 # of that file some time in the past. Thus our
460 # merge will appear as a normal local file
468 # merge will appear as a normal local file
461 # modification.
469 # modification.
462 if f2 == fd: # file not locally copied/moved
470 if f2 == fd: # file not locally copied/moved
463 repo.dirstate.normallookup(fd)
471 repo.dirstate.normallookup(fd)
464 if move:
472 if move:
465 repo.dirstate.drop(f)
473 repo.dirstate.drop(f)
466 elif m == "d": # directory rename
474 elif m == "d": # directory rename
467 f2, fd, flag = a[2:]
475 f2, fd, flag = a[2:]
468 if not f2 and f not in repo.dirstate:
476 if not f2 and f not in repo.dirstate:
469 # untracked file moved
477 # untracked file moved
470 continue
478 continue
471 if branchmerge:
479 if branchmerge:
472 repo.dirstate.add(fd)
480 repo.dirstate.add(fd)
473 if f:
481 if f:
474 repo.dirstate.remove(f)
482 repo.dirstate.remove(f)
475 repo.dirstate.copy(f, fd)
483 repo.dirstate.copy(f, fd)
476 if f2:
484 if f2:
477 repo.dirstate.copy(f2, fd)
485 repo.dirstate.copy(f2, fd)
478 else:
486 else:
479 repo.dirstate.normal(fd)
487 repo.dirstate.normal(fd)
480 if f:
488 if f:
481 repo.dirstate.drop(f)
489 repo.dirstate.drop(f)
482
490
483 def update(repo, node, branchmerge, force, partial, ancestor=None,
491 def update(repo, node, branchmerge, force, partial, ancestor=None,
484 mergeancestor=False):
492 mergeancestor=False):
485 """
493 """
486 Perform a merge between the working directory and the given node
494 Perform a merge between the working directory and the given node
487
495
488 node = the node to update to, or None if unspecified
496 node = the node to update to, or None if unspecified
489 branchmerge = whether to merge between branches
497 branchmerge = whether to merge between branches
490 force = whether to force branch merging or file overwriting
498 force = whether to force branch merging or file overwriting
491 partial = a function to filter file lists (dirstate not updated)
499 partial = a function to filter file lists (dirstate not updated)
492 mergeancestor = if false, merging with an ancestor (fast-forward)
500 mergeancestor = if false, merging with an ancestor (fast-forward)
493 is only allowed between different named branches. This flag
501 is only allowed between different named branches. This flag
494 is used by rebase extension as a temporary fix and should be
502 is used by rebase extension as a temporary fix and should be
495 avoided in general.
503 avoided in general.
496
504
497 The table below shows all the behaviors of the update command
505 The table below shows all the behaviors of the update command
498 given the -c and -C or no options, whether the working directory
506 given the -c and -C or no options, whether the working directory
499 is dirty, whether a revision is specified, and the relationship of
507 is dirty, whether a revision is specified, and the relationship of
500 the parent rev to the target rev (linear, on the same named
508 the parent rev to the target rev (linear, on the same named
501 branch, or on another named branch).
509 branch, or on another named branch).
502
510
503 This logic is tested by test-update-branches.t.
511 This logic is tested by test-update-branches.t.
504
512
505 -c -C dirty rev | linear same cross
513 -c -C dirty rev | linear same cross
506 n n n n | ok (1) x
514 n n n n | ok (1) x
507 n n n y | ok ok ok
515 n n n y | ok ok ok
508 n n y * | merge (2) (2)
516 n n y * | merge (2) (2)
509 n y * * | --- discard ---
517 n y * * | --- discard ---
510 y n y * | --- (3) ---
518 y n y * | --- (3) ---
511 y n n * | --- ok ---
519 y n n * | --- ok ---
512 y y * * | --- (4) ---
520 y y * * | --- (4) ---
513
521
514 x = can't happen
522 x = can't happen
515 * = don't-care
523 * = don't-care
516 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
524 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
517 2 = abort: crosses branches (use 'hg merge' to merge or
525 2 = abort: crosses branches (use 'hg merge' to merge or
518 use 'hg update -C' to discard changes)
526 use 'hg update -C' to discard changes)
519 3 = abort: uncommitted local changes
527 3 = abort: uncommitted local changes
520 4 = incompatible options (checked in commands.py)
528 4 = incompatible options (checked in commands.py)
521
529
522 Return the same tuple as applyupdates().
530 Return the same tuple as applyupdates().
523 """
531 """
524
532
525 onode = node
533 onode = node
526 wlock = repo.wlock()
534 wlock = repo.wlock()
527 try:
535 try:
528 wc = repo[None]
536 wc = repo[None]
529 if node is None:
537 if node is None:
530 # tip of current branch
538 # tip of current branch
531 try:
539 try:
532 node = repo.branchtip(wc.branch())
540 node = repo.branchtip(wc.branch())
533 except error.RepoLookupError:
541 except error.RepoLookupError:
534 if wc.branch() == "default": # no default branch!
542 if wc.branch() == "default": # no default branch!
535 node = repo.lookup("tip") # update to tip
543 node = repo.lookup("tip") # update to tip
536 else:
544 else:
537 raise util.Abort(_("branch %s not found") % wc.branch())
545 raise util.Abort(_("branch %s not found") % wc.branch())
538 overwrite = force and not branchmerge
546 overwrite = force and not branchmerge
539 pl = wc.parents()
547 pl = wc.parents()
540 p1, p2 = pl[0], repo[node]
548 p1, p2 = pl[0], repo[node]
541 if ancestor:
549 if ancestor:
542 pa = repo[ancestor]
550 pa = repo[ancestor]
543 else:
551 else:
544 pa = p1.ancestor(p2)
552 pa = p1.ancestor(p2)
545
553
546 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
554 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
547
555
548 ### check phase
556 ### check phase
549 if not overwrite and len(pl) > 1:
557 if not overwrite and len(pl) > 1:
550 raise util.Abort(_("outstanding uncommitted merges"))
558 raise util.Abort(_("outstanding uncommitted merges"))
551 if branchmerge:
559 if branchmerge:
552 if pa == p2:
560 if pa == p2:
553 raise util.Abort(_("merging with a working directory ancestor"
561 raise util.Abort(_("merging with a working directory ancestor"
554 " has no effect"))
562 " has no effect"))
555 elif pa == p1:
563 elif pa == p1:
556 if not mergeancestor and p1.branch() == p2.branch():
564 if not mergeancestor and p1.branch() == p2.branch():
557 raise util.Abort(_("nothing to merge"),
565 raise util.Abort(_("nothing to merge"),
558 hint=_("use 'hg update' "
566 hint=_("use 'hg update' "
559 "or check 'hg heads'"))
567 "or check 'hg heads'"))
560 if not force and (wc.files() or wc.deleted()):
568 if not force and (wc.files() or wc.deleted()):
561 raise util.Abort(_("outstanding uncommitted changes"),
569 raise util.Abort(_("outstanding uncommitted changes"),
562 hint=_("use 'hg status' to list changes"))
570 hint=_("use 'hg status' to list changes"))
563 for s in wc.substate:
571 for s in wc.substate:
564 if wc.sub(s).dirty():
572 if wc.sub(s).dirty():
565 raise util.Abort(_("outstanding uncommitted changes in "
573 raise util.Abort(_("outstanding uncommitted changes in "
566 "subrepository '%s'") % s)
574 "subrepository '%s'") % s)
567
575
568 elif not overwrite:
576 elif not overwrite:
569 if pa == p1 or pa == p2: # linear
577 if pa == p1 or pa == p2: # linear
570 pass # all good
578 pass # all good
571 elif wc.dirty(missing=True):
579 elif wc.dirty(missing=True):
572 raise util.Abort(_("crosses branches (merge branches or use"
580 raise util.Abort(_("crosses branches (merge branches or use"
573 " --clean to discard changes)"))
581 " --clean to discard changes)"))
574 elif onode is None:
582 elif onode is None:
575 raise util.Abort(_("crosses branches (merge branches or update"
583 raise util.Abort(_("crosses branches (merge branches or update"
576 " --check to force update)"))
584 " --check to force update)"))
577 else:
585 else:
578 # Allow jumping branches if clean and specific rev given
586 # Allow jumping branches if clean and specific rev given
579 pa = p1
587 pa = p1
580
588
581 ### calculate phase
589 ### calculate phase
582 action = []
590 action = []
583 folding = not util.checkcase(repo.path)
591 folding = not util.checkcase(repo.path)
584 if folding:
592 if folding:
585 # collision check is not needed for clean update
593 # collision check is not needed for clean update
586 if (not branchmerge and
594 if (not branchmerge and
587 (force or not wc.dirty(missing=True, branch=False))):
595 (force or not wc.dirty(missing=True, branch=False))):
588 _checkcollision(p2, None)
596 _checkcollision(p2, None)
589 else:
597 else:
590 _checkcollision(p2, wc)
598 _checkcollision(p2, wc)
591 if not force:
599 if not force:
592 _checkunknown(repo, wc, p2)
600 _checkunknown(repo, wc, p2)
593 action += _forgetremoved(wc, p2, branchmerge)
601 action += _forgetremoved(wc, p2, branchmerge)
594 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
602 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
595
603
596 ### apply phase
604 ### apply phase
597 if not branchmerge: # just jump to the new rev
605 if not branchmerge: # just jump to the new rev
598 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
606 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
599 if not partial:
607 if not partial:
600 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
608 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
601
609
602 stats = applyupdates(repo, action, wc, p2, pa, overwrite)
610 stats = applyupdates(repo, action, wc, p2, pa, overwrite)
603
611
604 if not partial:
612 if not partial:
605 repo.setparents(fp1, fp2)
613 repo.setparents(fp1, fp2)
606 recordupdates(repo, action, branchmerge)
614 recordupdates(repo, action, branchmerge)
607 if not branchmerge:
615 if not branchmerge:
608 repo.dirstate.setbranch(p2.branch())
616 repo.dirstate.setbranch(p2.branch())
609 finally:
617 finally:
610 wlock.release()
618 wlock.release()
611
619
612 if not partial:
620 if not partial:
613 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
621 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
614 return stats
622 return stats
@@ -1,158 +1,181 b''
1 $ hg init
1 $ hg init
2
2
3 $ echo "[merge]" >> .hg/hgrc
3 $ echo "[merge]" >> .hg/hgrc
4 $ echo "followcopies = 1" >> .hg/hgrc
4 $ echo "followcopies = 1" >> .hg/hgrc
5
5
6 $ echo foo > a
6 $ echo foo > a
7 $ echo foo > a2
7 $ echo foo > a2
8 $ hg add a a2
8 $ hg add a a2
9 $ hg ci -m "start"
9 $ hg ci -m "start"
10
10
11 $ hg mv a b
11 $ hg mv a b
12 $ hg mv a2 b2
12 $ hg mv a2 b2
13 $ hg ci -m "rename"
13 $ hg ci -m "rename"
14
14
15 $ hg co 0
15 $ hg co 0
16 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
16 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
17
17
18 $ echo blahblah > a
18 $ echo blahblah > a
19 $ echo blahblah > a2
19 $ echo blahblah > a2
20 $ hg mv a2 c2
20 $ hg mv a2 c2
21 $ hg ci -m "modify"
21 $ hg ci -m "modify"
22 created new head
22 created new head
23
23
24 $ hg merge -y --debug
24 $ hg merge -y --debug
25 searching for copies back to rev 1
25 searching for copies back to rev 1
26 unmatched files in local:
26 unmatched files in local:
27 c2
27 c2
28 unmatched files in other:
28 unmatched files in other:
29 b
29 b
30 b2
30 b2
31 all copies found (* = to merge, ! = divergent):
31 all copies found (* = to merge, ! = divergent):
32 c2 -> a2 !
32 c2 -> a2 !
33 b -> a *
33 b -> a *
34 b2 -> a2 !
34 b2 -> a2 !
35 checking for directory renames
35 checking for directory renames
36 a2: divergent renames -> dr
36 a2: divergent renames -> dr
37 resolving manifests
37 resolving manifests
38 overwrite: False, partial: False
38 overwrite: False, partial: False
39 ancestor: af1939970a1c, local: 044f8520aeeb+, remote: 85c198ef2f6c
39 ancestor: af1939970a1c, local: 044f8520aeeb+, remote: 85c198ef2f6c
40 a: remote moved to b -> m
40 a: remote moved to b -> m
41 b2: remote created -> g
41 b2: remote created -> g
42 preserving a for resolve of b
42 preserving a for resolve of b
43 removing a
43 removing a
44 updating: a 1/3 files (33.33%)
44 updating: a 1/3 files (33.33%)
45 picked tool 'internal:merge' for b (binary False symlink False)
45 picked tool 'internal:merge' for b (binary False symlink False)
46 merging a and b to b
46 merging a and b to b
47 my b@044f8520aeeb+ other b@85c198ef2f6c ancestor a@af1939970a1c
47 my b@044f8520aeeb+ other b@85c198ef2f6c ancestor a@af1939970a1c
48 premerge successful
48 premerge successful
49 updating: a2 2/3 files (66.67%)
49 updating: a2 2/3 files (66.67%)
50 note: possible conflict - a2 was renamed multiple times to:
50 note: possible conflict - a2 was renamed multiple times to:
51 c2
51 c2
52 b2
52 b2
53 updating: b2 3/3 files (100.00%)
53 updating: b2 3/3 files (100.00%)
54 getting b2
54 getting b2
55 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
55 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
56 (branch merge, don't forget to commit)
56 (branch merge, don't forget to commit)
57
57
58 $ hg status -AC
58 $ hg status -AC
59 M b
59 M b
60 a
60 a
61 M b2
61 M b2
62 R a
62 R a
63 C c2
63 C c2
64
64
65 $ cat b
65 $ cat b
66 blahblah
66 blahblah
67
67
68 $ hg ci -m "merge"
68 $ hg ci -m "merge"
69
69
70 $ hg debugindex b
70 $ hg debugindex b
71 rev offset length base linkrev nodeid p1 p2
71 rev offset length base linkrev nodeid p1 p2
72 0 0 67 0 1 57eacc201a7f 000000000000 000000000000
72 0 0 67 0 1 57eacc201a7f 000000000000 000000000000
73 1 67 72 1 3 4727ba907962 000000000000 57eacc201a7f
73 1 67 72 1 3 4727ba907962 000000000000 57eacc201a7f
74
74
75 $ hg debugrename b
75 $ hg debugrename b
76 b renamed from a:dd03b83622e78778b403775d0d074b9ac7387a66
76 b renamed from a:dd03b83622e78778b403775d0d074b9ac7387a66
77
77
78 This used to trigger a "divergent renames" warning, despite no renames
78 This used to trigger a "divergent renames" warning, despite no renames
79
79
80 $ hg cp b b3
80 $ hg cp b b3
81 $ hg cp b b4
81 $ hg cp b b4
82 $ hg ci -A -m 'copy b twice'
82 $ hg ci -A -m 'copy b twice'
83 $ hg up eb92d88a9712
83 $ hg up eb92d88a9712
84 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
84 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
85 $ hg up
85 $ hg up
86 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
86 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 $ hg rm b3 b4
87 $ hg rm b3 b4
88 $ hg ci -m 'clean up a bit of our mess'
88 $ hg ci -m 'clean up a bit of our mess'
89
89
90 We'd rather not warn on divergent renames done in the same changeset (issue2113)
90 We'd rather not warn on divergent renames done in the same changeset (issue2113)
91
91
92 $ hg cp b b3
92 $ hg cp b b3
93 $ hg mv b b4
93 $ hg mv b b4
94 $ hg ci -A -m 'divergent renames in same changeset'
94 $ hg ci -A -m 'divergent renames in same changeset'
95 $ hg up c761c6948de0
95 $ hg up c761c6948de0
96 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
96 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
97 $ hg up
97 $ hg up
98 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
98 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
99
99
100 Check for issue2642
100 Check for issue2642
101
101
102 $ hg init t
102 $ hg init t
103 $ cd t
103 $ cd t
104
104
105 $ echo c0 > f1
105 $ echo c0 > f1
106 $ hg ci -Aqm0
106 $ hg ci -Aqm0
107
107
108 $ hg up null -q
108 $ hg up null -q
109 $ echo c1 > f1 # backport
109 $ echo c1 > f1 # backport
110 $ hg ci -Aqm1
110 $ hg ci -Aqm1
111 $ hg mv f1 f2
111 $ hg mv f1 f2
112 $ hg ci -qm2
112 $ hg ci -qm2
113
113
114 $ hg up 0 -q
114 $ hg up 0 -q
115 $ hg merge 1 -q --tool internal:local
115 $ hg merge 1 -q --tool internal:local
116 $ hg ci -qm3
116 $ hg ci -qm3
117
117
118 $ hg merge 2
118 $ hg merge 2
119 merging f1 and f2 to f2
119 merging f1 and f2 to f2
120 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
120 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
121 (branch merge, don't forget to commit)
121 (branch merge, don't forget to commit)
122
122
123 $ cat f2
123 $ cat f2
124 c0
124 c0
125
125
126 $ cd ..
126 $ cd ..
127
127
128 Check for issue2089
128 Check for issue2089
129
129
130 $ hg init repo2089
130 $ hg init repo2089
131 $ cd repo2089
131 $ cd repo2089
132
132
133 $ echo c0 > f1
133 $ echo c0 > f1
134 $ hg ci -Aqm0
134 $ hg ci -Aqm0
135
135
136 $ hg up null -q
136 $ hg up null -q
137 $ echo c1 > f1
137 $ echo c1 > f1
138 $ hg ci -Aqm1
138 $ hg ci -Aqm1
139
139
140 $ hg up 0 -q
140 $ hg up 0 -q
141 $ hg merge 1 -q --tool internal:local
141 $ hg merge 1 -q --tool internal:local
142 $ echo c2 > f1
142 $ echo c2 > f1
143 $ hg ci -qm2
143 $ hg ci -qm2
144
144
145 $ hg up 1 -q
145 $ hg up 1 -q
146 $ hg mv f1 f2
146 $ hg mv f1 f2
147 $ hg ci -Aqm3
147 $ hg ci -Aqm3
148
148
149 $ hg up 2 -q
149 $ hg up 2 -q
150 $ hg merge 3
150 $ hg merge 3
151 merging f1 and f2 to f2
151 merging f1 and f2 to f2
152 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
152 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
153 (branch merge, don't forget to commit)
153 (branch merge, don't forget to commit)
154
154
155 $ cat f2
155 $ cat f2
156 c2
156 c2
157
157
158 $ cd ..
158 $ cd ..
159
160 Check for issue3074
161
162 $ hg init repo3074
163 $ cd repo3074
164 $ echo foo > file
165 $ hg add file
166 $ hg commit -m "added file"
167 $ hg mv file newfile
168 $ hg commit -m "renamed file"
169 $ hg update 0
170 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
171 $ hg rm file
172 $ hg commit -m "deleted file"
173 created new head
174 $ hg merge
175 note: possible conflict - file was deleted and renamed to:
176 newfile
177 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
178 (branch merge, don't forget to commit)
179 $ hg status
180 M newfile
181 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now