1
0
mirror of https://github.com/openSUSE/osc.git synced 2025-08-19 21:18:53 +02:00
Files
github.com_openSUSE_osc/osc/commands/fork.py

201 lines
8.6 KiB
Python

import re
import sys
import urllib.parse
import osc.commandline
import osc.commandline_git
class ForkCommand(osc.commandline.OscCommand):
"""
Fork a project or a package with sources managed in Gitea
"""
name = "fork"
# inject some git-obs methods to avoid duplicating code
add_argument_new_repo_name = osc.commandline_git.GitObsCommand.add_argument_new_repo_name
post_parse_args = osc.commandline_git.GitObsMainCommand.post_parse_args
print_gitea_settings = osc.commandline_git.GitObsCommand.print_gitea_settings
def init_arguments(self):
# inherit global options from the main git-obs command
osc.commandline_git.GitObsMainCommand.init_arguments(self)
self.add_argument(
"project",
help="Name of the project",
)
self.add_argument(
"package",
nargs="?",
help="Name of the package",
)
self.add_argument(
"--target-project",
help="Name of the target project (defaults to home:$user:branches)",
)
self.add_argument(
"--target-package",
help="Name of the package (defaults to $package)",
)
self.add_argument_new_repo_name()
self.add_argument(
"--no-devel-project",
action="store_true",
help="Fork the specified package instead the package from the devel project (which is the place where the package is developed)",
)
def run(self, args):
from osc import conf as osc_conf
from osc import gitea_api
from osc import obs_api
from osc.git_scm import GitStore
from osc.output import tty
# make a copy of project, package; if we change them, the original values remain in args
project = args.project
package = args.package
is_package = package is not None
use_devel_project = False
if not is_package and args.target_package:
self.parser.error("The '--target-package' option requires the 'package' argument to be set")
if not is_package and args.no_devel_project:
self.parser.error("The '--no-devel-project' option can be used only when forking a package")
if is_package:
# get the package meta from the OBS API first
pkg = obs_api.Package.from_api(args.apiurl, project, package)
if not args.no_devel_project:
# devel project is not set in package meta as usual but we parse it from "OBS:RejectBranch" attribute
attributes = obs_api.Attributes.from_api(args.apiurl, project, package, attr="OBS:RejectBranch").attribute_list
if attributes:
attribute = attributes[0].value
# the pattern starts with a non-greedy match so we capture the first url
match = re.match(r".*?(https://[^ ]+).*", attribute)
if match:
devel_project_url = match.group(1)
build_project = GitStore.get_build_project(devel_project_url)
# override the package we're cloning with the one from the devel project
use_devel_project = True
project = build_project
pkg = obs_api.Package.from_api(args.apiurl, project, package)
if not pkg.scmsync:
print(f"{tty.colorize('ERROR', 'red,bold')}: Forking is possible only with packages managed in Git (the <scmsync> element must be set in the package meta)")
sys.exit(1)
else:
# get the project meta from the OBS API first
project = obs_api.Project.from_api(args.apiurl, project)
if not project.scmsync:
raise RuntimeError(
"Forking is possible only with projects managed in Git (the <scmsync> element must be set in the project meta)"
)
# parse gitea url, owner, repo and branch from the scmsync url
if is_package:
parsed_scmsync_url = urllib.parse.urlparse(pkg.scmsync, scheme="https")
else:
parsed_scmsync_url = urllib.parse.urlparse(project.scmsync, scheme="https")
url = urllib.parse.urlunparse((parsed_scmsync_url.scheme, parsed_scmsync_url.netloc, "", "", "", ""))
owner, repo = parsed_scmsync_url.path.strip("/").split("/")
# remove trailing ".git" from repo
if repo.endswith(".git"):
repo = repo[:-4]
# temporary hack to allow people using fork atm at all, when packages
# are managed via git project.
# fallback always to default branch for now, but we actually need to
# parse the right branch instead from .gitmodules
#branch = parsed_scmsync_url.fragment or None
branch = None
# find a credentials entry for url and OBS user (there can be multiple users configured for a single URL in the config file)
gitea_conf = gitea_api.Config(args.gitea_config)
gitea_login = gitea_conf.get_login_by_url_user(url=url, user=osc_conf.get_apiurl_usr(args.apiurl))
gitea_conn = gitea_api.Connection(gitea_login)
# store the attributes for self.print_gitea_settings()
self.gitea_conf = gitea_conf
self.gitea_login = gitea_login
self.gitea_conn = gitea_conn
self.print_gitea_settings()
print(f"Forking git repo {owner}/{repo} ...", file=sys.stderr)
# the branch was not specified, fetch the default branch from the repo
if branch:
fork_branch = branch
else:
repo_data = gitea_api.Repo.get(gitea_conn, owner, repo).json()
branch = repo_data["default_branch"]
fork_branch = branch
# check if the scmsync branch exists in the source repo
parent_branch_data = gitea_api.Branch.get(gitea_conn, owner, repo, fork_branch).json()
try:
repo_data = gitea_api.Fork.create(gitea_conn, owner, repo, new_repo_name=args.new_repo_name).json()
fork_owner = repo_data["owner"]["login"]
fork_repo = repo_data["name"]
print(f" * Fork created: {fork_owner}/{fork_repo}")
except gitea_api.ForkExists as e:
fork_owner = e.fork_owner
fork_repo = e.fork_repo
print(f" * Fork already exists: {fork_owner}/{fork_repo}")
# XXX: implicit branch name should be forbidden; assumptions are bad
fork_scmsync = urllib.parse.urlunparse(
(parsed_scmsync_url.scheme, parsed_scmsync_url.netloc, f"{fork_owner}/{fork_repo}", "", "", fork_branch)
)
print()
if is_package:
print(f"Forking OBS package {project}/{package} ...")
if use_devel_project:
print(f" * {tty.colorize('NOTE', 'bold')}: Forking from the devel project instead of the specified {args.project}/{args.package}")
else:
print(f"Forking OBS project {project} ...")
print(f" * OBS apiurl: {args.apiurl}")
# we use a single API endpoint for forking both projects and packages (project requires setting package to "_project")
status = obs_api.Package.cmd_fork(
args.apiurl,
project,
package if is_package else "_project",
scmsync=fork_scmsync,
target_project=args.target_project,
target_package=args.target_package if is_package else None,
)
# XXX: the current OBS API is not ideal; we don't get any info whether the new package exists already; 404 would be probably nicer
target_project = status.data["targetproject"]
if is_package:
target_package = status.data["targetpackage"]
print(f" * Fork created: {target_project}/{target_package}")
else:
print(f" * Fork created: {target_project}")
print(f" * scmsync URL: {fork_scmsync}")
# check if the scmsync branch exists in the forked repo
fork_branch_data = gitea_api.Branch.get(gitea_conn, fork_owner, fork_repo, fork_branch).json()
parent_commit = parent_branch_data["commit"]["id"]
fork_commit = fork_branch_data["commit"]["id"]
if parent_commit != fork_commit:
print()
print(f"{tty.colorize('ERROR', 'red,bold')}: The branch in the forked repo is out of sync with the parent")
print(f" * Fork: {fork_owner}/{fork_repo}#{fork_branch}, commit: {fork_commit}")
print(f" * Parent: {owner}/{repo}#{fork_branch}, commit: {parent_commit}")
print(" * If this is not intentional, please clone the fork and fix the branch manually")
sys.exit(1)