strip: make --keep option not set all dirstate times to 0

hg strip -k was using dirstate.rebuild() which reset all the dirstate
entries timestamps to 0.  This meant that the next time hg status was
run every file was considered to be 'unsure', which caused it to do
expensive read operations on every filelog. On a repo with >150,000
files it took 70 seconds when everything was in memory.  From a cold
cache it took several minutes.

The fix is to only reset files that have changed between the working
context and the destination context.

For reference, --keep means the working directory is left alone during
the strip. We have users wanting to use this operation to store their
work-in-progress as a commit on a branch while they go work on another
branch, then come back later and be able to uncommit that work and
continue working.  They currently use 'git reset HARD^' to accomplish
this in git.
This commit is contained in:
Durham Goode 2013-03-06 20:13:09 -08:00
parent 97bdc357fb
commit 83b3faf2ec
3 changed files with 45 additions and 6 deletions

View File

@ -3037,7 +3037,22 @@ def strip(ui, repo, *revs, **opts):
wlock = repo.wlock()
try:
urev = repo.mq.qparents(repo, revs[0])
repo.dirstate.rebuild(urev, repo[urev].manifest())
uctx = repo[urev]
# only reset the dirstate for files that would actually change
# between the working context and uctx
descendantrevs = repo.revs("%s::." % uctx.rev())
changedfiles = []
for rev in descendantrevs:
# blindy reset the files, regardless of what actually changed
changedfiles.extend(repo[rev].files())
# reset files that only changed in the dirstate too
dirstate = repo.dirstate
dirchanges = [f for f in dirstate if dirstate[f] != 'n']
changedfiles.extend(dirchanges)
repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
repo.dirstate.write()
update = False
finally:

View File

@ -498,13 +498,18 @@ class dirstate(object):
self._lastnormaltime = 0
self._dirty = True
def rebuild(self, parent, files):
def rebuild(self, parent, allfiles, changedfiles=None):
changedfiles = changedfiles or allfiles
oldmap = self._map
self.clear()
for f in files:
if 'x' in files.flags(f):
self._map[f] = ('n', 0777, -1, 0)
for f in allfiles:
if f not in changedfiles:
self._map[f] = oldmap[f]
else:
self._map[f] = ('n', 0666, -1, 0)
if 'x' in allfiles.flags(f):
self._map[f] = ('n', 0777, -1, 0)
else:
self._map[f] = ('n', 0666, -1, 0)
self._pl = (parent, nullid)
self._dirty = True

View File

@ -420,6 +420,25 @@ Verify strip protects against stripping wc parent when there are uncommited mods
$ hg status
M bar
? b
Strip adds, removes, modifies with --keep
$ touch b
$ hg add b
$ hg commit -mb
$ touch c
$ hg add c
$ hg rm bar
$ hg commit -mc
$ echo b > b
$ echo d > d
$ hg strip --keep tip
saved backup bundle to $TESTTMP/test/.hg/strip-backup/*-backup.hg (glob)
$ hg status
M b
! bar
? c
? d
$ cd ..
stripping many nodes on a complex graph (issue3299)