11import functools
22import re
3+ from typing import Dict
34from typing import List
45from typing import Optional
56from 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