Skip to content

Commit

Permalink
Add 'git-obs pr' command
Browse files Browse the repository at this point in the history
  • Loading branch information
dmach committed Jan 14, 2025
1 parent 166cadb commit 4db0066
Show file tree
Hide file tree
Showing 13 changed files with 795 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ jobs:
run: |
podman pull ghcr.io/suse-autobuild/obs-server:latest
- name: "Configure git"
run: |
git config --global user.email "admin@example.com"
git config --global user.name "Admin"
- name: "Run tests"
run: |
cd behave
Expand Down
115 changes: 115 additions & 0 deletions behave/features/git-pr.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
Feature: `git-obs pr` command


Background:
Given I set working directory to "{context.osc.temp}"
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 "git push"
And I execute git-obs with args "pr create --title 'Change version' --description='some text'"


@destructive
Scenario: List pull requests
When I execute git-obs with args "pr list pool test-GitPkgA"
Then the exit code is 0
And stdout matches
"""
ID : pool/test-GitPkgA#1
URL : http://localhost:{context.podman.container.ports[gitea_http]}/pool/test-GitPkgA/pulls/1
Title : Change version
State : open
Draft : no
Merged : no
Author : Admin \(admin@example.com\)
Source : Admin/test-GitPkgA, branch: factory, commit: .*
Description : some text
"""
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
Total entries: 1
"""


@destructive
Scenario: Search pull requests
When I execute git-obs with args "pr search"
Then the exit code is 0
And stdout matches
"""
ID : pool/test-GitPkgA#1
URL : http://localhost:{context.podman.container.ports[gitea_http]}/pool/test-GitPkgA/pulls/1
Title : Change version
State : open
Author : Admin \(admin@example.com\)
Description : some text
"""
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
Total entries: 1
"""


@destructive
Scenario: Get a pull request
When I execute git-obs with args "pr get pool/test-GitPkgA#1"
Then the exit code is 0
And stdout matches
"""
ID : pool/test-GitPkgA#1
URL : http://localhost:{context.podman.container.ports[gitea_http]}/pool/test-GitPkgA/pulls/1
Title : Change version
State : open
Draft : no
Merged : no
Author : Admin \(admin@example.com\)
Source : Admin/test-GitPkgA, branch: factory, commit: .*
Description : some text
"""
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
Total entries: 1
"""


@destructive
Scenario: Get a pull request that doesn't exist
When I execute git-obs with args "pr get does-not/exist#1"
Then the exit code is 1
And stdout matches
"""
"""
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
Total entries: 0
ERROR: Couldn't retrieve the following pull requests: does-not/exist#1
"""
6 changes: 6 additions & 0 deletions behave/features/steps/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ def run_in_context(context, cmd, can_fail=False, **run_args):
raise AssertionError('Running command "%s" failed: %s' % (cmd, context.cmd_exitcode))


@behave.step("I execute \"{command}\"")
def step_impl(context, command):
command = command.format(context=context)
run_in_context(context, command, can_fail=True)


@behave.step("stdout contains \"{text}\"")
def step_impl(context, text):
if re.search(text.format(context=context), context.cmd_stdout):
Expand Down
5 changes: 4 additions & 1 deletion osc/commandline_git.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import subprocess
import sys

import osc.commandline_common
Expand Down Expand Up @@ -132,7 +133,9 @@ def main():
except oscerr.OscBaseError as e:
print_msg(str(e), print_to="error")
sys.exit(1)

except subprocess.CalledProcessError as e:
print_msg(str(e), print_to="error")
sys.exit(1)

if __name__ == "__main__":
main()
19 changes: 19 additions & 0 deletions osc/commands_git/pr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import osc.commandline_git


# we decided not to use the command name 'pull' because that could be confused
# with the completely unrelated 'git pull' command


class PullRequestCommand(osc.commandline_git.GitObsCommand):
"""
Manage pull requests
"""

name = "pr"

def init_arguments(self):
pass

def run(self, args):
self.parser.print_help()
212 changes: 212 additions & 0 deletions osc/commands_git/pr_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import os
import re
import subprocess
import sys
from typing import Optional

import osc.commandline_git


def get_editor() -> str:
import shutil

editor = os.getenv("EDITOR", None)
if editor:
candidates = [editor]
else:
candidates = ["vim", "vi"]

editor_path = None
for i in candidates:
editor_path = shutil.which(i)
if editor_path:
break

if not editor_path:
raise RuntimeError(f"Unable to start editor '{candidates[0]}'")

return editor_path


def run_editor(file_path: str):
cmd = [get_editor(), file_path]
subprocess.run(cmd)


def edit_message(template: Optional[str] = None) -> str:
import tempfile

with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", prefix="git_obs_message_") as f:
if template:
f.write(template)
f.flush()

run_editor(f.name)

f.seek(0)
return f.read()


