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"]) +