dirstate: make writing in-memory changes aware of transaction activity
This patch delays writing in-memory changes out, if transaction is
running.
'_getfsnow()' is defined as a function, to hook it easily for
ambiguous timestamp tests (see also fakedirstatewritetime.py)
'if tr:' code path in this patch is still disabled at this revision,
because there is no client invoking 'dirstate.write()' with repo
object.
BTW, this patch changes 'dirstate.invalidate()' semantics around
'dirstate.write()' in a transaction scope:
before:
with repo.transaction():
dirstate.CHANGE('A')
dirstate.write() # change for A is written out here
dirstate.CHANGE('B')
dirstate.invalidate() # discards only change for B
after:
with repo.transaction():
dirstate.CHANGE('A')
dirstate.write() # change for A is still kept in memory
dirstate.CHANGE('B')
dirstate.invalidate() # discards changes for A and B
Fortunately, there is no code path expecting the former, at least, in
Mercurial itself, because 'dirstateguard' was introduced to remove
such 'dirstate.invalidate()'.
--- a/mercurial/dirstate.py Wed Oct 14 02:49:17 2015 +0900
+++ b/mercurial/dirstate.py Wed Oct 14 02:49:17 2015 +0900
@@ -27,6 +27,15 @@
def join(self, obj, fname):
return obj._join(fname)
+def _getfsnow(vfs):
+ '''Get "now" timestamp on filesystem'''
+ tmpfd, tmpname = vfs.mkstemp()
+ try:
+ return util.statmtimesec(os.fstat(tmpfd))
+ finally:
+ os.close(tmpfd)
+ vfs.unlink(tmpname)
+
class dirstate(object):
def __init__(self, opener, ui, root, validate):
@@ -611,7 +620,7 @@
self._pl = (parent, nullid)
self._dirty = True
- def write(self):
+ def write(self, repo=None):
if not self._dirty:
return
@@ -622,7 +631,40 @@
import time # to avoid useless import
time.sleep(delaywrite)
- st = self._opener(self._filename, "w", atomictemp=True)
+ filename = self._filename
+ if not repo:
+ tr = None
+ if self._opener.lexists(self._pendingfilename):
+ # if pending file already exists, in-memory changes
+ # should be written into it, because it has priority
+ # to '.hg/dirstate' at reading under HG_PENDING mode
+ filename = self._pendingfilename
+ else:
+ tr = repo.currenttransaction()
+
+ if tr:
+ # 'dirstate.write()' is not only for writing in-memory
+ # changes out, but also for dropping ambiguous timestamp.
+ # delayed writing re-raise "ambiguous timestamp issue".
+ # See also the wiki page below for detail:
+ # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
+
+ # emulate dropping timestamp in 'parsers.pack_dirstate'
+ now = _getfsnow(repo.vfs)
+ dmap = self._map
+ for f, e in dmap.iteritems():
+ if e[0] == 'n' and e[3] == now:
+ dmap[f] = dirstatetuple(e[0], e[1], e[2], -1)
+
+ # emulate that all 'dirstate.normal' results are written out
+ self._lastnormaltime = 0
+
+ # delay writing in-memory changes out
+ tr.addfilegenerator('dirstate', (self._filename,),
+ self._writedirstate, location='plain')
+ return
+
+ st = self._opener(filename, "w", atomictemp=True)
self._writedirstate(st)
def _writedirstate(self, st):
--- a/mercurial/localrepo.py Wed Oct 14 02:49:17 2015 +0900
+++ b/mercurial/localrepo.py Wed Oct 14 02:49:17 2015 +0900
@@ -1000,6 +1000,11 @@
def releasefn(tr, success):
repo = reporef()
if success:
+ # this should be explicitly invoked here, because
+ # in-memory changes aren't written out at closing
+ # transaction, if tr.addfilegenerator (via
+ # dirstate.write or so) isn't invoked while
+ # transaction running
repo.dirstate.write()
else:
# prevent in-memory changes from being written out at
--- a/tests/fakedirstatewritetime.py Wed Oct 14 02:49:17 2015 +0900
+++ b/tests/fakedirstatewritetime.py Wed Oct 14 02:49:17 2015 +0900
@@ -5,7 +5,7 @@
# - 'workingctx._checklookup()' (= 'repo.status()')
# - 'committablectx.markcommitted()'
-from mercurial import context, extensions, parsers, util
+from mercurial import context, dirstate, extensions, parsers, util
def pack_dirstate(fakenow, orig, dmap, copymap, pl, now):
# execute what original parsers.pack_dirstate should do actually
@@ -34,13 +34,16 @@
fakenow = util.parsedate(fakenow, ['%Y%m%d%H%M'])[0]
orig_pack_dirstate = parsers.pack_dirstate
+ orig_dirstate_getfsnow = dirstate._getfsnow
wrapper = lambda *args: pack_dirstate(fakenow, orig_pack_dirstate, *args)
parsers.pack_dirstate = wrapper
+ dirstate._getfsnow = lambda *args: fakenow
try:
return func()
finally:
parsers.pack_dirstate = orig_pack_dirstate
+ dirstate._getfsnow = orig_dirstate_getfsnow
def _checklookup(orig, workingctx, files):
ui = workingctx.repo().ui