NEW_PULL_REQUEST_TEMPLATE = """
# title
{title}
# description
{description}
#
# Please enter pull request title and description in the following format:
# <title>
# <blank line>
# <description>
#
# Lines starting with '#' will be ignored, and an empty message aborts the operation.
#
# Creating {source_owner}/{source_repo}#{source_branch} -> {target_owner}/{target_repo}#{target_branch}
#
""".lstrip()


class PullRequestCreateCommand(osc.commandline_git.GitObsCommand):
"""
Create a pull request
"""

name = "create"
parent = "PullRequestCommand"

def init_arguments(self):
self.add_argument(
"--title",
metavar="TEXT",
help="Pull request title",
)
self.add_argument(
"--description",
metavar="TEXT",
help="Pull request description (body)",
)
self.add_argument(
"--source-owner",
metavar="OWNER",
help="Owner of the source repo (default: derived from remote URL in local git repo)",
)
self.add_argument(
"--source-repo",
metavar="REPO",
help="Name of the source repo (default: derived from remote URL in local git repo)",
)
self.add_argument(
"--source-branch",
metavar="BRANCH",
help="Source branch (default: the current branch in local git repo)",
)
self.add_argument(
"--target-branch",
metavar="BRANCH",
help="Target branch (default: derived from the current branch in local git repo)",
)

def run(self, args):
from osc import gitea_api

# the source args are optional, but if one of them is used, the others must be used too
source_args = (args.source_owner, args.source_repo, args.source_branch)
if sum((int(i is not None) for i in source_args)) not in (0, len(source_args)):
self.parser.error("All of the following options must be used together: --source-owner, --source-repo, --source-branch")

self.print_gitea_settings()

use_local_git = args.source_owner is None

if use_local_git:
# local git repo
git = gitea_api.Git(".")
local_owner, local_repo = git.get_owner_repo()
local_branch = git.current_branch
local_rev = git.get_branch_head(local_branch)

# remote git repo - source
if use_local_git:
source_owner = local_owner
source_repo = local_repo
source_branch = local_branch
else:
source_owner = args.source_owner
source_repo = args.source_repo
source_branch = args.source_branch
source_repo_data = gitea_api.Repo.get(self.gitea_conn, source_owner, source_repo).json()
source_branch_data = gitea_api.Branch.get(self.gitea_conn, source_owner, source_repo, source_branch).json()
source_rev = source_branch_data["commit"]["id"]

# remote git repo - target
target_owner, target_repo = source_repo_data["parent"]["full_name"].split("/")

if source_branch.startswith("for/"):
# source branch name format: for/<target-branch>/<what-the-branch-name-would-normally-be>
target_branch = source_branch.split("/")[1]
else:
target_branch = source_branch

target_branch_data = gitea_api.Branch.get(self.gitea_conn, target_owner, target_repo, target_branch).json()
target_rev = target_branch_data["commit"]["id"]

print("Creating a pull request ...", file=sys.stderr)
if use_local_git:
print(f" * Local git: branch: {local_branch}, rev: {local_rev}", file=sys.stderr)
print(f" * Source: {source_owner}/{source_repo}, branch: {source_branch}, rev: {source_rev}", file=sys.stderr)
print(f" * Target: {target_owner}/{target_repo}, branch: {target_branch}, rev: {target_rev}", file=sys.stderr)

if use_local_git and local_rev != source_rev:
from osc.output import tty
print(f"{tty.colorize('ERROR', 'red,bold')}: Local commit doesn't correspond with the latest commit in the remote source branch")
sys.exit(1)

if source_rev == target_rev:
from osc.output import tty
print(f"{tty.colorize('ERROR', 'red,bold')}: Source and target are identical, make and push changes to the remote source repo first")
sys.exit(1)

title = args.title or ""
description = args.description or ""

if not title or not description:
# TODO: add list of commits and list of changed files to the template; requires local git repo
message = edit_message(template=NEW_PULL_REQUEST_TEMPLATE.format(**locals()))

# remove comments
message = "\n".join([i for i in message.splitlines() if not i.startswith("#")])

# strip leading and trailing spaces
message = message.strip()

if not message:
raise RuntimeError("Aborting operation due to empty title and description.")

parts = re.split(r"\n\n", message, 1)
if len(parts) == 1:
# empty description
title = parts[0]
description = ""
else:
title = parts[0]
description = parts[1]

title = title.strip()
description = description.strip()

pull = gitea_api.PullRequest.create(
self.gitea_conn,
target_owner=target_owner,
target_repo=target_repo,
target_branch=target_branch,
source_owner=source_owner,
# source_repo is not required because the information lives in Gitea database
source_branch=source_branch,
title=title,
description=description,
).json()

print("", file=sys.stderr)
print("Pull request created:", file=sys.stderr)
print(gitea_api.PullRequest.to_human_readable_string(pull))
Loading

0 comments on commit 4db0066

Please sign in to comment.