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

login
register
mail settings
Submitter Matt Harbison
Date July 3, 2016, 10 p.m.
Message ID <e6da62c6d4b29f29a1ca.1467583212@Envy>
Download mbox | patch
Permalink /patch/15729/
State RFC, archived
Headers show

Comments

Matt Harbison - July 3, 2016, 10 p.m.
# HG changeset patch
# User Matt Harbison <matt_harbison@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.
timeless - July 7, 2016, 1:46 a.m.
so, we could do:

hg chmod -flag +executable paths

It could also be done using a command other than `chmod` once you
switch to using `-flag` (or similar) as the flag.

In this model, you could also support octal as the parser would come
from `-flag` instead of having to explain to the command parser that
you have arbitrary arguments that are either files or attributes.

On Sun, Jul 3, 2016 at 6:00 PM, Matt Harbison <mharbison72@gmail.com> wrote:
> # HG changeset patch
> # User Matt Harbison <matt_harbison@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
>
> +@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
> +
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Matt Harbison - July 7, 2016, 2:35 a.m.
On Wed, 06 Jul 2016 21:46:48 -0400, timeless <timeless@gmail.com> wrote:

> so, we could do:
>
> hg chmod -flag +executable paths
>
> It could also be done using a command other than `chmod` once you
> switch to using `-flag` (or similar) as the flag.
>
> In this model, you could also support octal as the parser would come
> from `-flag` instead of having to explain to the command parser that
> you have arbitrary arguments that are either files or attributes.

Interesting idea.  I wish it were less verbose, but I don't have a better  
idea.

The octal idea is interesting too, but since Mercurial really only tracks  
a single x-bit and then turns them on for each r-bit (see  
posix.setflags()), there will be a bunch of combinations that make little  
or no sense.  Should they be rejected?  Just the u+x bit tested?

> On Sun, Jul 3, 2016 at 6:00 PM, Matt Harbison <mharbison72@gmail.com>  
> wrote:
>> # HG changeset patch
>> # User Matt Harbison <matt_harbison@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
>>
>> +@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
>> +
>> _______________________________________________
>> Mercurial-devel mailing list
>> Mercurial-devel@mercurial-scm.org
>> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
timeless - July 7, 2016, 3:30 a.m.
Matt Harbison wrote:
> Interesting idea.  I wish it were less verbose, but I don't have a better
> idea.

the only thing that could make it less verbose is anchoring it to
another existing command -- I won't have resources to check this week.

> The octal idea is interesting too, but since Mercurial really only tracks a
> single x-bit and then turns them on for each r-bit (see posix.setflags()),
> there will be a bunch of combinations that make little or no sense.  Should
> they be rejected?  Just the u+x bit tested?

I'm not saying we should support it today, just noting that this
proposal could support it eventually.
Matt Mackall - July 8, 2016, 8:17 p.m.
On Wed, 2016-07-06 at 22:35 -0400, Matt Harbison wrote:
> On Wed, 06 Jul 2016 21:46:48 -0400, timeless <timeless@gmail.com> wrote:
> 
> > 
> > so, we could do:
> > 
> > hg chmod -flag +executable paths
> > 
> > It could also be done using a command other than `chmod` once you
> > switch to using `-flag` (or similar) as the flag.
> > 
> > In this model, you could also support octal as the parser would come
> > from `-flag` instead of having to explain to the command parser that
> > you have arbitrary arguments that are either files or attributes.
> Interesting idea.  I wish it were less verbose, but I don't have a better  
> idea.
> 
> The octal idea is interesting too, but since Mercurial really only tracks  
> a single x-bit and then turns them on for each r-bit (see  
> posix.setflags()),

Yeah, we should only allow Mercurial's own high-level 'x' and 'l' flags.

-- 
Mathematics is the supreme nostalgia of our time.

Patch

diff --git a/mercurial/commands.py b/mercurial/commands.py
--- a/mercurial/commands.py
+++ b/mercurial/commands.py
@@ -3970,6 +3970,50 @@ 
 
     return ret
 
+@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
+