From 26387ef0c1c2f281353f91356cd46edbeb14a6f1 Mon Sep 17 00:00:00 2001 From: Durham Goode Date: Thu, 20 Jun 2013 12:16:36 -0700 Subject: [PATCH] Initial commit: fbamend, smartlog --- .hgignore | 4 + Makefile | 24 +++++ hgext/fbamend.py | 177 +++++++++++++++++++++++++++++++ hgext/smartlog.py | 265 ++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 15 +++ 5 files changed, 485 insertions(+) create mode 100644 .hgignore create mode 100644 Makefile create mode 100644 hgext/fbamend.py create mode 100644 hgext/smartlog.py create mode 100644 setup.py diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000000..8354751593 --- /dev/null +++ b/.hgignore @@ -0,0 +1,4 @@ +syntax: regexp +\.pyc$ +\.so$ +^build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..44ab3bcd9c --- /dev/null +++ b/Makefile @@ -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 '{}' ';' diff --git a/hgext/fbamend.py b/hgext/fbamend.py new file mode 100644 index 0000000000..795e647985 --- /dev/null +++ b/hgext/fbamend.py @@ -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) diff --git a/hgext/smartlog.py b/hgext/smartlog.py new file mode 100644 index 0000000000..fad7d6661d --- /dev/null +++ b/hgext/smartlog.py @@ -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) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..d756563dbf --- /dev/null +++ b/setup.py @@ -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'] +)