--- a/README Tue May 16 23:37:10 2017 -0700
+++ b/README Wed May 17 09:52:06 2017 +0200
@@ -112,6 +112,17 @@
Changelog
=========
+6.2.0 - in progress
+-------------------
+
+ - add a debugobshistory command to inspect the obs-history of a changeset
+ - topic: have thg display topic name if possible,
+ - obscache: more efficient update in the (rare) case of a transaction adding
+ markers without changesets
+ - obshashrange-cache: update incrementally in the (common) case of a
+ transaction not affecting existing range,
+ - obshashrange-cache: keep the cache mostly warm after each transaction.
+
6.1.1 - in progress
-------------------
--- a/hgext3rd/evolve/__init__.py Tue May 16 23:37:10 2017 -0700
+++ b/hgext3rd/evolve/__init__.py Wed May 17 09:52:06 2017 +0200
@@ -162,6 +162,15 @@
obsexcmsg = utility.obsexcmsg
+colortable = {'evolve.short_node': 'yellow',
+ 'evolve.user': 'green',
+ 'evolve.rev': 'blue',
+ 'evolve.short_description': '',
+ 'evolve.date': 'cyan',
+ 'evolve.current_rev': 'bold',
+ 'evolve.verb': '',
+ }
+
_pack = struct.pack
_unpack = struct.unpack
@@ -3249,3 +3258,109 @@
f.write(orig.topic())
return merge.graft(repo, orig, pctx, ['local', 'graft'], True)
+
+@eh.command(
+ '^debugobshistory',
+ [] + commands.formatteropts,
+ _('hg debugobshistory [OPTION]... [REV]'))
+def debugobshistory(ui, repo, *revs, **opts):
+ revs = scmutil.revrange(repo, revs)
+ fm = ui.formatter('debugobshistory', opts)
+
+ revs.reverse()
+ _debugobshistorysingle(fm, repo, revs)
+
+ fm.end()
+
+def _debugobshistorysingle(fm, repo, revs):
+ """ Display the obsolescence history for a single revision
+ """
+ precursors = repo.obsstore.precursors
+ successors = repo.obsstore.successors
+ nodec = repo.changelog.node
+ nodes = [nodec(r) for r in revs]
+
+ seen = set(nodes)
+
+ while nodes:
+ ctxnode = nodes.pop()
+
+ _debugobshistorydisplaynode(fm, repo, ctxnode)
+
+ succs = successors.get(ctxnode, ())
+
+ markerfm = fm.nested("debugobshistory.markers")
+ for successor in sorted(succs):
+ _debugobshistorydisplaymarker(markerfm, repo, successor)
+ markerfm.end()
+
+ precs = precursors.get(ctxnode, ())
+ for p in sorted(precs):
+ # Only show nodes once
+ if p[0] not in seen:
+ seen.add(p[0])
+ nodes.append(p[0])
+
+def _debugobshistorydisplaynode(fm, repo, node):
+ if node in repo.unfiltered():
+ _debugobshistorydisplayctx(fm, repo.unfiltered()[node])
+ else:
+ _debugobshistorydisplaymissingctx(fm, node)
+
+def _debugobshistorydisplayctx(fm, ctx):
+ shortdescription = ctx.description().splitlines()[0]
+
+ fm.startitem()
+ fm.write('debugobshistory.node', '%s', str(ctx),
+ label="evolve.short_node")
+ fm.plain(' ')
+
+ fm.write('debugobshistory.rev', '(%d)', int(ctx),
+ label="evolve.rev")
+ fm.plain(' ')
+
+ fm.write('debugobshistory.shortdescription', '%s', shortdescription,
+ label="evolve.short_description")
+ fm.plain('\n')
+
+def _debugobshistorydisplaymissingctx(fm, nodewithoutctx):
+ hexnode = node.short(nodewithoutctx)
+ fm.startitem()
+ fm.write('debugobshistory.node', '%s', hexnode,
+ label="evolve.short_node evolve.missing_change_ctx")
+ fm.plain('\n')
+
+def _debugobshistorydisplaymarker(fm, repo, marker):
+ succnodes = marker[1]
+ date = marker[4]
+ metadata = dict(marker[3])
+
+ fm.startitem()
+ fm.plain(' ')
+
+ # Detect pruned revisions
+ if len(succnodes) == 0:
+ verb = 'pruned'
+ else:
+ verb = 'rewritten'
+
+ fm.write('debugobshistory.verb', '%s', verb,
+ label="evolve.verb")
+ fm.plain(' by ')
+
+ fm.write('debugobshistory.marker_user', '%s', metadata['user'],
+ label="evolve.user")
+ fm.plain(' ')
+
+ fm.write('debugobshistory.marker_date', '(%s)', fm.formatdate(date),
+ label="evolve.date")
+
+ if len(succnodes) > 0:
+ fm.plain(' as ')
+
+ shortsnodes = (node.short(succnode) for succnode in sorted(succnodes))
+ nodes = fm.formatlist(shortsnodes, 'debugobshistory.succnodes', sep=', ')
+ fm.write('debugobshistory.succnodes', '%s', nodes,
+ label="evolve.short_node")
+
+ fm.plain("\n")
--- a/hgext3rd/evolve/obscache.py Tue May 16 23:37:10 2017 -0700
+++ b/hgext3rd/evolve/obscache.py Wed May 17 09:52:06 2017 +0200
@@ -13,6 +13,7 @@
import errno
from mercurial import (
+ error,
localrepo,
obsolete,
phases,
@@ -20,6 +21,8 @@
util,
)
+from mercurial.i18n import _
+
from . import (
exthelper,
)
@@ -76,91 +79,226 @@
except (OSError, IOError) as e:
if e.errno != errno.ENOENT:
raise
- key = hashlib.sha1(keydata).digest()
+ if keydata:
+ key = hashlib.sha1(keydata).digest()
+ else:
+ # reusing an existing "empty" value make it easier to define a
+ # default cachekey for 'no data'.
+ key = node.nullid
return obsstoresize, key
obsstore.__class__ = cachekeyobsstore
return obsstore
-emptykey = (node.nullrev, node.nullid, 0, 0, node.nullid)
-
-def getcachekey(repo):
- """get a cache key covering the changesets and obsmarkers content
-
- IT contains the following data. Combined with 'upgradeneeded' it allows to
- do iterative upgrade for cache depending of theses two data.
+# XXX copied as is from Mercurial 4.2 and added the "offset" parameters
+@util.nogc
+def _readmarkers(data, offset=None):
+ """Read and enumerate markers from raw data"""
+ off = 0
+ diskversion = struct.unpack('>B', data[off:off + 1])[0]
+ if offset is None:
+ off += 1
+ else:
+ assert 1 <= offset
+ off = offset
+ if diskversion not in obsolete.formats:
+ raise error.Abort(_('parsing obsolete marker: unknown version %r')
+ % diskversion)
+ return diskversion, obsolete.formats[diskversion][0](data, off)
- The cache key parts are"
- - tip-rev,
- - tip-node,
- - obsstore-length (nb markers),
- - obsstore-file-size (in bytes),
- - obsstore "cache key"
- """
- assert repo.filtername is None
- cl = repo.changelog
- index, key = repo.obsstore.cachekey()
- tiprev = len(cl) - 1
- return (tiprev,
- cl.node(tiprev),
- len(repo.obsstore),
- index,
- key)
+def markersfrom(obsstore, byteoffset, firstmarker):
+ if not firstmarker:
+ return list(obsstore)
+ elif '_all' in vars(obsstore):
+ # if the data are in memory, just use that
+ return obsstore._all[firstmarker:]
+ else:
+ obsdata = obsstore.svfs.tryread('obsstore')
+ return _readmarkers(obsdata, byteoffset)[1]
-def upgradeneeded(repo, key):
- """return (valid, start-rev, start-obs-idx)
- 'valid': is "False" if older cache value needs invalidation,
+class dualsourcecache(object):
+ """An abstract class for cache that needs both changelog and obsstore
- 'start-rev': first revision not in the cache. None if cache is up to date,
-
- 'start-obs-idx': index of the first obs-markers not in the cache. None is
- up to date.
+ This class handle the tracking of changelog and obsstore update. It provide
+ data to performs incremental update (see the 'updatefrom' function for
+ details). This class can also detect stripping of the changelog or the
+ obsstore and can reset the cache in this cache (see the 'clear' function
+ for details).
"""
- # XXX ideally, this function would return a bounded amount of changeset and
- # obsmarkers and the associated new cache key. Otherwise we are exposed to
- # a race condition between the time the cache is updated and the new cache
- # key is computed. (however, we do not want to compute the full new cache
- # key in all case because we want to skip reading the obsstore content. We
- # could have a smarter implementation here.
+ # default key used for an empty cache
+ #
+ # The cache key covering the changesets and obsmarkers content
#
- # In pratice the cache is only updated after each transaction within a
- # lock. So we should be fine. We could enforce this with a new repository
- # requirement (or fix the race, that is not too hard).
- invalid = (False, 0, 0)
- if key is None:
- return invalid
+ # The cache key parts are:
+ # - tip-rev,
+ # - tip-node,
+ # - obsstore-length (nb markers),
+ # - obsstore-file-size (in bytes),
+ # - obsstore "cache key"
+ emptykey = (node.nullrev, node.nullid, 0, 0, node.nullid)
+
+ def __init__(self):
+ super(dualsourcecache, self).__init__()
+ self._cachekey = None
+
+ def _updatefrom(self, repo, revs, obsmarkers):
+ """override this method to update your cache data incrementally
+
+ revs: list of new revision in the changelog
+ obsmarker: list of new obsmarkers in the obsstore
+ """
+ raise NotImplementedError
+
+ def clear(self, reset=False):
+ """invalidate the cache content
+
+ if 'reset' is passed, we detected a strip and the cache will have to be
+ recomputed.
+ """
+ # /!\ IMPORTANT /!\
+ # You must overide this method to actually
+ self._cachekey = self.emptykey if reset else None
+ s = super(dualsourcecache, self)
+ if util.safehasattr(s, 'clear') and callable(s.clear):
+ s.clear()
+
+ def load(self, repo):
+ """Load data from disk
+
+ Do not forget to restore the "cachekey" attribute while doing so.
+ """
+ raise NotImplementedError
+
+ # Useful public function (no need to override them)
+
+ def uptodate(self, repo):
+ """return True if the cache content is up to date False otherwise
+
+ This method can be used to detect of the cache is lagging behind new
+ data in either changelog or obsstore.
+ """
+ if self._cachekey is None:
+ self.load(repo)
+ status = self._checkkey(repo.changelog, repo.obsstore)
+ return (status is not None
+ and status[0] == self._cachekey[0] # tiprev
+ and status[1] == self._cachekey[3]) # obssize
+
+ def update(self, repo):
+ """update the cache with new repository data
+
+ The update will be incremental when possible"""
+ repo = repo.unfiltered()
+ # If we do not have any data, try loading from disk
+ if self._cachekey is None:
+ self.load(repo)
+
+ assert repo.filtername is None
+ cl = repo.changelog
+
+ upgrade = self._upgradeneeded(repo)
+ if upgrade is None:
+ return
+
+ reset, revs, obsmarkers, obskeypair = upgrade
+ if reset or self._cachekey is None:
+ self.clear(reset=True)
+
+ self._updatefrom(repo, revs, obsmarkers)
- ### Is the cache valid ?
- keytiprev, keytipnode, keyobslength, keyobssize, keyobskey = key
- # check for changelog strip
- cl = repo.changelog
- tiprev = len(cl) - 1
- if (tiprev < keytiprev
- or cl.node(keytiprev) != keytipnode):
- return invalid
- # check for obsstore strip
- obssize, obskey = repo.obsstore.cachekey(index=keyobssize)
- if obskey != keyobskey:
- return invalid
+ # update the key from the new data
+ key = list(self._cachekey)
+ if revs:
+ key[0] = len(cl) - 1
+ key[1] = cl.node(key[0])
+ if obsmarkers:
+ key[2] += len(obsmarkers)
+ key[3], key[4] = obskeypair
+ self._cachekey = tuple(key)
+
+ # from here, there are internal function only
+
+ def _checkkey(self, changelog, obsstore):
+ """internal function"""
+ key = self._cachekey
+ if key is None:
+ return None
+
+ ### Is the cache valid ?
+ keytiprev, keytipnode, keyobslength, keyobssize, keyobskey = key
+ # check for changelog strip
+ tiprev = len(changelog) - 1
+ if (tiprev < keytiprev
+ or changelog.node(keytiprev) != keytipnode):
+ return None
+ # check for obsstore strip
+ obssize, obskey = obsstore.cachekey(index=keyobssize)
+ if obskey != keyobskey:
+ return None
+ return tiprev, obssize, obskey
+
+ def _upgradeneeded(self, repo):
+ """return (valid, start-rev, start-obs-idx)
+
+ 'valid': is "False" if older cache value needs invalidation,
+
+ 'start-rev': first revision not in the cache. None if cache is up to date,
+
+ 'start-obs-idx': index of the first obs-markers not in the cache. None is
+ up to date.
+ """
- ### cache is valid, is there anything to update
+ # We need to ensure we use the same changelog and obsstore through the
+ # processing. Otherwise some invalidation could update the object and their
+ # content after we computed the cache key.
+ cl = repo.changelog
+ obsstore = repo.obsstore
+ key = self._cachekey
+
+ reset = False
- # any new changesets ?
- startrev = None
- if keytiprev < tiprev:
- startrev = keytiprev + 1
+ status = self._checkkey(cl, obsstore)
+ if status is None:
+ reset = True
+ key = self.emptykey
+ obssize, obskey = obsstore.cachekey()
+ tiprev = len(cl) - 1
+ else:
+ tiprev, obssize, obskey = status
+
+ keytiprev, keytipnode, keyobslength, keyobssize, keyobskey = key
+
+ if not reset and keytiprev == tiprev and keyobssize == obssize:
+ return None # nothing to upgrade
- # any new markers
- startidx = None
- if keyobssize < obssize:
- startidx = keyobslength
+ ### cache is valid, is there anything to update
+
+ # any new changesets ?
+ revs = ()
+ if keytiprev < tiprev:
+ revs = list(cl.revs(start=keytiprev + 1, stop=tiprev))
- return True, startrev, startidx
+ # any new markers
+ markers = ()
+ if keyobssize < obssize:
+ # XXX Three are a small race change here. Since the obsstore might have
+ # move forward between the time we computed the cache key and we access
+ # the data. To fix this we need so "up to" argument when fetching the
+ # markers here. Otherwise we might return more markers than covered by
+ # the cache key.
+ #
+ # In pratice the cache is only updated after each transaction within a
+ # lock. So we should be fine. We could enforce this with a new repository
+ # requirement (or fix the race, that is not too hard).
+ markers = markersfrom(obsstore, keyobssize, keyobslength)
-class obscache(object):
+ return reset, revs, markers, (obssize, obskey)
+
+
+class obscache(dualsourcecache):
"""cache the "does a rev" is the precursors of some obsmarkers data
This is not directly holding the "is this revision obsolete" information,
@@ -197,16 +335,12 @@
_filepath = 'cache/evoext-obscache-00'
_headerformat = '>q20sQQ20s'
+ emptykey = (node.nullrev, node.nullid, 0, 0, node.nullid)
+
def __init__(self, repo):
+ super(obscache, self).__init__()
+ self._ondiskkey = None
self._vfs = repo.vfs
- # The cache key parts are"
- # - tip-rev,
- # - tip-node,
- # - obsstore-length (nb markers),
- # - obsstore-file-size (in bytes),
- # - obsstore "cache key"
- self._cachekey = None
- self._ondiskkey = None
self._data = bytearray()
def get(self, rev):
@@ -215,90 +349,56 @@
Make sure the cache has been updated to match the repository content before using it"""
return self._data[rev]
- def clear(self):
+ def clear(self, reset=False):
"""invalidate the cache content"""
- self._cachekey = None
+ super(obscache, self).clear(reset=reset)
self._data = bytearray()
- def uptodate(self, repo):
- if self._cachekey is None:
- self.load(repo)
- valid, startrev, startidx = upgradeneeded(repo, self._cachekey)
- return (valid and startrev is None and startidx is None)
+ def _updatefrom(self, repo, revs, obsmarkers):
+ if revs:
+ self._updaterevs(repo, revs)
+ if obsmarkers:
+ self._updatemarkers(repo, obsmarkers)
+
+ def _updaterevs(self, repo, revs):
+ """update the cache with new revisions
+
+ Newly added changeset might be affected by obsolescence markers
+ we already have locally. So we needs to have some global
+ knowledge about the markers to handle that question.
+
+ Right now this requires parsing all markers in the obsstore. We could
+ imagine using various optimisation (eg: another cache, network
+ exchange, etc).
- def update(self, repo):
- """Iteratively update the cache with new repository data"""
- # If we do not have any data, try loading from disk
- if self._cachekey is None:
- self.load(repo)
+ A possible approach to this is to build a set of all node used as
+ precursors in `obsstore._obscandidate`. If markers are not loaded yet,
+ we could initialize it by doing a quick scan through the obsstore data
+ and filling a (pre-sized) set. Doing so would be much faster than
+ parsing all the obsmarkers since we would access less data, not create
+ any object beside the nodes and not have to decode any complex data.
- valid, startrev, startidx = upgradeneeded(repo, self._cachekey)
- if not valid:
- self.clear()
-
- if startrev is None and startidx is None:
- return
-
- # process the new changesets
+ For now we stick to the simpler approach of paying the
+ performance cost on new changesets.
+ """
+ node = repo.changelog.node
+ succs = repo.obsstore.successors
+ for r in revs:
+ if node(r) in succs:
+ val = 1
+ else:
+ val = 0
+ self._data.append(val)
cl = repo.changelog
- if startrev is not None:
- node = cl.node
- # Note:
- #
- # Newly added changeset might be affected by obsolescence markers
- # we already have locally. So we needs to have soem global
- # knowledge about the markers to handle that question. Right this
- # requires parsing all markers in the obsstore. However, we could
- # imagine using various optimisation (eg: bloom filter, other on
- # disk cache) to remove this full parsing.
- #
- # For now we stick to the simpler approach or paying the
- # performance cost on new changesets.
- succs = repo.obsstore.successors
- for r in cl.revs(startrev):
- if node(r) in succs:
- val = 1
- else:
- val = 0
- self._data.append(val)
assert len(self._data) == len(cl), (len(self._data), len(cl))
- # process the new obsmarkers
- if startidx is not None:
- rev = cl.nodemap.get
- markers = repo.obsstore._all
- # Note:
- #
- # There are no actually needs to load the full obsstore here,
- # since we only read the latest ones. We do it for simplicity in
- # the first implementation. Loading the full obsstore has a
- # performance cost and should go away in this case too. We have
- # two simples options for that:
- #
- # 1) provide and API to start reading markers from a byte offset
- # (we have that data in the cache key)
- #
- # 2) directly update the cache at a lower level, in the code
- # responsible for adding a markers.
- #
- # Option 2 is probably a bit more invasive, but more solid on the long run
-
- for i in xrange(startidx, len(repo.obsstore)):
- r = rev(markers[i][0])
- # If markers affect a newly added nodes, it would have been
- # caught in the previous loop, (so we skip < startrev)
- if r is not None and (startrev is None or r < startrev):
- self._data[r] = 1
-
- assert repo._currentlock(repo._lockref) is not None
- # XXX note that there are a potential race condition here, since the
- # repo "might" have changed side the cache update above. However, this
- # code will only be running in a lock so we ignore the issue for now.
- #
- # To work around this, 'upgradeneeded' should return a bounded amount
- # of changeset and markers to read with their associated cachekey. see
- # 'upgradeneeded' for detail.
- self._cachekey = getcachekey(repo)
+ def _updatemarkers(self, repo, obsmarkers):
+ """update the cache with new markers"""
+ rev = repo.changelog.nodemap.get
+ for m in obsmarkers:
+ r = rev(m[0])
+ if r is not None:
+ self._data[r] = 1
def save(self, repo):
"""save the data to disk"""
@@ -319,7 +419,7 @@
data = repo.vfs.tryread(self._filepath)
if not data:
- self._cachekey = emptykey
+ self._cachekey = self.emptykey
self._data = bytearray()
else:
headersize = struct.calcsize(self._headerformat)
@@ -339,7 +439,7 @@
if notpublic:
obscache = repo.obsstore.obscache
# Since we warm the cache at the end of every transaction, the cache
- # should be up to date. However a non-enabled client might have touced
+ # should be up to date. However a non-enabled client might have touched
# the repository.
#
# Updating the cache without a lock is sloppy, so we fallback to the
@@ -348,7 +448,7 @@
#
# With the current implementation updating the cache will requires to
# load the obsstore anyway. Once loaded, hitting the obsstore directly
- # will be about as fast..
+ # will be about as fast...
if not obscache.uptodate(repo):
if repo.currenttransaction() is None:
repo.ui.log('evoext-obscache',
@@ -357,8 +457,9 @@
repo.ui.debug('obscache is out of date')
return orig(repo)
else:
- # If a transaction is open, it is worthwhile to update and use the
- # cache as it will be written on disk when the transaction close.
+ # If a transaction is open, it is worthwhile to update and use
+ # the cache, the lock prevent race and it will be written on
+ # disk when the transaction close.
obscache.update(repo)
isobs = obscache.get
for r in notpublic:
@@ -392,11 +493,10 @@
if repo is None:
return
repo = repo.unfiltered()
- # As pointed in 'obscache.update', we could have the
- # changelog and the obsstore in charge of updating the
- # cache when new items goes it. The tranaction logic would
- # then only be involved for the 'pending' and final saving
- # logic.
+ # As pointed in 'obscache.update', we could have the changelog
+ # and the obsstore in charge of updating the cache when new
+ # items goes it. The tranaction logic would then only be
+ # involved for the 'pending' and final writing on disk.
self.obsstore.obscache.update(repo)
self.obsstore.obscache.save(repo)
--- a/hgext3rd/evolve/obsdiscovery.py Tue May 16 23:37:10 2017 -0700
+++ b/hgext3rd/evolve/obsdiscovery.py Wed May 17 09:52:06 2017 +0200
@@ -46,6 +46,7 @@
from . import (
exthelper,
+ obscache,
utility,
stablerange,
)
@@ -241,6 +242,7 @@
entry = (h, 0)
addentry(entry)
+ local.obsstore.rangeobshashcache.update(local)
querycount = 0
ui.progress(_("comparing obsmarker with other"), querycount)
overflow = []
@@ -342,6 +344,7 @@
linetemplate = '%12d %12s %12d %12d %12d %12s\n'
headertemplate = linetemplate.replace('d', 's')
ui.status(headertemplate % headers)
+ repo.obsstore.rangeobshashcache.update(repo)
for r in ranges:
d = (r[0],
s(cl.node(r[0])),
@@ -392,10 +395,11 @@
_sqliteschema = [
"""CREATE TABLE meta(schemaversion INTEGER NOT NULL,
+ tiprev INTEGER NOT NULL,
+ tipnode BLOB NOT NULL,
nbobsmarker INTEGER NOT NULL,
- obstipdata BLOB NOT NULL,
- tiprev INTEGER NOT NULL,
- tipnode BLOB NOT NULL
+ obssize BLOB NOT NULL,
+ obskey BLOB NOT NULL
);""",
"""CREATE TABLE obshashrange(rev INTEGER NOT NULL,
idx INTEGER NOT NULL,
@@ -404,31 +408,46 @@
"CREATE INDEX range_index ON obshashrange(rev, idx);",
]
_queryexist = "SELECT name FROM sqlite_master WHERE type='table' AND name='meta';"
-_newmeta = """INSERT INTO meta (schemaversion, nbobsmarker, obstipdata, tiprev, tipnode)
- VALUES (?,?,?,?,?);"""
+_newmeta = """INSERT INTO meta (schemaversion, tiprev, tipnode, nbobsmarker, obssize, obskey)
+ VALUES (?,?,?,?,?, ?);"""
_updateobshash = "INSERT INTO obshashrange(rev, idx, obshash) VALUES (?,?,?);"
-_querymeta = "SELECT schemaversion, nbobsmarker, obstipdata, tiprev, tipnode FROM meta;"
+_querymeta = "SELECT schemaversion, tiprev, tipnode, nbobsmarker, obssize, obskey FROM meta;"
_queryobshash = "SELECT obshash FROM obshashrange WHERE (rev = ? AND idx = ?);"
-class _obshashcache(dict):
+_reset = "DELETE FROM obshashrange;"
- _schemaversion = 0
+class _obshashcache(obscache.dualsourcecache, dict):
+
+ _schemaversion = 1
def __init__(self, repo):
super(_obshashcache, self).__init__()
- self._path = repo.vfs.join('cache/evoext_obshashrange_v0.sqlite')
+ self._path = repo.vfs.join('cache/evoext_obshashrange_v1.sqlite')
self._new = set()
self._valid = True
self._repo = weakref.ref(repo.unfiltered())
# cache status
self._ondiskcachekey = None
- def clear(self):
+ def clear(self, reset=False):
self._valid = False
- super(_obshashcache, self).clear()
+ if reset:
+ con = self._con
+ if con is not None:
+ con.execute(_reset)
+ super(_obshashcache, self).clear(reset=reset)
self._new.clear()
+ if not reset and '_con' in vars(self):
+ del self._con
def get(self, rangeid):
+ # revision should be covered by out tiprev
+ # XXX should be a programming error
+ #
+ # XXX there are issue with cache warming, we hack around it for now
+ if not getattr(self, '_updating', False):
+ assert rangeid[0] <= self._cachekey[0]
+
value = super(_obshashcache, self).get(rangeid)
if value is None and self._con is not None:
nrange = (rangeid[0], rangeid[1])
@@ -441,16 +460,57 @@
self._new.add(rangeid)
super(_obshashcache, self).__setitem__(rangeid, obshash)
- def _cachekey(self, repo):
- # XXX for now the cache is very volatile, but this is still a win
- nbobsmarker = len(repo.obsstore._all)
- if nbobsmarker:
- tipdata = obsolete._fm1encodeonemarker(repo.obsstore._all[-1])
- else:
- tipdata = node.nullid
- tiprev = len(repo.changelog) - 1
- tipnode = repo.changelog.node(tiprev)
- return (self._schemaversion, nbobsmarker, tipdata, tiprev, tipnode)
+ def _updatefrom(self, repo, revs, obsmarkers):
+ """override this method to update your cache data incrementally
+
+ revs: list of new revision in the changelog
+ obsmarker: list of new obsmarkers in the obsstore
+ """
+ # XXX for now, we'll not actually update the cache, but we'll be
+ # smarter at invalidating it.
+ #
+ # 1) new revisions does not get their entry updated (not update)
+ # 2) if we detect markers affecting non-new revision we reset the cache
+
+ self._updating = True
+
+ setrevs = set(revs)
+ rev = repo.changelog.nodemap.get
+ # if we have a new markers affecting a node already covered by the
+ # cache, we must abort.
+ for m in obsmarkers:
+ # check successors and parent
+ for l in (m[1], m[5]):
+ if l is None:
+ continue
+ for p in l:
+ r = rev(p)
+ if r is not None and r not in setrevs:
+ self.clear(reset=True)
+ break
+ else:
+ continue
+ break
+
+ # XXX the current reset is too strong we could just drop the affected range
+
+ # XXX if we reset, we should warm the cache for existing heads (draft and public)
+
+ # warm the cache for the new revs
+ for r in revs:
+ _obshashrange(repo, (r, 0))
+
+ del self._updating
+
+ @property
+ def _fullcachekey(self):
+ return (self._schemaversion, ) + self._cachekey
+
+ def load(self, repo):
+ if self._con is None:
+ self._cachekey = self.emptykey
+ self._ondiskcachekey = self.emptykey
+ assert self._cachekey is not None
@util.propertycache
def _con(self):
@@ -459,7 +519,6 @@
repo = self._repo()
if repo is None:
return None
- cachekey = self._cachekey(repo)
con = sqlite3.connect(self._path)
con.text_factory = str
cur = con.execute(_queryexist)
@@ -467,17 +526,19 @@
self._valid = False
return None
meta = con.execute(_querymeta).fetchone()
- if meta != cachekey:
+ if meta is None or meta[0] != self._schemaversion:
self._valid = False
return None
- self._ondiskcachekey = meta
+ self._cachekey = self._ondiskcachekey = meta[1:]
return con
def save(self, repo):
+ if self._cachekey is None:
+ return
+ if self._cachekey == self._ondiskcachekey and not self._new:
+ return
repo = repo.unfiltered()
try:
- if not self._new:
- return
with repo.lock():
self._save(repo)
except error.LockError:
@@ -499,26 +560,27 @@
for req in _sqliteschema:
con.execute(req)
- con.execute(_newmeta, self._cachekey(repo))
+ con.execute(_newmeta, self._fullcachekey)
else:
con = self._con
if self._ondiskcachekey is not None:
meta = con.execute(_querymeta).fetchone()
- if meta != self._ondiskcachekey:
+ if meta[1:] != self._ondiskcachekey:
# drifting is currently an issue because this means another
# process might have already added the cache line we are about
# to add. This will confuse sqlite
msg = _('obshashrange cache: skipping write, '
'database drifted under my feet\n')
- data = (meta[2], meta[1], self._ondisktiprev, self._ondisktipnode)
+ data = (meta[2], meta[1], self._ondiskcachekey[0], self._ondiskcachekey[1])
repo.ui.warn(msg)
+ return
data = ((rangeid[0], rangeid[1], self[rangeid]) for rangeid in self._new)
con.executemany(_updateobshash, data)
- cachekey = self._cachekey(repo)
+ cachekey = self._fullcachekey
con.execute(_newmeta, cachekey)
con.commit()
self._new.clear()
- self._ondiskcachekey = cachekey
+ self._ondiskcachekey = self._cachekey
@eh.wrapfunction(obsolete.obsstore, '_addmarkers')
def _addmarkers(orig, obsstore, *args, **kwargs):
@@ -549,10 +611,31 @@
class obshashrepo(repo.__class__):
@localrepo.unfilteredmethod
def destroyed(self):
- if 'stablerange' in vars(self):
- del self.stablerange
+ if 'obsstore' in vars(self):
+ self.obsstore.rangeobshashcache.clear()
super(obshashrepo, self).destroyed()
+ def transaction(self, *args, **kwargs):
+ tr = super(obshashrepo, self).transaction(*args, **kwargs)
+ reporef = weakref.ref(self)
+
+ def _warmcache(tr):
+ repo = reporef()
+ if repo is None:
+ return
+ if not repo.ui.configbool('experimental', 'obshashrange', False):
+ return
+ repo = repo.unfiltered()
+ # As pointed in 'obscache.update', we could have the changelog
+ # and the obsstore in charge of updating the cache when new
+ # items goes it. The tranaction logic would then only be
+ # involved for the 'pending' and final writing on disk.
+ self.obsstore.rangeobshashcache.update(repo)
+ self.obsstore.rangeobshashcache.save(repo)
+
+ tr.addpostclose('warmcache-20-obscacherange', _warmcache)
+ return tr
+
repo.__class__ = obshashrepo
### wire protocol commands
@@ -570,6 +653,7 @@
if maxrev is not None:
repo.stablerange.warmup(repo, upto=maxrev)
result = []
+ repo.obsstore.rangeobshashcache.update(repo)
for r in ranges:
if r[0] is None:
result.append(node.wdirid)
--- a/hgext3rd/evolve/stablerange.py Tue May 16 23:37:10 2017 -0700
+++ b/hgext3rd/evolve/stablerange.py Wed May 17 09:52:06 2017 +0200
@@ -902,7 +902,7 @@
# new nodes !
repo.stablerange.warmup(repo)
- tr.addpostclose('warmcache-stablerange', _warmcache)
+ tr.addpostclose('warmcache-10-stablerange', _warmcache)
return tr
repo.__class__ = stablerangerepo
--- a/hgext3rd/topic/__init__.py Tue May 16 23:37:10 2017 -0700
+++ b/hgext3rd/topic/__init__.py Wed May 17 09:52:06 2017 +0200
@@ -100,6 +100,10 @@
'topic.stack.summary.behindcount': 'cyan',
'topic.stack.summary.behinderror': 'red',
'topic.stack.summary.headcount.multiple': 'yellow',
+ # default color to help log output and thg
+ # (first pick I could think off, update as needed
+ 'log.topic': 'green_background',
+ 'topic.active': 'green',
}
testedwith = '3.9'
@@ -157,6 +161,10 @@
if not isinstance(repo, localrepo.localrepository):
return # this can be a peer in the ssh case (puzzling)
+ if repo.ui.config('experimental', 'thg.displaynames', None) is None:
+ repo.ui.setconfig('experimental', 'thg.displaynames', 'topics',
+ source='topic-extension')
+
class topicrepo(repo.__class__):
def _restrictcapabilities(self, caps):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-check-commit.t Wed May 17 09:52:06 2017 +0200
@@ -0,0 +1,22 @@
+#require test-repo
+
+Enable obsolescence to avoid the warning issue when obsmarker are found
+
+ $ cat << EOF >> $HGRCPATH
+ > [experimental]
+ > evolution=all
+ > EOF
+
+Go back in the hg repo
+
+ $ cd $TESTDIR/..
+
+ $ for node in `hg log --rev 'not public() and ::. and not desc("# no-check-commit")' --template '{node|short}\n'`; do
+ > hg export $node | ${RUNTESTDIR}/../contrib/check-commit > ${TESTTMP}/check-commit.out
+ > if [ $? -ne 0 ]; then
+ > echo "Revision $node does not comply with rules"
+ > echo '------------------------------------------------------'
+ > cat ${TESTTMP}/check-commit.out
+ > echo
+ > fi
+ > done
--- a/tests/test-evolve-obshistory.t Tue May 16 23:37:10 2017 -0700
+++ b/tests/test-evolve-obshistory.t Wed May 17 09:52:06 2017 +0200
@@ -25,9 +25,11 @@
$ mkcommit ROOT
$ mkcommit A0
$ echo 42 >> A0
- $ hg amend -m "A1"
+ $ hg amend -m "A1
+ >
+ > Better commit message"
$ hg log --hidden -G
- @ changeset: 3:a468dc9b3633
+ @ changeset: 3:4ae3a4151de9
| tag: tip
| parent: 0:ea207398892e
| user: test
@@ -51,15 +53,69 @@
Actual test
-----------
-
+ $ hg debugobshistory 4ae3a4151de9
+ 4ae3a4151de9 (3) A1
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as 4ae3a4151de9 (glob)
+ $ hg debugobshistory 4ae3a4151de9 -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "4ae3a4151de9",
+ "debugobshistory.rev": 3,
+ "debugobshistory.shortdescription": "A1"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "4ae3a4151de9"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory --hidden 471f378eab4c
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as 4ae3a4151de9 (glob)
+ $ hg debugobshistory --hidden 471f378eab4c -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "4ae3a4151de9"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
$ hg update 471f378eab4c
abort: hidden revision '471f378eab4c'!
- (use --hidden to access hidden revisions; successor: a468dc9b3633)
+ (use --hidden to access hidden revisions; successor: 4ae3a4151de9)
[255]
$ hg update --hidden "desc(A0)"
1 files updated, 0 files merged, 0 files removed, 0 files unresolved
working directory parent is obsolete! (471f378eab4c)
- (use 'hg evolve' to update to its successor: a468dc9b3633)
+ (use 'hg evolve' to update to its successor: 4ae3a4151de9)
Test output with pruned commit
==============================
@@ -93,10 +149,59 @@
0 files updated, 0 files merged, 1 files removed, 0 files unresolved
working directory now at 471f378eab4c
1 changesets pruned
+ $ hg log --hidden -G
+ x changeset: 2:0dec01379d3b
+ | tag: tip
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: B0
+ |
+ @ changeset: 1:471f378eab4c
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A0
+ |
+ o changeset: 0:ea207398892e
+ user: test
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: ROOT
+
Actual test
-----------
+ $ hg debugobshistory 'desc(B0)' --hidden
+ 0dec01379d3b (2) B0
+ pruned by test (*20*) (glob)
+ $ hg debugobshistory 'desc(B0)' --hidden -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.verb": "pruned"
+ }
+ ],
+ "debugobshistory.node": "0dec01379d3b",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "B0"
+ }
+ ]
+ $ hg debugobshistory 'desc(A0)'
+ 471f378eab4c (1) A0
+ $ hg debugobshistory 'desc(A0)' -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
$ hg up 1
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
$ hg up 0dec01379d3b
@@ -195,6 +300,95 @@
Actual test
-----------
+ $ hg debugobshistory 471597cad322 --hidden
+ 471597cad322 (1) A0
+ rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+ $ hg debugobshistory 471597cad322 --hidden -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "337fec4d2edc",
+ "f257fde29c7a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471597cad322",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory 337fec4d2edc
+ 337fec4d2edc (2) A0
+ 471597cad322 (1) A0
+ rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+ $ hg debugobshistory 337fec4d2edc -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "337fec4d2edc",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "A0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "337fec4d2edc",
+ "f257fde29c7a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471597cad322",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory f257fde29c7a
+ f257fde29c7a (3) A0
+ 471597cad322 (1) A0
+ rewritten by test (*20*) as 337fec4d2edc, f257fde29c7a (glob)
+ $ hg debugobshistory f257fde29c7a -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "f257fde29c7a",
+ "debugobshistory.rev": 3,
+ "debugobshistory.shortdescription": "A0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "337fec4d2edc",
+ "f257fde29c7a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471597cad322",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
$ hg update 471597cad322
abort: hidden revision '471597cad322'!
(use --hidden to access hidden revisions; successors: 337fec4d2edc, f257fde29c7a)
@@ -357,6 +551,74 @@
Actual test
-----------
+ $ hg debugobshistory de7290d8b885 --hidden
+ de7290d8b885 (1) A0
+ rewritten by test (*20*) as 1ae8bc733a14, 337fec4d2edc, c7f044602e9b, f257fde29c7a (glob)
+ $ hg debugobshistory de7290d8b885 --hidden -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "1ae8bc733a14",
+ "337fec4d2edc",
+ "c7f044602e9b",
+ "f257fde29c7a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "de7290d8b885",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory c7f044602e9b
+ c7f044602e9b (5) A0
+ de7290d8b885 (1) A0
+ rewritten by test (*20*) as 1ae8bc733a14, 337fec4d2edc, c7f044602e9b, f257fde29c7a (glob)
+ $ hg debugobshistory c7f044602e9b -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "c7f044602e9b",
+ "debugobshistory.rev": 5,
+ "debugobshistory.shortdescription": "A0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "1ae8bc733a14",
+ "337fec4d2edc",
+ "c7f044602e9b",
+ "f257fde29c7a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "de7290d8b885",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory 2:5
+ 337fec4d2edc (2) A0
+ de7290d8b885 (1) A0
+ rewritten by test (*20*) as 1ae8bc733a14, 337fec4d2edc, c7f044602e9b, f257fde29c7a (glob)
+ f257fde29c7a (3) A0
+ 1ae8bc733a14 (4) A0
+ c7f044602e9b (5) A0
$ hg update de7290d8b885
abort: hidden revision 'de7290d8b885'!
(use --hidden to access hidden revisions; successors: 337fec4d2edc, f257fde29c7a and 2 more)
@@ -423,6 +685,105 @@
Actual test
-----------
+ $ hg debugobshistory --hidden 471f378eab4c
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ $ hg debugobshistory --hidden 471f378eab4c -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory --hidden 0dec01379d3b
+ 0dec01379d3b (2) B0
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ $ hg debugobshistory --hidden 0dec01379d3b -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "0dec01379d3b",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "B0"
+ }
+ ]
+ $ hg debugobshistory eb5a0daa2192
+ eb5a0daa2192 (3) C0
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ 0dec01379d3b (2) B0
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ $ hg debugobshistory eb5a0daa2192 -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "eb5a0daa2192",
+ "debugobshistory.rev": 3,
+ "debugobshistory.shortdescription": "C0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "0dec01379d3b",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "B0"
+ }
+ ]
$ hg update 471f378eab4c
abort: hidden revision '471f378eab4c'!
(use --hidden to access hidden revisions; successor: eb5a0daa2192)
@@ -506,6 +867,128 @@
Actual test
-----------
+ $ hg debugobshistory --hidden 471f378eab4c
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as 65b757b745b9 (glob)
+ rewritten by test (*20*) as fdf9bde5129a (glob)
+ $ hg debugobshistory --hidden 471f378eab4c -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "65b757b745b9"
+ ],
+ "debugobshistory.verb": "rewritten"
+ },
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "fdf9bde5129a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory fdf9bde5129a
+ fdf9bde5129a (2) A1
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as 65b757b745b9 (glob)
+ rewritten by test (*20*) as fdf9bde5129a (glob)
+ $ hg debugobshistory fdf9bde5129a -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "fdf9bde5129a",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "A1"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "65b757b745b9"
+ ],
+ "debugobshistory.verb": "rewritten"
+ },
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "fdf9bde5129a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory 65b757b745b9
+ 65b757b745b9 (3) A2
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as 65b757b745b9 (glob)
+ rewritten by test (*20*) as fdf9bde5129a (glob)
+ $ hg debugobshistory 65b757b745b9 -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "65b757b745b9",
+ "debugobshistory.rev": 3,
+ "debugobshistory.shortdescription": "A2"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "65b757b745b9"
+ ],
+ "debugobshistory.verb": "rewritten"
+ },
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "fdf9bde5129a"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
$ hg update 471f378eab4c
abort: hidden revision '471f378eab4c'!
(use --hidden to access hidden revisions; diverged)
@@ -514,3 +997,312 @@
0 files updated, 0 files merged, 0 files removed, 0 files unresolved
working directory parent is obsolete! (471f378eab4c)
(471f378eab4c has diverged, use 'hg evolve -list --divergent' to resolve the issue)
+
+Test output with amended + folded commit
+========================================
+
+Test setup
+----------
+
+ $ hg init $TESTTMP/local-amend-fold
+ $ cd $TESTTMP/local-amend-fold
+ $ mkcommit ROOT
+ $ mkcommit A0
+ $ mkcommit B0
+ $ hg amend -m "B1"
+ $ hg log --hidden -G
+ @ changeset: 3:b7ea6d14e664
+ | tag: tip
+ | parent: 1:471f378eab4c
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: B1
+ |
+ | x changeset: 2:0dec01379d3b
+ |/ user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: B0
+ |
+ o changeset: 1:471f378eab4c
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A0
+ |
+ o changeset: 0:ea207398892e
+ user: test
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: ROOT
+
+ $ hg fold --exact -r 'desc(A0) + desc(B1)' --date "0 0" -m "C0"
+ 2 changesets folded
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ hg log --hidden -G
+ @ changeset: 4:eb5a0daa2192
+ | tag: tip
+ | parent: 0:ea207398892e
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: C0
+ |
+ | x changeset: 3:b7ea6d14e664
+ | | parent: 1:471f378eab4c
+ | | user: test
+ | | date: Thu Jan 01 00:00:00 1970 +0000
+ | | summary: B1
+ | |
+ | | x changeset: 2:0dec01379d3b
+ | |/ user: test
+ | | date: Thu Jan 01 00:00:00 1970 +0000
+ | | summary: B0
+ | |
+ | x changeset: 1:471f378eab4c
+ |/ user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A0
+ |
+ o changeset: 0:ea207398892e
+ user: test
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: ROOT
+
+ Actual test
+ -----------
+
+ $ hg debugobshistory --hidden 471f378eab4c
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ $ hg debugobshistory --hidden 471f378eab4c -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg debugobshistory --hidden 0dec01379d3b
+ 0dec01379d3b (2) B0
+ rewritten by test (*20*) as b7ea6d14e664 (glob)
+ $ hg debugobshistory --hidden 0dec01379d3b -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "b7ea6d14e664"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "0dec01379d3b",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "B0"
+ }
+ ]
+ $ hg debugobshistory eb5a0daa2192
+ eb5a0daa2192 (4) C0
+ b7ea6d14e664 (3) B1
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ 0dec01379d3b (2) B0
+ rewritten by test (*20*) as b7ea6d14e664 (glob)
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as eb5a0daa2192 (glob)
+ $ hg debugobshistory eb5a0daa2192 -Tjson | python -m json.tool
+ [
+ {
+ "debugobshistory.markers": [],
+ "debugobshistory.node": "eb5a0daa2192",
+ "debugobshistory.rev": 4,
+ "debugobshistory.shortdescription": "C0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "b7ea6d14e664",
+ "debugobshistory.rev": 3,
+ "debugobshistory.shortdescription": "B1"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "b7ea6d14e664"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "0dec01379d3b",
+ "debugobshistory.rev": 2,
+ "debugobshistory.shortdescription": "B0"
+ },
+ {
+ "debugobshistory.markers": [
+ {
+ "debugobshistory.marker_date": [
+ *, (glob)
+ 0 (glob)
+ ],
+ "debugobshistory.marker_user": "test",
+ "debugobshistory.succnodes": [
+ "eb5a0daa2192"
+ ],
+ "debugobshistory.verb": "rewritten"
+ }
+ ],
+ "debugobshistory.node": "471f378eab4c",
+ "debugobshistory.rev": 1,
+ "debugobshistory.shortdescription": "A0"
+ }
+ ]
+ $ hg update 471f378eab4c
+ abort: hidden revision '471f378eab4c'!
+ (use --hidden to access hidden revisions; successor: eb5a0daa2192)
+ [255]
+ $ hg update --hidden 'desc(A0)'
+ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ working directory parent is obsolete! (471f378eab4c)
+ (use 'hg evolve' to update to its successor: eb5a0daa2192)
+ $ hg update --hidden 0dec01379d3b
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ working directory parent is obsolete! (0dec01379d3b)
+ (use 'hg evolve' to update to its successor: eb5a0daa2192)
+ $ hg update 0dec01379d3b
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ working directory parent is obsolete! (0dec01379d3b)
+ (use 'hg evolve' to update to its successor: eb5a0daa2192)
+ $ hg update --hidden 'desc(B0)'
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ working directory parent is obsolete! (0dec01379d3b)
+ (use 'hg evolve' to update to its successor: eb5a0daa2192)
+
+Test output with pushed and pulled obs markers
+==============================================
+
+Test setup
+----------
+
+ $ hg init $TESTTMP/local-remote-markers-1
+ $ cd $TESTTMP/local-remote-markers-1
+ $ mkcommit ROOT
+ $ mkcommit A0
+ $ hg log --hidden -G
+ @ changeset: 1:471f378eab4c
+ | tag: tip
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A0
+ |
+ o changeset: 0:ea207398892e
+ user: test
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: ROOT
+
+ $ hg clone $TESTTMP/local-remote-markers-1 $TESTTMP/local-remote-markers-2
+ updating to branch default
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cd $TESTTMP/local-remote-markers-2
+ $ hg log --hidden -G
+ @ changeset: 1:471f378eab4c
+ | tag: tip
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A0
+ |
+ o changeset: 0:ea207398892e
+ user: test
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: ROOT
+
+ $ cd $TESTTMP/local-remote-markers-1
+ $ hg amend -m "A1"
+ $ hg amend -m "A2"
+ $ hg log --hidden -G
+ @ changeset: 3:7a230b46bf61
+ | tag: tip
+ | parent: 0:ea207398892e
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A2
+ |
+ | x changeset: 2:fdf9bde5129a
+ |/ parent: 0:ea207398892e
+ | user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A1
+ |
+ | x changeset: 1:471f378eab4c
+ |/ user: test
+ | date: Thu Jan 01 00:00:00 1970 +0000
+ | summary: A0
+ |
+ o changeset: 0:ea207398892e
+ user: test
+ date: Thu Jan 01 00:00:00 1970 +0000
+ summary: ROOT
+
+ Actual test
+ -----------
+
+ $ hg debugobshistory 7a230b46bf61
+ 7a230b46bf61 (3) A2
+ fdf9bde5129a (2) A1
+ rewritten by test (*20*) as 7a230b46bf61 (glob)
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as fdf9bde5129a (glob)
+ $ cd $TESTTMP/local-remote-markers-2
+ $ hg pull
+ pulling from $TESTTMP/local-remote-markers-1
+ searching for changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 0 changes to 1 files (+1 heads)
+ 2 new obsolescence markers
+ (run 'hg heads' to see heads, 'hg merge' to merge)
+ working directory parent is obsolete! (471f378eab4c)
+ (use 'hg evolve' to update to its successor: 7a230b46bf61)
+ $ hg debugobshistory 7a230b46bf61 --traceback
+ 7a230b46bf61 (2) A2
+ fdf9bde5129a
+ rewritten by test (*20*) as 7a230b46bf61 (glob)
+ 471f378eab4c (1) A0
+ rewritten by test (*20*) as fdf9bde5129a (glob)
+ $ hg debugobshistory 7a230b46bf61 --color=debug
+ [evolve.short_node|7a230b46bf61] [evolve.rev|(2)] [evolve.short_description|A2]
+ [evolve.short_node evolve.missing_change_ctx|fdf9bde5129a]
+ [evolve.verb|rewritten] by [evolve.user|test] [evolve.date|(*20*)] as [evolve.short_node|7a230b46bf61] (glob)
+ [evolve.short_node|471f378eab4c] [evolve.rev|(1)] [evolve.short_description|A0]
+ [evolve.verb|rewritten] by [evolve.user|test] [evolve.date|(*20*)] as [evolve.short_node|fdf9bde5129a] (glob)