#!/usr/bin/python3 import configparser import os import re import sys import urllib.error import urllib.parse import urllib.request import xmlrpc.client from dataclasses import dataclass LIBDIR = '<%= @gitolite_commonhooksdir %>' sys.path.insert(0, LIBDIR) import bugz.settings import git_multimail # When editing this list, remember to edit the same list in # modules/cgit/templates/filter.commit-links.sh BUG_REFS = { 'Mageia': { 're': re.compile('mga#([0-9]+)'), 'replace': 'https://bugs.mageia.org/%s' }, 'Red Hat': { 're': re.compile('rhbz#([0-9]+)'), 'replace': 'https://bugzilla.redhat.com/show_bug.cgi?id=%s' }, 'Free Desktop': { 're': re.compile('fdo#([0-9]+)'), 'replace': 'https://bugs.freedesktop.org/show_bug.cgi?id=%s' }, 'KDE': { 're': re.compile('(?:bko|kde)#([0-9]+)'), 'replace': 'https://bugs.kde.org/show_bug.cgi?id=%s' }, 'GNOME': { 're': re.compile('(?:bgo|gnome)#([0-9]+)'), 'replace': 'https://bugzilla.gnome.org/show_bug.cgi?id=%s' }, 'Launchpad': { 're': re.compile('lp#([0-9]+)'), 'replace': 'https://launchpad.net/bugs/%s' }, } COMMIT_RE = re.compile('^commit ([a-f0-9]{40})') COMMIT_REPLACE = 'https://gitweb.mageia.org/%s/commit/?id=%s' MAGEIA_BUGZILLA_URL = 'https://bugs.mageia.org/xmlrpc.cgi' MAGEIA_BUGZILLA_PASSWORD_FILE = '.gitzilla-password' MAGEIA_BUGZILLA_APIKEY_FILE = '.gitzilla-apikey' # this holds a Bugzilla API key GITWEB_UPDATE_URL = 'http://gitweb.mageia.org:8000' # Bugzilla user USER_LOGIN = 'bot' # Recipient of i18n notifications I18N_MAIL = 'i18n-reports@ml.mageia.org' # Set to 1..3 for debug logging (WARNING: this will show passwords to git committers!) DEBUG = 0 git_multimail.FOOTER_TEMPLATE = """\ -- \n\ Mageia Git Monkeys. """ git_multimail.REVISION_FOOTER_TEMPLATE = git_multimail.FOOTER_TEMPLATE I18N_REVISION_HEADER_TEMPLATE = """\ Date: %(send_date)s To: %(recipients)s Subject: %(emailprefix)s%(oneline)s MIME-Version: 1.0 Content-Type: text/plain; charset=%(charset)s Content-Transfer-Encoding: 8bit From: %(fromaddr)s Reply-To: %(reply_to)s In-Reply-To: %(reply_to_msgid)s References: %(reply_to_msgid)s X-Git-Host: %(fqdn)s X-Git-Repo: %(repo_shortname)s X-Git-Refname: %(refname)s X-Git-Reftype: %(refname_type)s X-Git-Rev: %(rev)s Auto-Submitted: auto-generated """ REPO_NAME_RE = re.compile(r'^/git/(?P.+?)(?:\.git)?$') # Log a debug message when logging is enabled def debug(s, *a): if DEBUG > 0: print(s % a) def repo_shortname(): basename = os.path.abspath(git_multimail.get_git_dir()) m = REPO_NAME_RE.match(basename) if m: return m.group('name') else: return basename # Override the Environment class to generate an apporpriate short name which is # used in git links and as an email prefix class MageiaEnvironment(git_multimail.Environment): def get_repo_shortname(self): return repo_shortname() git_multimail.Environment = MageiaEnvironment # Override the Revision class to inject gitweb/cgit links and any referenced # bug URLs class MageiaLinksRevision(git_multimail.Revision): def __init__(self, reference_change, rev, num, tot): super().__init__(reference_change, rev, num, tot) self.bz = None def bugzilla_init(self): if not self.bz: tokenfile = os.path.join(os.environ['HOME'], MAGEIA_BUGZILLA_APIKEY_FILE) token = None try: token = open(tokenfile, 'r').readline().rstrip() except IOError: # Assume username/password will be used instead pass passwordfile = os.path.join(os.environ['HOME'], MAGEIA_BUGZILLA_PASSWORD_FILE) pword = None try: pword = open(passwordfile, 'r').readline().rstrip() except IOError: print('Error: no Bugzilla credentials available; trying anyway') # There's no real point in continuing, but why not class ConfigSettings: pass cfg = ConfigSettings() cfg.connection = 'Mageia' if token: # If an API key is found, that will be used instead of user/password cfg.key = token cfg.user = USER_LOGIN cfg.password = pword cfg.base = MAGEIA_BUGZILLA_URL cfg.debug = DEBUG cfile = configparser.ConfigParser() cfile.add_section(cfg.connection) self.bz = bugz.settings.Settings(cfg, cfile) def generate_email_body(self, push): """Show this revision.""" output = git_multimail.read_git_lines( ['log'] + self.environment.commitlogopts + ['-1', self.rev.sha1], keepends=True, ) bugs = {} commit = None idx = 0 # Modify the mail output on-the-fly to add links; this is sensitive to # the mail format produced by git_multimail. Also, update Mageia # Bugzilla if a bug reference is found. for line in output: idx += 1 if line == "---\n": if commit and COMMIT_REPLACE: output.insert(idx, "\n") output.insert(idx, " %s\n" % (COMMIT_REPLACE % (self.environment.get_repo_shortname(), commit))) output.insert(idx, " Commit Link:\n") idx += 3 if bugs: output.insert(idx, " Bug links:\n") idx += 1 for tracker, bugnos in bugs.items(): output.insert(idx, " %s\n" % tracker) idx += 1 for bugno in bugnos: output.insert(idx, " %s\n" % (BUG_REFS[tracker]['replace'] % bugno)) idx += 1 output.insert(idx, "\n") idx += 1 # Attempt to modify Bugzilla if "Mageia" in bugs: try: self.bugzilla_init() # Mask email address comment = None # Suppress the "Bug links:" section if only one bug # is referenced if len(bugs) == 1 and len(bugs['Mageia']) == 1: comment = output[0:idx-4] else: comment = output[0:idx] comment[1] = re.sub(r'^(Author: [^@]*)@.*(>)?', r'\1@...>', comment[1]) comment = "".join(comment) params = {} params['ids'] = bugs['Mageia'] params['comment'] = { 'body': comment } self.bz.call_bz(self.bz.bz.Bug.update, params) print("Updated bugzilla bugs: %s" % ", ".join(bugs['Mageia'])) except: print("Unable to post to bugzilla bugs: %s :(" % ", ".join(bugs['Mageia'])) print(sys.exc_info()[1]) break m = COMMIT_RE.search(line) if m: commit = m.group(1) for tracker in BUG_REFS.keys(): foundbugs = BUG_REFS[tracker]['re'].findall(line) if foundbugs: if tracker not in bugs: bugs[tracker] = foundbugs else: bugs[tracker] = list(set(bugs[tracker] + foundbugs)) return output # Override the Revision class to inject gitweb/cgit links and any referenced # bug URLs class MageiaI18NRevision(git_multimail.Revision): """A Change consisting of a single git commit.""" def __init__(self, reference_change, rev, num, tot): super().__init__(reference_change, rev, num, tot) # Don't send to any of the normal recipients self.recipients = False self.cc_recipients = '' i18n_folders = [] # Check files and find i18n folders for line in git_multimail.read_git_lines(['ls-tree', '-rd', self.rev.sha1]): (modetypesha1, name) = line.split("\t", 1) if name.endswith("/.tx"): i18n_folders.append(os.path.dirname(name)) if i18n_folders: self.output = git_multimail.read_git_lines( ['log', '-C', '--stat', '-p', '--no-walk', self.rev.sha1, '--'] + i18n_folders, keepends=True, ) if self.output: # We have some output so let's send the mail... self.recipients = I18N_MAIL print(f'Sending i8n notification to {self.recipients}') def generate_email_body(self, push): """Show this revision.""" return self.output def main(): # Attempt to write a last-updated file for cgit cosmetics git_dir = git_multimail.get_git_dir() infowebdir = os.path.join(git_dir, 'info', 'web') lastupdated = git_multimail.read_git_output( ['for-each-ref', '--sort=-committerdate', "--format=%(committerdate:iso8601)", '--count=1', 'refs/heads'], ) try: if not os.path.exists(infowebdir): os.makedirs(infowebdir) with open(os.path.join(infowebdir, 'last-modified'), 'w') as modfile: modfile.write(lastupdated) except Exception: debug('Warning: could not update git last-modified date: %s', sys.exc_info()[1]) # Contact the on-the-pull service on the gitweb server with the updated repo try: req = urllib.request.Request(GITWEB_UPDATE_URL, (repo_shortname() + '.git').encode('utf-8')) req.add_header('Content-Type', 'x-git/repo') fp = urllib.request.urlopen(req, timeout=5) fp.close() except Exception: debug('Warning: could not contact gitweb server: %s', sys.exc_info()[1]) config = git_multimail.Config('multimailhook') try: environment = git_multimail.choose_environment( config, osenv=os.environ, ) mailer = git_multimail.choose_mailer(config, environment) # For testing...send mail to stdout only #mailer = git_multimail.OutputMailer(sys.stdout) changes = [] for line in sys.stdin: (oldrev, newrev, refname) = line.strip().split(' ', 2) changes.append( git_multimail.ReferenceChange.create(environment, oldrev, newrev, refname) ) push = git_multimail.Push(environment, changes) # First pass - regular commit mails git_multimail.Revision = MageiaLinksRevision push.send_emails(mailer, body_filter=environment.filter_body) # Second pass - i18n commit mails git_multimail.REVISION_HEADER_TEMPLATE = I18N_REVISION_HEADER_TEMPLATE git_multimail.Revision = MageiaI18NRevision # Don't send the summary email, so nuke the change recipients for change in push.changes: change.recipients = False push.send_emails(mailer, body_filter=environment.filter_body) except git_multimail.ConfigurationException as e: sys.exit(str(e)) if __name__ == '__main__': main()