changeset 41289:593f6359681d

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.
author Boris Feld <boris.feld@octobus.net>
date Thu, 10 Jan 2019 14:57:01 +0100
parents 17941fc53ae9
children 593718ff5844 a322dbee4eda
files mercurial/posix.py mercurial/util.py mercurial/windows.py tests/test-update-atomic.t
diffstat 4 files changed, 157 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- 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):
     """
--- 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
 
--- 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):
--- /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 <<EOF
+  > 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 <<EOF
+  > 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 <<EOF
+  > [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