Patchwork [1,of,2] journal: add dirstate tracking

login
register
mail settings
Submitter Martijn Pieters
Date June 30, 2016, 12:41 p.m.
Message ID <09c7b17ab01c20246054.1467290485@mjpieters-mbp.dhcp.thefacebook.com>
Download mbox | patch
Permalink /patch/15670/
State Changes Requested
Delegated to: Yuya Nishihara
Headers show

Comments

Martijn Pieters - June 30, 2016, 12:41 p.m.
# HG changeset patch
# User Martijn Pieters <mjpieters@fb.com>
# Date 1467290363 -3600
#      Thu Jun 30 13:39:23 2016 +0100
# Node ID 09c7b17ab01c202460543f02d9d71ac643f85a20
# Parent  cf092a3d202a11a670f2bbfd514bc07d6cc4cee7
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.
Yuya Nishihara - July 3, 2016, 3:42 a.m.
On Thu, 30 Jun 2016 13:41:25 +0100, Martijn Pieters wrote:
> # HG changeset patch
> # User Martijn Pieters <mjpieters@fb.com>
> # Date 1467290363 -3600
> #      Thu Jun 30 13:39:23 2016 +0100
> # Node ID 09c7b17ab01c202460543f02d9d71ac643f85a20
> # Parent  cf092a3d202a11a670f2bbfd514bc07d6cc4cee7
> journal: add dirstate tracking

> +# hooks to record dirstate changes
> +def wrapdirstate(orig, repo):
> +    dirstate = orig(repo)
> +    if util.safehasattr(repo, 'journal'):
> +        dirstate.journalrepo = repo
> +    return dirstate

IIRC, it's considered layering violation to pass repo to dirstate. Perhaps,
it could be dirstate.journal = repo.journal, or carry journal by a transaction
object.

> +def recorddirstateparents(orig, dirstate, dirstatefp):
> +    """Records all dirstate parent changes in the journal."""
> +    if util.safehasattr(dirstate, 'journalrepo'):
> +        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.
> +            with dirstate._opener(dirstate._filename) 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.journalrepo.journal.record(
> +                wdirparenttype, '.', oldhashes, newhashes)
> +
> +    return orig(dirstate, dirstatefp)

dirstatefp could be a backup file. Maybe that's okay because the old state
should be identical to the current state for backups, but I'm not pretty sure.
Can you take a look and add comments?

> +    def _write(self, entry, _wait=False):
> +        try:
> +            with self.repo.wlock(wait=_wait):
> +                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:
> +                    f.seek(0, os.SEEK_SET)
> +                    # Read just enough bytes to get a version number (up to 2
> +                    # digits plus separator)
> +                    version = f.read(3).partition('\0')[0]
> +                    if version and version != str(storageversion):
> +                        # different version of the storage. Exit early (and
> +                        # not 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(
> +                            _("unsupported journal file version '%s'\n") %
> +                            version)
> +                        return
> +                    if not version:
> +                        # empty file, write version first
> +                        f.write(str(storageversion) + '\0')
> +                    f.seek(0, os.SEEK_END)
> +                    f.write(str(entry) + '\0')
> +        except error.LockHeld as lockerr:
> +            lock = self.repo._wlockref and self.repo._wlockref()
> +            if lock and lockerr.locker == '%s:%s' % (lock._host, lock.pid):
> +                # the dirstate can be written out during wlock unlock, before
> +                # the lockfile is removed. Re-run the write as a postrelease
> +                # function instead.
> +                lock.postrelease.append(
> +                    lambda: self._write(entry, _wait=True))
> +            else:
> +                # another process holds the lock, retry and wait
> +                self._write(entry, _wait=True)

I understand why this is necessary, but it seems wrong to try to take a lock
in lock.releasefn(), which can't guarantee consistency. We could delay
"lock.held = 0" after releasefn() as the lock wasn't released yet, but it
would result in two releasefn() calls.

