mirror of
https://github.com/openSUSE/osc.git
synced 2025-02-05 02:56:17 +01:00
Add 'git-obs pr checkout' command
This commit is contained in:
parent
0b5f9a0a8b
commit
9598f8070a
@ -6,8 +6,10 @@ Background:
|
||||
And I execute git-obs with args "repo fork pool/test-GitPkgA"
|
||||
And I execute git-obs with args "repo clone Admin/test-GitPkgA --no-ssh-strict-host-key-checking"
|
||||
And I set working directory to "{context.osc.temp}/test-GitPkgA"
|
||||
And I execute "sed -i 's@^\(Version.*\)@\1.1@' *.spec"
|
||||
And I execute "git commit -m 'Change version' -a"
|
||||
And I execute "sed -i 's@^\(Version: *\) .*@\1 v1.1@' *.spec"
|
||||
And I execute "git commit -m 'v1.1' -a"
|
||||
And I execute "sed -i 's@^\(Version: *\) .*@\1 v1.2@' *.spec"
|
||||
And I execute "git commit -m 'v1.2' -a"
|
||||
And I execute "git push"
|
||||
And I execute git-obs with args "pr create --title 'Change version' --description='some text'"
|
||||
|
||||
@ -24,6 +26,7 @@ Scenario: List pull requests
|
||||
State : open
|
||||
Draft : no
|
||||
Merged : no
|
||||
Allow edit : no
|
||||
Author : Admin \(admin@example.com\)
|
||||
Source : Admin/test-GitPkgA, branch: factory, commit: .*
|
||||
Description : some text
|
||||
@ -79,6 +82,7 @@ Scenario: Get a pull request
|
||||
State : open
|
||||
Draft : no
|
||||
Merged : no
|
||||
Allow edit : no
|
||||
Author : Admin \(admin@example.com\)
|
||||
Source : Admin/test-GitPkgA, branch: factory, commit: .*
|
||||
Description : some text
|
||||
@ -113,3 +117,108 @@ Scenario: Get a pull request that doesn't exist
|
||||
Total entries: 0
|
||||
ERROR: Couldn't retrieve the following pull requests: does-not/exist#1
|
||||
"""
|
||||
|
||||
|
||||
@destructive
|
||||
Scenario: Checkout a pull request
|
||||
Given I set working directory to "{context.osc.temp}"
|
||||
And I execute git-obs with args "repo clone pool/test-GitPkgA --no-ssh-strict-host-key-checking --directory=pool-test-GitPkgA"
|
||||
And I set working directory to "{context.osc.temp}/pool-test-GitPkgA"
|
||||
When I execute git-obs with args "pr checkout 1"
|
||||
Then the exit code is 0
|
||||
And stdout is
|
||||
"""
|
||||
"""
|
||||
And stderr is
|
||||
"""
|
||||
Using the following Gitea settings:
|
||||
* Config path: {context.git_obs.config}
|
||||
* Login (name of the entry in the config file): admin
|
||||
* URL: http://localhost:{context.podman.container.ports[gitea_http]}
|
||||
* User: Admin
|
||||
|
||||
Using core.sshCommand: ssh -o IdentitiesOnly=yes -o IdentityFile={context.fixtures}/ssh-keys/admin -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR
|
||||
From ssh://localhost:{context.podman.container.ports[gitea_ssh]}/Admin/test-GitPkgA
|
||||
* [new branch] factory -> Admin/factory
|
||||
Using core.sshCommand: ssh -o IdentitiesOnly=yes -o IdentityFile={context.fixtures}/ssh-keys/admin -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR
|
||||
From ssh://localhost:{context.podman.container.ports[gitea_ssh]}/pool/test-GitPkgA
|
||||
* [new ref] refs/pull/1/head -> pull/1
|
||||
Switched to branch 'pull/1'
|
||||
"""
|
||||
|
||||
@destructive
|
||||
Scenario: Checkout a pull request as a different user, make changes, commit, push
|
||||
Given I set working directory to "{context.osc.temp}"
|
||||
And I execute git-obs with args "repo clone pool/test-GitPkgA --no-ssh-strict-host-key-checking --directory=pool-test-GitPkgA -G alice"
|
||||
And I set working directory to "{context.osc.temp}/pool-test-GitPkgA"
|
||||
And I execute git-obs with args "pr checkout 1"
|
||||
And I execute git-obs with args "api -X PUT teams/1/members/alice"
|
||||
And I execute git-obs with args "pr set --allow-maintainer-edit=1 pool/test-GitPkgA#1"
|
||||
When I execute "sed -i 's@^\(Version: *\) .*@\1 v1.3@' *.spec"
|
||||
And I execute "git commit -m 'v1.3' -a"
|
||||
And I execute "git push"
|
||||
Then the exit code is 0
|
||||
|
||||
|
||||
@destructive
|
||||
Scenario: Rebase a pull request checkout to fast-forwardable changes
|
||||
# Alice makes a pull request checkout
|
||||
Given I set working directory to "{context.osc.temp}"
|
||||
And I execute git-obs with args "repo clone pool/test-GitPkgA --no-ssh-strict-host-key-checking --directory=alice-test-GitPkgA -G alice"
|
||||
And I set working directory to "{context.osc.temp}/alice-test-GitPkgA"
|
||||
And I execute git-obs with args "pr checkout 1"
|
||||
|
||||
# Admin pushes additional changes
|
||||
Given I set working directory to "{context.osc.temp}/test-GitPkgA"
|
||||
And I execute "sed -i 's@^\(Version: *\) .*@\1 v2@' *.spec"
|
||||
And I execute "git commit -m 'v2' -a"
|
||||
And I execute "git push"
|
||||
|
||||
# rebase Alice's checkout to the latest Admin's changes
|
||||
When I set working directory to "{context.osc.temp}/alice-test-GitPkgA"
|
||||
# `git fetch` is required to fetch all new changes before the rebase
|
||||
And I execute "git fetch Admin"
|
||||
And I execute "git rebase"
|
||||
And I execute "git log --pretty=format:%s HEAD^^..HEAD"
|
||||
Then the exit code is 0
|
||||
And stdout is
|
||||
"""
|
||||
v2
|
||||
v1.2
|
||||
"""
|
||||
|
||||
|
||||
@destructive
|
||||
Scenario: Rebase a pull request checkout to non fast-forwardable changes
|
||||
# Alice makes a pull request checkout
|
||||
Given I set working directory to "{context.osc.temp}"
|
||||
And I execute git-obs with args "repo clone pool/test-GitPkgA --no-ssh-strict-host-key-checking --directory=alice-test-GitPkgA -G alice"
|
||||
And I set working directory to "{context.osc.temp}/alice-test-GitPkgA"
|
||||
And I execute git-obs with args "pr checkout 1"
|
||||
And I execute "sed -i 's@^\(Version: *\) .*@\1 v123@' *.spec"
|
||||
And I execute "git commit -m 'v123' -a"
|
||||
|
||||
# Admin pushes a non fast-forwardable change
|
||||
Given I set working directory to "{context.osc.temp}/test-GitPkgA"
|
||||
And I execute "git reset --hard HEAD^"
|
||||
And I execute "git push --force"
|
||||
|
||||
# rebase Alice's checkout to the latest Admin's changes
|
||||
When I set working directory to "{context.osc.temp}/alice-test-GitPkgA"
|
||||
# `git fetch` is required to fetch all new changes before the rebase
|
||||
And I execute "git fetch Admin"
|
||||
And I execute "git rebase"
|
||||
# error due to a conflicting file
|
||||
And the exit code is 1
|
||||
# --theirs refers to Alice's version of the file
|
||||
And I execute "git checkout --theirs test-GitPkgA.spec"
|
||||
And I execute "git add test-GitPkgA.spec"
|
||||
# avoid opening an editor by setting the GIT_EDITOR env variable
|
||||
And I execute "GIT_EDITOR=true git rebase --continue"
|
||||
And I execute "git log --pretty=format:%s HEAD^^..HEAD"
|
||||
Then the exit code is 0
|
||||
And stdout is
|
||||
"""
|
||||
v123
|
||||
v1.1
|
||||
"""
|
||||
|
@ -39,6 +39,18 @@ class OwnerRepoPullAction(argparse.Action):
|
||||
setattr(namespace, self.dest, namespace_value)
|
||||
|
||||
|
||||
class BooleanAction(argparse.Action):
|
||||
def __call__(self, parser, namespace, value, option_string=None):
|
||||
if value is None:
|
||||
setattr(namespace, self.dest, None)
|
||||
elif value.lower() in ["0", "no", "false", "off"]:
|
||||
setattr(namespace, self.dest, False)
|
||||
elif value.lower() in ["1", "yes", "true", "on"]:
|
||||
setattr(namespace, self.dest, True)
|
||||
else:
|
||||
raise argparse.ArgumentError(self, f"Invalid boolean value: {value}")
|
||||
|
||||
|
||||
class GitObsCommand(osc.commandline_common.Command):
|
||||
@property
|
||||
def gitea_conf(self):
|
||||
|
63
osc/commands_git/pr_checkout.py
Normal file
63
osc/commands_git/pr_checkout.py
Normal file
@ -0,0 +1,63 @@
|
||||
import subprocess
|
||||
|
||||
import osc.commandline_git
|
||||
|
||||
|
||||
class PullRequestCheckoutCommand(osc.commandline_git.GitObsCommand):
|
||||
"""
|
||||
Check out a pull request
|
||||
"""
|
||||
|
||||
name = "checkout"
|
||||
parent = "PullRequestCommand"
|
||||
|
||||
def init_arguments(self):
|
||||
self.add_argument(
|
||||
"pull",
|
||||
type=int,
|
||||
help="Number of the pull request",
|
||||
)
|
||||
self.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Reset the existing local branch to the latest state of the pull request",
|
||||
)
|
||||
|
||||
def run(self, args):
|
||||
from osc import gitea_api
|
||||
|
||||
self.print_gitea_settings()
|
||||
|
||||
git = gitea_api.Git(".")
|
||||
owner, repo = git.get_owner_repo()
|
||||
|
||||
pr = gitea_api.PullRequest.get(self.gitea_conn, owner, repo, args.pull).json()
|
||||
|
||||
head_ssh_url = pr["head"]["repo"]["ssh_url"]
|
||||
head_owner = pr["head"]["repo"]["owner"]["login"]
|
||||
head_branch = pr["head"]["ref"]
|
||||
|
||||
try:
|
||||
git.add_remote(head_owner, head_ssh_url)
|
||||
except subprocess.CalledProcessError as e:
|
||||
# TODO: check if the remote url matches
|
||||
if e.returncode != 3:
|
||||
# returncode 3 means that the remote exists; see `man git-remote`
|
||||
raise
|
||||
git.fetch(head_owner)
|
||||
|
||||
local_branch = git.fetch_pull_request(args.pull, force=args.force)
|
||||
|
||||
# LFS data may not be accessible in the "origin" remote, we need to allow searching in all remotes
|
||||
git.set_config("lfs.remote.searchall", "1")
|
||||
|
||||
# configure branch for `git push`
|
||||
git.set_config(f"branch.{local_branch}.remote", head_owner)
|
||||
git.set_config(f"branch.{local_branch}.pushRemote", head_owner)
|
||||
git.set_config(f"branch.{local_branch}.merge", f"refs/heads/{head_branch}")
|
||||
|
||||
# allow `git push` with no arguments to push to a remote branch that is named differently than the local branch
|
||||
git.set_config("push.default", "upstream")
|
||||
|
||||
git.switch(local_branch)
|
@ -32,7 +32,7 @@ class PullRequestGetCommand(osc.commandline_git.GitObsCommand):
|
||||
failed_entries = []
|
||||
for owner, repo, pull in args.owner_repo_pull:
|
||||
try:
|
||||
pr = gitea_api.PullRequest.get(self.gitea_conn, owner, repo, pull).json()
|
||||
pr = gitea_api.PullRequest.get(self.gitea_conn, owner, repo, int(pull)).json()
|
||||
num_entries += 1
|
||||
except gitea_api.GiteaException as e:
|
||||
if e.status == 404:
|
||||
|
71
osc/commands_git/pr_set.py
Normal file
71
osc/commands_git/pr_set.py
Normal file
@ -0,0 +1,71 @@
|
||||
import sys
|
||||
|
||||
import osc.commandline_git
|
||||
|
||||
|
||||
def b(value: str):
|
||||
if value is not None:
|
||||
return value.lower() in ["1", "yes", "true", "on"]
|
||||
return None
|
||||
|
||||
|
||||
class PullRequestSetCommand(osc.commandline_git.GitObsCommand):
|
||||
"""
|
||||
Change a pull request
|
||||
"""
|
||||
|
||||
name = "set"
|
||||
parent = "PullRequestCommand"
|
||||
|
||||
def init_arguments(self):
|
||||
self.add_argument_owner_repo_pull(nargs="+")
|
||||
self.add_argument(
|
||||
"--title",
|
||||
)
|
||||
self.add_argument(
|
||||
"--description",
|
||||
)
|
||||
self.add_argument(
|
||||
"--allow-maintainer-edit",
|
||||
action=osc.commandline_git.BooleanAction,
|
||||
help="Users with write access to the base branch can also push to the pull request's head branch",
|
||||
)
|
||||
|
||||
def run(self, args):
|
||||
from osc import gitea_api
|
||||
from osc.core import highlight_diff
|
||||
from osc.output import tty
|
||||
|
||||
self.print_gitea_settings()
|
||||
print(args)
|
||||
|
||||
num_entries = 0
|
||||
failed_entries = []
|
||||
for owner, repo, pull in args.owner_repo_pull:
|
||||
try:
|
||||
pr = gitea_api.PullRequest.set(
|
||||
self.gitea_conn,
|
||||
owner,
|
||||
repo,
|
||||
int(pull),
|
||||
title=args.title,
|
||||
description=args.description,
|
||||
allow_maintainer_edit=args.allow_maintainer_edit,
|
||||
).json()
|
||||
num_entries += 1
|
||||
except gitea_api.GiteaException as e:
|
||||
if e.status == 404:
|
||||
failed_entries.append(f"{owner}/{repo}#{pull}")
|
||||
continue
|
||||
raise
|
||||
|
||||
print(gitea_api.PullRequest.to_human_readable_string(pr))
|
||||
print()
|
||||
|
||||
print(f"Total modified entries: {num_entries}", file=sys.stderr)
|
||||
if failed_entries:
|
||||
print(
|
||||
f"{tty.colorize('ERROR', 'red,bold')}: Couldn't change the following pull requests: {', '.join(failed_entries)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import subprocess
|
||||
import urllib
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
@ -8,9 +9,17 @@ class Git:
|
||||
def __init__(self, workdir):
|
||||
self.abspath = os.path.abspath(workdir)
|
||||
|
||||
def _run_git(self, args) -> str:
|
||||
def _run_git(self, args: list) -> str:
|
||||
return subprocess.check_output(["git"] + args, encoding="utf-8", cwd=self.abspath).strip()
|
||||
|
||||
def init(self, *, quiet=True):
|
||||
cmd = ["init"]
|
||||
if quiet:
|
||||
cmd += ["-q"]
|
||||
self._run_git(cmd)
|
||||
|
||||
# BRANCHES
|
||||
|
||||
@property
|
||||
def current_branch(self) -> str:
|
||||
return self._run_git(["branch", "--show-current"])
|
||||
@ -18,14 +27,58 @@ class Git:
|
||||
def get_branch_head(self, branch: str) -> str:
|
||||
return self._run_git(["rev-parse", branch])
|
||||
|
||||
def switch(self, branch: str):
|
||||
self._run_git(["switch", branch])
|
||||
|
||||
def fetch_pull_request(
|
||||
self,
|
||||
pull_number: int,
|
||||
*,
|
||||
remote: str = "origin",
|
||||
force: bool = False,
|
||||
):
|
||||
"""
|
||||
Fetch pull/$pull_number/head to pull/$pull_number branch
|
||||
"""
|
||||
target_branch = f"pull/{pull_number}"
|
||||
cmd = ["fetch", remote, f"pull/{pull_number}/head:{target_branch}"]
|
||||
if force:
|
||||
cmd += [
|
||||
"--force",
|
||||
"--update-head-ok",
|
||||
]
|
||||
self._run_git(cmd)
|
||||
return target_branch
|
||||
|
||||
# CONFIG
|
||||
|
||||
def set_config(self, key: str, value: str):
|
||||
self._run_git(["config", key, value])
|
||||
|
||||
# REMOTES
|
||||
|
||||
def get_remote_url(self, name: str = "origin") -> str:
|
||||
return self._run_git(["remote", "get-url", name])
|
||||
|
||||
def add_remote(self, name: str, url: str):
|
||||
self._run_git(["remote", "add", name, url])
|
||||
|
||||
def fetch(self, name: Optional[str] = None):
|
||||
if name:
|
||||
cmd = ["fetch", name]
|
||||
else:
|
||||
cmd = ["fetch", "--all"]
|
||||
self._run_git(cmd)
|
||||
|
||||
def get_owner_repo(self, remote: str = "origin") -> Tuple[str, str]:
|
||||
remote_url = self.get_remote_url(name=remote)
|
||||
if "@" in remote_url:
|
||||
# ssh://gitea@example.com:owner/repo.git
|
||||
# ssh://gitea@example.com:22/owner/repo.git
|
||||
remote_url = remote_url.rsplit("@", 1)[-1]
|
||||
parsed_remote_url = urllib.parse.urlparse(remote_url)
|
||||
path = parsed_remote_url.path
|
||||
if path.endswith(".git"):
|
||||
path = path[:-4]
|
||||
owner, repo = path.strip("/").split("/")[:2]
|
||||
owner, repo = path.strip("/").split("/")[-2:]
|
||||
return owner, repo
|
||||
|
@ -56,6 +56,7 @@ class PullRequest:
|
||||
if is_pull_request:
|
||||
table.add("Draft", yes_no(entry["draft"]))
|
||||
table.add("Merged", yes_no(entry["merged"]))
|
||||
table.add("Allow edit", yes_no(entry["allow_maintainer_edit"]))
|
||||
table.add("Author", f"{User.to_login_full_name_email_string(entry['user'])}")
|
||||
if is_pull_request:
|
||||
table.add("Source", f"{entry['head']['repo']['full_name']}, branch: {entry['head']['ref']}, commit: {entry['head']['sha']}")
|
||||
@ -113,7 +114,7 @@ class PullRequest:
|
||||
conn: Connection,
|
||||
owner: str,
|
||||
repo: str,
|
||||
number: str,
|
||||
number: int,
|
||||
) -> GiteaHTTPResponse:
|
||||
"""
|
||||
Get a pull request.
|
||||
@ -123,9 +124,40 @@ class PullRequest:
|
||||
:param repo: Name of the repo.
|
||||
:param number: Number of the pull request in the repo.
|
||||
"""
|
||||
url = conn.makeurl("repos", owner, repo, "pulls", number)
|
||||
url = conn.makeurl("repos", owner, repo, "pulls", str(number))
|
||||
return conn.request("GET", url)
|
||||
|
||||
@classmethod
|
||||
def set(
|
||||
cls,
|
||||
conn: Connection,
|
||||
owner: str,
|
||||
repo: str,
|
||||
number: int,
|
||||
*,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
allow_maintainer_edit: Optional[bool] = None,
|
||||
) -> GiteaHTTPResponse:
|
||||
"""
|
||||
Change a pull request.
|
||||
|
||||
:param conn: Gitea ``Connection`` instance.
|
||||
:param owner: Owner of the repo.
|
||||
:param repo: Name of the repo.
|
||||
:param number: Number of the pull request in the repo.
|
||||
:param title: Change pull request title.
|
||||
:param description: Change pull request description.
|
||||
:param allow_maintainer_edit: Change whether users with write access to the base branch can also push to the pull request's head branch.
|
||||
"""
|
||||
json_data = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"allow_maintainer_edit": allow_maintainer_edit,
|
||||
}
|
||||
url = conn.makeurl("repos", owner, repo, "pulls", str(number))
|
||||
return conn.request("PATCH", url, json_data=json_data)
|
||||
|
||||
@classmethod
|
||||
def list(
|
||||
cls,
|
||||
|
Loading…
Reference in New Issue
Block a user