diff --git a/mercurial/posix.py b/mercurial/posix.py --- a/mercurial/posix.py +++ b/mercurial/posix.py @@ -153,7 +153,7 @@ def setflags(f, l, x): # 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 @@ def copymode(src, dst, mode=None): 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 --git a/mercurial/util.py b/mercurial/util.py --- a/mercurial/util.py +++ b/mercurial/util.py @@ -2045,7 +2045,7 @@ def splitpath(path): 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 @@ def mktempcopy(name, emptyok=False, crea # 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 @@ class atomictempfile(object): 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 --git a/mercurial/windows.py b/mercurial/windows.py --- a/mercurial/windows.py +++ b/mercurial/windows.py @@ -248,7 +248,7 @@ def sshargs(sshcmd, host, user, port): 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 --git a/tests/test-update-atomic.t b/tests/test-update-atomic.t new file mode 100644 --- /dev/null +++ b/tests/test-update-atomic.t @@ -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