mirror of
https://github.com/facebook/sapling.git
synced 2024-10-06 23:07:18 +03:00
Initial commit: fbamend, smartlog
This commit is contained in:
commit
26387ef0c1
24
Makefile
Normal file
24
Makefile
Normal 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
177
hgext/fbamend.py
Normal 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
265
hgext/smartlog.py
Normal 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
15
setup.py
Normal 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']
|
||||
)
|
Loading…
Reference in New Issue
Block a user