Mercurial > public > mercurial-scm > hg-stable
diff hgext/journal.py @ 29502:8361131b4768
journal: add dirstate tracking
Note that now the default action for `hg journal` is to list the working copy
history, not all bookmarks. In its place is the `--all` switch which lists all
name changes recorded, including the name for which the change was recorded on
each line.
Locking is switched to using a dedicated lock to avoid issues with the dirstate
being written during wlock unlocking (you can't re-lock during that process).
author | Martijn Pieters <mjpieters@fb.com> |
---|---|
date | Mon, 11 Jul 2016 13:39:24 +0100 |
parents | cf092a3d202a |
children | 0103b673d6ca |
line wrap: on
line diff
--- a/hgext/journal.py Mon Jul 11 08:54:13 2016 -0500 +++ b/hgext/journal.py Mon Jul 11 13:39:24 2016 +0100 @@ -15,6 +15,7 @@ import collections import os +import weakref from mercurial.i18n import _ @@ -22,9 +23,12 @@ bookmarks, cmdutil, commands, + dirstate, dispatch, error, extensions, + localrepo, + lock, node, util, ) @@ -43,11 +47,16 @@ # namespaces bookmarktype = 'bookmark' +wdirparenttype = 'wdirparent' # Journal recording, register hooks and storage object def extsetup(ui): extensions.wrapfunction(dispatch, 'runcommand', runcommand) extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks) + extensions.wrapfunction( + dirstate.dirstate, '_writedirstate', recorddirstateparents) + extensions.wrapfunction( + localrepo.localrepository.dirstate, 'func', wrapdirstate) def reposetup(ui, repo): if repo.local(): @@ -58,6 +67,42 @@ journalstorage.recordcommand(*fullargs) return orig(lui, repo, cmd, fullargs, *args) +# hooks to record dirstate changes +def wrapdirstate(orig, repo): + """Make journal storage available to the dirstate object""" + dirstate = orig(repo) + if util.safehasattr(repo, 'journal'): + dirstate.journalstorage = repo.journal + return dirstate + +def recorddirstateparents(orig, dirstate, dirstatefp): + """Records all dirstate parent changes in the journal.""" + if util.safehasattr(dirstate, 'journalstorage'): + old = [node.nullid, node.nullid] + nodesize = len(node.nullid) + try: + # The only source for the old state is in the dirstate file still + # on disk; the in-memory dirstate object only contains the new + # state. dirstate._opendirstatefile() switches beteen .hg/dirstate + # and .hg/dirstate.pending depending on the transaction state. + with dirstate._opendirstatefile() as fp: + state = fp.read(2 * nodesize) + if len(state) == 2 * nodesize: + old = [state[:nodesize], state[nodesize:]] + except IOError: + pass + + new = dirstate.parents() + if old != new: + # only record two hashes if there was a merge + oldhashes = old[:1] if old[1] == node.nullid else old + newhashes = new[:1] if new[1] == node.nullid else new + dirstate.journalstorage.record( + wdirparenttype, '.', oldhashes, newhashes) + + return orig(dirstate, dirstatefp) + +# hooks to record bookmark changes (both local and remote) def recordbookmarks(orig, store, fp): """Records all bookmark changes in the journal.""" repo = store._repo @@ -117,12 +162,17 @@ The file format starts with an integer version, delimited by a NUL. + This storage uses a dedicated lock; this makes it easier to avoid issues + with adding entries that added when the regular wlock is unlocked (e.g. + the dirstate). + """ _currentcommand = () + _lockref = None def __init__(self, repo): - self.repo = repo self.user = util.getuser() + self.ui = repo.ui self.vfs = repo.vfs # track the current command for recording in journal entries @@ -142,6 +192,24 @@ # with a non-local repo (cloning for example). cls._currentcommand = fullargs + def jlock(self): + """Create a lock for the journal file""" + if self._lockref and self._lockref(): + raise error.Abort(_('journal lock does not support nesting')) + desc = _('journal of %s') % self.vfs.base + try: + l = lock.lock(self.vfs, 'journal.lock', 0, desc=desc) + except error.LockHeld as inst: + self.ui.warn( + _("waiting for lock on %s held by %r\n") % (desc, inst.locker)) + # default to 600 seconds timeout + l = lock.lock( + self.vfs, 'journal.lock', + int(self.ui.config("ui", "timeout", "600")), desc=desc) + self.ui.warn(_("got lock after %s seconds\n") % l.delay) + self._lockref = weakref.ref(l) + return l + def record(self, namespace, name, oldhashes, newhashes): """Record a new journal entry @@ -163,7 +231,7 @@ util.makedate(), self.user, self.command, namespace, name, oldhashes, newhashes) - with self.repo.wlock(): + with self.jlock(): version = None # open file in amend mode to ensure it is created if missing with self.vfs('journal', mode='a+b', atomictemp=True) as f: @@ -176,7 +244,7 @@ # write anything) if this is not a version we can handle or # the file is corrupt. In future, perhaps rotate the file # instead? - self.repo.ui.warn( + self.ui.warn( _("unsupported journal file version '%s'\n") % version) return if not version: @@ -229,15 +297,19 @@ _ignoreopts = ('no-merges', 'graph') @command( 'journal', [ + ('', 'all', None, 'show history for all names'), ('c', 'commits', None, 'show commit metadata'), ] + [opt for opt in commands.logopts if opt[1] not in _ignoreopts], '[OPTION]... [BOOKMARKNAME]') def journal(ui, repo, *args, **opts): - """show the previous position of bookmarks + """show the previous position of bookmarks and the working copy - The journal is used to see the previous commits of bookmarks. By default - the previous locations for all bookmarks are shown. Passing a bookmark - name will show all the previous positions of that bookmark. + The journal is used to see the previous commits that bookmarks and the + working copy pointed to. By default the previous locations for the working + copy. Passing a bookmark name will show all the previous positions of + that bookmark. Use the --all switch to show previous locations for all + bookmarks and the working copy; each line will then include the bookmark + name, or '.' for the working copy, as well. By default hg journal only shows the commit hash and the command that was running at that time. -v/--verbose will show the prior hash, the user, and @@ -250,22 +322,27 @@ `hg journal -T json` can be used to produce machine readable output. """ - bookmarkname = None + name = '.' + if opts.get('all'): + if args: + raise error.Abort( + _("You can't combine --all and filtering on a name")) + name = None if args: - bookmarkname = args[0] + name = args[0] fm = ui.formatter('journal', opts) if opts.get("template") != "json": - if bookmarkname is None: - name = _('all bookmarks') + if name is None: + displayname = _('the working copy and bookmarks') else: - name = "'%s'" % bookmarkname - ui.status(_("previous locations of %s:\n") % name) + displayname = "'%s'" % name + ui.status(_("previous locations of %s:\n") % displayname) limit = cmdutil.loglimit(opts) entry = None - for count, entry in enumerate(repo.journal.filtered(name=bookmarkname)): + for count, entry in enumerate(repo.journal.filtered(name=name)): if count == limit: break newhashesstr = ','.join([node.short(hash) for hash in entry.newhashes]) @@ -274,7 +351,8 @@ fm.startitem() fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr) fm.write('newhashes', '%s', newhashesstr) - fm.condwrite(ui.verbose, 'user', ' %s', entry.user.ljust(8)) + fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user) + fm.condwrite(opts.get('all'), 'name', ' %-8s', entry.name) timestring = util.datestr(entry.timestamp, '%Y-%m-%d %H:%M %1%2') fm.condwrite(ui.verbose, 'date', ' %s', timestring)