Initial commit: fbamend, smartlog

This commit is contained in:
Durham Goode 2013-06-20 12:16:36 -07:00
commit 26387ef0c1
5 changed files with 485 additions and 0 deletions

4
.hgignore Normal file
View File

@ -0,0 +1,4 @@
syntax: regexp
\.pyc$
\.so$
^build/

24
Makefile Normal file
View File

@ -0,0 +1,24 @@
PYTHON=python
.PHONY: tests
PREFIX=/usr/local
help:
@echo 'Commonly used make targets:'
@echo ' local - build for inplace use'
@echo ' install - install program and man pages to PREFIX ($(PREFIX))'
@echo ' clean - remove files created by other targets'
@echo ' (except installed files or dist source tarball)'
local:
$(PYTHON) setup.py \
build_py -c -d . \
build_ext -i
install:
$(PYTHON) setup.py $(PURE) install --prefix="$(PREFIX)" --force
clean:
-$(PYTHON) setup.py clean --all # ignore errors from this command
find . \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'

177
hgext/fbamend.py Normal file
View File

@ -0,0 +1,177 @@
# fbamend.py - improved amend functionality
#
# 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.
"""extends the existing commit amend functionality
Adds an hg amend command that amends the current parent commit with the
changes in the working copy. Similiar to the existing hg commit --amend
except it doesn't prompt for the commit message unless --edit is provided.
Allows amending commits that have children and can automatically rebase
the children onto the new version of the commit
"""
from hgext import rebase
from mercurial import util, cmdutil, phases, commands, bookmarks, repair
from mercurial import merge, extensions
from mercurial.node import hex
from mercurial.i18n import _
import errno, os, re
cmdtable = {}
command = cmdutil.command(cmdtable)
testedwith = 'internal'
amendopts = [('', 'rebase', None, _('rebases children commits after the amend')),
('', 'fixup', None, _('rebase children commits from a previous amend')),
]
def uisetup(ui):
entry = extensions.wrapcommand(commands.table, 'commit', commit)
for opt in amendopts:
opt = (opt[0], opt[1], opt[2], "(with --amend) " + opt[3])
entry[1].append(opt)
def commit(orig, ui, repo, *pats, **opts):
if opts.get("amend"):
# commit --amend default behavior is to prompt for edit
opts['edit'] = True
return amend(ui, repo, *pats, **opts)
else:
return orig(ui, repo, *pats, **opts)
@command('^amend', [
('e', 'edit', None, _('prompt to edit the commit message')),
] + amendopts + commands.walkopts + commands.commitopts,
_('hg amend [OPTION]...'))
def amend(ui, repo, *pats, **opts):
'''amend the current commit with more changes
'''
rebase = opts.get('rebase')
fixup = opts.get('fixup')
edit = opts.get('edit')
if fixup:
fixupamend(ui, repo)
return
old = repo['.']
if old.phase() == phases.public:
raise util.Abort(_('cannot amend public changesets'))
if len(repo[None].parents()) > 1:
raise util.Abort(_('cannot amend while merging'))
haschildren = len(old.children()) > 0
if not edit:
opts['message'] = old.description()
tempnode = []
def commitfunc(ui, repo, message, match, opts):
e = cmdutil.commiteditor
noderesult = repo.commit(message,
old.user(),
old.date(),
match,
editor=e,
extra={})
# the temporary commit is the very first commit
if not tempnode:
tempnode.append(noderesult)
return noderesult
current = repo._bookmarkcurrent
oldbookmarks = old.bookmarks()
if haschildren:
def fakestrip(orig, ui, repo, *args, **kwargs):
if tempnode:
if tempnode[0]:
# don't strip everything, just the temp node
# this is very hacky
orig(ui, repo, tempnode[0], backup='none')
tempnode.pop()
else:
orig(ui, repo, *args, **kwargs)
extensions.wrapfunction(repair, 'strip', fakestrip)
node = cmdutil.amend(ui, repo, commitfunc, old, {}, pats, opts)
if node == old.node():
ui.status(_("nothing changed\n"))
return 1
if haschildren and not rebase:
ui.status("warning: the commit's children were left behind " +
"(use hg amend --fixup to rebase them)\n")
# move bookmarks
newbookmarks = repo._bookmarks
for bm in oldbookmarks:
newbookmarks[bm] = node
# create preamend bookmark
if current:
bookmarks.setcurrent(repo, current)
if haschildren:
newbookmarks[current + "(preamend)"] = old.node()
else:
# no active bookmark
if haschildren:
newbookmarks[hex(node)[:12] + "(preamend)"] = old.node()
newbookmarks.write()
if rebase and haschildren:
fixupamend(ui, repo)
def fixupamend(ui, repo):
"""rebases any children found on the preamend commit and strips the
preamend commit
"""
current = repo['.']
preamendname = None
active = repo._bookmarkcurrent
if active:
preamendname = active + "(preamend)"
if not preamendname:
preamendname = hex(current.node())[:12] + "(preamend)"
if not preamendname in repo._bookmarks:
if active:
raise util.Abort(_('no %s(preamend) bookmark' % active))
else:
raise util.Abort(_('no %s(preamend) bookmark - is your bookmark not active?' %
hex(current.node())[:12]))
ui.status("rebasing the children of %s\n" % (preamendname))
old = repo[preamendname]
oldbookmarks = old.bookmarks()
opts = {
'rev' : [str(c.rev()) for c in old.descendants()],
'dest' : active
}
if opts['rev'][0]:
rebase.rebase(ui, repo, **opts)
repair.strip(ui, repo, old.node(), topic='preamend-backup')
for bookmark in oldbookmarks:
repo._bookmarks.pop(bookmark)
repo._bookmarks.write()
merge.update(repo, current.node(), False, True, False)
if active:
bookmarks.setcurrent(repo, active)

