From 1d0edf473c06337d1f3f44a2ec73d8fdfbfd40b0 Mon Sep 17 00:00:00 2001
From: Jessica McKellar <jesstess@humbughq.com>
Date: Sun, 27 Oct 2013 19:34:25 -0400
Subject: [PATCH] Add a first pass at Mercurial changegroup (hg push)
 integration.

(imported from commit 94c91aed6282a2fffcd7753a06dc68a298b7bdc7)
---
 integrations/hg/zulip-changegroup.py | 168 +++++++++++++++++++++++++++
 1 file changed, 168 insertions(+)
 create mode 100755 integrations/hg/zulip-changegroup.py

diff --git a/integrations/hg/zulip-changegroup.py b/integrations/hg/zulip-changegroup.py
new file mode 100755
index 00000000..4dcc27f5
--- /dev/null
+++ b/integrations/hg/zulip-changegroup.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Zulip hook for Mercurial changeset pushes.
+# Copyright © 2012-2013 Zulip, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+#
+# This hook is called when changesets are pushed to the master repository (ie
+# `hg push`). See https://zulip.com/integrations for installation instructions.
+
+import zulip
+
+VERSION = "0.9"
+
+def format_summary_line(web_url, user, base, tip, branch, node):
+    """
+    Format the first line of the message, which contains summary
+    information about the changeset and links to the changelog if a
+    web URL has been configured:
+
+    Jane Doe <jane@example.com> pushed 1 commit to master (170:e494a5be3393):
+    """
+    revcount = tip - base
+    plural = "s" if revcount > 1 else ""
+
+    if web_url:
+        shortlog_base_url = web_url.rstrip("/") + "/shortlog/"
+        summary_url = "{shortlog}{tip}?revcount={revcount}".format(
+            shortlog=shortlog_base_url, tip=tip - 1, revcount=revcount)
+        formatted_commit_count = "[{revcount} commit{s}]({url})".format(
+            revcount=revcount, s=plural, url=summary_url)
+    else:
+        formatted_commit_count = "{revcount} commit{s}".format(
+            revcount=revcount, s=plural)
+
+    return u"**{user}** pushed {commits} to **{branch}** (`{tip}:{node}`):\n\n".format(
+        user=user, commits=formatted_commit_count, branch=branch, tip=tip,
+        node=node[:12])
+
+def format_commit_lines(web_url, repo, base, tip):
+    """
+    Format the per-commit information for the message, including the one-line
+    commit summary and a link to the diff if a web URL has been configured:
+    """
+    if web_url:
+        rev_base_url = web_url.rstrip("/") + "/rev/"
+
+    commit_summaries = []
+    for rev in range(base, tip):
+        rev_node = repo.changelog.node(rev)
+        rev_ctx = repo.changectx(rev_node)
+        one_liner = rev_ctx.description().split("\n")[0]
+
+        if web_url:
+            summary_url = rev_base_url + str(rev_ctx)
+            summary = "* [{summary}]({url})".format(
+                summary=one_liner, url=summary_url)
+        else:
+            summary = "* {summary}".format(summary=one_liner)
+
+        commit_summaries.append(summary)
+
+    return "\n".join(summary for summary in commit_summaries)
+
+def send_zulip(email, api_key, stream, subject, content):
+    """
+    Send a message to Zulip using the provided credentials, which should be for
+    a bot in most cases.
+    """
+    client = zulip.Client(email=email, api_key=api_key,
+                          client="mercurial " + VERSION)
+
+    message_data = {
+        "type": "stream",
+        "to": stream,
+        "subject": subject,
+        "content": content,
+    }
+
+    client.send_message(message_data)
+
+def get_config(ui, item):
+    try:
+        # configlist returns everything in lists.
+        return ui.configlist('zulip', item)[0]
+    except IndexError:
+        return None
+
+def hook(ui, repo, **kwargs):
+    """
+    Invoked by configuring a [hook] entry in .hg/hgrc.
+    """
+    hooktype = kwargs["hooktype"]
+    node = kwargs["node"]
+
+    ui.debug("Zulip: received {hooktype} event\n".format(hooktype=hooktype))
+
+    if hooktype != "changegroup":
+        ui.warn("Zulip: {hooktype} not supported\n".format(hooktype=hooktype))
+        exit(1)
+
+    ctx = repo.changectx(node)
+    branch = ctx.branch()
+
+    # If `branches` isn't specified, notify on all branches.
+    branch_whitelist = get_config(ui, "branches")
+    branch_blacklist = get_config(ui, "ignore_branches")
+
+    if branch_whitelist:
+        # Only send notifications on branches we are watching.
+        watched_branches = [b.lower().strip() for b in branch_whitelist.split(",")]
+        if branch.lower() not in watched_branches:
+            ui.debug("Zulip: ignoring event for {branch}\n".format(branch=branch))
+            exit(0)
+
+    if branch_blacklist:
+        # Don't send notifications for branches we've ignored.
+        ignored_branches = [b.lower().strip() for b in branch_blacklist.split(",")]
+        if branch.lower() in ignored_branches:
+            ui.debug("Zulip: ignoring event for {branch}\n".format(branch=branch))
+            exit(0)
+
+    # The first and final commits in the changeset.
+    base = repo[node].rev()
+    tip = len(repo)
+
+    email = get_config(ui, "email")
+    api_key = get_config(ui, "api_key")
+
+    if not (email and api_key):
+        ui.warn("Zulip: missing email or api_key configurations\n")
+        ui.warn("in the [zulip] section of your .hg/hgrc.\n")
+        exit(1)
+
+    stream = get_config(ui, "stream")
+    # Give a default stream if one isn't provided.
+    if not stream:
+        stream = "commits"
+
+    web_url = get_config(ui, "web_url")
+    user = ctx.user()
+    content = format_summary_line(web_url, user, base, tip, branch, node)
+    content += format_commit_lines(web_url, repo, base, tip)
+
+    subject = branch
+
+    ui.debug("Sending to Zulip:\n")
+    ui.debug(content + "\n")
+
+    send_zulip(email, api_key, stream, subject, content)