##// END OF EJS Templates
dirstate: more explicit name, rename normaldirty() to otherparent()
Benoit Boissinot -
r10968:7a0d096e default
parent child Browse files
Show More
@@ -1,656 +1,661 b''
1 1 # dirstate.py - working directory tracking for mercurial
2 2 #
3 3 # Copyright 2005-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
9 9 from i18n import _
10 10 import util, ignore, osutil, parsers
11 11 import struct, os, stat, errno
12 12 import cStringIO
13 13
14 14 _format = ">cllll"
15 15 propertycache = util.propertycache
16 16
17 17 def _finddirs(path):
18 18 pos = path.rfind('/')
19 19 while pos != -1:
20 20 yield path[:pos]
21 21 pos = path.rfind('/', 0, pos)
22 22
23 23 def _incdirs(dirs, path):
24 24 for base in _finddirs(path):
25 25 if base in dirs:
26 26 dirs[base] += 1
27 27 return
28 28 dirs[base] = 1
29 29
30 30 def _decdirs(dirs, path):
31 31 for base in _finddirs(path):
32 32 if dirs[base] > 1:
33 33 dirs[base] -= 1
34 34 return
35 35 del dirs[base]
36 36
37 37 class dirstate(object):
38 38
39 39 def __init__(self, opener, ui, root):
40 40 '''Create a new dirstate object.
41 41
42 42 opener is an open()-like callable that can be used to open the
43 43 dirstate file; root is the root of the directory tracked by
44 44 the dirstate.
45 45 '''
46 46 self._opener = opener
47 47 self._root = root
48 48 self._rootdir = os.path.join(root, '')
49 49 self._dirty = False
50 50 self._dirtypl = False
51 51 self._ui = ui
52 52
53 53 @propertycache
54 54 def _map(self):
55 55 '''Return the dirstate contents as a map from filename to
56 56 (state, mode, size, time).'''
57 57 self._read()
58 58 return self._map
59 59
60 60 @propertycache
61 61 def _copymap(self):
62 62 self._read()
63 63 return self._copymap
64 64
65 65 @propertycache
66 66 def _foldmap(self):
67 67 f = {}
68 68 for name in self._map:
69 69 f[os.path.normcase(name)] = name
70 70 return f
71 71
72 72 @propertycache
73 73 def _branch(self):
74 74 try:
75 75 return self._opener("branch").read().strip() or "default"
76 76 except IOError:
77 77 return "default"
78 78
79 79 @propertycache
80 80 def _pl(self):
81 81 try:
82 82 st = self._opener("dirstate").read(40)
83 83 l = len(st)
84 84 if l == 40:
85 85 return st[:20], st[20:40]
86 86 elif l > 0 and l < 40:
87 87 raise util.Abort(_('working directory state appears damaged!'))
88 88 except IOError, err:
89 89 if err.errno != errno.ENOENT:
90 90 raise
91 91 return [nullid, nullid]
92 92
93 93 @propertycache
94 94 def _dirs(self):
95 95 dirs = {}
96 96 for f, s in self._map.iteritems():
97 97 if s[0] != 'r':
98 98 _incdirs(dirs, f)
99 99 return dirs
100 100
101 101 @propertycache
102 102 def _ignore(self):
103 103 files = [self._join('.hgignore')]
104 104 for name, path in self._ui.configitems("ui"):
105 105 if name == 'ignore' or name.startswith('ignore.'):
106 106 files.append(util.expandpath(path))
107 107 return ignore.ignore(self._root, files, self._ui.warn)
108 108
109 109 @propertycache
110 110 def _slash(self):
111 111 return self._ui.configbool('ui', 'slash') and os.sep != '/'
112 112
113 113 @propertycache
114 114 def _checklink(self):
115 115 return util.checklink(self._root)
116 116
117 117 @propertycache
118 118 def _checkexec(self):
119 119 return util.checkexec(self._root)
120 120
121 121 @propertycache
122 122 def _checkcase(self):
123 123 return not util.checkcase(self._join('.hg'))
124 124
125 125 def _join(self, f):
126 126 # much faster than os.path.join()
127 127 # it's safe because f is always a relative path
128 128 return self._rootdir + f
129 129
130 130 def flagfunc(self, fallback):
131 131 if self._checklink:
132 132 if self._checkexec:
133 133 def f(x):
134 134 p = self._join(x)
135 135 if os.path.islink(p):
136 136 return 'l'
137 137 if util.is_exec(p):
138 138 return 'x'
139 139 return ''
140 140 return f
141 141 def f(x):
142 142 if os.path.islink(self._join(x)):
143 143 return 'l'
144 144 if 'x' in fallback(x):
145 145 return 'x'
146 146 return ''
147 147 return f
148 148 if self._checkexec:
149 149 def f(x):
150 150 if 'l' in fallback(x):
151 151 return 'l'
152 152 if util.is_exec(self._join(x)):
153 153 return 'x'
154 154 return ''
155 155 return f
156 156 return fallback
157 157
158 158 def getcwd(self):
159 159 cwd = os.getcwd()
160 160 if cwd == self._root:
161 161 return ''
162 162 # self._root ends with a path separator if self._root is '/' or 'C:\'
163 163 rootsep = self._root
164 164 if not util.endswithsep(rootsep):
165 165 rootsep += os.sep
166 166 if cwd.startswith(rootsep):
167 167 return cwd[len(rootsep):]
168 168 else:
169 169 # we're outside the repo. return an absolute path.
170 170 return cwd
171 171
172 172 def pathto(self, f, cwd=None):
173 173 if cwd is None:
174 174 cwd = self.getcwd()
175 175 path = util.pathto(self._root, cwd, f)
176 176 if self._slash:
177 177 return util.normpath(path)
178 178 return path
179 179
180 180 def __getitem__(self, key):
181 181 '''Return the current state of key (a filename) in the dirstate.
182 182
183 183 States are:
184 184 n normal
185 185 m needs merging
186 186 r marked for removal
187 187 a marked for addition
188 188 ? not tracked
189 189 '''
190 190 return self._map.get(key, ("?",))[0]
191 191
192 192 def __contains__(self, key):
193 193 return key in self._map
194 194
195 195 def __iter__(self):
196 196 for x in sorted(self._map):
197 197 yield x
198 198
199 199 def parents(self):
200 200 return self._pl
201 201
202 202 def branch(self):
203 203 return self._branch
204 204
205 205 def setparents(self, p1, p2=nullid):
206 206 self._dirty = self._dirtypl = True
207 207 self._pl = p1, p2
208 208
209 209 def setbranch(self, branch):
210 210 if branch in ['tip', '.', 'null']:
211 211 raise util.Abort(_('the name \'%s\' is reserved') % branch)
212 212 self._branch = branch
213 213 self._opener("branch", "w").write(branch + '\n')
214 214
215 215 def _read(self):
216 216 self._map = {}
217 217 self._copymap = {}
218 218 try:
219 219 st = self._opener("dirstate").read()
220 220 except IOError, err:
221 221 if err.errno != errno.ENOENT:
222 222 raise
223 223 return
224 224 if not st:
225 225 return
226 226
227 227 p = parsers.parse_dirstate(self._map, self._copymap, st)
228 228 if not self._dirtypl:
229 229 self._pl = p
230 230
231 231 def invalidate(self):
232 232 for a in "_map _copymap _foldmap _branch _pl _dirs _ignore".split():
233 233 if a in self.__dict__:
234 234 delattr(self, a)
235 235 self._dirty = False
236 236
237 237 def copy(self, source, dest):
238 238 """Mark dest as a copy of source. Unmark dest if source is None."""
239 239 if source == dest:
240 240 return
241 241 self._dirty = True
242 242 if source is not None:
243 243 self._copymap[dest] = source
244 244 elif dest in self._copymap:
245 245 del self._copymap[dest]
246 246
247 247 def copied(self, file):
248 248 return self._copymap.get(file, None)
249 249
250 250 def copies(self):
251 251 return self._copymap
252 252
253 253 def _droppath(self, f):
254 254 if self[f] not in "?r" and "_dirs" in self.__dict__:
255 255 _decdirs(self._dirs, f)
256 256
257 257 def _addpath(self, f, check=False):
258 258 oldstate = self[f]
259 259 if check or oldstate == "r":
260 260 if '\r' in f or '\n' in f:
261 261 raise util.Abort(
262 262 _("'\\n' and '\\r' disallowed in filenames: %r") % f)
263 263 if f in self._dirs:
264 264 raise util.Abort(_('directory %r already in dirstate') % f)
265 265 # shadows
266 266 for d in _finddirs(f):
267 267 if d in self._dirs:
268 268 break
269 269 if d in self._map and self[d] != 'r':
270 270 raise util.Abort(
271 271 _('file %r in dirstate clashes with %r') % (d, f))
272 272 if oldstate in "?r" and "_dirs" in self.__dict__:
273 273 _incdirs(self._dirs, f)
274 274
275 275 def normal(self, f):
276 276 '''Mark a file normal and clean.'''
277 277 self._dirty = True
278 278 self._addpath(f)
279 279 s = os.lstat(self._join(f))
280 280 self._map[f] = ('n', s.st_mode, s.st_size, int(s.st_mtime))
281 281 if f in self._copymap:
282 282 del self._copymap[f]
283 283
284 284 def normallookup(self, f):
285 285 '''Mark a file normal, but possibly dirty.'''
286 286 if self._pl[1] != nullid and f in self._map:
287 287 # if there is a merge going on and the file was either
288 # in state 'm' or dirty before being removed, restore that state.
288 # in state 'm' (-1) or coming from other parent (-2) before
289 # being removed, restore that state.
289 290 entry = self._map[f]
290 291 if entry[0] == 'r' and entry[2] in (-1, -2):
291 292 source = self._copymap.get(f)
292 293 if entry[2] == -1:
293 294 self.merge(f)
294 295 elif entry[2] == -2:
295 self.normaldirty(f)
296 self.otherparent(f)
296 297 if source:
297 298 self.copy(source, f)
298 299 return
299 300 if entry[0] == 'm' or entry[0] == 'n' and entry[2] == -2:
300 301 return
301 302 self._dirty = True
302 303 self._addpath(f)
303 304 self._map[f] = ('n', 0, -1, -1)
304 305 if f in self._copymap:
305 306 del self._copymap[f]
306 307
307 def normaldirty(self, f):
308 '''Mark a file normal, but dirty.'''
308 def otherparent(self, f):
309 '''Mark as coming from the other parent, always dirty.'''
310 if self._pl[1] == nullid:
311 raise util.Abort(_("setting %r to other parent "
312 "only allowed in merges") % f)
309 313 self._dirty = True
310 314 self._addpath(f)
311 315 self._map[f] = ('n', 0, -2, -1)
312 316 if f in self._copymap:
313 317 del self._copymap[f]
314 318
315 319 def add(self, f):
316 320 '''Mark a file added.'''
317 321 self._dirty = True
318 322 self._addpath(f, True)
319 323 self._map[f] = ('a', 0, -1, -1)
320 324 if f in self._copymap:
321 325 del self._copymap[f]
322 326
323 327 def remove(self, f):
324 328 '''Mark a file removed.'''
325 329 self._dirty = True
326 330 self._droppath(f)
327 331 size = 0
328 332 if self._pl[1] != nullid and f in self._map:
333 # backup the previous state
329 334 entry = self._map[f]
330 if entry[0] == 'm':
335 if entry[0] == 'm': # merge
331 336 size = -1
332 elif entry[0] == 'n' and entry[2] == -2:
337 elif entry[0] == 'n' and entry[2] == -2: # other parent
333 338 size = -2
334 339 self._map[f] = ('r', 0, size, 0)
335 340 if size == 0 and f in self._copymap:
336 341 del self._copymap[f]
337 342
338 343 def merge(self, f):
339 344 '''Mark a file merged.'''
340 345 self._dirty = True
341 346 s = os.lstat(self._join(f))
342 347 self._addpath(f)
343 348 self._map[f] = ('m', s.st_mode, s.st_size, int(s.st_mtime))
344 349 if f in self._copymap:
345 350 del self._copymap[f]
346 351
347 352 def forget(self, f):
348 353 '''Forget a file.'''
349 354 self._dirty = True
350 355 try:
351 356 self._droppath(f)
352 357 del self._map[f]
353 358 except KeyError:
354 359 self._ui.warn(_("not in dirstate: %s\n") % f)
355 360
356 361 def _normalize(self, path, knownpath):
357 362 norm_path = os.path.normcase(path)
358 363 fold_path = self._foldmap.get(norm_path, None)
359 364 if fold_path is None:
360 365 if knownpath or not os.path.exists(os.path.join(self._root, path)):
361 366 fold_path = path
362 367 else:
363 368 fold_path = self._foldmap.setdefault(norm_path,
364 369 util.fspath(path, self._root))
365 370 return fold_path
366 371
367 372 def clear(self):
368 373 self._map = {}
369 374 if "_dirs" in self.__dict__:
370 375 delattr(self, "_dirs")
371 376 self._copymap = {}
372 377 self._pl = [nullid, nullid]
373 378 self._dirty = True
374 379
375 380 def rebuild(self, parent, files):
376 381 self.clear()
377 382 for f in files:
378 383 if 'x' in files.flags(f):
379 384 self._map[f] = ('n', 0777, -1, 0)
380 385 else:
381 386 self._map[f] = ('n', 0666, -1, 0)
382 387 self._pl = (parent, nullid)
383 388 self._dirty = True
384 389
385 390 def write(self):
386 391 if not self._dirty:
387 392 return
388 393 st = self._opener("dirstate", "w", atomictemp=True)
389 394
390 395 # use the modification time of the newly created temporary file as the
391 396 # filesystem's notion of 'now'
392 397 now = int(util.fstat(st).st_mtime)
393 398
394 399 cs = cStringIO.StringIO()
395 400 copymap = self._copymap
396 401 pack = struct.pack
397 402 write = cs.write
398 403 write("".join(self._pl))
399 404 for f, e in self._map.iteritems():
400 405 if e[0] == 'n' and e[3] == now:
401 406 # The file was last modified "simultaneously" with the current
402 407 # write to dirstate (i.e. within the same second for file-
403 408 # systems with a granularity of 1 sec). This commonly happens
404 409 # for at least a couple of files on 'update'.
405 410 # The user could change the file without changing its size
406 411 # within the same second. Invalidate the file's stat data in
407 412 # dirstate, forcing future 'status' calls to compare the
408 413 # contents of the file. This prevents mistakenly treating such
409 414 # files as clean.
410 415 e = (e[0], 0, -1, -1) # mark entry as 'unset'
411 416 self._map[f] = e
412 417
413 418 if f in copymap:
414 419 f = "%s\0%s" % (f, copymap[f])
415 420 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
416 421 write(e)
417 422 write(f)
418 423 st.write(cs.getvalue())
419 424 st.rename()
420 425 self._dirty = self._dirtypl = False
421 426
422 427 def _dirignore(self, f):
423 428 if f == '.':
424 429 return False
425 430 if self._ignore(f):
426 431 return True
427 432 for p in _finddirs(f):
428 433 if self._ignore(p):
429 434 return True
430 435 return False
431 436
432 437 def walk(self, match, subrepos, unknown, ignored):
433 438 '''
434 439 Walk recursively through the directory tree, finding all files
435 440 matched by match.
436 441
437 442 Return a dict mapping filename to stat-like object (either
438 443 mercurial.osutil.stat instance or return value of os.stat()).
439 444 '''
440 445
441 446 def fwarn(f, msg):
442 447 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
443 448 return False
444 449
445 450 def badtype(mode):
446 451 kind = _('unknown')
447 452 if stat.S_ISCHR(mode):
448 453 kind = _('character device')
449 454 elif stat.S_ISBLK(mode):
450 455 kind = _('block device')
451 456 elif stat.S_ISFIFO(mode):
452 457 kind = _('fifo')
453 458 elif stat.S_ISSOCK(mode):
454 459 kind = _('socket')
455 460 elif stat.S_ISDIR(mode):
456 461 kind = _('directory')
457 462 return _('unsupported file type (type is %s)') % kind
458 463
459 464 ignore = self._ignore
460 465 dirignore = self._dirignore
461 466 if ignored:
462 467 ignore = util.never
463 468 dirignore = util.never
464 469 elif not unknown:
465 470 # if unknown and ignored are False, skip step 2
466 471 ignore = util.always
467 472 dirignore = util.always
468 473
469 474 matchfn = match.matchfn
470 475 badfn = match.bad
471 476 dmap = self._map
472 477 normpath = util.normpath
473 478 listdir = osutil.listdir
474 479 lstat = os.lstat
475 480 getkind = stat.S_IFMT
476 481 dirkind = stat.S_IFDIR
477 482 regkind = stat.S_IFREG
478 483 lnkkind = stat.S_IFLNK
479 484 join = self._join
480 485 work = []
481 486 wadd = work.append
482 487
483 488 if self._checkcase:
484 489 normalize = self._normalize
485 490 else:
486 491 normalize = lambda x, y: x
487 492
488 493 exact = skipstep3 = False
489 494 if matchfn == match.exact: # match.exact
490 495 exact = True
491 496 dirignore = util.always # skip step 2
492 497 elif match.files() and not match.anypats(): # match.match, no patterns
493 498 skipstep3 = True
494 499
495 500 files = set(match.files())
496 501 if not files or '.' in files:
497 502 files = ['']
498 503 results = dict.fromkeys(subrepos)
499 504 results['.hg'] = None
500 505
501 506 # step 1: find all explicit files
502 507 for ff in sorted(files):
503 508 nf = normalize(normpath(ff), False)
504 509 if nf in results:
505 510 continue
506 511
507 512 try:
508 513 st = lstat(join(nf))
509 514 kind = getkind(st.st_mode)
510 515 if kind == dirkind:
511 516 skipstep3 = False
512 517 if nf in dmap:
513 518 #file deleted on disk but still in dirstate
514 519 results[nf] = None
515 520 match.dir(nf)
516 521 if not dirignore(nf):
517 522 wadd(nf)
518 523 elif kind == regkind or kind == lnkkind:
519 524 results[nf] = st
520 525 else:
521 526 badfn(ff, badtype(kind))
522 527 if nf in dmap:
523 528 results[nf] = None
524 529 except OSError, inst:
525 530 if nf in dmap: # does it exactly match a file?
526 531 results[nf] = None
527 532 else: # does it match a directory?
528 533 prefix = nf + "/"
529 534 for fn in dmap:
530 535 if fn.startswith(prefix):
531 536 match.dir(nf)
532 537 skipstep3 = False
533 538 break
534 539 else:
535 540 badfn(ff, inst.strerror)
536 541
537 542 # step 2: visit subdirectories
538 543 while work:
539 544 nd = work.pop()
540 545 skip = None
541 546 if nd == '.':
542 547 nd = ''
543 548 else:
544 549 skip = '.hg'
545 550 try:
546 551 entries = listdir(join(nd), stat=True, skip=skip)
547 552 except OSError, inst:
548 553 if inst.errno == errno.EACCES:
549 554 fwarn(nd, inst.strerror)
550 555 continue
551 556 raise
552 557 for f, kind, st in entries:
553 558 nf = normalize(nd and (nd + "/" + f) or f, True)
554 559 if nf not in results:
555 560 if kind == dirkind:
556 561 if not ignore(nf):
557 562 match.dir(nf)
558 563 wadd(nf)
559 564 if nf in dmap and matchfn(nf):
560 565 results[nf] = None
561 566 elif kind == regkind or kind == lnkkind:
562 567 if nf in dmap:
563 568 if matchfn(nf):
564 569 results[nf] = st
565 570 elif matchfn(nf) and not ignore(nf):
566 571 results[nf] = st
567 572 elif nf in dmap and matchfn(nf):
568 573 results[nf] = None
569 574
570 575 # step 3: report unseen items in the dmap hash
571 576 if not skipstep3 and not exact:
572 577 visit = sorted([f for f in dmap if f not in results and matchfn(f)])
573 578 for nf, st in zip(visit, util.statfiles([join(i) for i in visit])):
574 579 if not st is None and not getkind(st.st_mode) in (regkind, lnkkind):
575 580 st = None
576 581 results[nf] = st
577 582 for s in subrepos:
578 583 del results[s]
579 584 del results['.hg']
580 585 return results
581 586
582 587 def status(self, match, subrepos, ignored, clean, unknown):
583 588 '''Determine the status of the working copy relative to the
584 589 dirstate and return a tuple of lists (unsure, modified, added,
585 590 removed, deleted, unknown, ignored, clean), where:
586 591
587 592 unsure:
588 593 files that might have been modified since the dirstate was
589 594 written, but need to be read to be sure (size is the same
590 595 but mtime differs)
591 596 modified:
592 597 files that have definitely been modified since the dirstate
593 598 was written (different size or mode)
594 599 added:
595 600 files that have been explicitly added with hg add
596 601 removed:
597 602 files that have been explicitly removed with hg remove
598 603 deleted:
599 604 files that have been deleted through other means ("missing")
600 605 unknown:
601 606 files not in the dirstate that are not ignored
602 607 ignored:
603 608 files not in the dirstate that are ignored
604 609 (by _dirignore())
605 610 clean:
606 611 files that have definitely not been modified since the
607 612 dirstate was written
608 613 '''
609 614 listignored, listclean, listunknown = ignored, clean, unknown
610 615 lookup, modified, added, unknown, ignored = [], [], [], [], []
611 616 removed, deleted, clean = [], [], []
612 617
613 618 dmap = self._map
614 619 ladd = lookup.append # aka "unsure"
615 620 madd = modified.append
616 621 aadd = added.append
617 622 uadd = unknown.append
618 623 iadd = ignored.append
619 624 radd = removed.append
620 625 dadd = deleted.append
621 626 cadd = clean.append
622 627
623 628 for fn, st in self.walk(match, subrepos, listunknown,
624 629 listignored).iteritems():
625 630 if fn not in dmap:
626 631 if (listignored or match.exact(fn)) and self._dirignore(fn):
627 632 if listignored:
628 633 iadd(fn)
629 634 elif listunknown:
630 635 uadd(fn)
631 636 continue
632 637
633 638 state, mode, size, time = dmap[fn]
634 639
635 640 if not st and state in "nma":
636 641 dadd(fn)
637 642 elif state == 'n':
638 643 if (size >= 0 and
639 644 (size != st.st_size
640 645 or ((mode ^ st.st_mode) & 0100 and self._checkexec))
641 or size == -2
646 or size == -2 # other parent
642 647 or fn in self._copymap):
643 648 madd(fn)
644 649 elif time != int(st.st_mtime):
645 650 ladd(fn)
646 651 elif listclean:
647 652 cadd(fn)
648 653 elif state == 'm':
649 654 madd(fn)
650 655 elif state == 'a':
651 656 aadd(fn)
652 657 elif state == 'r':
653 658 radd(fn)
654 659
655 660 return (lookup, modified, added, removed, deleted, unknown, ignored,
656 661 clean)
@@ -1,519 +1,519 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 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._read()
18 18 def reset(self, node=None):
19 19 self._state = {}
20 20 if node:
21 21 self._local = node
22 22 shutil.rmtree(self._repo.join("merge"), True)
23 23 def _read(self):
24 24 self._state = {}
25 25 try:
26 26 localnode = None
27 27 f = self._repo.opener("merge/state")
28 28 for i, l in enumerate(f):
29 29 if i == 0:
30 30 localnode = l[:-1]
31 31 else:
32 32 bits = l[:-1].split("\0")
33 33 self._state[bits[0]] = bits[1:]
34 34 self._local = bin(localnode)
35 35 except IOError, err:
36 36 if err.errno != errno.ENOENT:
37 37 raise
38 38 def _write(self):
39 39 f = self._repo.opener("merge/state", "w")
40 40 f.write(hex(self._local) + "\n")
41 41 for d, v in self._state.iteritems():
42 42 f.write("\0".join([d] + v) + "\n")
43 43 def add(self, fcl, fco, fca, fd, flags):
44 44 hash = util.sha1(fcl.path()).hexdigest()
45 45 self._repo.opener("merge/" + hash, "w").write(fcl.data())
46 46 self._state[fd] = ['u', hash, fcl.path(), fca.path(),
47 47 hex(fca.filenode()), fco.path(), flags]
48 48 self._write()
49 49 def __contains__(self, dfile):
50 50 return dfile in self._state
51 51 def __getitem__(self, dfile):
52 52 return self._state[dfile][0]
53 53 def __iter__(self):
54 54 l = self._state.keys()
55 55 l.sort()
56 56 for f in l:
57 57 yield f
58 58 def mark(self, dfile, state):
59 59 self._state[dfile][0] = state
60 60 self._write()
61 61 def resolve(self, dfile, wctx, octx):
62 62 if self[dfile] == 'r':
63 63 return 0
64 64 state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
65 65 f = self._repo.opener("merge/" + hash)
66 66 self._repo.wwrite(dfile, f.read(), flags)
67 67 fcd = wctx[dfile]
68 68 fco = octx[ofile]
69 69 fca = self._repo.filectx(afile, fileid=anode)
70 70 r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
71 71 if not r:
72 72 self.mark(dfile, 'r')
73 73 return r
74 74
75 75 def _checkunknown(wctx, mctx):
76 76 "check for collisions between unknown files and files in mctx"
77 77 for f in wctx.unknown():
78 78 if f in mctx and mctx[f].cmp(wctx[f].data()):
79 79 raise util.Abort(_("untracked file in working directory differs"
80 80 " from file in requested revision: '%s'") % f)
81 81
82 82 def _checkcollision(mctx):
83 83 "check for case folding collisions in the destination context"
84 84 folded = {}
85 85 for fn in mctx:
86 86 fold = fn.lower()
87 87 if fold in folded:
88 88 raise util.Abort(_("case-folding collision between %s and %s")
89 89 % (fn, folded[fold]))
90 90 folded[fold] = fn
91 91
92 92 def _forgetremoved(wctx, mctx, branchmerge):
93 93 """
94 94 Forget removed files
95 95
96 96 If we're jumping between revisions (as opposed to merging), and if
97 97 neither the working directory nor the target rev has the file,
98 98 then we need to remove it from the dirstate, to prevent the
99 99 dirstate from listing the file when it is no longer in the
100 100 manifest.
101 101
102 102 If we're merging, and the other revision has removed a file
103 103 that is not present in the working directory, we need to mark it
104 104 as removed.
105 105 """
106 106
107 107 action = []
108 108 state = branchmerge and 'r' or 'f'
109 109 for f in wctx.deleted():
110 110 if f not in mctx:
111 111 action.append((f, state))
112 112
113 113 if not branchmerge:
114 114 for f in wctx.removed():
115 115 if f not in mctx:
116 116 action.append((f, "f"))
117 117
118 118 return action
119 119
120 120 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
121 121 """
122 122 Merge p1 and p2 with ancestor ma and generate merge action list
123 123
124 124 overwrite = whether we clobber working files
125 125 partial = function to filter file lists
126 126 """
127 127
128 128 def fmerge(f, f2, fa):
129 129 """merge flags"""
130 130 a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
131 131 if m == n: # flags agree
132 132 return m # unchanged
133 133 if m and n and not a: # flags set, don't agree, differ from parent
134 134 r = repo.ui.promptchoice(
135 135 _(" conflicting flags for %s\n"
136 136 "(n)one, e(x)ec or sym(l)ink?") % f,
137 137 (_("&None"), _("E&xec"), _("Sym&link")), 0)
138 138 if r == 1:
139 139 return "x" # Exec
140 140 if r == 2:
141 141 return "l" # Symlink
142 142 return ""
143 143 if m and m != a: # changed from a to m
144 144 return m
145 145 if n and n != a: # changed from a to n
146 146 return n
147 147 return '' # flag was cleared
148 148
149 149 def act(msg, m, f, *args):
150 150 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
151 151 action.append((f, m) + args)
152 152
153 153 action, copy = [], {}
154 154
155 155 if overwrite:
156 156 pa = p1
157 157 elif pa == p2: # backwards
158 158 pa = p1.p1()
159 159 elif pa and repo.ui.configbool("merge", "followcopies", True):
160 160 dirs = repo.ui.configbool("merge", "followdirs", True)
161 161 copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
162 162 for of, fl in diverge.iteritems():
163 163 act("divergent renames", "dr", of, fl)
164 164
165 165 repo.ui.note(_("resolving manifests\n"))
166 166 repo.ui.debug(" overwrite %s partial %s\n" % (overwrite, bool(partial)))
167 167 repo.ui.debug(" ancestor %s local %s remote %s\n" % (pa, p1, p2))
168 168
169 169 m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
170 170 copied = set(copy.values())
171 171
172 172 if not overwrite and '.hgsubstate' in m1:
173 173 # check whether sub state is modified
174 174 for s in p1.substate:
175 175 if p1.sub(s).dirty():
176 176 m1['.hgsubstate'] += "+"
177 177 break
178 178
179 179 # Compare manifests
180 180 for f, n in m1.iteritems():
181 181 if partial and not partial(f):
182 182 continue
183 183 if f in m2:
184 184 rflags = fmerge(f, f, f)
185 185 a = ma.get(f, nullid)
186 186 if n == m2[f] or m2[f] == a: # same or local newer
187 187 if m1.flags(f) != rflags:
188 188 act("update permissions", "e", f, rflags)
189 189 elif n == a: # remote newer
190 190 act("remote is newer", "g", f, rflags)
191 191 else: # both changed
192 192 act("versions differ", "m", f, f, f, rflags, False)
193 193 elif f in copied: # files we'll deal with on m2 side
194 194 pass
195 195 elif f in copy:
196 196 f2 = copy[f]
197 197 if f2 not in m2: # directory rename
198 198 act("remote renamed directory to " + f2, "d",
199 199 f, None, f2, m1.flags(f))
200 200 else: # case 2 A,B/B/B or case 4,21 A/B/B
201 201 act("local copied/moved to " + f2, "m",
202 202 f, f2, f, fmerge(f, f2, f2), False)
203 203 elif f in ma: # clean, a different, no remote
204 204 if n != ma[f]:
205 205 if repo.ui.promptchoice(
206 206 _(" local changed %s which remote deleted\n"
207 207 "use (c)hanged version or (d)elete?") % f,
208 208 (_("&Changed"), _("&Delete")), 0):
209 209 act("prompt delete", "r", f)
210 210 else:
211 211 act("prompt keep", "a", f)
212 212 elif n[20:] == "a": # added, no remote
213 213 act("remote deleted", "f", f)
214 214 elif n[20:] != "u":
215 215 act("other deleted", "r", f)
216 216
217 217 for f, n in m2.iteritems():
218 218 if partial and not partial(f):
219 219 continue
220 220 if f in m1 or f in copied: # files already visited
221 221 continue
222 222 if f in copy:
223 223 f2 = copy[f]
224 224 if f2 not in m1: # directory rename
225 225 act("local renamed directory to " + f2, "d",
226 226 None, f, f2, m2.flags(f))
227 227 elif f2 in m2: # rename case 1, A/A,B/A
228 228 act("remote copied to " + f, "m",
229 229 f2, f, f, fmerge(f2, f, f2), False)
230 230 else: # case 3,20 A/B/A
231 231 act("remote moved to " + f, "m",
232 232 f2, f, f, fmerge(f2, f, f2), True)
233 233 elif f not in ma:
234 234 act("remote created", "g", f, m2.flags(f))
235 235 elif n != ma[f]:
236 236 if repo.ui.promptchoice(
237 237 _("remote changed %s which local deleted\n"
238 238 "use (c)hanged version or leave (d)eleted?") % f,
239 239 (_("&Changed"), _("&Deleted")), 0) == 0:
240 240 act("prompt recreating", "g", f, m2.flags(f))
241 241
242 242 return action
243 243
244 244 def actionkey(a):
245 245 return a[1] == 'r' and -1 or 0, a
246 246
247 247 def applyupdates(repo, action, wctx, mctx):
248 248 "apply the merge action list to the working directory"
249 249
250 250 updated, merged, removed, unresolved = 0, 0, 0, 0
251 251 ms = mergestate(repo)
252 252 ms.reset(wctx.parents()[0].node())
253 253 moves = []
254 254 action.sort(key=actionkey)
255 255 substate = wctx.substate # prime
256 256
257 257 # prescan for merges
258 258 u = repo.ui
259 259 for a in action:
260 260 f, m = a[:2]
261 261 if m == 'm': # merge
262 262 f2, fd, flags, move = a[2:]
263 263 if f == '.hgsubstate': # merged internally
264 264 continue
265 265 repo.ui.debug("preserving %s for resolve of %s\n" % (f, fd))
266 266 fcl = wctx[f]
267 267 fco = mctx[f2]
268 268 fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev)
269 269 ms.add(fcl, fco, fca, fd, flags)
270 270 if f != fd and move:
271 271 moves.append(f)
272 272
273 273 # remove renamed files after safely stored
274 274 for f in moves:
275 275 if util.lexists(repo.wjoin(f)):
276 276 repo.ui.debug("removing %s\n" % f)
277 277 os.unlink(repo.wjoin(f))
278 278
279 279 audit_path = util.path_auditor(repo.root)
280 280
281 281 numupdates = len(action)
282 282 for i, a in enumerate(action):
283 283 f, m = a[:2]
284 284 u.progress('update', i + 1, item=f, total=numupdates, unit='files')
285 285 if f and f[0] == "/":
286 286 continue
287 287 if m == "r": # remove
288 288 repo.ui.note(_("removing %s\n") % f)
289 289 audit_path(f)
290 290 if f == '.hgsubstate': # subrepo states need updating
291 291 subrepo.submerge(repo, wctx, mctx, wctx)
292 292 try:
293 293 util.unlink(repo.wjoin(f))
294 294 except OSError, inst:
295 295 if inst.errno != errno.ENOENT:
296 296 repo.ui.warn(_("update failed to remove %s: %s!\n") %
297 297 (f, inst.strerror))
298 298 removed += 1
299 299 elif m == "m": # merge
300 300 if f == '.hgsubstate': # subrepo states need updating
301 301 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx))
302 302 continue
303 303 f2, fd, flags, move = a[2:]
304 304 r = ms.resolve(fd, wctx, mctx)
305 305 if r is not None and r > 0:
306 306 unresolved += 1
307 307 else:
308 308 if r is None:
309 309 updated += 1
310 310 else:
311 311 merged += 1
312 312 util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
313 313 if f != fd and move and util.lexists(repo.wjoin(f)):
314 314 repo.ui.debug("removing %s\n" % f)
315 315 os.unlink(repo.wjoin(f))
316 316 elif m == "g": # get
317 317 flags = a[2]
318 318 repo.ui.note(_("getting %s\n") % f)
319 319 t = mctx.filectx(f).data()
320 320 repo.wwrite(f, t, flags)
321 321 updated += 1
322 322 if f == '.hgsubstate': # subrepo states need updating
323 323 subrepo.submerge(repo, wctx, mctx, wctx)
324 324 elif m == "d": # directory rename
325 325 f2, fd, flags = a[2:]
326 326 if f:
327 327 repo.ui.note(_("moving %s to %s\n") % (f, fd))
328 328 t = wctx.filectx(f).data()
329 329 repo.wwrite(fd, t, flags)
330 330 util.unlink(repo.wjoin(f))
331 331 if f2:
332 332 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
333 333 t = mctx.filectx(f2).data()
334 334 repo.wwrite(fd, t, flags)
335 335 updated += 1
336 336 elif m == "dr": # divergent renames
337 337 fl = a[2]
338 338 repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f)
339 339 for nf in fl:
340 340 repo.ui.warn(" %s\n" % nf)
341 341 elif m == "e": # exec
342 342 flags = a[2]
343 343 util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
344 344 u.progress('update', None, total=numupdates, unit='files')
345 345
346 346 return updated, merged, removed, unresolved
347 347
348 348 def recordupdates(repo, action, branchmerge):
349 349 "record merge actions to the dirstate"
350 350
351 351 for a in action:
352 352 f, m = a[:2]
353 353 if m == "r": # remove
354 354 if branchmerge:
355 355 repo.dirstate.remove(f)
356 356 else:
357 357 repo.dirstate.forget(f)
358 358 elif m == "a": # re-add
359 359 if not branchmerge:
360 360 repo.dirstate.add(f)
361 361 elif m == "f": # forget
362 362 repo.dirstate.forget(f)
363 363 elif m == "e": # exec change
364 364 repo.dirstate.normallookup(f)
365 365 elif m == "g": # get
366 366 if branchmerge:
367 repo.dirstate.normaldirty(f)
367 repo.dirstate.otherparent(f)
368 368 else:
369 369 repo.dirstate.normal(f)
370 370 elif m == "m": # merge
371 371 f2, fd, flag, move = a[2:]
372 372 if branchmerge:
373 373 # We've done a branch merge, mark this file as merged
374 374 # so that we properly record the merger later
375 375 repo.dirstate.merge(fd)
376 376 if f != f2: # copy/rename
377 377 if move:
378 378 repo.dirstate.remove(f)
379 379 if f != fd:
380 380 repo.dirstate.copy(f, fd)
381 381 else:
382 382 repo.dirstate.copy(f2, fd)
383 383 else:
384 384 # We've update-merged a locally modified file, so
385 385 # we set the dirstate to emulate a normal checkout
386 386 # of that file some time in the past. Thus our
387 387 # merge will appear as a normal local file
388 388 # modification.
389 389 repo.dirstate.normallookup(fd)
390 390 if move:
391 391 repo.dirstate.forget(f)
392 392 elif m == "d": # directory rename
393 393 f2, fd, flag = a[2:]
394 394 if not f2 and f not in repo.dirstate:
395 395 # untracked file moved
396 396 continue
397 397 if branchmerge:
398 398 repo.dirstate.add(fd)
399 399 if f:
400 400 repo.dirstate.remove(f)
401 401 repo.dirstate.copy(f, fd)
402 402 if f2:
403 403 repo.dirstate.copy(f2, fd)
404 404 else:
405 405 repo.dirstate.normal(fd)
406 406 if f:
407 407 repo.dirstate.forget(f)
408 408
409 409 def update(repo, node, branchmerge, force, partial):
410 410 """
411 411 Perform a merge between the working directory and the given node
412 412
413 413 node = the node to update to, or None if unspecified
414 414 branchmerge = whether to merge between branches
415 415 force = whether to force branch merging or file overwriting
416 416 partial = a function to filter file lists (dirstate not updated)
417 417
418 418 The table below shows all the behaviors of the update command
419 419 given the -c and -C or no options, whether the working directory
420 420 is dirty, whether a revision is specified, and the relationship of
421 421 the parent rev to the target rev (linear, on the same named
422 422 branch, or on another named branch).
423 423
424 424 This logic is tested by test-update-branches.
425 425
426 426 -c -C dirty rev | linear same cross
427 427 n n n n | ok (1) x
428 428 n n n y | ok ok ok
429 429 n n y * | merge (2) (2)
430 430 n y * * | --- discard ---
431 431 y n y * | --- (3) ---
432 432 y n n * | --- ok ---
433 433 y y * * | --- (4) ---
434 434
435 435 x = can't happen
436 436 * = don't-care
437 437 1 = abort: crosses branches (use 'hg merge' or 'hg update -c')
438 438 2 = abort: crosses branches (use 'hg merge' to merge or
439 439 use 'hg update -C' to discard changes)
440 440 3 = abort: uncommitted local changes
441 441 4 = incompatible options (checked in commands.py)
442 442 """
443 443
444 444 onode = node
445 445 wlock = repo.wlock()
446 446 try:
447 447 wc = repo[None]
448 448 if node is None:
449 449 # tip of current branch
450 450 try:
451 451 node = repo.branchtags()[wc.branch()]
452 452 except KeyError:
453 453 if wc.branch() == "default": # no default branch!
454 454 node = repo.lookup("tip") # update to tip
455 455 else:
456 456 raise util.Abort(_("branch %s not found") % wc.branch())
457 457 overwrite = force and not branchmerge
458 458 pl = wc.parents()
459 459 p1, p2 = pl[0], repo[node]
460 460 pa = p1.ancestor(p2)
461 461 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
462 462 fastforward = False
463 463
464 464 ### check phase
465 465 if not overwrite and len(pl) > 1:
466 466 raise util.Abort(_("outstanding uncommitted merges"))
467 467 if branchmerge:
468 468 if pa == p2:
469 469 raise util.Abort(_("can't merge with ancestor"))
470 470 elif pa == p1:
471 471 if p1.branch() != p2.branch():
472 472 fastforward = True
473 473 else:
474 474 raise util.Abort(_("nothing to merge (use 'hg update'"
475 475 " or check 'hg heads')"))
476 476 if not force and (wc.files() or wc.deleted()):
477 477 raise util.Abort(_("outstanding uncommitted changes "
478 478 "(use 'hg status' to list changes)"))
479 479 elif not overwrite:
480 480 if pa == p1 or pa == p2: # linear
481 481 pass # all good
482 482 elif wc.files() or wc.deleted():
483 483 raise util.Abort(_("crosses branches (use 'hg merge' to merge "
484 484 "or use 'hg update -C' to discard changes)"))
485 485 elif onode is None:
486 486 raise util.Abort(_("crosses branches (use 'hg merge' or use "
487 487 "'hg update -c')"))
488 488 else:
489 489 # Allow jumping branches if clean and specific rev given
490 490 overwrite = True
491 491
492 492 ### calculate phase
493 493 action = []
494 494 if not force:
495 495 _checkunknown(wc, p2)
496 496 if not util.checkcase(repo.path):
497 497 _checkcollision(p2)
498 498 action += _forgetremoved(wc, p2, branchmerge)
499 499 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
500 500
501 501 ### apply phase
502 502 if not branchmerge: # just jump to the new rev
503 503 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
504 504 if not partial:
505 505 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
506 506
507 507 stats = applyupdates(repo, action, wc, p2)
508 508
509 509 if not partial:
510 repo.dirstate.setparents(fp1, fp2)
510 511 recordupdates(repo, action, branchmerge)
511 repo.dirstate.setparents(fp1, fp2)
512 512 if not branchmerge and not fastforward:
513 513 repo.dirstate.setbranch(p2.branch())
514 514 finally:
515 515 wlock.release()
516 516
517 517 if not partial:
518 518 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
519 519 return stats
General Comments 0
You need to be logged in to leave comments. Login now