Instead, I think it will be way simpler if the journal has a dedicated lock,
and we can get rid of the cycle between journalstrage and localrepo.
Martijn Pieters - July 4, 2016, 10:58 a.m.
On 3 July 2016 at 04:42, Yuya Nishihara <yuya@tcha.org> wrote:
> On Thu, 30 Jun 2016 13:41:25 +0100, Martijn Pieters wrote:
>> # HG changeset patch
>> # User Martijn Pieters <mjpieters@fb.com>
>> # Date 1467290363 -3600
>> #      Thu Jun 30 13:39:23 2016 +0100
>> # Node ID 09c7b17ab01c202460543f02d9d71ac643f85a20
>> # Parent  cf092a3d202a11a670f2bbfd514bc07d6cc4cee7
>> journal: add dirstate tracking
>
>> +# hooks to record dirstate changes
>> +def wrapdirstate(orig, repo):
>> +    dirstate = orig(repo)
>> +    if util.safehasattr(repo, 'journal'):
>> +        dirstate.journalrepo = repo
>> +    return dirstate
>
> IIRC, it's considered layering violation to pass repo to dirstate. Perhaps,
> it could be dirstate.journal = repo.journal, or carry journal by a transaction
> object.

Good point; all that is needed really is the journal storage, so
`dirstate.journalstorage = repo.journal` should suffice here. I'll
make the change and send a V2.

>> +def recorddirstateparents(orig, dirstate, dirstatefp):
>> +    """Records all dirstate parent changes in the journal."""
>> +    if util.safehasattr(dirstate, 'journalrepo'):
>> +        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.
>> +            with dirstate._opener(dirstate._filename) 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.journalrepo.journal.record(
>> +                wdirparenttype, '.', oldhashes, newhashes)
>> +
>> +    return orig(dirstate, dirstatefp)
>
> dirstatefp could be a backup file. Maybe that's okay because the old state
> should be identical to the current state for backups, but I'm not pretty sure.
> Can you take a look and add comments?

I'll take a look, thanks for the pointer.

>> +    def _write(self, entry, _wait=False):
>> +        try:
>> +            with self.repo.wlock(wait=_wait):
>> +                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:
>> +                    f.seek(0, os.SEEK_SET)
>> +                    # Read just enough bytes to get a version number (up to 2
>> +                    # digits plus separator)
>> +                    version = f.read(3).partition('\0')[0]
>> +                    if version and version != str(storageversion):
>> +                        # different version of the storage. Exit early (and
>> +                        # not 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(
>> +                            _("unsupported journal file version '%s'\n") %
>> +                            version)
>> +                        return
>> +                    if not version:
>> +                        # empty file, write version first
>> +                        f.write(str(storageversion) + '\0')
>> +                    f.seek(0, os.SEEK_END)
>> +                    f.write(str(entry) + '\0')
>> +        except error.LockHeld as lockerr:
>> +            lock = self.repo._wlockref and self.repo._wlockref()
>> +            if lock and lockerr.locker == '%s:%s' % (lock._host, lock.pid):
>> +                # the dirstate can be written out during wlock unlock, before
>> +                # the lockfile is removed. Re-run the write as a postrelease
>> +                # function instead.
>> +                lock.postrelease.append(
>> +                    lambda: self._write(entry, _wait=True))
>> +            else:
>> +                # another process holds the lock, retry and wait
>> +                self._write(entry, _wait=True)
>
> I understand why this is necessary, but it seems wrong to try to take a lock
> in lock.releasefn(), which can't guarantee consistency. We could delay
> "lock.held = 0" after releasefn() as the lock wasn't released yet, but it
> would result in two releasefn() calls.
>
> Instead, I think it will be way simpler if the journal has a dedicated lock,
> and we can get rid of the cycle between journalstrage and localrepo.

I still need access to the repo object to support shared repositories,
I think. I'll look into a dedicated lock, that'd make this a lot
simpler probably.
Yuya Nishihara - July 4, 2016, 1:09 p.m.
On Mon, 4 Jul 2016 11:58:37 +0100, Martijn Pieters wrote:
> On 3 July 2016 at 04:42, Yuya Nishihara <yuya@tcha.org> wrote:
> > On Thu, 30 Jun 2016 13:41:25 +0100, Martijn Pieters wrote:  
> >> # HG changeset patch
> >> # User Martijn Pieters <mjpieters@fb.com>
> >> # Date 1467290363 -3600
> >> #      Thu Jun 30 13:39:23 2016 +0100
> >> # Node ID 09c7b17ab01c202460543f02d9d71ac643f85a20
> >> # Parent  cf092a3d202a11a670f2bbfd514bc07d6cc4cee7
> >> journal: add dirstate tracking  
> >  
> >> +# hooks to record dirstate changes
> >> +def wrapdirstate(orig, repo):
> >> +    dirstate = orig(repo)
> >> +    if util.safehasattr(repo, 'journal'):
> >> +        dirstate.journalrepo = repo
> >> +    return dirstate  
> >
> > IIRC, it's considered layering violation to pass repo to dirstate. Perhaps,
> > it could be dirstate.journal = repo.journal, or carry journal by a transaction
> > object.  
> 
> Good point; all that is needed really is the journal storage, so
> `dirstate.journalstorage = repo.journal` should suffice here. I'll
> make the change and send a V2.

