hgext/journal.py
changeset 29502 8361131b4768
parent 29443 cf092a3d202a
child 29503 0103b673d6ca
--- 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)