| #!/usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # |
| # This is the post-receive git hook used to generate the activity feed |
| # public-inbox repository. It requires that ezpi is installed |
| # https://sr.ht/~monsieuricon/ezpi/ |
| # |
| # Copyright (C) 2020 by The Linux Foundation |
| # SPDX-License-Identifier: GPL-2.0-or-later |
| # |
| __author__ = 'Konstantin Ryabitsev <konstantin@linuxfoundation.org>' |
| |
| import os |
| import sys |
| import ezpi # noqa |
| import hashlib |
| import base64 |
| |
| from email.message import EmailMessage |
| from email.mime.text import MIMEText |
| from email.mime.multipart import MIMEMultipart |
| |
| from typing import Optional |
| |
| |
| def get_config_from_git(regexp: str, defaults: Optional[dict] = None) -> dict: |
| gitconfig = defaults if defaults else dict() |
| |
| args = ['config', '-z', '--get-regexp', regexp] |
| ee, out, err = ezpi.git_run_command('', args) |
| if ee > 0 or not len(out): |
| return gitconfig |
| |
| for line in out.decode().split('\x00'): |
| if not line: |
| continue |
| key, value = line.split('\n', 1) |
| try: |
| chunks = key.split('.') |
| cfgkey = chunks[-1] |
| gitconfig[cfgkey.lower()] = value |
| except ValueError: |
| pass |
| |
| return gitconfig |
| |
| |
| def run_hook(feedrepo: str, fromhdr: str, domain: str): |
| # Look if we have a GL_USER and GL_REPO in the env |
| user = os.getenv('GL_USER') |
| if not user: |
| user = os.getenv('USER') |
| repo = os.getenv('GL_REPO') |
| if not repo: |
| repo = os.getcwd() |
| ll = list() |
| attachments = dict() |
| ll.append('---') |
| ll.append('service: git-receive-pack') |
| ll.append(f'repo: {repo}') |
| ll.append(f'user: {user}') |
| # Do we have a ~/.activity-feed-secret? |
| secret = None |
| secretf = os.path.expanduser('~/.activity-feed-secret') |
| # The idea is to rotate it frequently, with the value logged in syslog. |
| # This allows us to see if a push is coming from the same remote IP address, |
| # but only within the same calendar day. |
| try: |
| with open(secretf) as fh: |
| secret = fh.read().strip() |
| except (FileNotFoundError, IOError): |
| pass |
| |
| if secret: |
| conn_info = os.getenv('SSH_CONNECTION') |
| if conn_info: |
| remote_ip = conn_info.split()[0] |
| ipline = f'{secret}{user}{remote_ip}' |
| iph = hashlib.sha1() |
| iph.update(ipline.encode()) |
| hashed = base64.b64encode(iph.digest()).decode() |
| ll.append(f'remote_ip: {hashed}') |
| |
| # Do we have a push cert? |
| cert = os.getenv('GIT_PUSH_CERT') |
| if cert: |
| gpcstatus = os.getenv('GIT_PUSH_CERT_STATUS') |
| ll.append(f'git_push_cert_status: {gpcstatus}') |
| args = ['cat-file', 'blob', cert] |
| ee, out, err = ezpi.git_run_command('', args) |
| if ee == 0 and out: |
| attachments['git-push-certificate.txt'] = out.decode() |
| |
| ll.append('changes:') |
| |
| seenranges = dict() |
| while True: |
| line = sys.stdin.readline() |
| if not line: |
| break |
| oldrev, newrev, ref = line.strip().split() |
| ll.append(f' - ref: {ref}') |
| ll.append(f' old: {oldrev}') |
| ll.append(f' new: {newrev}') |
| |
| if (oldrev, newrev) not in seenranges: |
| args = ['rev-list', '--max-count=1024', '--reverse', '--pretty=oneline', newrev] |
| if set(oldrev) != {0}: |
| args += [f'^{oldrev}'] |
| ee, out, err = ezpi.git_run_command('', args) |
| if ee > 0 or not len(out): |
| continue |
| seenranges[(oldrev, newrev)] = out |
| else: |
| out = seenranges[(oldrev, newrev)] |
| |
| if len(out) > 1024: |
| # Add it as attachment, unless we already have one with this name |
| filename = f'revlist-{oldrev[:12]}-{newrev[:12]}.txt' |
| if filename not in attachments: |
| attachments[filename] = out.decode() |
| ll.append(f' log: {filename}') |
| continue |
| |
| ll.append(' log: |') |
| for pretty in out.decode().split('\n'): |
| ll.append(f' {pretty}') |
| |
| body = '\n'.join(ll) + '\n' |
| |
| if attachments: |
| msg = MIMEMultipart() |
| msg.attach(MIMEText(body, 'plain')) |
| for attfilename, attbody in attachments.items(): |
| att = MIMEText(attbody, 'plain') |
| att.add_header('Content-Disposition', f'attachment; filename={attfilename}') |
| msg.attach(att) |
| else: |
| msg = EmailMessage() |
| msg.set_payload(body) |
| |
| msg['From'] = fromhdr |
| msg['Subject'] = f'post-receive: {repo}' |
| |
| try: |
| ezpi.add_rfc822(feedrepo, msg, domain) |
| sys.stderr.write('Recorded in the transparency log\n') |
| ezpi.run_hook(feedrepo) |
| except RuntimeError: |
| # Could not add it to the feed, complain |
| sys.stderr.write('FAILED writing to the transparency log!\n') |
| |
| |
| if __name__ == '__main__': |
| if sys.stdin.isatty(): |
| # Nothing passed via stdin, so nothing to add to the feed |
| sys.exit(0) |
| config = get_config_from_git(r'activityfeed\..*') |
| _feedrepo = config.get('repo') |
| if not config.get('repo'): |
| # The audit repo is not defined in gitconfig, so nothing for us to do. |
| sys.exit(0) |
| _fromhdr = config.get('from') |
| _domain = config.get('domain') |
| if not _domain: |
| _domain = 'localhost' |
| |
| if not _fromhdr: |
| _fromhdr = f'Post-Receive Hook <post-receive@{_domain}>' |
| |
| run_hook(_feedrepo, _fromhdr, _domain) |