[PATCH 2 of 3 RFC] flags: introduce a command for manipulating file flags (issue2020)

Matt Harbison mharbison72 at gmail.com
Sun Jul 3 21:44:04 UTC 2016


# HG changeset patch
# User Matt Harbison <matt_harbison at yahoo.com>
# Date 1467573637 14400
#      Sun Jul 03 15:20:37 2016 -0400
# Node ID e6da62c6d4b29f29a1cad36e72d98d9208186e51
# Parent  e9fce4275ce6b26b941f47044744015e90367e7b
flags: introduce a command for manipulating file flags (issue2020)

The need here is for tweaking the executable bit on Windows, but rather than
making this a platform specific command, Unix is supported with a simple
`chmod`, allowing the existing flag handling to continue unchanged.  I didn't
think a debug command was appropriate, because we probably don't want most users
to know that is a thing.

I suppose we could move the chmod into the flagstate class, but there's also the
issue of needing to setup the transaction here, so I'm not sure that it is worth
it.

In the workingctx class, the flagstate file is consulted first.  Only if there
is not an entry for a file does it fall back to the previous manifest based
code to fabricate the flags.  This is simpler than only keeping an entry if it
is the opposite of the manifest.  The file is discarded for both `hg update -C`
and `hg commit`.  This ignores revert for now, because there are some issues
with that.  This can also probably be used by record, since
test-commit-interactive-record.t is basically broken on Windows without it.

I hate this UI.  I was originally thinking `hg chmod [+|-]x` without thinking
that the args need to start with '-', so it ends up being '-x' sets the bit
whereas `chmod -x` clears it.  Yuck.  And the name isn't very intuitive for a
user.  So bikeshed away, I'm sure someone has a better idea.  As mentioned, I
was also trying to not block a '-l' and '-f' or some such thing to manipulate
symlinks in the future, though I don't currently have an interest in that.

I'll integrate the tests with something existing once this starts winding down.

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -3970,6 +3970,50 @@
 
     return ret
 
+ at command('^flags',
+    [('n', 'normal', None, _('clear the executable bit')),
+    ('x', 'executable', None, _('set the executable bit'))
+    ] + walkopts,
+    _('FILE...'),
+    inferrepo=True)
+def flags(ui, repo, *pats, **opts):
+    """modify the flags on the given files
+
+    Set or clear the executable bit for the named files.  On a filesystem that
+    tracks the executable bit, this is equivalent to ``chmod +x FILE``.
+
+    Returns 0 on success.
+    """
+    isexec = opts.get('executable')
+    isnorm = opts.get('normal')
+
+    if (isexec and isnorm) or (not isexec and not isnorm):
+        raise error.Abort(_("exactly one of -x and -n must be specified"))
+
+    wctx = repo[None]
+    m = scmutil.match(wctx, pats, opts)
+
+    if util.checkexec(repo.root):
+        for f in m.files():
+            repo.wvfs.chmod(f, isexec and 0o755 or 0o644 )
+    else:
+        wlock = lock = tr = None
+        try:
+            wlock = repo.wlock()
+            lock = repo.lock()
+            fs = wctx.flagstate
+
+            tr = repo.transaction('flags')
+            for f in m.files():
+                fs[f] = isexec and 'x' or ''
+
+            fs.write(tr)
+            tr.close()
+        finally:
+            lockmod.release(wlock, lock, tr)
+
+    return 0
+
 @command('^forget', walkopts, _('[OPTION]... FILE...'), inferrepo=True)
 def forget(ui, repo, *pats, **opts):
     """forget the specified files on the next commit
diff --git a/mercurial/context.py b/mercurial/context.py
--- a/mercurial/context.py
+++ b/mercurial/context.py
@@ -1188,6 +1188,7 @@
         # Create a fallback function for getting file flags when the
         # filesystem doesn't support them
 
+        fs = scmutil.flagstate(self._repo)
         copiesget = self._repo.dirstate.copies().get
         parents = self.parents()
         if len(parents) < 2:
@@ -1195,6 +1196,8 @@
             man = parents[0].manifest()
             def func(f):
                 f = copiesget(f, f)
+                if f in fs:
+                    return fs[f]
                 return man.flags(f)
         else:
             # merges are tricky: we try to reconstruct the unstored
@@ -1205,6 +1208,8 @@
 
             def func(f):
                 f = copiesget(f, f) # may be wrong for merges with copies
+                if f in fs:
+                    return fs[f]
                 fl1, fl2, fla = m1.flags(f), m2.flags(f), ma.flags(f)
                 if fl1 == fl2:
                     return fl1
@@ -1408,6 +1413,10 @@
             p = p[:-1]
         return [changectx(self._repo, x) for x in p]
 
+    @propertycache
+    def flagstate(self):
+        return scmutil.flagstate(self._repo)
+
     def filectx(self, path, filelog=None):
         """get a file context from the working directory"""
         return workingfilectx(self._repo, path, workingctx=self,
diff --git a/mercurial/hg.py b/mercurial/hg.py
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -691,6 +691,7 @@
     """forcibly switch the working directory to node, clobbering changes"""
     stats = updaterepo(repo, node, True)
     util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
+    repo[None].flagstate.discard()
     if show_stats:
         _showstats(repo, stats, quietempty)
     return stats[3] > 0
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -1638,6 +1638,7 @@
             # update bookmarks, dirstate and mergestate
             bookmarks.update(self, [p1, p2], ret)
             cctx.markcommitted(ret)
+            wctx.flagstate.discard()
             ms.reset()
             tr.close()
 
diff --git a/tests/test-flags2.t b/tests/test-flags2.t
new file mode 100644
--- /dev/null
+++ b/tests/test-flags2.t
@@ -0,0 +1,66 @@
+  $ hg init foo
+  $ cd foo
+  $ echo noexec > noexec.txt
+  $ echo noexec > noexec2.txt
+  $ hg ci -Aqm "no exec"
+  $ hg files -v
+           7   noexec.txt
+           7   noexec2.txt
+
+
+chmod +x, test and commit
+
+  $ hg flags -x noexec.txt
+  $ hg files -v
+           7 x noexec.txt
+           7   noexec2.txt
+
+  $ hg diff --git
+  diff --git a/noexec.txt b/noexec.txt
+  old mode 100644
+  new mode 100755
+
+  $ hg ci -m "set exec"
+  $ hg files -v
+           7 x noexec.txt
+           7   noexec2.txt
+  $ hg diff --git
+  $ hg diff -r '.^' --git
+  diff --git a/noexec.txt b/noexec.txt
+  old mode 100644
+  new mode 100755
+  $ hg manifest --debug
+  5bb662b3917ab12c167093eb2fa379a1b63142c3 755 * noexec.txt
+  5bb662b3917ab12c167093eb2fa379a1b63142c3 644   noexec2.txt
+
+
+chmod -x, test and commit
+
+#  $ sleep 2
+#  $ touch noexec.txt
+
+  $ hg flags -n noexec.txt
+  $ hg files -v
+           7   noexec.txt
+           7   noexec2.txt
+  $ hg diff --git
+  diff --git a/noexec.txt b/noexec.txt
+  old mode 100755
+  new mode 100644
+  $ hg ci -m "clear exec"
+  $ hg files -v
+           7   noexec.txt
+           7   noexec2.txt
+
+  $ hg flags -x noexec.txt
+  $ hg files -v
+           7 x noexec.txt
+           7   noexec2.txt
+
+Update -C to nuke it
+  $ hg update -C
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg files -v
+           7   noexec.txt
+           7   noexec2.txt
+ 



More information about the Mercurial mailing list