From ecb2493d218ad052ae61ee8c12a3021874ec90cd 2020-06-29 04:18:01
From: Branko Majic <branko@majic.rs>
Date: 2020-06-29 04:18:01
Subject: [PATCH] Implemented Mercurial hook for updating TBG issues with commit information. The hook works only with remote (HTTP) access. Resolves issue SCR-2.

---

diff --git a/hooks/thebuggenie_hg_remote.py b/hooks/thebuggenie_hg_remote.py
new file mode 100755
index 0000000000000000000000000000000000000000..dfff47048f3479d6e288f0ccba72b5f295d8aaef
--- /dev/null
+++ b/hooks/thebuggenie_hg_remote.py
@@ -0,0 +1,273 @@
+#
+# Hook for submitting Mercurial pushed commits to The Bug Genie using the HTTP
+# Access mode.
+#
+# The hook can be used either as a 'changegroup' or 'incoming' hook. When
+# executed as a changegroup hook, all pushed changes will be processed.
+#
+# In order to use same hook configuration for all of user's repositories, it is
+# required to modify the the user's ~/.hgrc Mercurial configuration file. In
+# order to have per-repository configuration for the hook, use induvidual
+# repository's Mercurial configuration file (.hg/hgrc file, relative to
+# repository's root directory). The recommended way is to use the per-repository
+# settings.
+#
+# The following parameters have to be defined (under the specified sections in
+# the configuration file):
+#
+# [extensions]
+#
+# buggenie = /path/to/thebuggenie_hg_remote.py
+#
+# [buggenie]
+# program = [curl|wget]
+# url = tbg_url
+# passkey = project_passkey
+# project = project_id
+# nosslverify = [true|false]
+# 
+# [hooks]
+# changegroup.buggenie = python:buggenie.hook
+#
+# The following parameters are mandatory:
+#
+# program
+#    Specifies the program that should be used for submitting the report to
+#    TBG. Supported programs are 'wget' and 'curl'.
+# url
+#    Specifies the base URL where The Bug Genie installation can be reached
+#    at.
+# passkey
+#    Specifies the passkey that should be used for authenticating with The Bug
+#    Genie instance for the specified project. The passkey can be obtained from
+#    the project's 'VCS Integration' page.
+# project
+#    Specifies the project identifier that should be used when submitting a
+#    report to The Bug Genie. The project ID can be obtained from the project's
+#    'VCS Integration' page.
+#
+# The following parameters are optional:
+#
+# nosslverify
+#     Specifies that the calling program should _not_ verify the certificate (if
+#     the URL specified begins with https). This can be used in conjunction with
+#     the self-signed certificates.
+#
+
+import mercurial.node.bin
+import mercurial.node.short
+from urllib import quote
+import subprocess
+
+def call_program(program, arguments, url):
+    """
+    Calls the specified program, passing it the arguments. The URL is passed
+    through stdin to the program.
+
+
+    Arguments:
+
+    program - Binary that should be called.
+
+    arguments - Arguments that should be passed to the program.
+
+    url - TBG report submit URL that should be passed for processing to the
+    program.
+
+    Returns:
+
+    Tuple consisting out of error_code, stdout, and stderr generated by call.
+    """
+
+    # Prefix the arguments with program.
+    arguments.insert(0, program)
+
+    # Set-up the subprocess for execution.
+    process = subprocess.Popen(arguments, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
+
+    # curl expects slightly different format when reading URL from stdin.
+    if program == "curl":
+        url = "url=%s" % url
+
+    # Obtain the contents of stdout and stderr.
+    stdout, stderr = process.communicate(url)
+
+    # Return the exit code of process, and message that should be shown to user.
+    return process.returncode, stdout, stderr
+
+def extract_report(repo, context):
+    """
+    Extract the commit report from provided context.
+
+
+    Arguments:
+
+    repo - Mercurial repository object.
+
+    context - Mercurial commit context for which the report should be extracted.
+
+
+    Returns:
+
+    Dictionary containing the key/value pairs that should be used for
+    constructing the GET arguments of a TBG update URL.
+    """
+
+    # Extract some basic information first.
+    report = {}
+    report['author'] = context.user()
+    report['rev'] = "%d:%s" % (context.rev(), mercurial.node.short(context.node()))
+    report['commit_msg'] = context.description()
+    report['date'] = str(context.date()[0]).split(".0")[0]
+
+    # Extract parent revision identifier.
+    parent = context.parents()[0]
+    report['oldrev'] = "%d:%s" % (parent.rev(), mercurial.node.short(parent.node()))
+
+    # Get the list of changed files compared to previous commit.
+    files_changed = repo.status(parent.node(), context.node())
+
+    # Create a string containing information about all changes, one file per
+    # line, first column being type of change, second column being filename.
+    files_changed_status = []
+    for f in files_changed[0]:
+        files_changed_status.append("A %s" % f)
+    for f in files_changed[1]:
+        files_changed_status.append("D %s" % f)
+    for f in files_changed[2]:
+        files_changed_status.append("U %s" % f)
+    report['changed'] = "\n".join(files_changed_status)
+
+    # Finally return the report.
+    return report
+
+def submit_report(commit, repo, ui, program, program_params, base_url, project_id, passkey):
+    """
+    Submits report to TBG for a single commit.
+
+    Arguments:
+    
+    commit - Mercurial commit identifier.
+
+    repo - Mercurial repository object.
+
+    ui - Mercurial user interface object.
+
+    program - Name of the program that should be used.
+
+    program_params - Parameters that should be passed to the program.
+
+    base_url - Base URL at which TBG can be found.
+
+    project_id - TBG project identifier.
+
+    passkey - Passkey that should be used for authentication.
+    """
+
+    # Get the commit hash and context.
+    commit_hash = repo.changelog.node(commit)
+    commit_context = repo.changectx(commit_hash)
+    # Extract information for the commit.
+    report = extract_report(repo, commit_context)
+    # Add the passkey to report, it will be used for constructing the URL.
+    report["passkey"] = passkey
+
+    # Generate the URL.
+    # Strip the trailing slash.
+    base_url = base_url.rstrip("/")
+    # Set-up the base link for project.
+    url = "%s/vcs_integration/report/%s/?" % (base_url, project_id)
+    # Pass the GET arguments.
+    args = [ "%s=%s" % (quote(key), quote(value)) for key, value in report.iteritems()]
+    url += "&".join(args)
+
+    # Call the program.
+    exit_code, stdout, stderr = call_program(program, program_params, url)
+
+    # Since wget has a bit poor way of outputting error, filter out the URL used
+    # (in order to avoid passkey leakage).
+    if program == "wget":
+        stderr = "\n".join(stderr.split("\n")[1:])
+
+    # Output the stderr content if program returned with an error. Otherwise
+    # output the information retrieved from TBG.
+    if exit_code != 0:
+        ui.warn("TBG Hook: An error happened while trying to submit the data.\n")
+        ui.warn("TBG Hook: Program returned error message:\n")
+        # Strip out the whitespaces and newlines from the end, and make sure we
+        # have a single final newline.
+        stderr.rstrip()
+        stderr += "\n"
+        ui.warn(stderr)
+    else:
+        # Strip out the whitespaces and newlines from the end, and make sure we
+        # have a single final newline.
+        stdout.rstrip()
+        stdout += "\n"
+        ui.warn(stdout)
+
+def hook(ui, repo, hooktype, node = None, url = None, **kwargs):
+    """
+    Commit and changegroup hook for Mercurial.
+
+
+    Arguments:
+
+    ui - Mercurial user interface object.
+
+    repo - Mercurial repository object.
+
+    node - Mercurial revision ID.
+
+    url - Mercurial path/URL.
+
+    kwargs - Additional keyword arguments.
+    """
+
+    # Read the configuration options.
+    config = dict((param, value) for param, value in ui.configitems('buggenie'))
+
+    # Make sure the required settings were specified.
+    if not "program" in config:
+        ui.warn("TBG HG Hook: No program was specified. Check your settings.\n")
+        return
+    if not "url" in config:
+        ui.warn("TBG HG Hook: No base URL was specified. Check your settings.\n")
+        return
+    if not "project" in config:
+        ui.warn("TBG HG Hook: No project ID was specified. Check your settings.\n")
+        return
+    if not "passkey" in config:
+        ui.warn("TBG HG Hook: No passkey was specified. Check your settings.\n")
+        return
+
+    # Set-up parameters to be passed onto program.
+    if config["program"] == "wget":
+        # Use dots for progress, output to stdout, read link from stdin.
+        program_params = [ "--progress=dot", "-O", "-", "-i", "-" ]
+        if config.get("nosslverify") == "true":
+            program_params.append("--no-check-certificate")
+    elif config["program"] == "curl":
+        # Quiet, but show errors, fail on server error, and read link from stdin.
+        program_params = [ "-s", "-S", "-f", "-K", "-" ]
+        if config.get("nosslverify") == "true":
+            program_params.append("--insecure")
+    else:
+        ui.warn("TBG HG Hook: Specified program '%s' is not supported. Check your settings.\n")
+        return
+
+    # Get the node hash.
+    node_hash = mercurial.node.bin(node)
+
+    # If the hook is called as a changegroup hook, process all related commits.
+    if hooktype == "changegroup":
+        start = repo.changelog.rev(node_hash)
+        end = len(repo.changelog)
+
+        for commit in xrange(start, end):
+            submit_report(commit, repo, ui, config["program"], program_params, config["url"], config["project"], config["passkey"])
+
+    # If the hook is called as a commit, hook, process just the single commit.
+    elif hooktype == "commit":
+        submit_report(commit, repo, ui, config["program"], program_params, config["url"], config["project"], config["passkey"])
+