# # 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. # from urllib import quote import subprocess import mercurial.node 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("U %s" % f) for f in files_changed[1]: files_changed_status.append("A %s" % f) for f in files_changed[2]: files_changed_status.append("D %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"])