##// END OF EJS Templates
merge: make test for fast-forward merge stricter (issue619)...
Matt Mackall -
r4748:8808ea7d default
parent child Browse files
Show More
@@ -0,0 +1,20
1 #!/bin/sh
2
3 mkdir t
4 cd t
5 hg init
6 echo a > a
7 hg ci -Ama -d '1000000000 0'
8 echo b > b
9 hg branch b
10 hg ci -Amb -d '1000000000 0'
11 hg co -C 0
12
13 echo fast-forward
14 hg merge b
15 hg ci -Ammerge -d '1000000000 0'
16
17 echo bogus fast-forward should fail
18 hg merge b
19
20 echo done
@@ -0,0 +1,10
1 adding a
2 marked working directory as branch b
3 adding b
4 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
5 fast-forward
6 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
7 (branch merge, don't forget to commit)
8 bogus fast-forward should fail
9 abort: there is nothing to merge, just use 'hg update' or look at 'hg heads'
10 done
@@ -1,519 +1,525
1 1 # context.py - changeset and file context objects 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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 10 import ancestor, bdiff, repo, revlog, util, os, errno
11 11
12 12 class changectx(object):
13 13 """A changecontext object makes access to data related to a particular
14 14 changeset convenient."""
15 15 def __init__(self, repo, changeid=None):
16 16 """changeid is a revision number, node, or tag"""
17 17 self._repo = repo
18 18
19 19 if not changeid and changeid != 0:
20 20 p1, p2 = self._repo.dirstate.parents()
21 21 self._rev = self._repo.changelog.rev(p1)
22 22 if self._rev == -1:
23 23 changeid = 'tip'
24 24 else:
25 25 self._node = p1
26 26 return
27 27
28 28 self._node = self._repo.lookup(changeid)
29 29 self._rev = self._repo.changelog.rev(self._node)
30 30
31 31 def __str__(self):
32 32 return short(self.node())
33 33
34 34 def __repr__(self):
35 35 return "<changectx %s>" % str(self)
36 36
37 37 def __eq__(self, other):
38 38 try:
39 39 return self._rev == other._rev
40 40 except AttributeError:
41 41 return False
42 42
43 def __ne__(self, other):
44 return not (self == other)
45
43 46 def __nonzero__(self):
44 47 return self._rev != nullrev
45 48
46 49 def __getattr__(self, name):
47 50 if name == '_changeset':
48 51 self._changeset = self._repo.changelog.read(self.node())
49 52 return self._changeset
50 53 elif name == '_manifest':
51 54 self._manifest = self._repo.manifest.read(self._changeset[0])
52 55 return self._manifest
53 56 elif name == '_manifestdelta':
54 57 md = self._repo.manifest.readdelta(self._changeset[0])
55 58 self._manifestdelta = md
56 59 return self._manifestdelta
57 60 else:
58 61 raise AttributeError, name
59 62
60 63 def changeset(self): return self._changeset
61 64 def manifest(self): return self._manifest
62 65
63 66 def rev(self): return self._rev
64 67 def node(self): return self._node
65 68 def user(self): return self._changeset[1]
66 69 def date(self): return self._changeset[2]
67 70 def files(self): return self._changeset[3]
68 71 def description(self): return self._changeset[4]
69 72 def branch(self): return self._changeset[5].get("branch")
70 73 def tags(self): return self._repo.nodetags(self._node)
71 74
72 75 def parents(self):
73 76 """return contexts for each parent changeset"""
74 77 p = self._repo.changelog.parents(self._node)
75 78 return [changectx(self._repo, x) for x in p]
76 79
77 80 def children(self):
78 81 """return contexts for each child changeset"""
79 82 c = self._repo.changelog.children(self._node)
80 83 return [changectx(self._repo, x) for x in c]
81 84
82 85 def filenode(self, path):
83 86 if '_manifest' in self.__dict__:
84 87 try:
85 88 return self._manifest[path]
86 89 except KeyError:
87 90 raise revlog.LookupError(_("'%s' not found in manifest") % path)
88 91 if '_manifestdelta' in self.__dict__ or path in self.files():
89 92 if path in self._manifestdelta:
90 93 return self._manifestdelta[path]
91 94 node, flag = self._repo.manifest.find(self._changeset[0], path)
92 95 if not node:
93 96 raise revlog.LookupError(_("'%s' not found in manifest") % path)
94 97
95 98 return node
96 99
97 100 def filectx(self, path, fileid=None, filelog=None):
98 101 """get a file context from this changeset"""
99 102 if fileid is None:
100 103 fileid = self.filenode(path)
101 104 return filectx(self._repo, path, fileid=fileid,
102 105 changectx=self, filelog=filelog)
103 106
104 107 def filectxs(self):
105 108 """generate a file context for each file in this changeset's
106 109 manifest"""
107 110 mf = self.manifest()
108 111 m = mf.keys()
109 112 m.sort()
110 113 for f in m:
111 114 yield self.filectx(f, fileid=mf[f])
112 115
113 116 def ancestor(self, c2):
114 117 """
115 118 return the ancestor context of self and c2
116 119 """
117 120 n = self._repo.changelog.ancestor(self._node, c2._node)
118 121 return changectx(self._repo, n)
119 122
120 123 class filectx(object):
121 124 """A filecontext object makes access to data related to a particular
122 125 filerevision convenient."""
123 126 def __init__(self, repo, path, changeid=None, fileid=None,
124 127 filelog=None, changectx=None):
125 128 """changeid can be a changeset revision, node, or tag.
126 129 fileid can be a file revision or node."""
127 130 self._repo = repo
128 131 self._path = path
129 132
130 133 assert (changeid is not None
131 134 or fileid is not None
132 135 or changectx is not None)
133 136
134 137 if filelog:
135 138 self._filelog = filelog
136 139
137 140 if fileid is None:
138 141 if changectx is None:
139 142 self._changeid = changeid
140 143 else:
141 144 self._changectx = changectx
142 145 else:
143 146 self._fileid = fileid
144 147
145 148 def __getattr__(self, name):
146 149 if name == '_changectx':
147 150 self._changectx = changectx(self._repo, self._changeid)
148 151 return self._changectx
149 152 elif name == '_filelog':
150 153 self._filelog = self._repo.file(self._path)
151 154 return self._filelog
152 155 elif name == '_changeid':
153 156 self._changeid = self._filelog.linkrev(self._filenode)
154 157 return self._changeid
155 158 elif name == '_filenode':
156 159 if '_fileid' in self.__dict__:
157 160 self._filenode = self._filelog.lookup(self._fileid)
158 161 else:
159 162 self._filenode = self._changectx.filenode(self._path)
160 163 return self._filenode
161 164 elif name == '_filerev':
162 165 self._filerev = self._filelog.rev(self._filenode)
163 166 return self._filerev
164 167 else:
165 168 raise AttributeError, name
166 169
167 170 def __nonzero__(self):
168 171 try:
169 172 n = self._filenode
170 173 return True
171 174 except revlog.LookupError:
172 175 # file is missing
173 176 return False
174 177
175 178 def __str__(self):
176 179 return "%s@%s" % (self.path(), short(self.node()))
177 180
178 181 def __repr__(self):
179 182 return "<filectx %s>" % str(self)
180 183
181 184 def __eq__(self, other):
182 185 try:
183 186 return (self._path == other._path
184 187 and self._changeid == other._changeid)
185 188 except AttributeError:
186 189 return False
187 190
191 def __ne__(self, other):
192 return not (self == other)
193
188 194 def filectx(self, fileid):
189 195 '''opens an arbitrary revision of the file without
190 196 opening a new filelog'''
191 197 return filectx(self._repo, self._path, fileid=fileid,
192 198 filelog=self._filelog)
193 199
194 200 def filerev(self): return self._filerev
195 201 def filenode(self): return self._filenode
196 202 def filelog(self): return self._filelog
197 203
198 204 def rev(self):
199 205 if '_changectx' in self.__dict__:
200 206 return self._changectx.rev()
201 207 return self._filelog.linkrev(self._filenode)
202 208
203 209 def node(self): return self._changectx.node()
204 210 def user(self): return self._changectx.user()
205 211 def date(self): return self._changectx.date()
206 212 def files(self): return self._changectx.files()
207 213 def description(self): return self._changectx.description()
208 214 def branch(self): return self._changectx.branch()
209 215 def manifest(self): return self._changectx.manifest()
210 216 def changectx(self): return self._changectx
211 217
212 218 def data(self): return self._filelog.read(self._filenode)
213 219 def renamed(self): return self._filelog.renamed(self._filenode)
214 220 def path(self): return self._path
215 221 def size(self): return self._filelog.size(self._filerev)
216 222
217 223 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
218 224
219 225 def parents(self):
220 226 p = self._path
221 227 fl = self._filelog
222 228 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
223 229
224 230 r = self.renamed()
225 231 if r:
226 232 pl[0] = (r[0], r[1], None)
227 233
228 234 return [filectx(self._repo, p, fileid=n, filelog=l)
229 235 for p,n,l in pl if n != nullid]
230 236
231 237 def children(self):
232 238 # hard for renames
233 239 c = self._filelog.children(self._filenode)
234 240 return [filectx(self._repo, self._path, fileid=x,
235 241 filelog=self._filelog) for x in c]
236 242
237 243 def annotate(self, follow=False):
238 244 '''returns a list of tuples of (ctx, line) for each line
239 245 in the file, where ctx is the filectx of the node where
240 246 that line was last changed'''
241 247
242 248 def decorate(text, rev):
243 249 return ([rev] * len(text.splitlines()), text)
244 250
245 251 def pair(parent, child):
246 252 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
247 253 child[0][b1:b2] = parent[0][a1:a2]
248 254 return child
249 255
250 256 getlog = util.cachefunc(lambda x: self._repo.file(x))
251 257 def getctx(path, fileid):
252 258 log = path == self._path and self._filelog or getlog(path)
253 259 return filectx(self._repo, path, fileid=fileid, filelog=log)
254 260 getctx = util.cachefunc(getctx)
255 261
256 262 def parents(f):
257 263 # we want to reuse filectx objects as much as possible
258 264 p = f._path
259 265 if f._filerev is None: # working dir
260 266 pl = [(n.path(), n.filerev()) for n in f.parents()]
261 267 else:
262 268 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
263 269
264 270 if follow:
265 271 r = f.renamed()
266 272 if r:
267 273 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
268 274
269 275 return [getctx(p, n) for p, n in pl if n != nullrev]
270 276
271 277 # use linkrev to find the first changeset where self appeared
272 278 if self.rev() != self._filelog.linkrev(self._filenode):
273 279 base = self.filectx(self.filerev())
274 280 else:
275 281 base = self
276 282
277 283 # find all ancestors
278 284 needed = {base: 1}
279 285 visit = [base]
280 286 files = [base._path]
281 287 while visit:
282 288 f = visit.pop(0)
283 289 for p in parents(f):
284 290 if p not in needed:
285 291 needed[p] = 1
286 292 visit.append(p)
287 293 if p._path not in files:
288 294 files.append(p._path)
289 295 else:
290 296 # count how many times we'll use this
291 297 needed[p] += 1
292 298
293 299 # sort by revision (per file) which is a topological order
294 300 visit = []
295 301 for f in files:
296 302 fn = [(n.rev(), n) for n in needed.keys() if n._path == f]
297 303 visit.extend(fn)
298 304 visit.sort()
299 305 hist = {}
300 306
301 307 for r, f in visit:
302 308 curr = decorate(f.data(), f)
303 309 for p in parents(f):
304 310 if p != nullid:
305 311 curr = pair(hist[p], curr)
306 312 # trim the history of unneeded revs
307 313 needed[p] -= 1
308 314 if not needed[p]:
309 315 del hist[p]
310 316 hist[f] = curr
311 317
312 318 return zip(hist[f][0], hist[f][1].splitlines(1))
313 319
314 320 def ancestor(self, fc2):
315 321 """
316 322 find the common ancestor file context, if any, of self, and fc2
317 323 """
318 324
319 325 acache = {}
320 326
321 327 # prime the ancestor cache for the working directory
322 328 for c in (self, fc2):
323 329 if c._filerev == None:
324 330 pl = [(n.path(), n.filenode()) for n in c.parents()]
325 331 acache[(c._path, None)] = pl
326 332
327 333 flcache = {self._path:self._filelog, fc2._path:fc2._filelog}
328 334 def parents(vertex):
329 335 if vertex in acache:
330 336 return acache[vertex]
331 337 f, n = vertex
332 338 if f not in flcache:
333 339 flcache[f] = self._repo.file(f)
334 340 fl = flcache[f]
335 341 pl = [(f, p) for p in fl.parents(n) if p != nullid]
336 342 re = fl.renamed(n)
337 343 if re:
338 344 pl.append(re)
339 345 acache[vertex] = pl
340 346 return pl
341 347
342 348 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
343 349 v = ancestor.ancestor(a, b, parents)
344 350 if v:
345 351 f, n = v
346 352 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
347 353
348 354 return None
349 355
350 356 class workingctx(changectx):
351 357 """A workingctx object makes access to data related to
352 358 the current working directory convenient."""
353 359 def __init__(self, repo):
354 360 self._repo = repo
355 361 self._rev = None
356 362 self._node = None
357 363
358 364 def __str__(self):
359 365 return str(self._parents[0]) + "+"
360 366
361 367 def __nonzero__(self):
362 368 return True
363 369
364 370 def __getattr__(self, name):
365 371 if name == '_parents':
366 372 self._parents = self._repo.parents()
367 373 return self._parents
368 374 if name == '_status':
369 375 self._status = self._repo.status()
370 376 return self._status
371 377 if name == '_manifest':
372 378 self._buildmanifest()
373 379 return self._manifest
374 380 else:
375 381 raise AttributeError, name
376 382
377 383 def _buildmanifest(self):
378 384 """generate a manifest corresponding to the working directory"""
379 385
380 386 man = self._parents[0].manifest().copy()
381 387 is_exec = util.execfunc(self._repo.root, man.execf)
382 388 is_link = util.linkfunc(self._repo.root, man.linkf)
383 389 copied = self._repo.dirstate.copies()
384 390 modified, added, removed, deleted, unknown = self._status[:5]
385 391 for i, l in (("a", added), ("m", modified), ("u", unknown)):
386 392 for f in l:
387 393 man[f] = man.get(copied.get(f, f), nullid) + i
388 394 try:
389 395 man.set(f, is_exec(f), is_link(f))
390 396 except OSError:
391 397 pass
392 398
393 399 for f in deleted + removed:
394 400 if f in man:
395 401 del man[f]
396 402
397 403 self._manifest = man
398 404
399 405 def manifest(self): return self._manifest
400 406
401 407 def user(self): return self._repo.ui.username()
402 408 def date(self): return util.makedate()
403 409 def description(self): return ""
404 410 def files(self):
405 411 f = self.modified() + self.added() + self.removed()
406 412 f.sort()
407 413 return f
408 414
409 415 def modified(self): return self._status[0]
410 416 def added(self): return self._status[1]
411 417 def removed(self): return self._status[2]
412 418 def deleted(self): return self._status[3]
413 419 def unknown(self): return self._status[4]
414 420 def clean(self): return self._status[5]
415 421 def branch(self): return self._repo.dirstate.branch()
416 422
417 423 def tags(self):
418 424 t = []
419 425 [t.extend(p.tags()) for p in self.parents()]
420 426 return t
421 427
422 428 def parents(self):
423 429 """return contexts for each parent changeset"""
424 430 return self._parents
425 431
426 432 def children(self):
427 433 return []
428 434
429 435 def filectx(self, path, filelog=None):
430 436 """get a file context from the working directory"""
431 437 return workingfilectx(self._repo, path, workingctx=self,
432 438 filelog=filelog)
433 439
434 440 def ancestor(self, c2):
435 441 """return the ancestor context of self and c2"""
436 442 return self._parents[0].ancestor(c2) # punt on two parents for now
437 443
438 444 class workingfilectx(filectx):
439 445 """A workingfilectx object makes access to data related to a particular
440 446 file in the working directory convenient."""
441 447 def __init__(self, repo, path, filelog=None, workingctx=None):
442 448 """changeid can be a changeset revision, node, or tag.
443 449 fileid can be a file revision or node."""
444 450 self._repo = repo
445 451 self._path = path
446 452 self._changeid = None
447 453 self._filerev = self._filenode = None
448 454
449 455 if filelog:
450 456 self._filelog = filelog
451 457 if workingctx:
452 458 self._changectx = workingctx
453 459
454 460 def __getattr__(self, name):
455 461 if name == '_changectx':
456 462 self._changectx = workingctx(self._repo)
457 463 return self._changectx
458 464 elif name == '_repopath':
459 465 self._repopath = (self._repo.dirstate.copied(self._path)
460 466 or self._path)
461 467 return self._repopath
462 468 elif name == '_filelog':
463 469 self._filelog = self._repo.file(self._repopath)
464 470 return self._filelog
465 471 else:
466 472 raise AttributeError, name
467 473
468 474 def __nonzero__(self):
469 475 return True
470 476
471 477 def __str__(self):
472 478 return "%s@%s" % (self.path(), self._changectx)
473 479
474 480 def filectx(self, fileid):
475 481 '''opens an arbitrary revision of the file without
476 482 opening a new filelog'''
477 483 return filectx(self._repo, self._repopath, fileid=fileid,
478 484 filelog=self._filelog)
479 485
480 486 def rev(self):
481 487 if '_changectx' in self.__dict__:
482 488 return self._changectx.rev()
483 489 return self._filelog.linkrev(self._filenode)
484 490
485 491 def data(self): return self._repo.wread(self._path)
486 492 def renamed(self):
487 493 rp = self._repopath
488 494 if rp == self._path:
489 495 return None
490 496 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
491 497
492 498 def parents(self):
493 499 '''return parent filectxs, following copies if necessary'''
494 500 p = self._path
495 501 rp = self._repopath
496 502 pcl = self._changectx._parents
497 503 fl = self._filelog
498 504 pl = [(rp, pcl[0]._manifest.get(rp, nullid), fl)]
499 505 if len(pcl) > 1:
500 506 if rp != p:
501 507 fl = None
502 508 pl.append((p, pcl[1]._manifest.get(p, nullid), fl))
503 509
504 510 return [filectx(self._repo, p, fileid=n, filelog=l)
505 511 for p,n,l in pl if n != nullid]
506 512
507 513 def children(self):
508 514 return []
509 515
510 516 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
511 517 def date(self):
512 518 t, tz = self._changectx.date()
513 519 try:
514 520 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
515 521 except OSError, err:
516 522 if err.errno != errno.ENOENT: raise
517 523 return (t, tz)
518 524
519 525 def cmp(self, text): return self._repo.wread(self._path) == text
@@ -1,564 +1,564
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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 10 import errno, util, os, tempfile, context
11 11
12 12 def filemerge(repo, fw, fo, wctx, mctx):
13 13 """perform a 3-way merge in the working directory
14 14
15 15 fw = filename in the working directory
16 16 fo = filename in other parent
17 17 wctx, mctx = working and merge changecontexts
18 18 """
19 19
20 20 def temp(prefix, ctx):
21 21 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
22 22 (fd, name) = tempfile.mkstemp(prefix=pre)
23 23 data = repo.wwritedata(ctx.path(), ctx.data())
24 24 f = os.fdopen(fd, "wb")
25 25 f.write(data)
26 26 f.close()
27 27 return name
28 28
29 29 fcm = wctx.filectx(fw)
30 30 fco = mctx.filectx(fo)
31 31
32 32 if not fco.cmp(fcm.data()): # files identical?
33 33 return None
34 34
35 35 fca = fcm.ancestor(fco)
36 36 if not fca:
37 37 fca = repo.filectx(fw, fileid=nullrev)
38 38 a = repo.wjoin(fw)
39 39 b = temp("base", fca)
40 40 c = temp("other", fco)
41 41
42 42 if fw != fo:
43 43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
44 44 else:
45 45 repo.ui.status(_("merging %s\n") % fw)
46 46
47 47 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
48 48
49 49 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
50 50 or "hgmerge")
51 51 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
52 52 environ={'HG_FILE': fw,
53 53 'HG_MY_NODE': str(wctx.parents()[0]),
54 54 'HG_OTHER_NODE': str(mctx)})
55 55 if r:
56 56 repo.ui.warn(_("merging %s failed!\n") % fw)
57 57
58 58 os.unlink(b)
59 59 os.unlink(c)
60 60 return r
61 61
62 62 def checkunknown(wctx, mctx):
63 63 "check for collisions between unknown files and files in mctx"
64 64 man = mctx.manifest()
65 65 for f in wctx.unknown():
66 66 if f in man:
67 67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
68 68 raise util.Abort(_("untracked local file '%s' differs"
69 69 " from remote version") % f)
70 70
71 71 def checkcollision(mctx):
72 72 "check for case folding collisions in the destination context"
73 73 folded = {}
74 74 for fn in mctx.manifest():
75 75 fold = fn.lower()
76 76 if fold in folded:
77 77 raise util.Abort(_("case-folding collision between %s and %s")
78 78 % (fn, folded[fold]))
79 79 folded[fold] = fn
80 80
81 81 def forgetremoved(wctx, mctx):
82 82 """
83 83 Forget removed files
84 84
85 85 If we're jumping between revisions (as opposed to merging), and if
86 86 neither the working directory nor the target rev has the file,
87 87 then we need to remove it from the dirstate, to prevent the
88 88 dirstate from listing the file when it is no longer in the
89 89 manifest.
90 90 """
91 91
92 92 action = []
93 93 man = mctx.manifest()
94 94 for f in wctx.deleted() + wctx.removed():
95 95 if f not in man:
96 96 action.append((f, "f"))
97 97
98 98 return action
99 99
100 100 def findcopies(repo, m1, m2, ma, limit):
101 101 """
102 102 Find moves and copies between m1 and m2 back to limit linkrev
103 103 """
104 104
105 105 def nonoverlap(d1, d2, d3):
106 106 "Return list of elements in d1 not in d2 or d3"
107 107 l = [d for d in d1 if d not in d3 and d not in d2]
108 108 l.sort()
109 109 return l
110 110
111 111 def dirname(f):
112 112 s = f.rfind("/")
113 113 if s == -1:
114 114 return ""
115 115 return f[:s]
116 116
117 117 def dirs(files):
118 118 d = {}
119 119 for f in files:
120 120 f = dirname(f)
121 121 while f not in d:
122 122 d[f] = True
123 123 f = dirname(f)
124 124 return d
125 125
126 126 wctx = repo.workingctx()
127 127
128 128 def makectx(f, n):
129 129 if len(n) == 20:
130 130 return repo.filectx(f, fileid=n)
131 131 return wctx.filectx(f)
132 132 ctx = util.cachefunc(makectx)
133 133
134 134 def findold(fctx):
135 135 "find files that path was copied from, back to linkrev limit"
136 136 old = {}
137 137 seen = {}
138 138 orig = fctx.path()
139 139 visit = [fctx]
140 140 while visit:
141 141 fc = visit.pop()
142 142 s = str(fc)
143 143 if s in seen:
144 144 continue
145 145 seen[s] = 1
146 146 if fc.path() != orig and fc.path() not in old:
147 147 old[fc.path()] = 1
148 148 if fc.rev() < limit:
149 149 continue
150 150 visit += fc.parents()
151 151
152 152 old = old.keys()
153 153 old.sort()
154 154 return old
155 155
156 156 copy = {}
157 157 fullcopy = {}
158 158 diverge = {}
159 159
160 160 def checkcopies(c, man):
161 161 '''check possible copies for filectx c'''
162 162 for of in findold(c):
163 163 fullcopy[c.path()] = of # remember for dir rename detection
164 164 if of not in man: # original file not in other manifest?
165 165 if of in ma:
166 166 diverge.setdefault(of, []).append(c.path())
167 167 continue
168 168 c2 = ctx(of, man[of])
169 169 ca = c.ancestor(c2)
170 170 if not ca: # unrelated?
171 171 continue
172 172 # named changed on only one side?
173 173 if ca.path() == c.path() or ca.path() == c2.path():
174 174 if c == ca or c2 == ca: # no merge needed, ignore copy
175 175 continue
176 176 copy[c.path()] = of
177 177
178 178 if not repo.ui.configbool("merge", "followcopies", True):
179 179 return {}, {}
180 180
181 181 # avoid silly behavior for update from empty dir
182 182 if not m1 or not m2 or not ma:
183 183 return {}, {}
184 184
185 185 u1 = nonoverlap(m1, m2, ma)
186 186 u2 = nonoverlap(m2, m1, ma)
187 187
188 188 for f in u1:
189 189 checkcopies(ctx(f, m1[f]), m2)
190 190
191 191 for f in u2:
192 192 checkcopies(ctx(f, m2[f]), m1)
193 193
194 194 d2 = {}
195 195 for of, fl in diverge.items():
196 196 for f in fl:
197 197 fo = list(fl)
198 198 fo.remove(f)
199 199 d2[f] = (of, fo)
200 200 #diverge = d2
201 201
202 202 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
203 203 return copy, diverge
204 204
205 205 # generate a directory move map
206 206 d1, d2 = dirs(m1), dirs(m2)
207 207 invalid = {}
208 208 dirmove = {}
209 209
210 210 # examine each file copy for a potential directory move, which is
211 211 # when all the files in a directory are moved to a new directory
212 212 for dst, src in fullcopy.items():
213 213 dsrc, ddst = dirname(src), dirname(dst)
214 214 if dsrc in invalid:
215 215 # already seen to be uninteresting
216 216 continue
217 217 elif dsrc in d1 and ddst in d1:
218 218 # directory wasn't entirely moved locally
219 219 invalid[dsrc] = True
220 220 elif dsrc in d2 and ddst in d2:
221 221 # directory wasn't entirely moved remotely
222 222 invalid[dsrc] = True
223 223 elif dsrc in dirmove and dirmove[dsrc] != ddst:
224 224 # files from the same directory moved to two different places
225 225 invalid[dsrc] = True
226 226 else:
227 227 # looks good so far
228 228 dirmove[dsrc + "/"] = ddst + "/"
229 229
230 230 for i in invalid:
231 231 if i in dirmove:
232 232 del dirmove[i]
233 233
234 234 del d1, d2, invalid
235 235
236 236 if not dirmove:
237 237 return copy, diverge
238 238
239 239 # check unaccounted nonoverlapping files against directory moves
240 240 for f in u1 + u2:
241 241 if f not in fullcopy:
242 242 for d in dirmove:
243 243 if f.startswith(d):
244 244 # new file added in a directory that was moved, move it
245 245 copy[f] = dirmove[d] + f[len(d):]
246 246 break
247 247
248 248 return copy, diverge
249 249
250 250 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
251 251 """
252 252 Merge p1 and p2 with ancestor ma and generate merge action list
253 253
254 254 overwrite = whether we clobber working files
255 255 partial = function to filter file lists
256 256 """
257 257
258 258 repo.ui.note(_("resolving manifests\n"))
259 259 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
260 260 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
261 261
262 262 m1 = p1.manifest()
263 263 m2 = p2.manifest()
264 264 ma = pa.manifest()
265 265 backwards = (pa == p2)
266 266 action = []
267 267 copy = {}
268 268 diverge = {}
269 269
270 270 def fmerge(f, f2=None, fa=None):
271 271 """merge flags"""
272 272 if not f2:
273 273 f2 = f
274 274 fa = f
275 275 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
276 276 if ((a^b) | (a^c)) ^ a:
277 277 return 'x'
278 278 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
279 279 if ((a^b) | (a^c)) ^ a:
280 280 return 'l'
281 281 return ''
282 282
283 283 def act(msg, m, f, *args):
284 284 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
285 285 action.append((f, m) + args)
286 286
287 287 if not (backwards or overwrite):
288 288 copy, diverge = findcopies(repo, m1, m2, ma, pa.rev())
289 289
290 290 for of, fl in diverge.items():
291 291 act("divergent renames", "dr", of, fl)
292 292
293 293 copied = dict.fromkeys(copy.values())
294 294
295 295 # Compare manifests
296 296 for f, n in m1.iteritems():
297 297 if partial and not partial(f):
298 298 continue
299 299 if f in m2:
300 300 # are files different?
301 301 if n != m2[f]:
302 302 a = ma.get(f, nullid)
303 303 # are both different from the ancestor?
304 304 if not overwrite and n != a and m2[f] != a:
305 305 act("versions differ", "m", f, f, f, fmerge(f), False)
306 306 # are we clobbering?
307 307 # is remote's version newer?
308 308 # or are we going back in time and clean?
309 309 elif overwrite or m2[f] != a or (backwards and not n[20:]):
310 310 act("remote is newer", "g", f, m2.flags(f))
311 311 # local is newer, not overwrite, check mode bits
312 312 elif fmerge(f) != m1.flags(f):
313 313 act("update permissions", "e", f, m2.flags(f))
314 314 # contents same, check mode bits
315 315 elif m1.flags(f) != m2.flags(f):
316 316 if overwrite or fmerge(f) != m1.flags(f):
317 317 act("update permissions", "e", f, m2.flags(f))
318 318 elif f in copied:
319 319 continue
320 320 elif f in copy:
321 321 f2 = copy[f]
322 322 if f2 not in m2: # directory rename
323 323 act("remote renamed directory to " + f2, "d",
324 324 f, None, f2, m1.flags(f))
325 325 elif f2 in m1: # case 2 A,B/B/B
326 326 act("local copied to " + f2, "m",
327 327 f, f2, f, fmerge(f, f2, f2), False)
328 328 else: # case 4,21 A/B/B
329 329 act("local moved to " + f2, "m",
330 330 f, f2, f, fmerge(f, f2, f2), False)
331 331 elif f in ma:
332 332 if n != ma[f] and not overwrite:
333 333 if repo.ui.prompt(
334 334 (_(" local changed %s which remote deleted\n") % f) +
335 335 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
336 336 act("prompt delete", "r", f)
337 337 else:
338 338 act("other deleted", "r", f)
339 339 else:
340 340 # file is created on branch or in working directory
341 341 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
342 342 act("remote deleted", "r", f)
343 343
344 344 for f, n in m2.iteritems():
345 345 if partial and not partial(f):
346 346 continue
347 347 if f in m1:
348 348 continue
349 349 if f in copied:
350 350 continue
351 351 if f in copy:
352 352 f2 = copy[f]
353 353 if f2 not in m1: # directory rename
354 354 act("local renamed directory to " + f2, "d",
355 355 None, f, f2, m2.flags(f))
356 356 elif f2 in m2: # rename case 1, A/A,B/A
357 357 act("remote copied to " + f, "m",
358 358 f2, f, f, fmerge(f2, f, f2), False)
359 359 else: # case 3,20 A/B/A
360 360 act("remote moved to " + f, "m",
361 361 f2, f, f, fmerge(f2, f, f2), True)
362 362 elif f in ma:
363 363 if overwrite or backwards:
364 364 act("recreating", "g", f, m2.flags(f))
365 365 elif n != ma[f]:
366 366 if repo.ui.prompt(
367 367 (_("remote changed %s which local deleted\n") % f) +
368 368 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
369 369 act("prompt recreating", "g", f, m2.flags(f))
370 370 else:
371 371 act("remote created", "g", f, m2.flags(f))
372 372
373 373 return action
374 374
375 375 def applyupdates(repo, action, wctx, mctx):
376 376 "apply the merge action list to the working directory"
377 377
378 378 updated, merged, removed, unresolved = 0, 0, 0, 0
379 379 action.sort()
380 380 for a in action:
381 381 f, m = a[:2]
382 382 if f and f[0] == "/":
383 383 continue
384 384 if m == "r": # remove
385 385 repo.ui.note(_("removing %s\n") % f)
386 386 util.audit_path(f)
387 387 try:
388 388 util.unlink(repo.wjoin(f))
389 389 except OSError, inst:
390 390 if inst.errno != errno.ENOENT:
391 391 repo.ui.warn(_("update failed to remove %s: %s!\n") %
392 392 (f, inst.strerror))
393 393 removed += 1
394 394 elif m == "m": # merge
395 395 f2, fd, flags, move = a[2:]
396 396 r = filemerge(repo, f, f2, wctx, mctx)
397 397 if r > 0:
398 398 unresolved += 1
399 399 else:
400 400 if r is None:
401 401 updated += 1
402 402 else:
403 403 merged += 1
404 404 if f != fd:
405 405 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
406 406 repo.wwrite(fd, repo.wread(f), flags)
407 407 if move:
408 408 repo.ui.debug(_("removing %s\n") % f)
409 409 os.unlink(repo.wjoin(f))
410 410 util.set_exec(repo.wjoin(fd), "x" in flags)
411 411 elif m == "g": # get
412 412 flags = a[2]
413 413 repo.ui.note(_("getting %s\n") % f)
414 414 t = mctx.filectx(f).data()
415 415 repo.wwrite(f, t, flags)
416 416 updated += 1
417 417 elif m == "d": # directory rename
418 418 f2, fd, flags = a[2:]
419 419 if f:
420 420 repo.ui.note(_("moving %s to %s\n") % (f, fd))
421 421 t = wctx.filectx(f).data()
422 422 repo.wwrite(fd, t, flags)
423 423 util.unlink(repo.wjoin(f))
424 424 if f2:
425 425 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
426 426 t = mctx.filectx(f2).data()
427 427 repo.wwrite(fd, t, flags)
428 428 updated += 1
429 429 elif m == "dr": # divergent renames
430 430 fl = a[2]
431 431 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
432 432 for nf in fl:
433 433 repo.ui.warn(" %s\n" % nf)
434 434 elif m == "e": # exec
435 435 flags = a[2]
436 436 util.set_exec(repo.wjoin(f), flags)
437 437
438 438 return updated, merged, removed, unresolved
439 439
440 440 def recordupdates(repo, action, branchmerge):
441 441 "record merge actions to the dirstate"
442 442
443 443 for a in action:
444 444 f, m = a[:2]
445 445 if m == "r": # remove
446 446 if branchmerge:
447 447 repo.dirstate.update([f], 'r')
448 448 else:
449 449 repo.dirstate.forget([f])
450 450 elif m == "f": # forget
451 451 repo.dirstate.forget([f])
452 452 elif m == "g": # get
453 453 if branchmerge:
454 454 repo.dirstate.update([f], 'n', st_mtime=-1)
455 455 else:
456 456 repo.dirstate.update([f], 'n')
457 457 elif m == "m": # merge
458 458 f2, fd, flag, move = a[2:]
459 459 if branchmerge:
460 460 # We've done a branch merge, mark this file as merged
461 461 # so that we properly record the merger later
462 462 repo.dirstate.update([fd], 'm')
463 463 if f != f2: # copy/rename
464 464 if move:
465 465 repo.dirstate.update([f], 'r')
466 466 if f != fd:
467 467 repo.dirstate.copy(f, fd)
468 468 else:
469 469 repo.dirstate.copy(f2, fd)
470 470 else:
471 471 # We've update-merged a locally modified file, so
472 472 # we set the dirstate to emulate a normal checkout
473 473 # of that file some time in the past. Thus our
474 474 # merge will appear as a normal local file
475 475 # modification.
476 476 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
477 477 if move:
478 478 repo.dirstate.forget([f])
479 479 elif m == "d": # directory rename
480 480 f2, fd, flag = a[2:]
481 481 if branchmerge:
482 482 repo.dirstate.update([fd], 'a')
483 483 if f:
484 484 repo.dirstate.update([f], 'r')
485 485 repo.dirstate.copy(f, fd)
486 486 if f2:
487 487 repo.dirstate.copy(f2, fd)
488 488 else:
489 489 repo.dirstate.update([fd], 'n')
490 490 if f:
491 491 repo.dirstate.forget([f])
492 492
493 493 def update(repo, node, branchmerge, force, partial, wlock):
494 494 """
495 495 Perform a merge between the working directory and the given node
496 496
497 497 branchmerge = whether to merge between branches
498 498 force = whether to force branch merging or file overwriting
499 499 partial = a function to filter file lists (dirstate not updated)
500 500 wlock = working dir lock, if already held
501 501 """
502 502
503 503 if not wlock:
504 504 wlock = repo.wlock()
505 505
506 506 wc = repo.workingctx()
507 507 if node is None:
508 508 # tip of current branch
509 509 try:
510 510 node = repo.branchtags()[wc.branch()]
511 511 except KeyError:
512 512 raise util.Abort(_("branch %s not found") % wc.branch())
513 513 overwrite = force and not branchmerge
514 514 forcemerge = force and branchmerge
515 515 pl = wc.parents()
516 516 p1, p2 = pl[0], repo.changectx(node)
517 517 pa = p1.ancestor(p2)
518 518 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
519 519 fastforward = False
520 520
521 521 ### check phase
522 522 if not overwrite and len(pl) > 1:
523 523 raise util.Abort(_("outstanding uncommitted merges"))
524 524 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
525 525 if branchmerge:
526 if p1.branch() != p2.branch():
526 if p1.branch() != p2.branch() and pa != p2:
527 527 fastforward = True
528 528 else:
529 529 raise util.Abort(_("there is nothing to merge, just use "
530 530 "'hg update' or look at 'hg heads'"))
531 531 elif not (overwrite or branchmerge):
532 532 raise util.Abort(_("update spans branches, use 'hg merge' "
533 533 "or 'hg update -C' to lose changes"))
534 534 if branchmerge and not forcemerge:
535 535 if wc.files():
536 536 raise util.Abort(_("outstanding uncommitted changes"))
537 537
538 538 ### calculate phase
539 539 action = []
540 540 if not force:
541 541 checkunknown(wc, p2)
542 542 if not util.checkfolding(repo.path):
543 543 checkcollision(p2)
544 544 if not branchmerge:
545 545 action += forgetremoved(wc, p2)
546 546 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
547 547
548 548 ### apply phase
549 549 if not branchmerge: # just jump to the new rev
550 550 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
551 551 if not partial:
552 552 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
553 553
554 554 stats = applyupdates(repo, action, wc, p2)
555 555
556 556 if not partial:
557 557 recordupdates(repo, action, branchmerge)
558 558 repo.dirstate.setparents(fp1, fp2)
559 559 if not branchmerge and not fastforward:
560 560 repo.dirstate.setbranch(p2.branch())
561 561 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
562 562
563 563 return stats
564 564
General Comments 0
You need to be logged in to leave comments. Login now