diff --git a/add-bugzilla-versions/add-versions.py b/add-bugzilla-versions/add-versions.py index 4c1228f..a167a1c 100644 --- a/add-bugzilla-versions/add-versions.py +++ b/add-bugzilla-versions/add-versions.py @@ -1,134 +1,148 @@ #!/usr/bin/env python3 # Copyright 2015 Jonathan Riddell # Copyright 2017 Adrian Chaves # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from pathlib import Path import re from shutil import rmtree from subprocess import run, DEVNULL, PIPE from tempfile import mkdtemp, mkstemp from bs4 import BeautifulSoup from click import command, echo, option from requests import RequestException, Session ERROR = 'needs a legitimate login and password to continue' URL = 'https://bugs.kde.org' EDIT_VERSION_URL = '{}/editversions.cgi'.format(URL) -VERSION_RE = re.compile(r'(?m)^BUGZILLA_APPLICATION_VERSION:STRING=(.*)$') +VERSION_RE = re.compile(r'(?i):\s+project\s*\(\s*\S+\s+' + r'VERSION\s+(\d+(?:\.\d+(?:\.\d+(?:\.\d+)?)?)?)') def response_error(response): html = response.text soup = BeautifulSoup(html, 'html.parser') error_message = soup.find('div', {'id': 'error_msg'}) if not error_message: return None return re.sub(r'\s+', ' ', error_message.get_text()).strip() def log_in(session, email, password): data = {'Bugzilla_login': email, 'Bugzilla_password': password} headers = {'Referer': 'https://bugs.kde.org/'} try: response = session.post(URL, data=data, headers=headers) error = response_error(response) if error: echo(error) exit(1) except RequestException: echo('Unexpected login error. Please, contact the maintainers of this ' 'script.') exit(1) def products(): modules_path = Path(__file__).resolve().parent.parent / 'modules.git' with modules_path.open() as modules_file: for line in modules_file: match = re.match('^\S+', line) if match: yield match.group(0) +def cmake_trace(folder): + temp_folder = mkdtemp() + try: + command = ['cmake', '--trace-expand', folder] + execution = run(command, cwd=temp_folder, stdout=DEVNULL, stderr=PIPE) + return execution.stderr.decode() + finally: + rmtree(temp_folder) + + +def project_version_from_cmake_trace(trace): + match = None + for match in VERSION_RE.finditer(trace): + pass + if not match: + return None + return match.group(1) + + +def bugzilla_csrf_token(product): + params = {'action': 'add', 'product': product} + response = session.get(EDIT_VERSION_URL, params=params) + soup = BeautifulSoup(response.text, 'html.parser') + try: + return soup.find('input', {'name': 'token'})['value'] + except: + raise RuntimeError('Could not parse token from \'{}\''.format( + response.url)) + + +class AddVersionError(RuntimeError): + + def __init__(self, cause=None): + self.cause = cause + + +def add_version_to_bugzilla_project(session, product): + params = {'version': version, 'action': 'new', 'product': product, + 'token': bugzilla_csrf_token(session, product)} + try: + response = session.get(EDIT_VERSION_URL, params=params) + response.raise_for_status() + except RequestException: + raise AddVersionError() + error = response_error(response) + if error: + raise AddVersionError(error) + + @command() @option('-s', '--srcdir', prompt=True) @option('-e', '--email', prompt=True) @option('-p', '--password', prompt=True, hide_input=True) def main(srcdir, email, password): srcdir = srcdir.rstrip('/') session = Session() log_in(session, email, password) for product in products(): - folder = mkdtemp() try: - command = ['cmake', '-L', srcdir + '/' + product] - execution = run(command, cwd=folder, stdout=PIPE, stderr=DEVNULL) - output = execution.stdout.decode() - if '-- Cache values' not in output: - output_file = mkstemp() - with open(output_file, 'w') as file: - file.write(output) - raise RuntimeError( - 'Unexpected CMake output for \'{product}\'.\n' - '\n' - '\'-- Cache values\' string not found in CMake output for ' - '\'{product}\'. Please report this issue to the script ' - 'maintainers and attach the CMake output, which has been ' - 'saved to \'{output_file}\'.'.format( - product=product, output_file=output_file)) - match = VERSION_RE.search(output) - if not match: + trace = cmake_trace(srcdir + '/' + product) + version = project_version_from_cmake_trace(trace) + if version is None: echo('{} (skipped)'.format(product)) continue - version = match.group(1) - if not version: - raise RuntimeError( - 'BUGZILLA_APPLICATION_VERSION has been found, but its ' - 'value is an empty string. Please, review your CMake set ' - 'command.') - params = {'action': 'add', 'product': product} - response = session.get(EDIT_VERSION_URL, params=params) - soup = BeautifulSoup(response.text, 'html.parser') try: - token = soup.find('input', {'name': 'token'})['value'] - except: - raise RuntimeError('Could not parse token from \'{}\''.format( - response.url)) - params = {'version': version, 'action': 'new', 'product': product, - 'token': token} - try: - response = session.get(EDIT_VERSION_URL, params=params) - response.raise_for_status() - except RequestException: - echo('{} (error)'.format(product)) - continue - error = response_error(response) - if not error: - echo(product) - else: - echo('{} (error: {})'.format(product, error)) + add_version_to_bugzilla_project(session, product) + except AddVersionError as error: + if error.cause: + echo('{} (error: {})'.format(product, error.cause)) + else: + echo('{} (error)'.format(product)) + echo(product) except RuntimeError as error: echo('ERROR: {}'.format(error)) exit(1) - finally: - rmtree(folder) if __name__ == '__main__': main()