mirror of
https://github.com/facebook/sapling.git
synced 2024-10-12 01:39:21 +03:00
f0dcadfa28
Summary: The old code basically selects ancestors of heads. Rewrite the logic using revsets. Assuming we're only interested in ancestors that are drafts, we can take advantage of `draft() & ::x` optimization. The new logic also assumes master rev is public. Otherwise it can be slightly different from the old logic. The new code is much faster on my repo: New code: quark@devvm33994 ~/fbcode/scm/hg % ./hg log -r 'smartlog()' --hidden -T . --time | wc -c time: real 0.630 secs (user 0.550+0.000 sys 0.030+0.000) 6716 Old code: quark@devvm33994 ~/fbcode/scm/hg % hg.real log -r 'smartlog()' --hidden -T . --time | wc -c time: real 5.470 secs (user 3.920+0.000 sys 1.550+0.000) 6716 This might make the ancestorcache hack (D5135746) unnecessary. Reviewed By: DurhamG, sfilipco Differential Revision: D14436008 fbshipit-source-id: 3c3bf47ccb67ea0e238542995009da9b9250b43b
790 lines
25 KiB
Python
790 lines
25 KiB
Python
# 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
|
|
|
|
# Data. Adjust "recentdays" so that draft commits before "hide-before" are
|
|
# hidden. This is used to migrate away from the "recent days" behavior and
|
|
# eventually show all visible commits.
|
|
hide-before = 2019-2-22
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import contextlib
|
|
import datetime
|
|
import itertools
|
|
import re
|
|
import time
|
|
|
|
from edenscm.mercurial import (
|
|
bookmarks,
|
|
cmdutil,
|
|
commands,
|
|
dagop,
|
|
error,
|
|
extensions,
|
|
graphmod,
|
|
mutation,
|
|
node as nodemod,
|
|
obsutil,
|
|
phases,
|
|
registrar,
|
|
revlog,
|
|
revset,
|
|
revsetlang,
|
|
scmutil,
|
|
smartset,
|
|
templatekw,
|
|
templater,
|
|
util,
|
|
)
|
|
from edenscm.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
|
|
|
|
cmdtable = {}
|
|
command = registrar.command(cmdtable)
|
|
revsetpredicate = registrar.revsetpredicate()
|
|
|
|
testedwith = "ships-with-fb-hgext"
|
|
commit_info = False
|
|
hiddenchanges = 0
|
|
|
|
# Remove unsupported --limit option.
|
|
logopts = [opt for opt in commands.logopts if opt[1] != "limit"]
|
|
|
|
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)
|
|
|
|
|
|
def uisetup(ui):
|
|
# Hide output for fake nodes
|
|
def show(orig, self, ctx, *args):
|
|
if ctx.node() == "...":
|
|
self.ui.write("\n\n\n")
|
|
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
|
|
|
|
extensions.wrapfunction(cmdutil.changeset_printer, "_show", show)
|
|
extensions.wrapfunction(cmdutil.changeset_templater, "_show", show)
|
|
|
|
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)
|
|
|
|
|
|
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."""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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("shelveenabled")
|
|
def shelveenabled(repo, ctx, **args):
|
|
"""Bool. Return true if shelve extension is enabled"""
|
|
return "shelve" in extensions.enabled().keys()
|
|
|
|
|
|
@templatekeyword("rebasesuccessors")
|
|
def rebasesuccessors(repo, ctx, **args):
|
|
"""Return all of the node's successors created as a result of rebase"""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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"""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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"""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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"""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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
|
|
"""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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"""
|
|
if mutation.enabled(repo):
|
|
return ""
|
|
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)
|
|
|
|
|
|
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:
|
|
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)
|
|
|
|
# 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)
|
|
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):
|
|
|
|
# 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
|
|
|
|
def node(self):
|
|
return "..."
|
|
|
|
def obsolete(self):
|
|
return False
|
|
|
|
def invisible(self):
|
|
return False
|
|
|
|
def phase(self):
|
|
return None
|
|
|
|
def rev(self):
|
|
return self._rev
|
|
|
|
def files(self):
|
|
return []
|
|
|
|
def closesbranch(self):
|
|
return False
|
|
|
|
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()
|
|
|
|
# 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
|
|
]
|
|
)
|
|
)
|
|
# 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)
|
|
]
|
|
|
|
for mpar in mpars:
|
|
gp = gpcache.get(mpar)
|
|
if gp is None:
|
|
gp = gpcache[mpar] = dagop.reachableroots(
|
|
repo, smartset.baseset(revs), [mpar]
|
|
)
|
|
if not gp:
|
|
parents.append((graphmod.MISSINGPARENT, mpar))
|
|
else:
|
|
gp = [g for g in gp if g not in unzip(parents)]
|
|
for g in gp:
|
|
parents.append((graphmod.GRANDPARENT, g))
|
|
|
|
results.append((ctx.rev(), "C", ctx, parents))
|
|
|
|
# Compute parent rev->parents mapping
|
|
lookup = {}
|
|
for r in results:
|
|
lookup[r[0]] = unzip(r[3])
|
|
|
|
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:
|
|
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))
|
|
|
|
# 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
|
|
|
|
|
|
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
|
|
|
|
|
|
@revsetpredicate("smartlog([master], [recentdays=N])")
|
|
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
|
|
)
|
|
|
|
heads = set(repo.revs("interestingbookmarks() + ."))
|
|
|
|
global hiddenchanges
|
|
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()
|
|
if masterrev == nodemod.nullrev:
|
|
# repo is empty
|
|
return smartset.baseset()
|
|
|
|
# Remove "null" commit. "::x" does not support it.
|
|
if nodemod.nullrev in heads:
|
|
heads.remove(nodemod.nullrev)
|
|
# Select ancestors that are draft.
|
|
drafts = repo.revs("draft() & ::%ld", heads)
|
|
# Include parents of drafts, and public heads.
|
|
revs = repo.revs("parents(%ld) + %ld + %ld + %d", drafts, drafts, heads, masterrev)
|
|
|
|
return subset & revs
|
|
|
|
|
|
@revsetpredicate("interestingbookmarks()")
|
|
def interestingheads(repo, subset, x):
|
|
"""Set of interesting bookmarks (local and remote)"""
|
|
rev = repo.changelog.rev
|
|
heads = set()
|
|
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]))
|
|
|
|
return subset & smartset.baseset(heads)
|
|
|
|
|
|
@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,
|
|
_("[OPTION]... [[-r] REV]"),
|
|
)
|
|
def smartlog(ui, repo, *pats, **opts):
|
|
"""show a graph of the commits that are relevant to you
|
|
|
|
Includes:
|
|
|
|
- Your local commits
|
|
- The master bookmark for your repository
|
|
- Any commits with local bookmarks
|
|
|
|
Excludes:
|
|
|
|
- All commits under master that aren't related to your commits
|
|
- Your local commits that are older than a specified date"""
|
|
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)
|
|
|
|
revs = set()
|
|
|
|
global hiddenchanges
|
|
hiddenchanges = 0
|
|
|
|
global commit_info
|
|
commit_info = opts.get("commit_info")
|
|
|
|
if not opts.get("rev"):
|
|
if opts.get("all"):
|
|
recentdays = -1
|
|
else:
|
|
before = ui.config("smartlog", "hide-before")
|
|
if before:
|
|
date = util.parsedate(before)[0]
|
|
recentdays = (int(time.time()) - date) // (3600 * 24)
|
|
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
|
|
|
|
# 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
|
|
|
|
revs = sorted(list(revs), reverse=True)
|
|
|
|
if len(revs) == 0:
|
|
return
|
|
|
|
# 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)
|