# HG changeset patch # User Boris Feld # Date 1547128621 -3600 # Node ID 593f6359681dd59d5fbea690a203e09da10d7d4a # Parent 17941fc53ae90a2a12fc4f2a34fc7f231a79de33 update: fix edge-case with update.atomic-file and read-only files We used to create the tempfile with the original file mode. That means creating a read-only tempfile when the original file is read-only, which crash if we need to write on the tempfile. The file in the working directory ends up being writable with and without the atomic update config, so the behavior is the same. diff -r 17941fc53ae9 -r 593f6359681d mercurial/posix.py --- a/mercurial/posix.py Wed Jan 16 16:49:15 2019 -0800 +++ b/mercurial/posix.py Thu Jan 10 14:57:01 2019 +0100 @@ -153,7 +153,7 @@ # Turn off all +x bits os.chmod(f, s & 0o666) -def copymode(src, dst, mode=None): +def copymode(src, dst, mode=None, enforcewritable=False): '''Copy the file mode from the file at path src to dst. If src doesn't exist, we're using mode instead. If mode is None, we're using umask.''' @@ -166,7 +166,13 @@ if st_mode is None: st_mode = ~umask st_mode &= 0o666 - os.chmod(dst, st_mode) + + new_mode = st_mode + + if enforcewritable: + new_mode |= stat.S_IWUSR + + os.chmod(dst, new_mode) def checkexec(path): """ diff -r 17941fc53ae9 -r 593f6359681d mercurial/util.py --- a/mercurial/util.py Wed Jan 16 16:49:15 2019 -0800 +++ b/mercurial/util.py Thu Jan 10 14:57:01 2019 +0100 @@ -2045,7 +2045,7 @@ function if need.''' return path.split(pycompat.ossep) -def mktempcopy(name, emptyok=False, createmode=None): +def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False): """Create a temporary file with the same contents from name The permission bits are copied from the original file. @@ -2061,7 +2061,8 @@ # Temporary files are created with mode 0600, which is usually not # what we want. If the original file already exists, just copy # its mode. Otherwise, manually obey umask. - copymode(name, temp, createmode) + copymode(name, temp, createmode, enforcewritable) + if emptyok: return temp try: @@ -2204,7 +2205,9 @@ def __init__(self, name, mode='w+b', createmode=None, checkambig=False): self.__name = name # permanent name self._tempname = mktempcopy(name, emptyok=('w' in mode), - createmode=createmode) + createmode=createmode, + enforcewritable=('w' in mode)) + self._fp = posixfile(self._tempname, mode) self._checkambig = checkambig diff -r 17941fc53ae9 -r 593f6359681d mercurial/windows.py --- a/mercurial/windows.py Wed Jan 16 16:49:15 2019 -0800 +++ b/mercurial/windows.py Thu Jan 10 14:57:01 2019 +0100 @@ -248,7 +248,7 @@ def setflags(f, l, x): pass -def copymode(src, dst, mode=None): +def copymode(src, dst, mode=None, enforcewritable=False): pass def checkexec(path): diff -r 17941fc53ae9 -r 593f6359681d tests/test-update-atomic.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-update-atomic.t Thu Jan 10 14:57:01 2019 +0100 @@ -0,0 +1,142 @@ +#require execbit unix-permissions + +Checking that experimental.atomic-file works. + + $ cat > $TESTTMP/show_mode.py < from __future__ import print_function + > import sys + > import os + > from stat import ST_MODE + > + > for file_path in sys.argv[1:]: + > file_stat = os.stat(file_path) + > octal_mode = oct(file_stat[ST_MODE] & 0o777) + > print("%s:%s" % (file_path, octal_mode)) + > + > EOF + + $ hg init repo + $ cd repo + + $ cat > .hg/showwrites.py < def uisetup(ui): + > from mercurial import vfs + > class newvfs(vfs.vfs): + > def __call__(self, *args, **kwargs): + > print('vfs open', args, sorted(list(kwargs.items()))) + > return super(newvfs, self).__call__(*args, **kwargs) + > vfs.vfs = newvfs + > EOF + + $ for v in a1 a2 b1 b2 c ro; do echo $v > $v; done + $ chmod +x b* + $ hg commit -Aqm _ + +# We check that +# - the changes are actually atomic +# - that permissions are correct (all 4 cases of (executable before) * (executable after)) +# - that renames work, though they should be atomic anyway +# - that it works when source files are read-only (but directories are read-write still) + + $ for v in a1 a2 b1 b2 ro; do echo changed-$v > $v; done + $ chmod -x *1; chmod +x *2 + $ hg rename c d + $ hg commit -qm _ + +Check behavior without update.atomic-file + + $ hg update -r 0 -q + $ hg update -r 1 --config extensions.showwrites=.hg/showwrites.py 2>&1 | grep "a1'.*wb" + ('vfs open', ('a1', 'wb'), [('atomictemp', False), ('backgroundclose', True)]) + + $ python $TESTTMP/show_mode.py * + a1:0644 + a2:0755 + b1:0644 + b2:0755 + d:0644 + ro:0644 + +Add a second revision for the ro file so we can test update when the file is +present or not + + $ echo "ro" > ro + + $ hg commit -qm _ + +Check behavior without update.atomic-file first + + $ hg update -C -r 0 -q + + $ hg update -r 1 + 6 files updated, 0 files merged, 1 files removed, 0 files unresolved + + $ python $TESTTMP/show_mode.py * + a1:0644 + a2:0755 + b1:0644 + b2:0755 + d:0644 + ro:0644 + +Manually reset the mode of the read-only file + + $ chmod a-w ro + + $ python $TESTTMP/show_mode.py ro + ro:0444 + +Now the file is present, try to update and check the permissions of the file + + $ hg up -r 2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ python $TESTTMP/show_mode.py ro + ro:0644 + +# The file which was read-only is now writable in the default behavior + +Check behavior with update.atomic-files + + + $ cat >> .hg/hgrc < [experimental] + > update.atomic-file = true + > EOF + + $ hg update -C -r 0 -q + $ hg update -r 1 --config extensions.showwrites=.hg/showwrites.py 2>&1 | grep "a1'.*wb" + ('vfs open', ('a1', 'wb'), [('atomictemp', True), ('backgroundclose', True)]) + $ hg st -A --rev 1 + C a1 + C a2 + C b1 + C b2 + C d + C ro + +Check the file permission after update + $ python $TESTTMP/show_mode.py * + a1:0644 + a2:0755 + b1:0644 + b2:0755 + d:0644 + ro:0644 + +Manually reset the mode of the read-only file + + $ chmod a-w ro + + $ python $TESTTMP/show_mode.py ro + ro:0444 + +Now the file is present, try to update and check the permissions of the file + + $ hg update -r 2 --traceback + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ python $TESTTMP/show_mode.py ro + ro:0644 + +# The behavior is the same as without atomic update