# HG changeset patch # User Matt Mackall # Date 1324083959 21600 # Node ID 7b7f03502b5aa9cbe3fa254d9bedb70b4dd4d205 # Parent c7b0bedbb07ac3752243db046f54c9a4bbc3273d# Parent d550168f11ce80e62caaff18b6c29ec1477b8803 merge with stable diff -r c7b0bedbb07a -r 7b7f03502b5a hgext/largefiles/lfcommands.py --- a/hgext/largefiles/lfcommands.py Thu Dec 15 16:50:21 2011 -0600 +++ b/hgext/largefiles/lfcommands.py Fri Dec 16 19:05:59 2011 -0600 @@ -446,7 +446,11 @@ os.chmod(abslfile, mode) ret = 1 else: - if os.path.exists(abslfile): + # Remove lfiles for which the standin is deleted, unless the + # lfile is added to the repository again. This happens when a + # largefile is converted back to a normal file: the standin + # disappears, but a new (normal) file appears as the lfile. + if os.path.exists(abslfile) and lfile not in repo[None]: os.unlink(abslfile) ret = -1 state = repo.dirstate[lfutil.standin(lfile)] diff -r c7b0bedbb07a -r 7b7f03502b5a hgext/largefiles/overrides.py --- a/hgext/largefiles/overrides.py Thu Dec 15 16:50:21 2011 -0600 +++ b/hgext/largefiles/overrides.py Fri Dec 16 19:05:59 2011 -0600 @@ -242,6 +242,90 @@ wlock.release() return orig(ui, repo, *pats, **opts) +# Before starting the manifest merge, merge.updates will call +# _checkunknown to check if there are any files in the merged-in +# changeset that collide with unknown files in the working copy. +# +# The largefiles are seen as unknown, so this prevents us from merging +# in a file 'foo' if we already have a largefile with the same name. +# +# The overridden function filters the unknown files by removing any +# largefiles. This makes the merge proceed and we can then handle this +# case further in the overridden manifestmerge function below. +def override_checkunknown(origfn, wctx, mctx, folding): + origunknown = wctx.unknown() + wctx._unknown = filter(lambda f: lfutil.standin(f) not in wctx, origunknown) + try: + return origfn(wctx, mctx, folding) + finally: + wctx._unknown = origunknown + +# The manifest merge handles conflicts on the manifest level. We want +# to handle changes in largefile-ness of files at this level too. +# +# The strategy is to run the original manifestmerge and then process +# the action list it outputs. There are two cases we need to deal with: +# +# 1. Normal file in p1, largefile in p2. Here the largefile is +# detected via its standin file, which will enter the working copy +# with a "get" action. It is not "merge" since the standin is all +# Mercurial is concerned with at this level -- the link to the +# existing normal file is not relevant here. +# +# 2. Largefile in p1, normal file in p2. Here we get a "merge" action +# since the largefile will be present in the working copy and +# different from the normal file in p2. Mercurial therefore +# triggers a merge action. +# +# In both cases, we prompt the user and emit new actions to either +# remove the standin (if the normal file was kept) or to remove the +# normal file and get the standin (if the largefile was kept). The +# default prompt answer is to use the largefile version since it was +# presumably changed on purpose. +# +# Finally, the merge.applyupdates function will then take care of +# writing the files into the working copy and lfcommands.updatelfiles +# will update the largefiles. +def override_manifestmerge(origfn, repo, p1, p2, pa, overwrite, partial): + actions = origfn(repo, p1, p2, pa, overwrite, partial) + processed = [] + + for action in actions: + if overwrite: + processed.append(action) + continue + f, m = action[:2] + + choices = (_('&Largefile'), _('&Normal file')) + if m == "g" and lfutil.splitstandin(f) in p1 and f in p2: + # Case 1: normal file in the working copy, largefile in + # the second parent + lfile = lfutil.splitstandin(f) + standin = f + msg = _('%s has been turned into a largefile\n' + 'use (l)argefile or keep as (n)ormal file?') % lfile + if repo.ui.promptchoice(msg, choices, 0) == 0: + processed.append((lfile, "r")) + processed.append((standin, "g", p2.flags(standin))) + else: + processed.append((standin, "r")) + elif m == "m" and lfutil.standin(f) in p1 and f in p2: + # Case 2: largefile in the working copy, normal file in + # the second parent + standin = lfutil.standin(f) + lfile = f + msg = _('%s has been turned into a normal file\n' + 'keep as (l)argefile or use (n)ormal file?') % lfile + if repo.ui.promptchoice(msg, choices, 0) == 0: + processed.append((lfile, "r")) + else: + processed.append((standin, "r")) + processed.append((lfile, "g", p2.flags(lfile))) + else: + processed.append(action) + + return processed + # Override filemerge to prompt the user about how they wish to merge # largefiles. This will handle identical edits, and copy/rename + # edit without prompting the user. diff -r c7b0bedbb07a -r 7b7f03502b5a hgext/largefiles/reposetup.py --- a/hgext/largefiles/reposetup.py Thu Dec 15 16:50:21 2011 -0600 +++ b/hgext/largefiles/reposetup.py Fri Dec 16 19:05:59 2011 -0600 @@ -215,9 +215,18 @@ continue if lfile not in lfdirstate: removed.append(lfile) - # Handle unknown and ignored differently - lfiles = (modified, added, removed, missing, [], [], clean) + + # Filter result lists result = list(result) + + # Largefiles are not really removed when they're + # still in the normal dirstate. Likewise, normal + # files are not really removed if it's still in + # lfdirstate. This happens in merges where files + # change type. + removed = [f for f in removed if f not in repo.dirstate] + result[2] = [f for f in result[2] if f not in lfdirstate] + # Unknown files unknown = set(unknown).difference(ignored) result[4] = [f for f in unknown @@ -230,6 +239,7 @@ normals = [[fn for fn in filelist if not lfutil.isstandin(fn)] for filelist in result] + lfiles = (modified, added, removed, missing, [], [], clean) result = [sorted(list1 + list2) for (list1, list2) in zip(normals, lfiles)] else: diff -r c7b0bedbb07a -r 7b7f03502b5a hgext/largefiles/uisetup.py --- a/hgext/largefiles/uisetup.py Thu Dec 15 16:50:21 2011 -0600 +++ b/hgext/largefiles/uisetup.py Fri Dec 16 19:05:59 2011 -0600 @@ -9,7 +9,7 @@ '''setup for largefiles extension: uisetup''' from mercurial import archival, cmdutil, commands, extensions, filemerge, hg, \ - httprepo, localrepo, sshrepo, sshserver, wireproto + httprepo, localrepo, merge, sshrepo, sshserver, wireproto from mercurial.i18n import _ from mercurial.hgweb import hgweb_mod, protocol @@ -63,6 +63,10 @@ overrides.override_update) entry = extensions.wrapcommand(commands.table, 'pull', overrides.override_pull) + entry = extensions.wrapfunction(merge, '_checkunknown', + overrides.override_checkunknown) + entry = extensions.wrapfunction(merge, 'manifestmerge', + overrides.override_manifestmerge) entry = extensions.wrapfunction(filemerge, 'filemerge', overrides.override_filemerge) entry = extensions.wrapfunction(cmdutil, 'copy', diff -r c7b0bedbb07a -r 7b7f03502b5a hgext/progress.py --- a/hgext/progress.py Thu Dec 15 16:50:21 2011 -0600 +++ b/hgext/progress.py Fri Dec 16 19:05:59 2011 -0600 @@ -271,17 +271,21 @@ class progressui(ui.__class__): _progbar = None + def _quiet(self): + return self.debugflag or self.quiet + def progress(self, *args, **opts): - self._progbar.progress(*args, **opts) + if not self._quiet(): + self._progbar.progress(*args, **opts) return super(progressui, self).progress(*args, **opts) def write(self, *args, **opts): - if self._progbar.printed: + if not self._quiet() and self._progbar.printed: self._progbar.clear() return super(progressui, self).write(*args, **opts) def write_err(self, *args, **opts): - if self._progbar.printed: + if not self._quiet() and self._progbar.printed: self._progbar.clear() return super(progressui, self).write_err(*args, **opts) diff -r c7b0bedbb07a -r 7b7f03502b5a hgext/win32mbcs.py --- a/hgext/win32mbcs.py Thu Dec 15 16:50:21 2011 -0600 +++ b/hgext/win32mbcs.py Fri Dec 16 19:05:59 2011 -0600 @@ -127,7 +127,7 @@ # NOTE: os.path.dirname() and os.path.basename() are safe because # they use result of os.path.split() funcs = '''os.path.join os.path.split os.path.splitext - os.path.splitunc os.path.normpath os.path.normcase os.makedirs + os.path.splitunc os.path.normpath os.makedirs mercurial.util.endswithsep mercurial.util.splitpath mercurial.util.checkcase mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath mercurial.util.checkwinfilename mercurial.util.checkosfilename''' diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/changelog.py --- a/mercurial/changelog.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/changelog.py Fri Dec 16 19:05:59 2011 -0600 @@ -24,9 +24,20 @@ return text.replace('\0', '\\0') def decodeextra(text): + """ + >>> decodeextra(encodeextra({'foo': 'bar', 'baz': chr(0) + '2'})) + {'foo': 'bar', 'baz': '\\x002'} + >>> decodeextra(encodeextra({'foo': 'bar', 'baz': chr(92) + chr(0) + '2'})) + {'foo': 'bar', 'baz': '\\\\\\x002'} + """ extra = {} for l in text.split('\0'): if l: + if '\\0' in l: + # fix up \0 without getting into trouble with \\0 + l = l.replace('\\\\', '\\\\\n') + l = l.replace('\\0', '\0') + l = l.replace('\n', '') k, v = l.decode('string_escape').split(':', 1) extra[k] = v return extra diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/dirstate.py --- a/mercurial/dirstate.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/dirstate.py Fri Dec 16 19:05:59 2011 -0600 @@ -65,10 +65,15 @@ return self._copymap @propertycache + def _normroot(self): + return util.normcase(self._root) + + @propertycache def _foldmap(self): f = {} for name in self._map: f[util.normcase(name)] = name + f['.'] = '.' # prevents useless util.fspath() invocation return f @propertycache @@ -383,7 +388,7 @@ folded = path else: folded = self._foldmap.setdefault(normed, - util.fspath(path, self._root)) + util.fspath(normed, self._normroot)) return folded def normalize(self, path, isknown=False): diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/encoding.py --- a/mercurial/encoding.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/encoding.py Fri Dec 16 19:05:59 2011 -0600 @@ -171,3 +171,22 @@ return lu.encode(encoding) except UnicodeError: return s.lower() # we don't know how to fold this except in ASCII + except LookupError, k: + raise error.Abort(k, hint="please check your locale settings") + +def upper(s): + "best-effort encoding-aware case-folding of local string s" + try: + if isinstance(s, localstr): + u = s._utf8.decode("utf-8") + else: + u = s.decode(encoding, encodingmode) + + uu = u.upper() + if u == uu: + return s # preserve localstring + return uu.encode(encoding) + except UnicodeError: + return s.upper() # we don't know how to fold this except in ASCII + except LookupError, k: + raise error.Abort(k, hint="please check your locale settings") diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/merge.py --- a/mercurial/merge.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/merge.py Fri Dec 16 19:05:59 2011 -0600 @@ -96,7 +96,7 @@ raise util.Abort(_("untracked file in working directory differs" " from file in requested revision: '%s'") % fn) -def _checkcollision(mctx): +def _checkcollision(mctx, wctx): "check for case folding collisions in the destination context" folded = {} for fn in mctx: @@ -106,6 +106,14 @@ % (fn, folded[fold])) folded[fold] = fn + if wctx: + for fn in wctx: + fold = util.normcase(fn) + mfn = folded.get(fold, None) + if mfn and (mfn != fn): + raise util.Abort(_("case-folding collision between %s and %s") + % (mfn, fn)) + def _forgetremoved(wctx, mctx, branchmerge): """ Forget removed files @@ -551,7 +559,7 @@ if not force: _checkunknown(wc, p2, folding) if folding: - _checkcollision(p2) + _checkcollision(p2, branchmerge and p1) action += _forgetremoved(wc, p2, branchmerge) action += manifestmerge(repo, wc, p2, pa, overwrite, partial) diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/posix.py --- a/mercurial/posix.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/posix.py Fri Dec 16 19:05:59 2011 -0600 @@ -164,6 +164,9 @@ st2 = os.lstat(fpath2) return st1.st_dev == st2.st_dev +encodinglower = None +encodingupper = None + # os.path.normcase is a no-op, which doesn't help us on non-native filesystems def normcase(path): return path.lower() diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/scmutil.py --- a/mercurial/scmutil.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/scmutil.py Fri Dec 16 19:05:59 2011 -0600 @@ -76,18 +76,22 @@ self.auditeddir = set() self.root = root self.callback = callback + if os.path.lexists(root) and not util.checkcase(root): + self.normcase = util.normcase + else: + self.normcase = lambda x: x def __call__(self, path): '''Check the relative path. path may contain a pattern (e.g. foodir/**.txt)''' - if path in self.audited: + normpath = self.normcase(path) + if normpath in self.audited: return # AIX ignores "/" at end of path, others raise EISDIR. if util.endswithsep(path): raise util.Abort(_("path ends in directory separator: %s") % path) - normpath = os.path.normcase(path) - parts = util.splitpath(normpath) + parts = util.splitpath(path) if (os.path.splitdrive(path)[0] or parts[0].lower() in ('.hg', '.hg.', '') or os.pardir in parts): @@ -101,11 +105,16 @@ raise util.Abort(_("path '%s' is inside nested repo %r") % (path, base)) + normparts = util.splitpath(normpath) + assert len(parts) == len(normparts) + parts.pop() + normparts.pop() prefixes = [] while parts: prefix = os.sep.join(parts) - if prefix in self.auditeddir: + normprefix = os.sep.join(normparts) + if normprefix in self.auditeddir: break curpath = os.path.join(self.root, prefix) try: @@ -125,10 +134,11 @@ if not self.callback or not self.callback(curpath): raise util.Abort(_("path '%s' is inside nested repo %r") % (path, prefix)) - prefixes.append(prefix) + prefixes.append(normprefix) parts.pop() + normparts.pop() - self.audited.add(path) + self.audited.add(normpath) # only add prefixes to the cache after checking everything: we don't # want to add "foo/bar/baz" before checking if there's a "foo/.hg" self.auditeddir.update(prefixes) diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/util.py --- a/mercurial/util.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/util.py Fri Dec 16 19:05:59 2011 -0600 @@ -24,6 +24,9 @@ else: import posix as platform +platform.encodinglower = encoding.lower +platform.encodingupper = encoding.upper + cachestat = platform.cachestat checkexec = platform.checkexec checklink = platform.checklink @@ -593,9 +596,12 @@ """ s1 = os.stat(path) d, b = os.path.split(path) - p2 = os.path.join(d, b.upper()) - if path == p2: - p2 = os.path.join(d, b.lower()) + b2 = b.upper() + if b == b2: + b2 = b.lower() + if b == b2: + return True # no evidence against case sensitivity + p2 = os.path.join(d, b2) try: s2 = os.stat(p2) if s2 == s1: @@ -611,9 +617,11 @@ The name is either relative to root, or it is an absolute path starting with root. Note that this function is unnecessary, and should not be called, for case-sensitive filesystems (simply because it's expensive). + + Both name and root should be normcase-ed. ''' # If name is absolute, make it relative - if name.lower().startswith(root.lower()): + if name.startswith(root): l = len(root) if name[l] == os.sep or name[l] == os.altsep: l = l + 1 @@ -628,7 +636,7 @@ # Protect backslashes. This gets silly very quickly. seps.replace('\\','\\\\') pattern = re.compile(r'([^%s]+)|([%s]+)' % (seps, seps)) - dir = os.path.normcase(os.path.normpath(root)) + dir = os.path.normpath(root) result = [] for part, sep in pattern.findall(name): if sep: @@ -639,16 +647,15 @@ _fspathcache[dir] = os.listdir(dir) contents = _fspathcache[dir] - lpart = part.lower() lenp = len(part) for n in contents: - if lenp == len(n) and n.lower() == lpart: + if lenp == len(n) and normcase(n) == part: result.append(n) break else: # Cannot happen, as the file exists! result.append(part) - dir = os.path.join(dir, lpart) + dir = os.path.join(dir, part) return ''.join(result) diff -r c7b0bedbb07a -r 7b7f03502b5a mercurial/windows.py --- a/mercurial/windows.py Thu Dec 15 16:50:21 2011 -0600 +++ b/mercurial/windows.py Fri Dec 16 19:05:59 2011 -0600 @@ -131,7 +131,11 @@ def normpath(path): return pconvert(os.path.normpath(path)) -normcase = os.path.normcase +encodinglower = None +encodingupper = None + +def normcase(path): + return encodingupper(path) def realpath(path): ''' diff -r c7b0bedbb07a -r 7b7f03502b5a tests/test-casecollision-merge.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-casecollision-merge.t Fri Dec 16 19:05:59 2011 -0600 @@ -0,0 +1,109 @@ +run only on case-insensitive filesystems + + $ "$TESTDIR/hghave" icasefs || exit 80 + +################################ +test for branch merging +################################ + + $ hg init repo1 + $ cd repo1 + +create base revision + + $ echo base > base.txt + $ hg add base.txt + $ hg commit -m 'base' + +add same file in different case on both heads + + $ echo a > a.txt + $ hg add a.txt + $ hg commit -m 'add a.txt' + + $ hg update 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + + $ echo A > A.TXT + $ hg add A.TXT + $ hg commit -m 'add A.TXT' + created new head + +merge another, and fail with case-folding collision + + $ hg merge + abort: case-folding collision between a.txt and A.TXT + [255] + +check clean-ness of working directory + + $ hg status + $ hg parents --template '{rev}\n' + 2 + $ cd .. + +################################ +test for linear updates +################################ + + $ hg init repo2 + $ cd repo2 + +create base revision (rev:0) + + $ hg import --bypass --exact - < # HG changeset patch + > # User null + > # Date 1 0 + > # Node ID e1bdf414b0ea9c831fd3a14e94a0a18e1410f98b + > # Parent 0000000000000000000000000000000000000000 + > add a + > + > diff --git a/a b/a + > new file mode 100644 + > --- /dev/null + > +++ b/a + > @@ -0,0 +1,3 @@ + > +this is line 1 + > +this is line 2 + > +this is line 3 + > EOF + applying patch from stdin + +create rename revision (rev:1) + + $ hg import --bypass --exact - < # HG changeset patch + > # User null + > # Date 1 0 + > # Node ID 9dca9f19bb91851bc693544b598b0740629edfad + > # Parent e1bdf414b0ea9c831fd3a14e94a0a18e1410f98b + > rename a to A + > + > diff --git a/a b/A + > rename from a + > rename to A + > EOF + applying patch from stdin + +update to base revision, and modify 'a' + + $ hg update 0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 'this is added line' >> a + +update to current tip linearly + + $ hg update 1 + merging a and A to A + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + +check status and contents of file + + $ hg status -A + M A + $ cat A + this is line 1 + this is line 2 + this is line 3 + this is added line diff -r c7b0bedbb07a -r 7b7f03502b5a tests/test-issue3084.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-issue3084.t Fri Dec 16 19:05:59 2011 -0600 @@ -0,0 +1,108 @@ + + $ echo "[extensions]" >> $HGRCPATH + $ echo "largefiles =" >> $HGRCPATH + +Create the repository outside $HOME since largefiles write to +$HOME/.cache/largefiles. + + $ hg init test + $ cd test + $ echo "root" > root + $ hg add root + $ hg commit -m "Root commit" + + $ echo "large" > foo + $ hg add --large foo + $ hg commit -m "Add foo as a largefile" + + $ hg update -r 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + getting changed largefiles + 0 largefiles updated, 1 removed + + $ echo "normal" > foo + $ hg add foo + $ hg commit -m "Add foo as normal file" + created new head + +Normal file in the working copy, keeping the normal version: + + $ echo "n" | hg merge --config ui.interactive=Yes + foo has been turned into a largefile + use (l)argefile or keep as (n)ormal file? 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + (branch merge, don't forget to commit) + + $ hg status + $ cat foo + normal + +Normal file in the working copy, keeping the largefile version: + + $ hg update -q -C + $ echo "l" | hg merge --config ui.interactive=Yes + foo has been turned into a largefile + use (l)argefile or keep as (n)ormal file? 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + (branch merge, don't forget to commit) + getting changed largefiles + 1 largefiles updated, 0 removed + + $ hg status + M foo + + $ hg diff --nodates + diff -r fa129ab6b5a7 .hglf/foo + --- /dev/null + +++ b/.hglf/foo + @@ -0,0 +1,1 @@ + +7f7097b041ccf68cc5561e9600da4655d21c6d18 + diff -r fa129ab6b5a7 foo + --- a/foo + +++ /dev/null + @@ -1,1 +0,0 @@ + -normal + + $ cat foo + large + +Largefile in the working copy, keeping the normal version: + + $ hg update -q -C -r 1 + $ echo "n" | hg merge --config ui.interactive=Yes + foo has been turned into a normal file + keep as (l)argefile or use (n)ormal file? 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + (branch merge, don't forget to commit) + getting changed largefiles + 0 largefiles updated, 0 removed + + $ hg status + M foo + + $ hg diff --nodates + diff -r ff521236428a .hglf/foo + --- a/.hglf/foo + +++ /dev/null + @@ -1,1 +0,0 @@ + -7f7097b041ccf68cc5561e9600da4655d21c6d18 + diff -r ff521236428a foo + --- /dev/null + +++ b/foo + @@ -0,0 +1,1 @@ + +normal + + $ cat foo + normal + +Largefile in the working copy, keeping the largefile version: + + $ hg update -q -C -r 1 + $ echo "l" | hg merge --config ui.interactive=Yes + foo has been turned into a normal file + keep as (l)argefile or use (n)ormal file? 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + (branch merge, don't forget to commit) + getting changed largefiles + 1 largefiles updated, 0 removed + + $ hg status + + $ cat foo + large