diff --git a/swh/foo/bar.py b/swh/foo/bar.py deleted file mode 100644 index 8dd21f7..0000000 --- a/swh/foo/bar.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (C) 2015-2016 The Software Heritage developers -# See the AUTHORS file at the top-level directory of this distribution -# License: GNU General Public License version 3, or any later version -# See top-level LICENSE file for more information diff --git a/swh/mirror/forge/api.py b/swh/mirror/forge/api.py new file mode 100644 index 0000000..d4e3f45 --- /dev/null +++ b/swh/mirror/forge/api.py @@ -0,0 +1,31 @@ +# Copyright (C) 2017 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +"""Higher level api bindings to a phabricator forge. + +""" + +from .request import Request + + +class RepositorySearch(Request): + def url(self): + return 'diffusion.repository.search' + + def parse_response(self, data): + return data['data'] + + +class PassphraseSearch(Request): + def url(self): + return 'passphrase.query' + + def parse_response(self, data): + return data['data'] + + +class DiffusionUriEdit(Request): + def url(self): + return 'diffusion.uri.edit' diff --git a/swh/mirror/forge/request.py b/swh/mirror/forge/request.py new file mode 100644 index 0000000..6b9349d --- /dev/null +++ b/swh/mirror/forge/request.py @@ -0,0 +1,65 @@ +# Copyright (C) 2017 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +"""Lower level api bindings to a phabricator forge api. + +""" + +import json + +from abc import ABCMeta, abstractmethod +from subprocess import PIPE, Popen, check_output + + +class Request(metaclass=ABCMeta): + """Class in charge of providing connection request to forge's api. + + """ + def __init__(self, forge_url, api_token): + self.forge_url = forge_url + self.api_token = api_token + + @abstractmethod + def url(self): + """Api url (e.g diffusion.findsymbols, repository.search, + repository.edit.uri, etc...) + + """ + pass + + def parse_response(self, data): + """Parsing the query response. By default, identity function. + + """ + return data + + def request(self, **kwargs): + """Actual execution of the request. + + Note: Actual implementation depends on arcanist. I Did not + yet find the right way to use 'requests' with api token (that + is no oauth session...) + + """ + query = dict(**kwargs) + json_parameters = json.dumps(query) + + try: + with Popen(['echo', json_parameters], stdout=PIPE) as dump: + cmd = ['arc', 'call-conduit', + '--conduit-uri', self.forge_url, + '--conduit-token', self.api_token, + self.url()] + json_response = check_output(cmd, stdin=dump.stdout, + universal_newlines=True) + except Exception as e: + raise e + else: + if json_response: + data = json.loads(json_response) + + if 'errorMessage' in data and data['errorMessage'] is not None: + raise ValueError("Error: %s" % data['errorMessage']) + return self.parse_response(data['response']) diff --git a/swh/mirror/forge/sync.py b/swh/mirror/forge/sync.py new file mode 100755 index 0000000..41584bf --- /dev/null +++ b/swh/mirror/forge/sync.py @@ -0,0 +1,132 @@ +# Copyright (C) 2017 The Software Heritage developers +# See the AUTHORS file at the top-level directory of this distribution +# License: GNU General Public License version 3, or any later version +# See top-level LICENSE file for more information + +import click +import json +import os +import sys +import requests + +from .api import RepositorySearch, PassphraseSearch, DiffusionUriEdit + + +FORGE_API_URL = 'https://forge.softwareheritage.org' + + +def load_token_from_file(filepath): + """Loading the token.""" + with open(filepath, 'r') as f: + return f.read().strip() + + +def prepare_token(): + """Prepare the needed token from the disk. + + Returns: + tuple (token-forge, token-github) + + """ + token_forge_filepath = os.getenv("HOME") + '/.config/swh/forge-token' + token_forge = load_token_from_file(token_forge_filepath) + if not token_forge: + print("""Install the phabricator forge's token in %s +(https://forge.softwareheritage.org/settings/user//page/apitokens/). + +Once the installation is done, you can trigger this script again. + """ % token_forge_filepath) + sys.exit(1) + + token_github_filepath = os.getenv("HOME") + '/.config/swh/github-token' + token_github = load_token_from_file(token_github_filepath) + if not token_github: + print("""Install one personal github token in %s with scope +public_repo (https://github.com/settings/tokens). + +You must be associated to https://github.com/softwareheritage +organization. Once the installation is done, you can trigger this +script again. + """ % token_github_filepath) + sys.exit(1) + + return token_forge, token_github + + +@click.command() +@click.option('--repo-callsign', + help="Repository's callsign") +@click.option('--repo-name', + help="Repository name (used in github)") +@click.option('--repo-url', + help="Repository's forge url (used in github)") +@click.option('--repo-description', + help="Repository's description (used in github)") +@click.option('--credential-key-id', + help="credential to use for access from phabricator's forge to github") +@click.option('--github/--nogithub', default=True) +def run(repo_callsign, repo_name, repo_url, repo_description, + credential_key_id, github): + """This will instantiate a mirror from a repository forge to github. + + """ + ### Retrieve credential access to github and phabricator's forge + token_forge, token_github = prepare_token() + + if github: + ### Create repository in github + r = requests.post( + 'https://api.github.com/orgs/SoftwareHeritage/repos', + headers={'Authorization': 'token %s' % token_github}, + data=json.dumps({ + "name": repo_name, + "description": repo_description, + "homepage": repo_url, + "private": False, + "has_issues": False, + "has_wiki": False, + "has_downloads": True + })) + + if not r.ok: + print("""Failure to create the repository in github. +Status: %s""" % r.status_code) + sys.exit(1) + + ### Retrieve repository information + + query = RepositorySearch(FORGE_API_URL, token_forge) + data = query.request(constraints={ + "callsigns": [repo_callsign] + }) + + repo_phid = data[0]['phid'] + + ### Retrieve credential information + + query = PassphraseSearch(FORGE_API_URL, token_forge) + data = query.request(ids=[credential_key_id]) + + # Retrieve the phid for that passphrase + key_phid = list(data.values())[0]['phid'] + + repo_url_github = 'git@github.com:SoftwareHeritage/%s.git' % repo_name + + ### Install the github mirror in the forge + + query = DiffusionUriEdit(FORGE_API_URL, token_forge) + data = query.request(transactions=[ + {"type": "repository", "value": repo_phid}, + {"type": "uri", "value": repo_url_github}, + {"type": "io", "value": "mirror"}, + {"type": "display", "value": "never"}, + {"type": "disable", "value": False}, + {"type": "credential", "value": key_phid}, + ]) + + print("Repository %s mirrored at %s." % (repo_url, repo_url_github)) + sys.exit(0) + + +if __name__ == '__main__': + run()