--- 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)]
--- 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.
--- 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:
--- 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',
--- 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)
--- 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'''
--- 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
--- 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):
--- 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")
--- 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)
--- 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()
--- 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)
--- 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)
--- 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):
'''
--- /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 - <<EOF
+ > # 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 - <<EOF
+ > # 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
--- /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