1
0
mirror of https://github.com/openSUSE/osc.git synced 2025-11-30 16:18:19 +01:00

Implement 'git-obs repo list' command

This commit is contained in:
2025-06-20 10:49:18 +02:00
parent 4a24707c84
commit 63067fd8ae
4 changed files with 202 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
Feature: `git-obs repo list` command
Background:
Given I set working directory to "{context.osc.temp}"
@destructive
Scenario: List repos owned by an organization
When I execute git-obs with args "repo list --org=pool"
Then the exit code is 0
And stdout is
"""
pool/test-GitPkgA
"""
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 repos: 1
"""
@destructive
Scenario: List repos owned by a user
Given I execute git-obs with args "repo fork pool/test-GitPkgA"
When I execute git-obs with args "repo list --user=Admin"
Then the exit code is 0
And stdout is
"""
Admin/test-GitPkgA
"""
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 repos: 1
"""
@destructive
Scenario: List repos owned by a user and an organization
Given I execute git-obs with args "repo fork pool/test-GitPkgA"
When I execute git-obs with args "repo list --user=Admin --org=pool"
Then the exit code is 0
And stdout is
"""
pool/test-GitPkgA
Admin/test-GitPkgA
"""
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 repos: 2
"""

View File

@@ -0,0 +1,52 @@
import sys
import osc.commandline_git
class RepoListCommand(osc.commandline_git.GitObsCommand):
"""
List repos
Required permissions:
read:organization
read:user
"""
name = "list"
parent = "RepoCommand"
def init_arguments(self):
self.parser.add_argument(
"--org",
dest="org_list",
action="append",
help="List repos owned by the specified organizations",
)
self.parser.add_argument(
"--user",
dest="user_list",
action="append",
help="List repos owned by the specified users",
)
def run(self, args):
from osc import gitea_api
if not args.org_list and not args.user_list:
self.parser.error("Please specify at least one --org or --user option")
self.print_gitea_settings()
repo_obj_list = []
for org in sorted(set(args.org_list or [])):
repo_obj_list += gitea_api.Repo.list_org_repos(self.gitea_conn, org)
for user in sorted(set(args.user_list or [])):
repo_obj_list += gitea_api.Repo.list_user_repos(self.gitea_conn, user)
for repo_obj in sorted(repo_obj_list):
print(f"{repo_obj.owner}/{repo_obj.repo}")
print("", file=sys.stderr)
print(f"Total repos: {len(repo_obj_list)}", file=sys.stderr)

View File

@@ -1,8 +1,11 @@
import copy
import http.client
import json
import re
import time
import urllib.parse
from typing import Dict
from typing import Generator
from typing import Optional
import urllib3
@@ -12,6 +15,19 @@ import urllib3.response
from .conf import Login
RE_HTTP_HEADER_LINK = re.compile('<(?P<url>.*?)>; rel="(?P<rel>.*?)",?')
def parse_http_header_link(link: str) -> Dict[str, str]:
"""
Parse RFC8288 "link" http headers into {"rel": "url"}
"""
result = {}
for match in RE_HTTP_HEADER_LINK.findall(link):
result[match[1]] = match[0]
return result
class GiteaHTTPResponse:
"""
A ``urllib3.response.HTTPResponse`` wrapper
@@ -153,3 +169,24 @@ class Connection:
raise response_to_exception(response, context=context)
return response
def request_all_pages(
self, method, url, json_data: Optional[dict] = None, *, context: Optional[dict] = None
) -> Generator[GiteaHTTPResponse, None, None]:
"""
Make a request and yield ``GiteaHTTPResponse`` instances for each page.
Arguments are forwarded to the underlying ``request()`` call.
"""
while True:
response = self.request(method, url, json_data=json_data, context=context)
yield response
if "link" not in response.headers:
break
links = parse_http_header_link(response.headers["link"])
if "next" in links:
url = links["next"]
else:
break

View File

@@ -1,6 +1,8 @@
import functools
import os
import re
import subprocess
from typing import List
from typing import Optional
from typing import Tuple
@@ -9,11 +11,18 @@ from .connection import GiteaHTTPResponse
from .user import User
@functools.total_ordering
class Repo:
def __init__(self, data: dict, *, response: Optional[GiteaHTTPResponse] = None):
self._data = data
self._response = response
def __eq__(self, other):
(self.owner, self.repo) == (other.owner, other.repo)
def __lt__(self, other):
(self.owner, self.repo) < (other.owner, other.repo)
@property
def owner(self) -> str:
return self._data["owner"]["login"]
@@ -179,3 +188,37 @@ class Repo:
subprocess.run(cmd, cwd=cwd, check=True)
return directory_abspath
@classmethod
def list_org_repos(cls, conn: Connection, owner: str) -> List["Repo"]:
"""
List repos owned by an organization.
:param conn: Gitea ``Connection`` instance.
"""
q = {
# XXX: limit works in range 1..50, setting it any higher doesn't help, we need to handle paginated results
"limit": 10**6,
}
url = conn.makeurl("orgs", owner, "repos", query=q)
obj_list = []
for response in conn.request_all_pages("GET", url):
obj_list.extend([cls(i, response=response) for i in response.json()])
return obj_list
@classmethod
def list_user_repos(cls, conn: Connection, owner: str) -> List["Repo"]:
"""
List repos owned by a user.
:param conn: Gitea ``Connection`` instance.
"""
q = {
# XXX: limit works in range 1..50, setting it any higher doesn't help, we need to handle paginated results
"limit": 10**6,
}
url = conn.makeurl("users", owner, "repos", query=q)
obj_list = []
for response in conn.request_all_pages("GET", url):
obj_list.extend([cls(i, response=response) for i in response.json()])
return obj_list