mirror of
https://git.centos.org/centos/centpkg.git
synced 2025-02-23 16:22:55 +00:00
Create fork of the active repository by subcommand
Adds new command 'fork' that calls API method which forks active repository for the given (or active) user and creates a remote record (named after user) in git configuration. GitLab Personal Access Token have to be added to the config for proper functionality. Signed-off-by: Ondrej Nosek <onosek@redhat.com>
This commit is contained in:
parent
065f9c3885
commit
80f38de2d6
3 changed files with 238 additions and 3 deletions
|
@ -26,3 +26,7 @@ git_excludes =
|
|||
/.build-*.log
|
||||
results_*/
|
||||
clog
|
||||
|
||||
[centpkg.distgit]
|
||||
apibaseurl = https://gitlab.com
|
||||
token =
|
||||
|
|
|
@ -14,7 +14,13 @@
|
|||
# the full text of the license.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import textwrap
|
||||
|
||||
from centpkg.utils import config_get_safely, do_add_remote, do_fork
|
||||
from pyrpkg.cli import cliClient
|
||||
from six.moves.urllib_parse import urlparse
|
||||
|
||||
|
||||
class centpkgClient(cliClient):
|
||||
|
@ -25,10 +31,89 @@ class centpkgClient(cliClient):
|
|||
self.setup_centos_subparsers()
|
||||
|
||||
def setup_centos_subparsers(self):
|
||||
self.register_parser()
|
||||
self.register_do_fork()
|
||||
|
||||
def register_parser(self):
|
||||
pass
|
||||
def register_do_fork(self):
|
||||
help_msg = 'Create a new fork of the current repository'
|
||||
distgit_section = '{0}.distgit'.format(self.name)
|
||||
distgit_api_base_url = config_get_safely(self.config, distgit_section, "apibaseurl")
|
||||
description = textwrap.dedent('''
|
||||
Create a new fork of the current repository
|
||||
|
||||
Before the operation, you need to generate an API token at
|
||||
https://{1}/-/profile/personal_access_tokens, select the relevant
|
||||
scope(s) and save it in your local user configuration located
|
||||
at ~/.config/rpkg/{0}.conf. For example:
|
||||
|
||||
[{0}.distgit]
|
||||
token = <api_key_here>
|
||||
|
||||
Below is a basic example of the command to fork a current repository:
|
||||
|
||||
{0} fork
|
||||
|
||||
Operation requires username (GITLAB_ID). by default, current logged
|
||||
username is taken. It could be overridden by reusing an argument:
|
||||
|
||||
{0} --user GITLAB_ID fork
|
||||
'''.format(self.name, urlparse(distgit_api_base_url).netloc))
|
||||
|
||||
fork_parser = self.subparsers.add_parser(
|
||||
'fork',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help=help_msg,
|
||||
description=description)
|
||||
fork_parser.set_defaults(command=self.do_distgit_fork)
|
||||
|
||||
def do_distgit_fork(self):
|
||||
"""create fork of the distgit repository
|
||||
That includes creating fork itself using API call and then adding
|
||||
remote tracked repository
|
||||
"""
|
||||
distgit_section = '{0}.distgit'.format(self.name)
|
||||
distgit_api_base_url = config_get_safely(self.config, distgit_section, "apibaseurl")
|
||||
distgit_remote_base_url = self.config.get(
|
||||
'{0}'.format(self.name),
|
||||
"gitbaseurl",
|
||||
vars={'user': self.cmd.user, 'repo': self.cmd.repo_name},
|
||||
)
|
||||
distgit_token = config_get_safely(self.config, distgit_section, 'token')
|
||||
|
||||
ret = do_fork(
|
||||
logger=self.log,
|
||||
base_url=distgit_api_base_url,
|
||||
token=distgit_token,
|
||||
repo_name=self.cmd.repo_name,
|
||||
namespace=self.cmd.ns,
|
||||
cli_name=self.name,
|
||||
)
|
||||
|
||||
# assemble url of the repo in web browser
|
||||
fork_url = '{0}/{1}/{2}'.format(
|
||||
distgit_api_base_url.rstrip('/'),
|
||||
self.cmd.user,
|
||||
self.cmd.repo_name,
|
||||
)
|
||||
|
||||
if ret:
|
||||
msg = "Fork of the repository has been created: '{0}'"
|
||||
else:
|
||||
msg = "Repo '{0}' already exists."
|
||||
self.log.info(msg.format(fork_url))
|
||||
|
||||
ret = do_add_remote(
|
||||
base_url=distgit_api_base_url,
|
||||
remote_base_url=distgit_remote_base_url,
|
||||
username=self.cmd.user,
|
||||
repo=self.cmd.repo,
|
||||
repo_name=self.cmd.repo_name,
|
||||
namespace=self.cmd.ns,
|
||||
)
|
||||
if ret:
|
||||
msg = "Adding as remote '{0}'."
|
||||
else:
|
||||
msg = "Remote with name '{0}' already exists."
|
||||
self.log.info(msg.format(self.cmd.user))
|
||||
|
||||
|
||||
class centpkgClientSig(cliClient):
|
||||
|
|
146
src/centpkg/utils.py
Normal file
146
src/centpkg/utils.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# utils.py - a module with support methods for centpkg
|
||||
#
|
||||
# Copyright (C) 2021 Red Hat Inc.
|
||||
# Author(s): Ondrej Nosek <onosek@redhat.com>
|
||||
#
|
||||
# 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. See http://www.gnu.org/copyleft/gpl.html for
|
||||
# the full text of the license.
|
||||
|
||||
import json
|
||||
|
||||
import git
|
||||
import requests
|
||||
from pyrpkg import rpkgError
|
||||
from requests.exceptions import ConnectionError
|
||||
from six.moves.configparser import NoOptionError, NoSectionError
|
||||
from six.moves.urllib.parse import quote_plus, urlparse
|
||||
|
||||
|
||||
def do_fork(logger, base_url, token, repo_name, namespace, cli_name):
|
||||
"""
|
||||
Creates a fork of the project.
|
||||
:param logger: A logger object
|
||||
:param base_url: a string of the URL repository
|
||||
:param token: a string of the API token that has rights to make a fork
|
||||
:param repo_name: a string of the repository name
|
||||
:param namespace: a string determines a type of the repository
|
||||
:param cli_name: string of the CLI's name (e.g. centpkg)
|
||||
:return: a bool; True when fork was created, False when already exists
|
||||
"""
|
||||
api_url = '{0}/api/v4'.format(base_url.rstrip('/'))
|
||||
project_id = quote_plus("redhat/centos-stream/{0}/{1}".format(namespace, repo_name))
|
||||
fork_url = '{0}/projects/{1}/fork'.format(api_url, project_id)
|
||||
|
||||
headers = {
|
||||
'PRIVATE-TOKEN': token,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
payload = json.dumps({})
|
||||
try:
|
||||
rv = requests.post(
|
||||
fork_url, headers=headers, data=payload, timeout=60)
|
||||
except ConnectionError as error:
|
||||
error_msg = ('The connection to API failed while trying to '
|
||||
'create a new fork. The error was: {0}'.format(str(error)))
|
||||
raise rpkgError(error_msg)
|
||||
|
||||
try:
|
||||
# Extract response json for debugging
|
||||
rv_json = rv.json()
|
||||
logger.debug("Pagure API response: '{0}'".format(rv_json))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
base_error_msg = ('The following error occurred while creating a new fork: {0}')
|
||||
if not rv.ok:
|
||||
# fork was already created
|
||||
if rv.status_code == 409 or rv.reason == "Conflict":
|
||||
return False
|
||||
# show hint for invalid, expired or revoked token
|
||||
elif rv.status_code == 401 or rv.reason == "Unauthorized":
|
||||
base_error_msg += '\nFor invalid or expired token refer to ' \
|
||||
'"{0} fork -h" to set a token in your user ' \
|
||||
'configuration.'.format(cli_name)
|
||||
raise rpkgError(base_error_msg.format(rv.text))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def do_add_remote(base_url, remote_base_url, username, repo, repo_name,
|
||||
namespace):
|
||||
"""
|
||||
Adds remote tracked repository
|
||||
:param base_url: a string of the URL repository
|
||||
:param remote_base_url: a string of the remote tracked repository
|
||||
:param username: a string of the (FAS) user name
|
||||
:param repo: object, current project git repository
|
||||
:param repo_name: a string of the repository name
|
||||
:param namespace: a string determines a type of the repository
|
||||
:return: a bool; True if remote was created, False when already exists
|
||||
"""
|
||||
parsed_url = urlparse(remote_base_url)
|
||||
remote_url = '{0}://{1}/{2}/{3}.git'.format(
|
||||
parsed_url.scheme,
|
||||
parsed_url.netloc,
|
||||
username,
|
||||
repo_name,
|
||||
)
|
||||
|
||||
# check already existing remote
|
||||
for remote in repo.remotes:
|
||||
if remote.name == username:
|
||||
return False
|
||||
|
||||
try:
|
||||
# create remote with username as its name
|
||||
repo.create_remote(username, url=remote_url)
|
||||
except git.exc.GitCommandError as e:
|
||||
error_msg = "During create remote:\n {0}\n {1}".format(
|
||||
" ".join(e.command), e.stderr)
|
||||
raise rpkgError(error_msg)
|
||||
return True
|
||||
|
||||
|
||||
def config_get_safely(config, section, option):
|
||||
"""
|
||||
Returns option from the user's configuration file. In case of missing
|
||||
section or option method throws an exception with a human-readable
|
||||
warning and a possible hint.
|
||||
The method should be used especially in situations when there are newly
|
||||
added sections/options into the config. In this case, there is a risk that
|
||||
the user's config wasn't properly upgraded.
|
||||
|
||||
:param config: ConfigParser object
|
||||
:param section: section name in the config
|
||||
:param option: name of the option
|
||||
:return: option value from the right section
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
hint = (
|
||||
"First (if possible), refer to the help of the current command "
|
||||
"(-h/--help).\n"
|
||||
"There also might be a new version of the config after upgrade.\n"
|
||||
"Hint: you can check if you have 'centpkg.conf.rpmnew' or "
|
||||
"'centpkg.conf.rpmsave' in the config directory. If yes, try to merge "
|
||||
"your changes to the config with the maintainer provided version "
|
||||
"(or replace centpkg.conf file with 'centpkg.conf.rpmnew')."
|
||||
)
|
||||
|
||||
try:
|
||||
return config.get(section, option)
|
||||
except NoSectionError:
|
||||
msg = "Missing section '{0}' in the config file.".format(section)
|
||||
raise rpkgError("{0}\n{1}".format(msg, hint))
|
||||
except NoOptionError:
|
||||
msg = "Missing option '{0}' in the section '{1}' of the config file.".format(
|
||||
option, section
|
||||
)
|
||||
raise rpkgError("{0}\n{1}".format(msg, hint))
|
||||
except Exception:
|
||||
raise
|
Loading…
Add table
Reference in a new issue