journal.py
603 lines
| 19.9 KiB
| text/x-python
|
PythonLexer
/ hgext / journal.py
|
r29443 | # 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. | ||||
|
r31600 | """track previous positions of bookmarks (EXPERIMENTAL) | ||
|
r29443 | |||
This extension adds a new command: `hg journal`, which shows you where | ||||
bookmarks were previously located. | ||||
""" | ||||
import collections | ||||
import os | ||||
|
r29502 | import weakref | ||
|
r29443 | |||
from mercurial.i18n import _ | ||||
|
r46729 | from mercurial.node import ( | ||
bin, | ||||
hex, | ||||
) | ||||
|
r29443 | |||
from mercurial import ( | ||||
bookmarks, | ||||
cmdutil, | ||||
dispatch, | ||||
|
r36684 | encoding, | ||
|
r29443 | error, | ||
extensions, | ||||
|
r29503 | hg, | ||
|
r29502 | localrepo, | ||
lock, | ||||
|
r35906 | logcmdutil, | ||
|
r35001 | pycompat, | ||
|
r32337 | registrar, | ||
|
r29443 | util, | ||
) | ||||
|
r37102 | from mercurial.utils import ( | ||
dateutil, | ||||
|
r37138 | procutil, | ||
|
r37102 | stringutil, | ||
) | ||||
|
r29443 | |||
cmdtable = {} | ||||
|
r32337 | command = registrar.command(cmdtable) | ||
|
r29443 | |||
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
|
r29443 | # 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. | ||||
|
r43347 | testedwith = b'ships-with-hg-core' | ||
|
r29443 | |||
# storage format version; increment when the format changes | ||||
storageversion = 0 | ||||
# namespaces | ||||
|
r43347 | bookmarktype = b'bookmark' | ||
wdirparenttype = b'wdirparent' | ||||
|
r29503 | # In a shared repository, what shared feature name is used | ||
# to indicate this namespace is shared with the source? | ||||
sharednamespaces = { | ||||
bookmarktype: hg.sharedbookmarks, | ||||
} | ||||
|
r29443 | |||
# Journal recording, register hooks and storage object | ||||
def extsetup(ui): | ||||
|
r43347 | extensions.wrapfunction(dispatch, b'runcommand', runcommand) | ||
extensions.wrapfunction(bookmarks.bmstore, b'_write', recordbookmarks) | ||||
|
r33384 | extensions.wrapfilecache( | ||
|
r43347 | localrepo.localrepository, b'dirstate', wrapdirstate | ||
|
r43346 | ) | ||
|
r43347 | extensions.wrapfunction(hg, b'postshare', wrappostshare) | ||
extensions.wrapfunction(hg, b'copystore', unsharejournal) | ||||
|
r29443 | |||
|
r43346 | |||
|
r29443 | def reposetup(ui, repo): | ||
if repo.local(): | ||||
repo.journal = journalstorage(repo) | ||||
|
r43347 | repo._wlockfreeprefix.add(b'namejournal') | ||
|
r29443 | |||
|
r43347 | dirstate, cached = localrepo.isfilecached(repo, b'dirstate') | ||
|
r33383 | if cached: | ||
# already instantiated dirstate isn't yet marked as | ||||
# "journal"-ing, even though repo.dirstate() was already | ||||
# wrapped by own wrapdirstate() | ||||
_setupdirstate(repo, dirstate) | ||||
|
r43346 | |||
|
r29443 | def runcommand(orig, lui, repo, cmd, fullargs, *args): | ||
"""Track the command line options for recording in the journal""" | ||||
journalstorage.recordcommand(*fullargs) | ||||
return orig(lui, repo, cmd, fullargs, *args) | ||||
|
r43346 | |||
|
r33383 | def _setupdirstate(repo, dirstate): | ||
dirstate.journalstorage = repo.journal | ||||
|
r43347 | dirstate.addparentchangecallback(b'journal', recorddirstateparents) | ||
|
r33383 | |||
|
r43346 | |||
|
r29502 | # hooks to record dirstate changes | ||
def wrapdirstate(orig, repo): | ||||
"""Make journal storage available to the dirstate object""" | ||||
dirstate = orig(repo) | ||||
|
r43385 | if util.safehasattr(repo, 'journal'): | ||
|
r33383 | _setupdirstate(repo, dirstate) | ||
|
r29502 | return dirstate | ||
|
r43346 | |||
|
r29773 | def recorddirstateparents(dirstate, old, new): | ||
|
r29502 | """Records all dirstate parent changes in the journal.""" | ||
|
r29773 | old = list(old) | ||
new = list(new) | ||||
|
r43385 | if util.safehasattr(dirstate, 'journalstorage'): | ||
|
r29773 | # only record two hashes if there was a merge | ||
|
r47771 | oldhashes = old[:1] if old[1] == dirstate._nodeconstants.nullid else old | ||
newhashes = new[:1] if new[1] == dirstate._nodeconstants.nullid else new | ||||
|
r29773 | dirstate.journalstorage.record( | ||
|
r43347 | wdirparenttype, b'.', oldhashes, newhashes | ||
|
r43346 | ) | ||
|
r29502 | |||
# hooks to record bookmark changes (both local and remote) | ||||
|
r29443 | def recordbookmarks(orig, store, fp): | ||
"""Records all bookmark changes in the journal.""" | ||||
repo = store._repo | ||||
|
r43385 | if util.safehasattr(repo, 'journal'): | ||
|
r29443 | oldmarks = bookmarks.bmstore(repo) | ||
|
r49768 | for mark, value in store.items(): | ||
|
r47771 | oldvalue = oldmarks.get(mark, repo.nullid) | ||
|
r29443 | if value != oldvalue: | ||
repo.journal.record(bookmarktype, mark, oldvalue, value) | ||||
return orig(store, fp) | ||||
|
r43346 | |||
|
r29503 | # shared repository support | ||
def _readsharedfeatures(repo): | ||||
"""A set of shared features for this repository""" | ||||
try: | ||||
|
r43347 | return set(repo.vfs.read(b'shared').splitlines()) | ||
|
r50201 | except FileNotFoundError: | ||
|
r29503 | return set() | ||
|
r43346 | |||
|
r29503 | def _mergeentriesiter(*iterables, **kwargs): | ||
"""Given a set of sorted iterables, yield the next entry in merged order | ||||
Note that by default entries go from most recent to oldest. | ||||
""" | ||||
|
r43906 | order = kwargs.pop('order', max) | ||
|
r29503 | iterables = [iter(it) for it in iterables] | ||
# this tracks still active iterables; iterables are deleted as they are | ||||
# exhausted, which is why this is a dictionary and why each entry also | ||||
# stores the key. Entries are mutable so we can store the next value each | ||||
# time. | ||||
iterable_map = {} | ||||
for key, it in enumerate(iterables): | ||||
try: | ||||
iterable_map[key] = [next(it), key, it] | ||||
except StopIteration: | ||||
# empty entry, can be ignored | ||||
pass | ||||
while iterable_map: | ||||
|
r49790 | value, key, it = order(iterable_map.values()) | ||
|
r29503 | yield value | ||
try: | ||||
iterable_map[key][0] = next(it) | ||||
except StopIteration: | ||||
# this iterable is empty, remove it from consideration | ||||
del iterable_map[key] | ||||
|
r43346 | |||
|
r29503 | def wrappostshare(orig, sourcerepo, destrepo, **kwargs): | ||
"""Mark this shared working copy as sharing journal information""" | ||||
|
r29756 | with destrepo.wlock(): | ||
orig(sourcerepo, destrepo, **kwargs) | ||||
|
r43347 | with destrepo.vfs(b'shared', b'a') as fp: | ||
fp.write(b'journal\n') | ||||
|
r29503 | |||
|
r43346 | |||
|
r29503 | def unsharejournal(orig, ui, repo, repopath): | ||
"""Copy shared journal entries into this repo when unsharing""" | ||||
|
r43346 | if ( | ||
repo.path == repopath | ||||
and repo.shared() | ||||
|
r43385 | and util.safehasattr(repo, 'journal') | ||
|
r43346 | ): | ||
|
r36177 | sharedrepo = hg.sharedreposource(repo) | ||
|
r29503 | sharedfeatures = _readsharedfeatures(repo) | ||
|
r43347 | if sharedrepo and sharedfeatures > {b'journal'}: | ||
|
r29503 | # there is a shared repository and there are shared journal entries | ||
# to copy. move shared date over from source to destination but | ||||
# move the local file first | ||||
|
r43347 | if repo.vfs.exists(b'namejournal'): | ||
journalpath = repo.vfs.join(b'namejournal') | ||||
util.rename(journalpath, journalpath + b'.bak') | ||||
|
r29503 | storage = repo.journal | ||
local = storage._open( | ||||
|
r43347 | repo.vfs, filename=b'namejournal.bak', _newestfirst=False | ||
|
r43346 | ) | ||
|
r29503 | shared = ( | ||
|
r43346 | e | ||
for e in storage._open(sharedrepo.vfs, _newestfirst=False) | ||||
if sharednamespaces.get(e.namespace) in sharedfeatures | ||||
) | ||||
|
r29503 | for entry in _mergeentriesiter(local, shared, order=min): | ||
storage._write(repo.vfs, entry) | ||||
return orig(ui, repo, repopath) | ||||
|
r43346 | |||
class journalentry( | ||||
collections.namedtuple( | ||||
|
r43906 | 'journalentry', | ||
'timestamp user command namespace name oldhashes newhashes', | ||||
|
r43346 | ) | ||
): | ||||
|
r29443 | """Individual journal entry | ||
* timestamp: a mercurial (time, timezone) tuple | ||||
* user: the username that ran the command | ||||
* namespace: the entry namespace, an opaque string | ||||
* name: the name of the changed item, opaque string with meaning in the | ||||
namespace | ||||
* command: the hg command that triggered this record | ||||
* oldhashes: a tuple of one or more binary hashes for the old location | ||||
* newhashes: a tuple of one or more binary hashes for the new location | ||||
Handles serialisation from and to the storage format. Fields are | ||||
separated by newlines, hashes are written out in hex separated by commas, | ||||
timestamp and timezone are separated by a space. | ||||
""" | ||||
|
r43346 | |||
|
r29443 | @classmethod | ||
def fromstorage(cls, line): | ||||
|
r43346 | ( | ||
time, | ||||
user, | ||||
command, | ||||
namespace, | ||||
name, | ||||
oldhashes, | ||||
newhashes, | ||||
|
r43347 | ) = line.split(b'\n') | ||
|
r29443 | timestamp, tz = time.split() | ||
timestamp, tz = float(timestamp), int(tz) | ||||
|
r46729 | oldhashes = tuple(bin(hash) for hash in oldhashes.split(b',')) | ||
newhashes = tuple(bin(hash) for hash in newhashes.split(b',')) | ||||
|
r29443 | return cls( | ||
|
r43346 | (timestamp, tz), | ||
user, | ||||
command, | ||||
namespace, | ||||
name, | ||||
oldhashes, | ||||
newhashes, | ||||
) | ||||
|
r29443 | |||
|
r36684 | def __bytes__(self): | ||
"""bytes representation for storage""" | ||||
|
r43347 | time = b' '.join(map(pycompat.bytestr, self.timestamp)) | ||
|
r46729 | oldhashes = b','.join([hex(hash) for hash in self.oldhashes]) | ||
newhashes = b','.join([hex(hash) for hash in self.newhashes]) | ||||
|
r43347 | return b'\n'.join( | ||
|
r43346 | ( | ||
time, | ||||
self.user, | ||||
self.command, | ||||
self.namespace, | ||||
self.name, | ||||
oldhashes, | ||||
newhashes, | ||||
) | ||||
) | ||||
|
r29443 | |||
|
r36684 | __str__ = encoding.strmethod(__bytes__) | ||
|
r43346 | |||
|
r49801 | class journalstorage: | ||
|
r29443 | """Storage for journal entries | ||
|
r29503 | Entries are divided over two files; one with entries that pertain to the | ||
local working copy *only*, and one with entries that are shared across | ||||
multiple working copies when shared using the share extension. | ||||
|
r29443 | Entries are stored with NUL bytes as separators. See the journalentry | ||
class for the per-entry structure. | ||||
The file format starts with an integer version, delimited by a NUL. | ||||
|
r29502 | 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). | ||||
|
r29443 | """ | ||
|
r43346 | |||
|
r29443 | _currentcommand = () | ||
|
r29502 | _lockref = None | ||
|
r29443 | |||
def __init__(self, repo): | ||||
|
r37138 | self.user = procutil.getuser() | ||
|
r29502 | self.ui = repo.ui | ||
|
r29443 | self.vfs = repo.vfs | ||
|
r29503 | # is this working copy using a shared storage? | ||
self.sharedfeatures = self.sharedvfs = None | ||||
if repo.shared(): | ||||
features = _readsharedfeatures(repo) | ||||
|
r36177 | sharedrepo = hg.sharedreposource(repo) | ||
|
r43347 | if sharedrepo is not None and b'journal' in features: | ||
|
r29503 | self.sharedvfs = sharedrepo.vfs | ||
self.sharedfeatures = features | ||||
|
r29443 | # track the current command for recording in journal entries | ||
@property | ||||
def command(self): | ||||
|
r43347 | commandstr = b' '.join( | ||
|
r43346 | map(procutil.shellquote, journalstorage._currentcommand) | ||
) | ||||
|
r43347 | if b'\n' in commandstr: | ||
|
r29443 | # truncate multi-line commands | ||
|
r43347 | commandstr = commandstr.partition(b'\n')[0] + b' ...' | ||
|
r29443 | return commandstr | ||
@classmethod | ||||
def recordcommand(cls, *fullargs): | ||||
"""Set the current hg arguments, stored with recorded entries""" | ||||
# Set the current command on the class because we may have started | ||||
# with a non-local repo (cloning for example). | ||||
cls._currentcommand = fullargs | ||||
|
r29928 | def _currentlock(self, lockref): | ||
"""Returns the lock if it's held, or None if it's not. | ||||
(This is copied from the localrepo class) | ||||
""" | ||||
if lockref is None: | ||||
return None | ||||
l = lockref() | ||||
if l is None or not l.held: | ||||
return None | ||||
return l | ||||
|
r29503 | def jlock(self, vfs): | ||
|
r29502 | """Create a lock for the journal file""" | ||
|
r29928 | if self._currentlock(self._lockref) is not None: | ||
|
r43347 | raise error.Abort(_(b'journal lock does not support nesting')) | ||
desc = _(b'journal of %s') % vfs.base | ||||
|
r29502 | try: | ||
|
r43347 | l = lock.lock(vfs, b'namejournal.lock', 0, desc=desc) | ||
|
r29502 | except error.LockHeld as inst: | ||
self.ui.warn( | ||||
|
r43347 | _(b"waiting for lock on %s held by %r\n") % (desc, inst.locker) | ||
|
r43346 | ) | ||
|
r29502 | # default to 600 seconds timeout | ||
l = lock.lock( | ||||
|
r43346 | vfs, | ||
|
r43347 | b'namejournal.lock', | ||
self.ui.configint(b"ui", b"timeout"), | ||||
|
r43346 | desc=desc, | ||
) | ||||
|
r43347 | self.ui.warn(_(b"got lock after %s seconds\n") % l.delay) | ||
|
r29502 | self._lockref = weakref.ref(l) | ||
return l | ||||
|
r29443 | def record(self, namespace, name, oldhashes, newhashes): | ||
"""Record a new journal entry | ||||
* namespace: an opaque string; this can be used to filter on the type | ||||
of recorded entries. | ||||
* name: the name defining this entry; for bookmarks, this is the | ||||
bookmark name. Can be filtered on when retrieving entries. | ||||
* oldhashes and newhashes: each a single binary hash, or a list of | ||||
binary hashes. These represent the old and new position of the named | ||||
item. | ||||
""" | ||||
if not isinstance(oldhashes, list): | ||||
oldhashes = [oldhashes] | ||||
if not isinstance(newhashes, list): | ||||
newhashes = [newhashes] | ||||
entry = journalentry( | ||||
|
r43346 | dateutil.makedate(), | ||
self.user, | ||||
self.command, | ||||
namespace, | ||||
name, | ||||
oldhashes, | ||||
newhashes, | ||||
) | ||||
|
r29443 | |||
|
r29503 | vfs = self.vfs | ||
if self.sharedvfs is not None: | ||||
# write to the shared repository if this feature is being | ||||
# shared between working copies. | ||||
if sharednamespaces.get(namespace) in self.sharedfeatures: | ||||
vfs = self.sharedvfs | ||||
self._write(vfs, entry) | ||||
def _write(self, vfs, entry): | ||||
with self.jlock(vfs): | ||||
|
r29443 | # open file in amend mode to ensure it is created if missing | ||
|
r43347 | with vfs(b'namejournal', mode=b'a+b') as f: | ||
|
r29443 | f.seek(0, os.SEEK_SET) | ||
# Read just enough bytes to get a version number (up to 2 | ||||
# digits plus separator) | ||||
|
r43347 | version = f.read(3).partition(b'\0')[0] | ||
if version and version != b"%d" % storageversion: | ||||
|
r29443 | # 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? | ||||
|
r29502 | self.ui.warn( | ||
|
r43347 | _(b"unsupported journal file version '%s'\n") % version | ||
|
r43346 | ) | ||
|
r29443 | return | ||
if not version: | ||||
# empty file, write version first | ||||
|
r43347 | f.write((b"%d" % storageversion) + b'\0') | ||
|
r29443 | f.seek(0, os.SEEK_END) | ||
|
r43347 | f.write(bytes(entry) + b'\0') | ||
|
r29443 | |||
def filtered(self, namespace=None, name=None): | ||||
"""Yield all journal entries with the given namespace or name | ||||
Both the namespace and the name are optional; if neither is given all | ||||
entries in the journal are produced. | ||||
|
r29504 | Matching supports regular expressions by using the `re:` prefix | ||
(use `literal:` to match names or namespaces that start with `re:`) | ||||
|
r29443 | """ | ||
|
r29504 | if namespace is not None: | ||
|
r37102 | namespace = stringutil.stringmatcher(namespace)[-1] | ||
|
r29504 | if name is not None: | ||
|
r37102 | name = stringutil.stringmatcher(name)[-1] | ||
|
r29443 | for entry in self: | ||
|
r29504 | if namespace is not None and not namespace(entry.namespace): | ||
|
r29443 | continue | ||
|
r29504 | if name is not None and not name(entry.name): | ||
|
r29443 | continue | ||
yield entry | ||||
def __iter__(self): | ||||
"""Iterate over the storage | ||||
Yields journalentry instances for each contained journal record. | ||||
""" | ||||
|
r29503 | local = self._open(self.vfs) | ||
if self.sharedvfs is None: | ||||
return local | ||||
# iterate over both local and shared entries, but only those | ||||
# shared entries that are among the currently shared features | ||||
shared = ( | ||||
|
r43346 | e | ||
for e in self._open(self.sharedvfs) | ||||
if sharednamespaces.get(e.namespace) in self.sharedfeatures | ||||
) | ||||
|
r29503 | return _mergeentriesiter(local, shared) | ||
|
r43347 | def _open(self, vfs, filename=b'namejournal', _newestfirst=True): | ||
|
r29503 | if not vfs.exists(filename): | ||
|
r29443 | return | ||
|
r29503 | with vfs(filename) as f: | ||
|
r29443 | raw = f.read() | ||
|
r43347 | lines = raw.split(b'\0') | ||
|
r29443 | version = lines and lines[0] | ||
|
r43347 | if version != b"%d" % storageversion: | ||
version = version or _(b'not available') | ||||
raise error.Abort(_(b"unknown journal file version '%s'") % version) | ||||
|
r29443 | |||
|
r29503 | # Skip the first line, it's a version number. Normally we iterate over | ||
# these in reverse order to list newest first; only when copying across | ||||
# a shared storage do we forgo reversing. | ||||
lines = lines[1:] | ||||
if _newestfirst: | ||||
lines = reversed(lines) | ||||
|
r29443 | for line in lines: | ||
if not line: | ||||
continue | ||||
yield journalentry.fromstorage(line) | ||||
|
r43346 | |||
|
r29443 | # journal reading | ||
# log options that don't make sense for journal | ||||
|
r43347 | _ignoreopts = (b'no-merges', b'graph') | ||
|
r43346 | |||
|
r29443 | @command( | ||
|
r43347 | b'journal', | ||
|
r43346 | [ | ||
|
r43347 | (b'', b'all', None, b'show history for all names'), | ||
(b'c', b'commits', None, b'show commit metadata'), | ||||
|
r43346 | ] | ||
+ [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts], | ||||
|
r43347 | b'[OPTION]... [BOOKMARKNAME]', | ||
|
r43346 | helpcategory=command.CATEGORY_CHANGE_ORGANIZATION, | ||
) | ||||
|
r29443 | def journal(ui, repo, *args, **opts): | ||
|
r29502 | """show the previous position of bookmarks and the working copy | ||
|
r29443 | |||
|
r29502 | 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. | ||||
|
r29443 | |||
|
r29504 | If `name` starts with `re:`, the remainder of the name is treated as | ||
a regular expression. To match a name that actually starts with `re:`, | ||||
use the prefix `literal:`. | ||||
|
r29443 | 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 | ||||
the time at which it happened. | ||||
Use -c/--commits to output log information on each commit hash; at this | ||||
point you can use the usual `--patch`, `--git`, `--stat` and `--template` | ||||
switches to alter the log output for these. | ||||
`hg journal -T json` can be used to produce machine readable output. | ||||
""" | ||||
|
r35001 | opts = pycompat.byteskwargs(opts) | ||
|
r43347 | name = b'.' | ||
if opts.get(b'all'): | ||||
|
r29502 | if args: | ||
raise error.Abort( | ||||
|
r43347 | _(b"You can't combine --all and filtering on a name") | ||
|
r43346 | ) | ||
|
r29502 | name = None | ||
|
r29443 | if args: | ||
|
r29502 | name = args[0] | ||
|
r29443 | |||
|
r43347 | fm = ui.formatter(b'journal', opts) | ||
|
r43346 | |||
|
r39738 | def formatnodes(nodes): | ||
|
r43347 | return fm.formatlist(map(fm.hexfunc, nodes), name=b'node', sep=b',') | ||
|
r29443 | |||
|
r43347 | if opts.get(b"template") != b"json": | ||
|
r29502 | if name is None: | ||
|
r43347 | displayname = _(b'the working copy and bookmarks') | ||
|
r29443 | else: | ||
|
r43347 | displayname = b"'%s'" % name | ||
ui.status(_(b"previous locations of %s:\n") % displayname) | ||||
|
r29443 | |||
|
r35906 | limit = logcmdutil.getlimit(opts) | ||
|
r29443 | entry = None | ||
|
r43347 | ui.pager(b'journal') | ||
|
r29502 | for count, entry in enumerate(repo.journal.filtered(name=name)): | ||
|
r29443 | if count == limit: | ||
break | ||||
fm.startitem() | ||||
|
r43346 | fm.condwrite( | ||
|
r43347 | ui.verbose, b'oldnodes', b'%s -> ', formatnodes(entry.oldhashes) | ||
|
r43346 | ) | ||
|
r43347 | fm.write(b'newnodes', b'%s', formatnodes(entry.newhashes)) | ||
fm.condwrite(ui.verbose, b'user', b' %-8s', entry.user) | ||||
|
r29504 | fm.condwrite( | ||
|
r43347 | opts.get(b'all') or name.startswith(b're:'), | ||
b'name', | ||||
b' %-8s', | ||||
|
r43346 | entry.name, | ||
) | ||||
|
r29443 | |||
|
r43346 | fm.condwrite( | ||
ui.verbose, | ||||
|
r43347 | b'date', | ||
b' %s', | ||||
fm.formatdate(entry.timestamp, b'%Y-%m-%d %H:%M %1%2'), | ||||
|
r43346 | ) | ||
|
r43347 | fm.write(b'command', b' %s\n', entry.command) | ||
|
r29443 | |||
|
r43347 | if opts.get(b"commits"): | ||
|
r39740 | if fm.isplain(): | ||
displayer = logcmdutil.changesetdisplayer(ui, repo, opts) | ||||
else: | ||||
displayer = logcmdutil.changesetformatter( | ||||
|
r43347 | ui, repo, fm.nested(b'changesets'), diffopts=opts | ||
|
r43346 | ) | ||
|
r29443 | for hash in entry.newhashes: | ||
try: | ||||
ctx = repo[hash] | ||||
displayer.show(ctx) | ||||
except error.RepoLookupError as e: | ||||
|
r43347 | fm.plain(b"%s\n\n" % pycompat.bytestr(e)) | ||
|
r29443 | displayer.close() | ||
fm.end() | ||||
if entry is None: | ||||
|
r43347 | ui.status(_(b"no recorded locations\n")) | ||