265
hgext/smartlog.py Normal file
View File

@ -0,0 +1,265 @@
# 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.
from mercurial import extensions, util, cmdutil, graphmod, templatekw, scmutil
from mercurial import bookmarks, commands
from mercurial.extensions import wrapfunction
from mercurial.node import hex, nullrev
from mercurial.i18n import _
import errno, os, re
cmdtable = {}
command = cmdutil.command(cmdtable)
testedwith = 'internal'
def uisetup(ui):
# Hide output for fake nodes
def show(orig, self, ctx, copies, matchfn, props):
if ctx.node() == "...":
self.ui.write('\n\n\n')
return
return orig(self, ctx, copies, matchfn, props)
wrapfunction(cmdutil.changeset_printer, '_show', show)
wrapfunction(cmdutil.changeset_templater, '_show', show)
def ascii(orig, ui, state, type, char, text, coldata):
# Show . for fake nodes
if type == 'F':
char = "."
# Color the current commits. @ is too subtle
if char == "@":
newtext = []
for line in text:
line = "\033[35m" + line + "\033[0m"
newtext.append(line)
text = newtext
return orig(ui, state, type, char, text, coldata)
wrapfunction(graphmod, 'ascii', ascii)
def drawedges(orig, edges, nodeline, interline):
orig(edges, nodeline, interline)
for (start, end) in edges:
if start == end:
# terrible hack, but this makes the line below
# the commit marker (.) also be a .
if '.' in nodeline:
interline[2 * start] = "."
wrapfunction(graphmod, '_drawedges', drawedges)
# copied from graphmod or cmdutil or somewhere...
def grandparent(cl, lowestrev, roots, head):
"""Return all ancestors of head in roots which revision is
greater or equal to lowestrev.
"""
pending = set([head])
seen = set()
kept = set()
llowestrev = max(nullrev, lowestrev)
while pending:
r = pending.pop()
if r >= llowestrev and r not in seen:
if r in roots:
kept.add(r)
else:
pending.update([p for p in cl.parentrevs(r)])
seen.add(r)
return sorted(kept)
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:
roots.append(n)
def childsort(x, y):
xm = x in masters
ym = y in masters
# 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.
if xm and not ym:
return 1
elif not xm and ym:
return -1
else:
# If both children are not in the master line, show the oldest first,
# so the graph is approximately in chronological order.
return x - y
# 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, cmp=childsort)
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(repo, revs, master):
cl = repo.changelog
lowestrev = min(revs)
# 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 rev(self):
return self._rev
def files(self):
return []
fakes = {}
knownrevs = set(revs)
gpcache = {}
results = []
# 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([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() != nullrev and p.rev() not in parents]
fake = None
for mpar in mpars:
gp = gpcache.get(mpar)
if gp is None:
gp = gpcache[mpar] = grandparent(cl, lowestrev, revs, mpar)
if not gp:
parents.append(mpar)
else:
gp = [g for g in gp if g not in parents]
for g in gp:
# Insert fake nods in between children and grandparents.
# Reuse them across multiple chidlren when the grandparent
# is the same.
if not g in fakes:
fakes[g] = (mpar, 'F', fakectx(mpar), [g])
fake = fakes[g]
parents.append(fakes[g][0])
results.append((ctx.rev(), 'C', ctx, parents))
if fake:
results.append(fake)
# Compute parent rev->parents mapping
lookup = {}
for r in results:
lookup[r[0]] = 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 not m in masters:
masters.add(m)
queue.extend(lookup.get(m, []))
# Topologically sort the noderev numbers
order = sortnodes([r[0] for r in results], parentfunc, masters)
# Sort the actual results based on their position in the 'order'
return sorted(results, key=lambda x: order.index(x[0]) , reverse=True)
@command('^smartlog|slog', [
('', 'template', '', _('display with template'), _('TEMPLATE')),
('', 'master', None, _('master bookmark'), ''),
] + commands.logopts, _('hg smartlog|slog'))
def mylog(ui, repo, *pats, **opts):
master = opts.get('master')
revs = set()
heads = set()
rev = repo.changelog.rev
ancestor = repo.changelog.ancestor
node = repo.changelog.node
# Find all bookmarks and recent heads
books = bookmarks.bmstore(repo)
for b in books:
heads.add(rev(books[b]))
heads.update(repo.revs('head() & date(-14) & branch(.)'))
if not master:
if '@' in books:
master = '@'
elif 'master' in books:
master = 'master'
elif 'trunk' in books:
master = 'trunk'
else:
master = 'tip'
master = repo.revs(master)[0]
# Find ancestors of heads that are not in master
# Don't use revsets, they are too slow
revs.update(heads)
for head in heads:
anc = rev(ancestor(node(head), node(master)))
queue = [head]
while queue:
current = queue.pop(0)
revs.add(current)
if current != anc:
parents = repo.changelog.parentrevs(current)
for p in parents:
if p != nullrev and p != anc:
queue.append(p)
# add context: master, current commit, and the common ancestor
revs.add(master)
revs.update(repo.revs('.'))
revs.update(repo.revs('ancestor(%s)' % (','.join([str(r) for r in revs]))))
revs = sorted(list(revs), reverse=True)
# Print it!
revdag = getdag(repo, revs, master)
displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True)
showparents = [ctx.node() for ctx in repo[None].parents()]
cmdutil.displaygraph(ui, revdag, displayer, showparents,
graphmod.asciiedges, None, None)

15
setup.py Normal file
View File

@ -0,0 +1,15 @@
from distutils.core import setup, Extension
setup(
name='fbhgextensions',
version='0.1.0',
author='Durham Goode',
maintainer='Durham Goode',
maintainer_email='durham@fb.com',
url='',
description='Facebook specific mercurial extensions',
long_description="",
keywords='fb hg mercurial',
license='',
packages=['hgext']
)