[snip]

> > I understand why this is necessary, but it seems wrong to try to take a lock
> > in lock.releasefn(), which can't guarantee consistency. We could delay
> > "lock.held = 0" after releasefn() as the lock wasn't released yet, but it
> > would result in two releasefn() calls.
> >
> > Instead, I think it will be way simpler if the journal has a dedicated lock,
> > and we can get rid of the cycle between journalstrage and localrepo.  
> 
> I still need access to the repo object to support shared repositories,
> I think. I'll look into a dedicated lock, that'd make this a lot
> simpler probably.

I guess shared path can be resolved when attaching the journalstorage to the
repo, but I just looked over the code quickly, I might be wrong. Strictly
speaking, it would be layering violation to pass an object holding repo to
dirstate.
Martijn Pieters - July 4, 2016, 1:17 p.m.
On 4 July 2016 at 14:09, Yuya Nishihara <yuya@tcha.org> wrote:
>> > I understand why this is necessary, but it seems wrong to try to take a lock
>> > in lock.releasefn(), which can't guarantee consistency. We could delay
>> > "lock.held = 0" after releasefn() as the lock wasn't released yet, but it
>> > would result in two releasefn() calls.
>> >
>> > Instead, I think it will be way simpler if the journal has a dedicated lock,
>> > and we can get rid of the cycle between journalstrage and localrepo.
>>
>> I still need access to the repo object to support shared repositories,
>> I think. I'll look into a dedicated lock, that'd make this a lot
>> simpler probably.
>
> I guess shared path can be resolved when attaching the journalstorage to the
> repo, but I just looked over the code quickly, I might be wrong. Strictly
> speaking, it would be layering violation to pass an object holding repo to
> dirstate.

I'm already working on a new patch here; we can indeed avoid storing
the current repo at least by resolving sharing info when creating the
journalstorage instance.

Patch

diff --git a/hgext/journal.py b/hgext/journal.py
--- a/hgext/journal.py
+++ b/hgext/journal.py
@@ -22,9 +22,11 @@ 
     bookmarks,
     cmdutil,
     commands,
+    dirstate,
     dispatch,
     error,
     extensions,
+    localrepo,
     node,
     util,
 )
@@ -43,11 +45,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 +65,40 @@ 
     journalstorage.recordcommand(*fullargs)
     return orig(lui, repo, cmd, fullargs, *args)
 
+# hooks to record dirstate changes
+def wrapdirstate(orig, repo):
+    dirstate = orig(repo)
+    if util.safehasattr(repo, 'journal'):
+        dirstate.journalrepo = repo
+    return dirstate
+
+def recorddirstateparents(orig, dirstate, dirstatefp):
+    """Records all dirstate parent changes in the journal."""
+    if util.safehasattr(dirstate, 'journalrepo'):
+        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.
+            with dirstate._opener(dirstate._filename) 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.journalrepo.journal.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
@@ -162,28 +203,43 @@ 
         entry = journalentry(
             util.makedate(), self.user, self.command, namespace, name,
             oldhashes, newhashes)
+        self._write(entry)
 
