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