Skip to content

Commit 5d244c9

Browse files
authored
Merge pull request openSUSE#1966 from dmach/git-obs-pr-list-filter-label
Add filtering by label to 'git-obs pr list'
2 parents d052060 + e91c904 commit 5d244c9

File tree

5 files changed

+147
-2
lines changed

5 files changed

+147
-2
lines changed

behave/features/git-pr.feature

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,40 @@ Scenario: Get a pull request
113113
"""
114114

115115

116+
@destructive
117+
Scenario: Get a pull request with labels
118+
Given I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/labels --data='{{"name": "bug", "color": "cc0000"}}'"
119+
And I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/labels --data='{{"name": "feature", "color": "00cc00"}}'"
120+
And I execute git-obs with args "api -X POST /repos/pool/test-GitPkgA/issues/1/labels --data='{{"labels": ["bug", "feature"]}}'"
121+
When I execute git-obs with args "pr get pool/test-GitPkgA#1"
122+
Then the exit code is 0
123+
And stdout matches
124+
"""
125+
ID : pool/test-GitPkgA#1
126+
URL : http://localhost:{context.podman.container.ports[gitea_http]}/pool/test-GitPkgA/pulls/1
127+
Title : Change version
128+
State : open
129+
Labels : bug feature
130+
Draft : no
131+
Merged : no
132+
Allow edit : no
133+
Author : Admin \([email protected]\)
134+
Source : Admin/test-GitPkgA, branch: factory, commit: .*
135+
Target : pool/test-GitPkgA, branch: factory, commit: .*
136+
Description : some text
137+
"""
138+
And stderr is
139+
"""
140+
Using the following Gitea settings:
141+
* Config path: {context.git_obs.config}
142+
* Login (name of the entry in the config file): admin
143+
* URL: http://localhost:{context.podman.container.ports[gitea_http]}
144+
* User: Admin
145+
146+
Total entries: 1
147+
"""
148+
149+
116150
@destructive
117151
Scenario: Get a pull request that doesn't exist
118152
When I execute git-obs with args "pr get does-not/exist#1"

osc/commands_git/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def init_arguments(self):
1515
self.add_argument(
1616
"-X",
1717
"--method",
18-
choices=["GET", "HEAD", "POST", "PATCH", "PUT"],
18+
choices=["GET", "HEAD", "POST", "PATCH", "PUT", "DELETE"],
1919
default="GET",
2020
)
2121
self.add_argument(

osc/commands_git/pr_list.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ def init_arguments(self):
4343
action="store_true",
4444
help="Filter by draft flag. Exclude pull requests with draft flag set.",
4545
)
46+
self.add_argument(
47+
"--label",
48+
dest="labels",
49+
action="append",
50+
help="Filter by label. Can be specified multiple times.",
51+
)
4652
self.add_argument(
4753
"--export",
4854
action="store_true",
@@ -62,6 +68,11 @@ def run(self, args):
6268
if args.no_draft:
6369
pr_obj_list = [i for i in pr_obj_list if not i.draft]
6470

71+
if args.labels:
72+
# keep pull requests that contain at least one specified label
73+
specified_labels = set(args.labels)
74+
pr_obj_list = [pr for pr in pr_obj_list if not specified_labels.isdisjoint(pr.labels)]
75+
6576
if args.target_branches:
6677
pr_obj_list = [i for i in pr_obj_list if i.base_branch in args.target_branches]
6778

osc/gitea_api/connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def request(
127127
if self.login.token:
128128
headers["Authorization"] = f"token {self.login.token}"
129129

130-
if json_data:
130+
if json_data and isinstance(json_data, dict):
131131
json_data = dict(((key, value) for key, value in json_data.items() if value is not None))
132132

133133
body = json.dumps(json_data) if json_data else None

osc/gitea_api/pr.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import functools
22
import re
3+
from typing import Dict
34
from typing import List
45
from typing import Optional
56
from typing import Tuple
@@ -262,6 +263,10 @@ def url(self) -> str:
262263
# HACK: search API returns issues, the URL needs to be transformed to a pull request URL
263264
return re.sub(r"^(.*)/api/v1/repos/(.+/.+)/issues/([0-9]+)$", r"\1/\2/pulls/\3", self._data["url"])
264265

266+
@property
267+
def labels(self) -> List[str]:
268+
return [label["name"] for label in self._data.get("labels", [])]
269+
265270
def to_human_readable_string(self):
266271
from osc.output import KeyValueTable
267272

@@ -273,6 +278,8 @@ def yes_no(value):
273278
table.add("URL", self.url)
274279
table.add("Title", self.title)
275280
table.add("State", self.state)
281+
if self.labels:
282+
table.add("Labels", " ".join(self.labels))
276283
if self.is_pull_request:
277284
table.add("Draft", yes_no(self.draft))
278285
table.add("Merged", yes_no(self.merged))
@@ -698,3 +705,96 @@ def reopen(
698705
response = conn.request("PATCH", url, json_data=json_data, context={"owner": owner, "repo": repo})
699706
obj = cls(response.json(), response=response, conn=conn)
700707
return obj
708+
709+
@classmethod
710+
def _get_label_ids(cls, conn: Connection, owner: str, repo: str) -> Dict[str, int]:
711+
"""
712+
Helper to map labels to their IDs
713+
"""
714+
result = {}
715+
url = conn.makeurl("repos", owner, repo, "labels")
716+
response = conn.request("GET", url)
717+
labels = response.json()
718+
for label in labels:
719+
result[label["id"]] = label["name"]
720+
return result
721+
722+
@classmethod
723+
def add_labels(
724+
cls,
725+
conn: Connection,
726+
owner: str,
727+
repo: str,
728+
number: int,
729+
labels: List[str],
730+
) -> "GiteaHTTPResponse":
731+
"""
732+
Add one or more labels to a pull request.
733+
734+
:param conn: Gitea Connection instance.
735+
:param owner: Owner of the repo.
736+
:param repo: Name of the repo.
737+
:param number: Number of the pull request.
738+
:param labels: A list of label names to add.
739+
"""
740+
from .exceptions import GitObsRuntimeError
741+
742+
label_id_list = []
743+
invalid_labels = []
744+
label_name_id_map = cls._get_label_ids(conn, owner, repo)
745+
for label in labels:
746+
label_id = label_name_id_map.get(label, None)
747+
if not label_id:
748+
invalid_labels.append(label)
749+
continue
750+
label_id_list.append(label_id)
751+
if invalid_labels:
752+
msg = f"The following labels do not exist in {owner}/{repo}: {' '.join(invalid_labels)}"
753+
raise GitObsRuntimeError(msg)
754+
755+
url = conn.makeurl("repos", owner, repo, "issues", str(number), "labels")
756+
json_data = {
757+
"labels": label_id_list,
758+
}
759+
return conn.request("POST", url, json_data=json_data)
760+
761+
@classmethod
762+
def remove_labels(
763+
cls,
764+
conn: Connection,
765+
owner: str,
766+
repo: str,
767+
number: int,
768+
labels: List[str],
769+
):
770+
"""
771+
Remove labels from a pull request.
772+
773+
:param conn: Gitea Connection instance.
774+
:param owner: Owner of the repo.
775+
:param repo: Name of the repo.
776+
:param number: Number of the pull request.
777+
:param labels: A list of label names to remove.
778+
"""
779+
from .exceptions import GitObsRuntimeError
780+
781+
label_id_list = []
782+
invalid_labels = []
783+
label_name_id_map = cls._get_label_ids(conn, owner, repo)
784+
for label in labels:
785+
label_id = label_name_id_map.get(label, None)
786+
if not label_id:
787+
invalid_labels.append(label)
788+
continue
789+
label_id_list.append(label_id)
790+
if invalid_labels:
791+
msg = f"The following labels do not exist in {owner}/{repo}: {' '.join(invalid_labels)}"
792+
raise GitObsRuntimeError(msg)
793+
794+
# DELETE /repos/<owner>/<repo>/issues/<number>/labels with data == {"labels": [1, 2, 3, ...]} doesn't work and always deletes all labels.
795+
# Retrieving all labels, filtering them and sending back is prone to race conditions.
796+
# Let's trigger DELETE /repos/<owner>/<repo>/issues/<number>/labels/<id> instead to stay at the safe side.
797+
798+
for label_id in label_id_list:
799+
url = conn.makeurl("repos", owner, repo, "issues", str(number), "labels", str(label_id))
800+
conn.request("DELETE", url)

0 commit comments

Comments
 (0)