-        with self.repo.wlock():
-            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:
-                f.seek(0, os.SEEK_SET)
-                # Read just enough bytes to get a version number (up to 2
-                # digits plus separator)
-                version = f.read(3).partition('\0')[0]
-                if version and version != str(storageversion):
-                    # different version of the storage. Exit early (and not
-                    # 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(
-                        _("unsupported journal file version '%s'\n") % version)
-                    return
-                if not version:
-                    # empty file, write version first
-                    f.write(str(storageversion) + '\0')
-                f.seek(0, os.SEEK_END)
-                f.write(str(entry) + '\0')
+    def _write(self, entry, _wait=False):
+        try:
+            with self.repo.wlock(wait=_wait):
+                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:
+                    f.seek(0, os.SEEK_SET)
+                    # Read just enough bytes to get a version number (up to 2
+                    # digits plus separator)
+                    version = f.read(3).partition('\0')[0]
+                    if version and version != str(storageversion):
+                        # different version of the storage. Exit early (and
+                        # not 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(
+                            _("unsupported journal file version '%s'\n") %
+                            version)
+                        return
+                    if not version:
+                        # empty file, write version first
+                        f.write(str(storageversion) + '\0')
+                    f.seek(0, os.SEEK_END)
+                    f.write(str(entry) + '\0')
+        except error.LockHeld as lockerr:
+            lock = self.repo._wlockref and self.repo._wlockref()
+            if lock and lockerr.locker == '%s:%s' % (lock._host, lock.pid):
+                # the dirstate can be written out during wlock unlock, before
+                # the lockfile is removed. Re-run the write as a postrelease
+                # function instead.
+                lock.postrelease.append(
+                    lambda: self._write(entry, _wait=True))
+            else:
+                # another process holds the lock, retry and wait
+                self._write(entry, _wait=True)
 
     def filtered(self, namespace=None, name=None):
         """Yield all journal entries with the given namespace or name
@@ -229,15 +285,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 +310,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])
@@ -275,6 +340,7 @@ 
         fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr)
         fm.write('newhashes', '%s', newhashesstr)
         fm.condwrite(ui.verbose, 'user', ' %s', entry.user.ljust(8))
+        fm.condwrite(opts.get('all'), 'name', '  %s', entry.name.ljust(8))
 
         timestring = util.datestr(entry.timestamp, '%Y-%m-%d %H:%M %1%2')
         fm.condwrite(ui.verbose, 'date', ' %s', timestring)
diff --git a/tests/test-journal.t b/tests/test-journal.t
--- a/tests/test-journal.t
+++ b/tests/test-journal.t
@@ -32,22 +32,37 @@ 
 
   $ hg init repo
   $ cd repo
-  $ echo a > a
-  $ hg commit -Aqm a
-  $ echo b > a
-  $ hg commit -Aqm b
-  $ hg up 0
-  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
 
 Test empty journal
 
   $ hg journal
-  previous locations of all bookmarks:
+  previous locations of '.':
   no recorded locations
   $ hg journal foo
   previous locations of 'foo':
   no recorded locations
 
+Test that working copy changes are tracked
+
+  $ echo a > a
+  $ hg commit -Aqm a
+  $ hg journal
+  previous locations of '.':
+  cb9a9f314b8b  commit -Aqm a
+  $ echo b > a
+  $ hg commit -Aqm b
+  $ hg journal
+  previous locations of '.':
+  1e6c11564562  commit -Aqm b
+  cb9a9f314b8b  commit -Aqm a
+  $ hg up 0
+  1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ hg journal
+  previous locations of '.':
+  cb9a9f314b8b  up 0
+  1e6c11564562  commit -Aqm b
+  cb9a9f314b8b  commit -Aqm a
+
 Test that bookmarks are tracked
 
   $ hg book -r tip bar
@@ -68,22 +83,32 @@ 
   cb9a9f314b8b  book -f bar
   1e6c11564562  book -r tip bar
 
-Test that you can list all bookmarks as well as limit the list or filter on them
+Test that bookmarks and working copy tracking is not mixed
+
+  $ hg journal
+  previous locations of '.':
+  1e6c11564562  up
+  cb9a9f314b8b  up 0
+  1e6c11564562  commit -Aqm b
+  cb9a9f314b8b  commit -Aqm a
+
+Test that you can list all entries as well as limit the list or filter on them
 
   $ hg book -r tip baz
-  $ hg journal
-  previous locations of all bookmarks:
-  1e6c11564562  book -r tip baz
+  $ hg journal --all
+  previous locations of the working copy and bookmarks:
+  1e6c11564562  baz       book -r tip baz
+  1e6c11564562  bar       up
+  1e6c11564562  .         up
+  cb9a9f314b8b  bar       book -f bar
+  1e6c11564562  bar       book -r tip bar
+  cb9a9f314b8b  .         up 0
+  1e6c11564562  .         commit -Aqm b
+  cb9a9f314b8b  .         commit -Aqm a
+  $ hg journal --limit 2
+  previous locations of '.':
   1e6c11564562  up
-  cb9a9f314b8b  book -f bar
-  1e6c11564562  book -r tip bar
-  $ hg journal --limit 2
-  previous locations of all bookmarks:
-  1e6c11564562  book -r tip baz
-  1e6c11564562  up
-  $ hg journal baz
-  previous locations of 'baz':
-  1e6c11564562  book -r tip baz
+  cb9a9f314b8b  up 0
   $ hg journal bar
   previous locations of 'bar':
   1e6c11564562  up
@@ -92,26 +117,27 @@ 
   $ hg journal foo
   previous locations of 'foo':
   no recorded locations
+  $ hg journal .
+  previous locations of '.':
+  1e6c11564562  up
+  cb9a9f314b8b  up 0
+  1e6c11564562  commit -Aqm b
+  cb9a9f314b8b  commit -Aqm a
 
 Test that verbose and commit output work
 
-  $ hg journal --verbose
-  previous locations of all bookmarks:
-  000000000000 -> 1e6c11564562 foobar   1970-01-01 00:00 +0000  book -r tip baz
-  cb9a9f314b8b -> 1e6c11564562 foobar   1970-01-01 00:00 +0000  up
-  1e6c11564562 -> cb9a9f314b8b foobar   1970-01-01 00:00 +0000  book -f bar
-  000000000000 -> 1e6c11564562 foobar   1970-01-01 00:00 +0000  book -r tip bar
+  $ hg journal --verbose --all
+  previous locations of the working copy and bookmarks:
+  000000000000 -> 1e6c11564562 foobar    baz      1970-01-01 00:00 +0000  book -r tip baz
+  cb9a9f314b8b -> 1e6c11564562 foobar    bar      1970-01-01 00:00 +0000  up
+  cb9a9f314b8b -> 1e6c11564562 foobar    .        1970-01-01 00:00 +0000  up
+  1e6c11564562 -> cb9a9f314b8b foobar    bar      1970-01-01 00:00 +0000  book -f bar
+  000000000000 -> 1e6c11564562 foobar    bar      1970-01-01 00:00 +0000  book -r tip bar
+  1e6c11564562 -> cb9a9f314b8b foobar    .        1970-01-01 00:00 +0000  up 0
+  cb9a9f314b8b -> 1e6c11564562 foobar    .        1970-01-01 00:00 +0000  commit -Aqm b
+  000000000000 -> cb9a9f314b8b foobar    .        1970-01-01 00:00 +0000  commit -Aqm a
   $ hg journal --commit
-  previous locations of all bookmarks:
-  1e6c11564562  book -r tip baz
-  changeset:   1:1e6c11564562
-  bookmark:    bar
-  bookmark:    baz
-  tag:         tip
-  user:        test
-  date:        Thu Jan 01 00:00:00 1970 +0000
-  summary:     b
-  
+  previous locations of '.':
   1e6c11564562  up
   changeset:   1:1e6c11564562
   bookmark:    bar
@@ -121,13 +147,13 @@ 
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     b
   
-  cb9a9f314b8b  book -f bar
+  cb9a9f314b8b  up 0
   changeset:   0:cb9a9f314b8b
   user:        test
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     a
   
-  1e6c11564562  book -r tip bar
+  1e6c11564562  commit -Aqm b
   changeset:   1:1e6c11564562
   bookmark:    bar
   bookmark:    baz
@@ -136,12 +162,18 @@ 
   date:        Thu Jan 01 00:00:00 1970 +0000
   summary:     b
   
+  cb9a9f314b8b  commit -Aqm a
+  changeset:   0:cb9a9f314b8b
+  user:        test
+  date:        Thu Jan 01 00:00:00 1970 +0000
+  summary:     a
+  
 
 Test for behaviour on unexpected storage version information
 
   $ printf '42\0' > .hg/journal
   $ hg journal
-  previous locations of all bookmarks:
+  previous locations of '.':
   abort: unknown journal file version '42'
   [255]
   $ hg book -r tip doomed