[PATCH] journal: new expiremental extension

Martijn Pieters mj at zopatista.com
Fri Jun 17 18:28:35 EDT 2016


On 17 June 2016 at 21:03, Durham Goode <durham at fb.com> wrote:
> On 6/17/16 12:32 AM, Martijn Pieters wrote:
>>
>> # HG changeset patch
>> # User Martijn Pieters <mjpieters at fb.com>
>> # Date 1466145392 -3600
>> #      Fri Jun 17 07:36:32 2016 +0100
>> # Node ID b1bce97848c09e3e96f949d32fab67d5fbbea862
>> # Parent  af849596752cc9663146160518a3125d50077f09
>> journal: new expiremental extension
>
> "experimental"

Good catch! Corrected locally.

>>
>> Records bookmark locations and shows you where bookmarks were located in
>> the
>> past.
>>
>> This is the first in a planned series of locations to be recorded; a
>> future
>> patch will add working copy (dirstate) tracking, and remote bookmarks will
>> be
>> supported as well, so the journal storage format should be fairly generic
>> to
>> support those use-cases.
>>
>> diff --git a/hgext/journal.py b/hgext/journal.py
>> new file mode 100644
>> --- /dev/null
>> +++ b/hgext/journal.py
>> @@ -0,0 +1,226 @@
>> +# journal.py
>> +#
>> +# Copyright 2014-2016 Facebook, Inc.
>> +#
>> +# This software may be used and distributed according to the terms of the
>> +# GNU General Public License version 2 or any later version.
>> +"""Track previous positions of bookmarks (EXPERIMENTAL)
>> +
>> +This extension adds a new command: `hg journal`, which shows you where
>> +bookmarks were previously located.
>> +
>> +"""
>> +
>> +from __future__ import absolute_import
>> +
>> +import os
>> +
>> +from mercurial.i18n import _
>> +
>> +from mercurial import (
>> +    bookmarks,
>> +    cmdutil,
>> +    commands,
>> +    dispatch,
>> +    error,
>> +    extensions,
>> +    node,
>> +    scmutil,
>> +    util,
>> +)
>> +
>> +cmdtable = {}
>> +command = cmdutil.command(cmdtable)
>> +
>> +# Note for extension authors: ONLY specify testedwith = 'internal' for
>> +# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
>> +# be specifying the version(s) of Mercurial they are tested with, or
>> +# leave the attribute unspecified.
>> +testedwith = 'internal'
>> +
>> +# storage format version; increment when the format changes
>> +storage_version = 0
>> +
>> +# namespaces
>> +bookmarktype = 'bookmark'
>> +
>> +# Journal recording, register hooks and storage object
>> +def extsetup(ui):
>> +    extensions.wrapfunction(dispatch, 'runcommand', runcommand)
>> +    extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
>> +
>> +def reposetup(ui, repo):
>> +    if repo.local():
>> +        repo.journal = journalstorage(repo)
>> +
>> +def runcommand(orig, lui, repo, cmd, fullargs, *args):
>> +    """Track the command line options for recording in the journal"""
>> +    if util.safehasattr(repo, 'journal'):
>> +        repo.journal.command = fullargs
>> +    return orig(lui, repo, cmd, fullargs, *args)
>> +
>> +def recordbookmarks(orig, store, fp):
>> +    """Records all bookmark changes in the journal."""
>> +    repo = store._repo
>> +    if util.safehasattr(repo, 'journal'):
>> +        oldmarks = bookmarks.bmstore(repo)
>> +        for mark, value in store.iteritems():
>> +            oldvalue = oldmarks.get(mark, node.nullid)
>> +            if value != oldvalue:
>> +                repo.journal.record(bookmarktype, mark, oldvalue, value)
>> +    return orig(store, fp)
>> +
>> +class journalstorage(object):
>> +    def __init__(self, repo):
>> +        self.repo = repo
>> +        self.user = util.getuser()
>> +        if repo.shared():
>> +            self.vfs = scmutil.vfs(repo.sharedpath)
>> +        else:
>> +            self.vfs = repo.vfs
>
> Hmm, I wonder how the journal will work with shared working copies. Like, we
> may need to store the '.' reference with information about the which working
> copy was moved.  We can figure that out later.
>>
>> +
>> +    # track the current command for recording in journal entries
>> +    @property
>> +    def command(self):
>> +        commandstr = ' '.join(map(util.shellquote, self._currentcommand))
>> +        if '\n' in commandstr:
>> +            # truncate multi-line commands
>> +            commandstr = commandstr.partition('\n')[0] + ' ...'
>> +        return commandstr
>> +
>> +    @command.setter
>> +    def command(self, fullargs):
>> +        self._currentcommand = fullargs
>
> Seems a bit weird that the command property is not symmetrical?

I already had to get rid of the setter; when repos switch (pull,
clone) the command was lost. I have made it a class attribute instead
with a recordcommand() classmethod. I'll send an update soon.

>> +
>> +    def record(self, namespace, name, oldhashes, newhashes):
>
> I'd add a doc comment explaining the contract of this function, so future
> people adding new journal namespaces know how to use this function
> correctly.  (for instance, call out that the hashes are expected to be in
> binary form, and that they can be a list).

Yes, good point. I'll add this in.

>>
>> +        if not isinstance(oldhashes, list):
>> +            oldhashes = [oldhashes]
>> +        if not isinstance(newhashes, list):
>> +            newhashes = [newhashes]
>> +
>> +        timestamp, tz = map(str, util.makedate())
>> +        date = ' '.join((timestamp, tz))
>> +        oldhashes = ','.join([node.hex(hash) for hash in oldhashes])
>> +        newhashes = ','.join([node.hex(hash) for hash in newhashes])
>> +        data = '\n'.join((
>> +            date, self.user, self.command, namespace, name, oldhashes,
>> +            newhashes))
>> +
>> +        with self.repo.wlock():
>> +            version = None
>> +            with self.vfs('journal', mode='a+b') as f:
>
> I wonder if we will regret naming this file the same as the store journal.
> I don't have a good alternative though.

