sapling/hgext/smartlog.py

783 lines
25 KiB
Python
Raw Normal View History

2013-06-20 23:16:36 +04:00
# smartlog.py
#
# Copyright 2013 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.
"""command to display a relevant subgraph
With this extension installed, Mercurial gains one new command: smartlog.
It displays a subgraph of changesets containing only the changesets relevant
to the user.
::
[smartlog]
# (remote) names to show
repos = , remote/, default/
names = @, master, stable
# move the top non-public stack to the second column
indentnonpublic = True
# whether to use ancestor cache (speed up on huge repos)
useancestorcache = False
"""
from __future__ import absolute_import
import contextlib
import datetime
import itertools
import re
import time
2013-06-20 23:16:36 +04:00
from mercurial import (
bookmarks,
cmdutil,
commands,
dagop,
error,
extensions,
graphmod,
node as nodemod,
obsutil,
phases,
registrar,
revlog,
revset,
revsetlang,
scmutil,
smartset,
templatekw,
templater,
util,
)
from mercurial.i18n import _
try:
# gdbm is preferred for its performance
import gdbm as dbm
dbm.open
except ImportError:
# fallback to anydbm
import anydbm as dbm
2013-06-20 23:16:36 +04:00
cmdtable = {}
command = registrar.command(cmdtable)
testedwith = "ships-with-fb-hgext"
commit_info = False
hiddenchanges = 0
2013-06-20 23:16:36 +04:00
# Remove unsupported --limit option.
logopts = [opt for opt in commands.logopts if opt[1] != "limit"]
flake8: enable F821 check Summary: This check is useful and detects real errors (ex. fbconduit). Unfortunately `arc lint` will run it with both py2 and py3 so a lot of py2 builtins will still be warned. I didn't find a clean way to disable py3 check. So this diff tries to fix them. For `xrange`, the change was done by a script: ``` import sys import redbaron headertypes = {'comment', 'endl', 'from_import', 'import', 'string', 'assignment', 'atomtrailers'} xrangefix = '''try: xrange(0) except NameError: xrange = range ''' def isxrange(x): try: return x[0].value == 'xrange' except Exception: return False def main(argv): for i, path in enumerate(argv): print('(%d/%d) scanning %s' % (i + 1, len(argv), path)) content = open(path).read() try: red = redbaron.RedBaron(content) except Exception: print(' warning: failed to parse') continue hasxrange = red.find('atomtrailersnode', value=isxrange) hasxrangefix = 'xrange = range' in content if hasxrangefix or not hasxrange: print(' no need to change') continue # find a place to insert the compatibility statement changed = False for node in red: if node.type in headertypes: continue # node.insert_before is an easier API, but it has bugs changing # other "finally" and "except" positions. So do the insert # manually. # # node.insert_before(xrangefix) line = node.absolute_bounding_box.top_left.line - 1 lines = content.splitlines(1) content = ''.join(lines[:line]) + xrangefix + ''.join(lines[line:]) changed = True break if changed: # "content" is faster than "red.dumps()" open(path, 'w').write(content) print(' updated') if __name__ == "__main__": sys.exit(main(sys.argv[1:])) ``` For other py2 builtins that do not have a py3 equivalent, some `# noqa` were added as a workaround for now. Reviewed By: DurhamG Differential Revision: D6934535 fbshipit-source-id: 546b62830af144bc8b46788d2e0fd00496838939
2018-02-10 04:31:44 +03:00
try:
xrange(0)
except NameError:
xrange = range
@contextlib.contextmanager
def ancestorcache(path):
# simple cache to speed up revlog.ancestors
try:
db = dbm.open(path, "c")
except dbm.error:
# database locked, fail gracefully
yield
else:
def revlogancestor(orig, self, a, b):
key = a + b
try:
return db[key]
except KeyError:
result = orig(self, a, b)
db[key] = result
return result
extensions.wrapfunction(revlog.revlog, "ancestor", revlogancestor)
try:
yield
finally:
extensions.unwrapfunction(revlog.revlog, "ancestor", revlogancestor)
try:
db.close()
except Exception:
# database corruption, we just nuke the database
util.tryunlink(path)
def _drawendinglines(orig, lines, extra, edgemap, seen):
# if we are going to have only one single column, draw the missing '|'s
# and restore everything to normal. see comment in 'ascii' below for an
# example of what will be changed. note: we do not respect 'graphstyle'
# but always draw '|' here, for simplicity.
if len(seen) == 1 or any(l[0:2] != [" ", " "] for l in lines):
# draw '|' from bottom to top in the 1st column to connect to
# something, like a '/' in the 2nd column, or a '+' in the 1st column.
for line in reversed(lines):
if line[0:2] != [" ", " "]:
break
line[0] = "|"
# undo the wrapfunction
extensions.unwrapfunction(graphmod, "_drawendinglines", _drawendinglines)
# restore the space to '|'
for k, v in edgemap.iteritems():
if v == " ":
edgemap[k] = "|"
orig(lines, extra, edgemap, seen)
2013-06-20 23:16:36 +04:00
def uisetup(ui):
# Hide output for fake nodes
def show(orig, self, ctx, *args):
2013-06-20 23:16:36 +04:00
if ctx.node() == "...":
self.ui.write("\n\n\n")
2013-06-20 23:16:36 +04:00
return
res = orig(self, ctx, *args)
if commit_info and ctx == self.repo["."]:
changes = ctx.p1().status(ctx)
prefixes = ["M", "A", "R", "!", "?", "I", "C"]
for prefix, change in zip(prefixes, changes):
for fname in change:
self.ui.write(" {0} {1}\n".format(prefix, fname))
self.ui.write("\n")
return res
2013-06-20 23:16:36 +04:00
extensions.wrapfunction(cmdutil.changeset_printer, "_show", show)
extensions.wrapfunction(cmdutil.changeset_templater, "_show", show)
2013-06-20 23:16:36 +04:00
def ascii(orig, ui, state, type, char, text, coldata):
if type == "F":
# the fake node is used to move draft changesets to the 2nd column.
# there can be at most one fake node, which should also be at the
# top of the graph.
# we should not draw the fake node and its edges, so change its
# edge style to a space, and return directly.
# these are very hacky but it seems to work well and it seems there
# is no other easy choice for now.
edgemap = state["edges"]
for k in edgemap.iterkeys():
edgemap[k] = " "
# also we need to hack _drawendinglines to draw the missing '|'s:
# (before) (after)
# o draft o draft
# / /
# |
# o o
extensions.wrapfunction(graphmod, "_drawendinglines", _drawendinglines)
return
orig(ui, state, type, char, text, coldata)
extensions.wrapfunction(graphmod, "ascii", ascii)
revset.symbols["smartlog"] = smartlogrevset
revset.safesymbols.add("smartlog")
2013-06-20 23:16:36 +04:00
templatekeyword = registrar.templatekeyword()
templatefunc = registrar.templatefunc()
@templatekeyword("singlepublicsuccessor")
def singlepublicsuccessor(repo, ctx, templ, **args):
"""String. Get a single public successor for a
given node. If there's none or more than one, return empty string.
This is intended to be used for "Landed as" marking
in `hg sl` output."""
successorssets = obsutil.successorssets(repo, ctx.node())
unfiltered = repo.unfiltered()
ctxs = (unfiltered[n] for n in itertools.chain.from_iterable(successorssets))
public = (c.hex() for c in ctxs if not c.mutable() and c != ctx)
first = next(public, "")
second = next(public, "")
return "" if first and second else first
@templatekeyword("obsshelveenabled")
def obsshelveenabled(repo, ctx, **args):
"""Bool. Return true if obsshelve extension is enabled"""
return "obsshelve" in extensions.enabled().keys()
@templatekeyword("rebasesuccessors")
def rebasesuccessors(repo, ctx, **args):
"""Return all of the node's successors created as a result of rebase"""
rsnodes = list(modifysuccessors(ctx, "rebase"))
return templatekw.showlist("rebasesuccessor", rsnodes, args)
@templatekeyword("amendsuccessors")
def amendsuccessors(repo, ctx, **args):
"""Return all of the node's successors created as a result of amend"""
asnodes = list(modifysuccessors(ctx, "amend"))
return templatekw.showlist("amendsuccessor", asnodes, args)
@templatekeyword("splitsuccessors")
def splitsuccessors(repo, ctx, **args):
"""Return all of the node's successors created as a result of split"""
asnodes = list(modifysuccessors(ctx, "split"))
return templatekw.showlist("splitsuccessor", asnodes, args)
@templatekeyword("foldsuccessors")
def foldsuccessors(repo, ctx, **args):
"""Return all of the node's successors created as a result of fold"""
asnodes = list(modifysuccessors(ctx, "fold"))
return templatekw.showlist("foldsuccessor", asnodes, args)
@templatekeyword("histeditsuccessors")
def histeditsuccessors(repo, ctx, **args):
"""Return all of the node's successors created as a result of
histedit
"""
asnodes = list(modifysuccessors(ctx, "histedit"))
return templatekw.showlist("histeditsuccessor", asnodes, args)
@templatekeyword("undosuccessors")
def undosuccessors(repo, ctx, **args):
"""Return all of the node's successors created as a result of undo"""
asnodes = list(modifysuccessors(ctx, "undo"))
return templatekw.showlist("undosuccessor", asnodes, args)
def successormarkers(ctx):
for data in ctx.repo().obsstore.successors.get(ctx.node(), ()):
yield obsutil.marker(ctx.repo(), data)
def modifysuccessors(ctx, operation):
"""Return all of the node's successors which were created as a result
of a given modification operation"""
repo = ctx.repo().filtered("visible")
for m in successormarkers(ctx):
if m.metadata().get("operation") == operation:
for node in m.succnodes():
try:
repo[node]
except Exception:
# filtered or unknown node
pass
else:
yield nodemod.hex(node)
2013-06-20 23:16:36 +04:00
def sortnodes(nodes, parentfunc, masters):
"""Topologically sorts the nodes, using the parentfunc to find
the parents of nodes. Given a topological tie between children,
any node in masters is chosen last."""
nodes = set(nodes)
childmap = {}
parentmap = {}
roots = []
# Build a child and parent map
for n in nodes:
parents = [p for p in parentfunc(n) if p in nodes]
parentmap[n] = set(parents)
for p in parents:
childmap.setdefault(p, set()).add(n)
if not parents or (len(parents) == 1 and parents[0] == -1) and n != -1:
2013-06-20 23:16:36 +04:00
roots.append(n)
def childsortkey(x):
# Process children in the master line last. This makes them always
# appear on the left side of the dag, resulting in a nice straight
# master line in the ascii output. Otherwise show the oldest first, so
# the graph is approximately in chronological order.
return (x in masters, x)
2013-06-20 23:16:36 +04:00
# Process roots, adding children to the queue as they become roots
results = []
while roots:
n = roots.pop(0)
results.append(n)
if n in childmap:
children = list(childmap[n])
# reverse=True here because we insert(0) below, resulting
# in a reversed insertion of the children.
children = sorted(children, reverse=True, key=childsortkey)
2013-06-20 23:16:36 +04:00
for c in children:
childparents = parentmap[c]
childparents.remove(n)
if len(childparents) == 0:
# insert at the beginning, that way child nodes
# are likely to be output immediately after their
# parents.
roots.insert(0, c)
return results
def getdag(ui, repo, revs, master):
2013-06-20 23:16:36 +04:00
# Fake ctx that we stick in the dag so we can special case it later
class fakectx(object):
def __init__(self, rev):
self._rev = rev
2013-06-20 23:16:36 +04:00
def node(self):
return "..."
2013-06-20 23:16:36 +04:00
def obsolete(self):
return False
def phase(self):
return None
2013-06-20 23:16:36 +04:00
def rev(self):
return self._rev
2013-06-20 23:16:36 +04:00
def files(self):
return []
2015-03-13 19:29:29 +03:00
def closesbranch(self):
return False
2013-06-20 23:16:36 +04:00
knownrevs = set(revs)
gpcache = {}
results = []
# we store parents together with the parent type information
# but sometimes we need just a list of parents
# [(a,b), (c,d), (e,f)] => [b, d, f]
def unzip(parents):
if parents:
return list(zip(*parents)[1])
else:
return list()
2013-06-20 23:16:36 +04:00
# For each rev we need to show, compute it's parents in the dag.
# If we have to reach for a grandparent, insert a fake node so we
# can show '...' in the graph.
# Use 'reversed' to start at the lowest commit so fake nodes are
# placed at their lowest possible positions.
for rev in reversed(revs):
ctx = repo[rev]
# Parents in the dag
parents = sorted(
set(
[
(graphmod.PARENT, p.rev())
for p in ctx.parents()
if p.rev() in knownrevs
]
)
)
2013-06-20 23:16:36 +04:00
# Parents not in the dag
mpars = [
p.rev()
for p in ctx.parents()
if p.rev() != nodemod.nullrev and p.rev() not in unzip(parents)
]
2013-06-20 23:16:36 +04:00
for mpar in mpars:
gp = gpcache.get(mpar)
if gp is None:
gp = gpcache[mpar] = dagop.reachableroots(
repo, smartset.baseset(revs), [mpar]
)
2013-06-20 23:16:36 +04:00
if not gp:
parents.append((graphmod.MISSINGPARENT, mpar))
2013-06-20 23:16:36 +04:00
else:
gp = [g for g in gp if g not in unzip(parents)]
2013-06-20 23:16:36 +04:00
for g in gp:
parents.append((graphmod.GRANDPARENT, g))
2013-06-20 23:16:36 +04:00
results.append((ctx.rev(), "C", ctx, parents))
2013-06-20 23:16:36 +04:00
# Compute parent rev->parents mapping
lookup = {}
for r in results:
lookup[r[0]] = unzip(r[3])
2013-06-20 23:16:36 +04:00
def parentfunc(node):
return lookup.get(node, [])
# Compute the revs on the master line. We use this for sorting later.
masters = set()
queue = [master]
while queue:
m = queue.pop()
if m not in masters:
2013-06-20 23:16:36 +04:00
masters.add(m)
queue.extend(lookup.get(m, []))
# Topologically sort the noderev numbers. Note: unlike the vanilla
# topological sorting, we move master to the top.
order = sortnodes([r[0] for r in results], parentfunc, masters)
order = dict((e[1], e[0]) for e in enumerate(order))
2013-06-20 23:16:36 +04:00
# Sort the actual results based on their position in the 'order'
try:
results.sort(key=lambda x: order[x[0]], reverse=True)
except ValueError: # Happened when 'order' is empty
msg = _("note: smartlog encountered an error\n")
hint = _("(so the sorting might be wrong.\n\n)")
ui.warn(msg)
ui.warn(hint)
results.reverse()
# indent the top non-public stack
if ui.configbool("smartlog", "indentnonpublic", False):
rev, ch, ctx, parents = results[0]
if ctx.phase() != phases.public:
# find a public parent and add a fake node, so the non-public nodes
# will be shown in the non-first column
prev = None
for i in xrange(1, len(results)):
pctx = results[i][2]
if pctx.phase() == phases.public:
prev = results[i][0]
break
# append the fake node to occupy the first column
if prev:
fakerev = rev + 1
results.insert(0, (fakerev, "F", fakectx(fakerev), [("P", prev)]))
return results
2013-06-20 23:16:36 +04:00
def _masterrevset(ui, repo, masterstring):
"""
Try to find the name of ``master`` -- usually a bookmark.
Defaults to the last public revision, if no suitable local or remote
bookmark is found.
"""
if not masterstring:
masterstring = ui.config("smartlog", "master")
if masterstring:
return masterstring
names = set(bookmarks.bmstore(repo).keys())
if util.safehasattr(repo, "names") and "remotebookmarks" in repo.names:
names.update(set(repo.names["remotebookmarks"].listnames(repo)))
for name in _reposnames(ui):
if name in names:
return name
return "last(public())"
def _reposnames(ui):
# '' is local repo. This also defines an order precedence for master.
repos = ui.configlist("smartlog", "repos", ["", "remote/", "default/"])
names = ui.configlist("smartlog", "names", ["@", "master", "stable"])
for repo in repos:
for name in names:
yield repo + name
def _masterrev(repo, masterrevset):
try:
master = scmutil.revsingle(repo, masterrevset)
except error.RepoLookupError:
master = scmutil.revsingle(repo, _masterrevset(repo.ui, repo, ""))
except error.Abort: # empty revision set
return None
if master:
return master.rev()
return None
def smartlogrevset(repo, subset, x):
"""``smartlog([master], [recentdays=N])``
Changesets relevent to you.
'master' is the head of the public branch.
Unnamed heads will be hidden unless it's within 'recentdays'.
"""
args = revset.getargsdict(x, "smartlogrevset", "master recentdays")
if "master" in args:
masterstring = revsetlang.getstring(
args["master"], _("master must be a string")
)
else:
masterstring = ""
recentdays = revsetlang.getinteger(
args.get("recentdays"), _("recentdays should be int"), -1
)
revs = set()
heads = set()
rev = repo.changelog.rev
ancestor = repo.changelog.ancestor
node = repo.changelog.node
parentrevs = repo.changelog.parentrevs
books = bookmarks.bmstore(repo)
ignore = re.compile(repo.ui.config("smartlog", "ignorebookmarks", "!"))
for b in books:
if not ignore.match(b):
heads.add(rev(books[b]))
# add 'interesting' remote bookmarks as well
remotebooks = set()
if util.safehasattr(repo, "names") and "remotebookmarks" in repo.names:
ns = repo.names["remotebookmarks"]
remotebooks = set(ns.listnames(repo))
for name in _reposnames(repo.ui):
if name in remotebooks:
heads.add(rev(ns.namemap(repo, name)[0]))
heads.update(repo.revs("."))
global hiddenchanges
headquery = "head()"
if remotebooks:
# When we have remote bookmarks, only show draft heads, since public
# heads should have a remote bookmark indicating them. This allows us
# to force push server bookmarks to new locations, and not have the
# commits clutter the user's smartlog.
headquery = "heads(draft())"
allheads = set(repo.revs(headquery))
if recentdays >= 0:
recentquery = revsetlang.formatspec("%r & date(-%d)", headquery, recentdays)
recentrevs = set(repo.revs(recentquery))
hiddenchanges += len(allheads - heads) - len(recentrevs - heads)
heads.update(recentrevs)
else:
heads.update(allheads)
masterrevset = _masterrevset(repo.ui, repo, masterstring)
masterrev = _masterrev(repo, masterrevset)
if masterrev is None:
masterrev = repo["tip"].rev()
masternode = node(masterrev)
# Find all draft ancestors and latest public ancestor of heads
# that are not in master.
# We don't want to draw all public commits because there can be too
# many of them.
# Don't use revsets, they are too slow
for head in heads:
anc = rev(ancestor(node(head), masternode))
queue = [head]
while queue:
current = queue.pop(0)
if current not in revs:
revs.add(current)
# stop as soon as we find public commit
ispublic = repo[current].phase() == phases.public
if current != anc and not ispublic:
parents = parentrevs(current)
for p in parents:
if p >= anc:
queue.append(p)
# add context: master, current commit, and the common ancestor
revs.add(masterrev)
return subset & revs
@templatefunc("simpledate(date[, tz])")
def simpledate(context, mapping, args):
"""Date. Returns a human-readable date/time that is simplified for
dates and times in the recent past.
"""
ctx = mapping["ctx"]
repo = ctx.repo()
date = templater.evalfuncarg(context, mapping, args[0])
tz = None
if len(args) == 2:
tzname = templater.evalstring(context, mapping, args[1])
if tzname:
try:
import pytz
tz = pytz.timezone(tzname)
except ImportError:
msg = "Couldn't import pytz, using default time zone\n"
repo.ui.warn(msg)
except pytz.UnknownTimeZoneError:
msg = "Unknown time zone: %s\n" % tzname
repo.ui.warn(msg)
then = datetime.datetime.fromtimestamp(date[0], tz)
now = datetime.datetime.now(tz)
td = now.date() - then.date()
if then > now:
# Time is in the future, render it in full
return then.strftime("%Y-%m-%d %H:%M")
elif td.days == 0:
# Today ("Today at HH:MM")
return then.strftime("Today at %H:%M")
elif td.days == 1:
# Yesterday ("Yesterday at HH:MM")
return then.strftime("Yesterday at %H:%M")
elif td.days <= 6:
# In the last week (e.g. "Monday at HH:MM")
return then.strftime("%A at %H:%M")
elif now.year == then.year or td.days <= 90:
# This year or in the last 3 months (e.g. "Jan 05 at HH:MM")
return then.strftime("%b %d at %H:%M")
else:
# Before, render it in full
return then.strftime("%Y-%m-%d %H:%M")
@templatefunc("smartdate(date, threshold, recent, other)")
def smartdate(context, mapping, args):
"""Date. Returns one of two values depending on whether the date provided
is in the past and recent or not."""
date = templater.evalfuncarg(context, mapping, args[0])
threshold = templater.evalinteger(context, mapping, args[1])
now = time.time()
then = date[0]
if now - threshold <= then <= now:
return templater.evalstring(context, mapping, args[2])
else:
return templater.evalstring(context, mapping, args[3])
@command(
"^smartlog|slog",
[
("", "master", "", _("master bookmark"), _("BOOKMARK")),
("r", "rev", [], _("show the specified revisions or range"), _("REV")),
("", "all", False, _("don't hide old local changesets"), ""),
("", "commit-info", False, _("show changes in current changeset"), ""),
]
+ logopts,
_("hg smartlog|slog"),
)
def smartlog(ui, repo, *pats, **opts):
"""displays the graph of changesets that are relevant to you
Includes:
- Your bookmarks
- The @ or master bookmark (or tip if no bookmarks present).
- Your local heads that don't have bookmarks.
Excludes:
- All changesets under @/master/tip that aren't related to your changesets.
- Your local heads that are older than 2 weeks."""
if ui.configbool("smartlog", "useancestorcache"):
cachevfs = repo.cachevfs
# The cache directory must exist before we pass the db path to
# ancestorcache.
if not cachevfs.exists(""):
cachevfs.makedir()
with ancestorcache(cachevfs.join("smartlog-ancestor.db")):
return _smartlog(ui, repo, *pats, **opts)
else:
return _smartlog(ui, repo, *pats, **opts)
def _smartlog(ui, repo, *pats, **opts):
masterstring = opts.get("master")
masterrevset = _masterrevset(ui, repo, masterstring)
2013-06-20 23:16:36 +04:00
revs = set()
2013-06-20 23:16:36 +04:00
global hiddenchanges
hiddenchanges = 0
global commit_info
commit_info = opts.get("commit_info")
if not opts.get("rev"):
if opts.get("all"):
recentdays = -1
2013-06-20 23:16:36 +04:00
else:
recentdays = 14
masterrev = _masterrev(repo, masterrevset)
revstring = revsetlang.formatspec(
"smartlog(%s, %s)", masterrev or "", recentdays
)
revs.update(scmutil.revrange(repo, [revstring]))
else:
revs.update(scmutil.revrange(repo, opts.get("rev")))
masterrev = _masterrev(repo, masterrevset)
if masterrev not in revs:
try:
masterrev = repo.revs(".").first()
except error.RepoLookupError:
masterrev = revs[0]
if -1 in revs:
revs.remove(-1)
# It's important that these function caches come after the revsets above,
# because the revsets may cause extra nodes to become visible, which in
# turn invalidates the changelog instance.
rev = repo.changelog.rev
ancestor = repo.changelog.ancestor
node = repo.changelog.node
2016-03-31 22:43:16 +03:00
# Find lowest common ancestors of revs. If we have multiple roots in the
# repo the following will find one ancestor per group of revs with the
# same root.
ancestors = set()
for r in revs:
added = False
for anc in list(ancestors):
lca = rev(ancestor(node(anc), node(r)))
if lca != -1:
if anc != lca:
ancestors.discard(anc)
ancestors.add(lca)
added = True
if not added:
ancestors.add(r)
revs |= ancestors
2013-06-20 23:16:36 +04:00
revs = sorted(list(revs), reverse=True)
2014-03-22 03:28:00 +04:00
if len(revs) == 0:
return
2013-06-20 23:16:36 +04:00
# Print it!
overrides = {}
if ui.config("experimental", "graphstyle.grandparent", "2.") == "|":
overrides[("experimental", "graphstyle.grandparent")] = "2."
with ui.configoverride(overrides, "smartlog"):
revdag = getdag(ui, repo, revs, masterrev)
displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
ui.pager("smartlog")
cmdutil.displaygraph(
ui, repo, revdag, displayer, graphmod.asciiedges, None, None
)
try:
with open(repo.localvfs.join("completionhints"), "w+") as f:
for rev in revdag:
commit_hash = rev[2].node()
# Skip fakectxt nodes
if commit_hash != "...":
f.write(nodemod.short(commit_hash) + "\n")
except IOError:
# No write access. No big deal.
pass
if hiddenchanges:
msg = _("note: hiding %s old heads without bookmarks\n") % hiddenchanges
hint = _("(use --all to see them)\n")
ui.warn(msg)
ui.warn(hint)