[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