I'm open to suggestions, I haven't been able to come up with a better
name either.

>> +                f.seek(0, os.SEEK_SET)
>> +                version = f.read(5).partition('\0')[0]
>> +                if version and version != str(storage_version):
>> +                    # different version of the storage.  Since there have
>> +                    # been no previous versions, just abort, as this can
>> +                    # only mean the file is corrupt.
>> +                    self.repo.ui.warn(
>> +                        _("unknown journal file version '%s'\n") %
>> version)
>> +                    return
>> +                if not version:
>> +                    # empty file, write version first
>> +                    f.write(str(storage_version) + '\0')
>> +                f.seek(0, os.SEEK_END)
>> +                f.write(data + '\0')
>> +
>> +    def filtered(self, namespace=None, name=None):
>> +        for entry in self:
>> +            entry_ns, entry_name = entry[3:5]
>> +            if namespace is not None and entry_ns != namespace:
>> +                continue
>> +            if name is not None and entry_name != name:
>> +                continue
>> +            yield entry
>> +
>> +    def __iter__(self):
>> +        if not self.vfs.exists('journal'):
>> +            return
>> +
>> +        with self.repo.wlock():
>> +            with self.vfs('journal') as f:
>> +                raw = f.read()
>> +
>> +        lines = raw.split('\0')
>> +        version = lines and lines[0]
>> +        if version != str(storage_version):
>> +            version = version or _('not available')
>> +            raise error.Abort(_("unknown journal file version '%s'") %
>> version)
>> +
>> +        # Skip the first line, it's a version number. Reverse the rest.
>> +        lines = reversed(lines[1:])
>> +
>> +        for line in lines:
>> +            if not line:
>> +                continue
>> +            parts = tuple(line.split('\n'))
>> +            timestamp, tz = parts[0].split()
>> +            timestamp, tz = float(timestamp), int(tz)
>> +            oldhashes, newhashes = parts[-2:]
>> +            oldhashes = oldhashes.split(',')
>> +            newhashes = newhashes.split(',')
>> +            yield ((timestamp, tz),) + parts[1:-2] + (oldhashes,
>> newhashes)
>> +
>> +# journal reading
>> + at command(
>> +    'journal', [
>> +        ('c', 'commits', None, 'show commit metadata'),
>> +    ] + commands.logopts, '[OPTION]... [BOOKMARKNAME]')
>> +def journal(ui, repo, *args, **opts):
>> +    """show the previous position of bookmarks
>> +
>> +    The journal is used to see the previous commits that bookmarks. By
>> default
>
> s/that bookmarks/of bookmarks/
>>
>> +    the previous locations for all bookmarks are shown.  Passing a
>> bookmark
>> +    name will show all the previous positions of that bookmark.
>> +
>> +    By default the reflog only shows the commit hash and the command that
>> was
>
> s/reflog/journal/
>>
>> +    running at that time. -v/--verbose will show the prior hash, the
>> user, and
>> +    the time at which it happened.
>> +
>> +    Use in -c/--commits to output log information on each commit hash.
>> +
>> +    `hg journal -T json` can be used to produce machine readable output.
>> +
>> +    """
>> +    bookmarkname = None
>> +    if args:
>> +        bookmarkname = args[0]
>> +
>> +    fm = ui.formatter('journal', opts)
>> +
>> +    if opts.get("template") != "json":
>> +        if bookmarkname is None:
>> +            name = 'all bookmarks'
>
> I think this needs localization _() around it.
>
> Also, if we add working copy tracking later, we might change this behavior
> to default to the history of the working copy.  I don't know if that would
> constitute a BC change since this extension is experimental.

I was trying to make sure this version could stand on its own.

Adding in working copy support does indeed require a different default
(the working copy being the default) and an `--all` switch. But that
just didn't make sense in this version.

>> +        else:
>> +            name = "'%s'" % bookmarkname
>> +        ui.status(_("Previous locations of %s:\n") % name)
>> +
>> +    entry = None
>> +    for count, entry in
>> enumerate(repo.journal.filtered(name=bookmarkname)):
>> +        timestamp, user, command, namespace, name, oldhashes, newhashes =
>> entry
>> +        newhashesstr = ','.join([hash[:12] for hash in newhashes])
>> +        oldhashesstr = ','.join([hash[:12] for hash in oldhashes])
>> +
>> +        fm.startitem()
>> +        fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr)
>> +        fm.write('newhashes', '%s', newhashesstr)
>> +        fm.condwrite(ui.verbose, 'user', ' %s', user.ljust(8))
>> +
>> +        timestring = util.datestr(timestamp, '%Y-%m-%d %H:%M %1%2')
>> +        fm.condwrite(ui.verbose, 'date', ' %s', timestring)
>> +        fm.write('command', '  %s\n', command)
>> +
>> +        if opts.get("commits"):
>> +            displayer = cmdutil.show_changeset(ui, repo, opts,
>> buffered=False)
>> +            for hash in newhashes:
>> +                try:
>> +                    ctx = repo[hash]
>> +                    displayer.show(ctx)
>> +                except error.RepoLookupError as e:
>> +                    fm.write('repolookuperror', "%s\n\n", str(e))
>> +            displayer.close()
>> +
>> +    fm.end()
>> +
>> +    if entry is None:
>> +        ui.status(_("no recorded locations\n"))
>
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel



-- 
Martijn Pieters


More information about the Mercurial-devel mailing list