#!/usr/bin/env python3 """A script to set up COMMITTERS.rst according to gitlab committers.""" import os import sys import subprocess import argparse import json import urllib.request import urllib.parse from jinja2 import Environment, FileSystemLoader from collections import OrderedDict GIT_ERROR = 1 MERGE_REQUEST = 'https://gitlab.com/api/v4/projects/1975139/merge_requests' ALL_MEMBERS = 'https://gitlab.com/api/v4/projects/1975139/members/all' PROTECTED = 'https://gitlab.com/api/v4/projects/1975139/protected_branches/master' USERS = 'https://gitlab.com/api/v4/users/{}' MAX_LEN = len('Full Name ') def get_committers(token: str) -> OrderedDict: request = urllib.request.Request(PROTECTED) request.add_header('PRIVATE-TOKEN', token) response = urllib.request.urlopen(request).read().decode('utf-8') named_developers = [x['user_id'] for x in json.loads(response)['merge_access_levels']] request = urllib.request.Request(ALL_MEMBERS) request.add_header('PRIVATE-TOKEN', token) all_members = json.loads(urllib.request.urlopen(request).read().decode('utf-8')) names_usernames_dictionary = OrderedDict() for contributor in all_members: if contributor['access_level'] >= 40: names_usernames_dictionary[contributor['name']] = contributor['username'] for contributor in named_developers: if contributor: request = urllib.request.Request(USERS.format(contributor)) response = json.loads(urllib.request.urlopen(request).read().decode('utf-8')) if response['name'] != 'bst-marge-bot': names_usernames_dictionary[response['name']] = response['username'] return names_usernames_dictionary def get_table_entry(entry: str) -> str: res = entry for _ in range(MAX_LEN - len(entry)): res = res + ' ' return res def find_repository_root() -> str: root = os.getcwd() try: root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel']) except subprocess.CalledProcessError as e: print('The current working directory is not a git repository. \ \"git rev-parse --show-toplevel\" exited with code {}.'.format(e.returncode)) sys.exit(GIT_ERROR) return root.rstrip().decode('utf-8') def create_committers_file(committers: OrderedDict): repository_root = find_repository_root() contrib_directory = os.path.join(repository_root, 'contrib') file_loader = FileSystemLoader(contrib_directory) env = Environment(loader=file_loader, autoescape=True) template = env.get_template('COMMITTERS.rst.j2') render_output = template.render(committers=committers, get_table_entry=get_table_entry) committers_file = os.path.join(repository_root, 'COMMITTERS.rst') with open(committers_file, 'w') as f: f.write(render_output) def commit_changes_if_needed(token: str): committers_file = os.path.join(find_repository_root(), 'COMMITTERS.rst') git_diff = subprocess.call(['git', 'diff', '--quiet', committers_file]) if git_diff: commit_message = '\'Update COMMITTERS.rst\'' branch_name = 'update_committers' subprocess.call(['git', 'add', committers_file]) subprocess.call(['git', 'commit', '-m', commit_message]) try: subprocess.check_call(['git', 'push', '-u', 'origin', branch_name], stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: print('Could not push to remote branch. \"git push -u origin {}\" \ exited with code {}.'.format(branch_name, e.returncode)) sys.exit(GIT_ERROR) data = urllib.parse.urlencode({'source_branch': 'update_committers', 'target_branch': 'master', 'title': 'Update COMMITTERS.rst file'}) request = urllib.request.Request(MERGE_REQUEST, data=bytearray(data, 'ASCII')) request.add_header('PRIVATE-TOKEN', token) response = urllib.request.urlopen(request) if response.getcode() != 200: print("Failed to open MR; please open an MR for branch {} manually".format(branch_name)) def main(): parser = argparse.ArgumentParser( description="Update gitlab committers according to COMMITTERS.rst" ) parser.add_argument( "token", type=str, help="Your private access token. See https://gitlab.com/profile/personal_access_tokens." ) args = parser.parse_args() committers = get_committers(args.token) create_committers_file(committers) commit_changes_if_needed(args.token) if __name__ == '__main__': main()