diff --git a/osc/_private/__init__.py b/osc/_private/__init__.py
index 868c741e..e26b045c 100644
--- a/osc/_private/__init__.py
+++ b/osc/_private/__init__.py
@@ -13,4 +13,5 @@ from .api_source import release
from .common import print_msg
from .common import format_msg_project_package_options
from .package import ApiPackage
+from .package import LocalPackage
from .request import forward_request
diff --git a/osc/_private/package.py b/osc/_private/package.py
index c1fca72e..3f767155 100644
--- a/osc/_private/package.py
+++ b/osc/_private/package.py
@@ -1,5 +1,6 @@
import functools
+from .. import oscerr
from . import api
@@ -18,6 +19,7 @@ class PackageBase:
self.files = []
directory_node = self._get_directory_node()
self._load_from_directory_node(directory_node)
+ self._meta_node = None
def __str__(self):
return f"{self.project}/{self.name}"
@@ -60,6 +62,19 @@ class PackageBase:
# the name is omitted and we want it present for overall sanity
self.linkinfo.package = self.name
+ def _get_meta_node(self):
+ raise NotImplementedError()
+
+ def get_meta_value(self, option):
+ if not self._meta_node:
+ self._meta_node = self._get_meta_node()
+ if not self._meta_node:
+ return None
+ node = api.find_node(self._meta_node, "package", option)
+ if node is None or not node.text:
+ raise oscerr.APIError(f"Couldn't get '{option}' from package _meta")
+ return node.text
+
class ApiPackage(PackageBase):
def __init__(self, apiurl, project, package, rev=None):
@@ -75,6 +90,11 @@ class ApiPackage(PackageBase):
url_query["rev"] = self.__rev
return api.get(self.apiurl, url_path, url_query)
+ def _get_meta_node(self):
+ url_path = ["source", self.project, self.name, "_meta"]
+ url_query = {}
+ return api.get(self.apiurl, url_path, url_query)
+
class LocalPackage(PackageBase):
def __init__(self, path):
@@ -86,3 +106,6 @@ class LocalPackage(PackageBase):
def _get_directory_node(self):
return self.store.read_xml_node("_files", "directory").getroot()
+
+ def _get_meta_node(self):
+ return self.store._meta_node
diff --git a/osc/core.py b/osc/core.py
index d83363cf..0991bd93 100644
--- a/osc/core.py
+++ b/osc/core.py
@@ -2749,6 +2749,23 @@ class ReviewState(AbstractState):
review_node.find('comment').text:
self.comment = review_node.find('comment').text.strip()
+ def __repr__(self):
+ result = super().__repr__()
+ result += "("
+ result += f"{self.state}"
+
+ if self.who:
+ result += f" by {self.who}"
+
+ for by in ("user", "group", "project", "package"):
+ by_value = getattr(self, f"by_{by}", None)
+ if by_value:
+ result += f" [{by} {by_value}])"
+ break
+
+ result += ")"
+ return result
+
def get_node_attrs(self):
return ('state', 'by_user', 'by_group', 'by_project', 'by_package', 'who', 'when')
@@ -2879,6 +2896,9 @@ class Action:
prefix_to_elm = {'src': 'source', 'tgt': 'target', 'opt': 'options'}
def __init__(self, type, **kwargs):
+ self.apiurl = kwargs.pop("apiurl", None)
+ self._src_pkg_object = None
+ self._tgt_pkg_object = None
if type not in Action.type_args.keys():
raise oscerr.WrongArgs('invalid action type: \'%s\'' % type)
self.type = type
@@ -2889,6 +2909,60 @@ class Action:
for i in Action.type_args[type]:
setattr(self, i, kwargs.get(i))
+ def __repr__(self):
+ result = super().__repr__()
+ result += "("
+ result += f"type={self.type}"
+
+ src_pkg = self.src_pkg_object
+ if src_pkg:
+ result += f" source={src_pkg.project}/{src_pkg.name}"
+ elif getattr(self, "src_project", None):
+ result += f" source={self.src_project}"
+
+ tgt_pkg = self.tgt_pkg_object
+ if tgt_pkg:
+ result += f" target={tgt_pkg.project}/{tgt_pkg.name}"
+ elif getattr(self, "tgt_project", None):
+ result += f" target={self.tgt_project}"
+
+ result += ")"
+ return result
+
+ @property
+ def src_pkg_object(self):
+ if not getattr(self, "src_project", None) or not getattr(self, "src_package", None):
+ return None
+ if not self._src_pkg_object:
+ src_rev = getattr(self, "src_rev", None)
+ self._src_pkg_object = _private.ApiPackage(self.apiurl, self.src_project, self.src_package, src_rev)
+ return self._src_pkg_object
+
+ @property
+ def tgt_pkg_object(self):
+ if not self._tgt_pkg_object:
+ if self.type == "maintenance_incident":
+ # the target project for maintenance incidents is virtual and cannot be queried
+ # the actual target project is in the "releaseproject" attribute
+ #
+ # tgt_releaseproject is always set for a maintenance_incident
+ # pylint: disable=no-member
+ tgt_project = self.tgt_releaseproject
+
+ # the target package is not specified
+ # we need to extract it from source package's _meta
+ src_package_meta_releasename = self.src_pkg_object.get_meta_value("releasename")
+ tgt_package = src_package_meta_releasename.split(".")[0]
+ else:
+ if not getattr(self, "tgt_project", None) or not getattr(self, "tgt_package", None):
+ return None
+ # tgt_project and tgt_package are checked above
+ # pylint: disable=no-member
+ tgt_project = self.tgt_project
+ tgt_package = self.tgt_package
+ self._tgt_pkg_object = _private.ApiPackage(self.apiurl, tgt_project, tgt_package)
+ return self._tgt_pkg_object
+
def to_xml(self):
"""
Serialize object to XML.
@@ -2935,7 +3009,7 @@ class Action:
return ET.tostring(root, encoding=ET_ENCODING)
@staticmethod
- def from_xml(action_node):
+ def from_xml(action_node, apiurl=None):
"""create action from XML"""
if action_node is None or \
action_node.get('type') not in Action.type_args.keys() or \
@@ -2960,6 +3034,7 @@ class Action:
l.append(v)
else:
kwargs[k] = v
+ kwargs["apiurl"] = apiurl
return Action(action_node.get('type'), **kwargs)
@@ -2967,6 +3042,12 @@ class Action:
class Request:
"""Represents a request (````)"""
+ @classmethod
+ def from_api(cls, apiurl: str, req_id: int):
+ # TODO: deprecate get_request() or move its content here
+ req_id = str(req_id)
+ return get_request(apiurl, req_id)
+
def __init__(self):
self._init_attributes()
@@ -2982,6 +3063,7 @@ class Request:
self.actions = []
self.statehistory = []
self.reviews = []
+ self._issues = None
def __eq__(self, other):
return int(self.reqid) == int(other.reqid)
@@ -2989,9 +3071,20 @@ class Request:
def __lt__(self, other):
return int(self.reqid) < int(other.reqid)
- def read(self, root):
+ @property
+ def id(self):
+ return self.reqid
+
+ @property
+ def issues(self):
+ if self._issues is None:
+ self._issues = get_request_issues(self.apiurl, self.id)
+ return self._issues
+
+ def read(self, root, apiurl=None):
"""read in a request"""
self._init_attributes()
+ self.apiurl = apiurl
if not root.get('id'):
raise oscerr.APIError('invalid request: %s\n' % ET.tostring(root, encoding=ET_ENCODING))
self.reqid = root.get('id')
@@ -3008,7 +3101,7 @@ class Request:
i.set('type', 'submit')
action_nodes.append(i)
for action in action_nodes:
- self.actions.append(Action.from_xml(action))
+ self.actions.append(Action.from_xml(action, self.apiurl))
for review in root.findall('review'):
self.reviews.append(ReviewState(review))
for history_element in root.findall('history'):
@@ -4500,7 +4593,7 @@ def get_request(apiurl: str, reqid):
root = ET.parse(f).getroot()
r = Request()
- r.read(root)
+ r.read(root, apiurl=apiurl)
return r
diff --git a/osc/store.py b/osc/store.py
index fd6d306f..a4f8f4fa 100644
--- a/osc/store.py
+++ b/osc/store.py
@@ -299,3 +299,13 @@ class Store:
if len(value) != 3:
raise ValueError("A list with exactly 3 items is expected: [repo, arch, vm_type]")
self.write_list("_last_buildroot", value)
+
+ @property
+ def _meta_node(self):
+ if not self.exists("_meta"):
+ return None
+ if self.is_package:
+ root = self.read_xml_node("_meta", "package").getroot()
+ else:
+ root = self.read_xml_node("_meta", "project").getroot()
+ return root
diff --git a/tests/test_store.py b/tests/test_store.py
index 58a1e751..f77cf613 100644
--- a/tests/test_store.py
+++ b/tests/test_store.py
@@ -203,6 +203,29 @@ class TestStore(unittest.TestCase):
store2 = Store(self.tmpdir)
self.assertEqual(store2.last_buildroot, ["repo", "arch", "vm_type"])
+ def test_meta_node(self):
+ self.store.write_string(
+ "_meta",
+ """
+ title
+ desc
+ name
+
+
+
+
+""",
+ )
+ node = self.store._meta_node
+ self.assertNotEqual(node, None)
+
+ # try to read the _meta via a package class
+ from osc._private import LocalPackage
+
+ self.store.files = []
+ pkg = LocalPackage(self.tmpdir)
+ self.assertEqual(pkg.get_meta_value("releasename"), "name")
+
if __name__ == "__main__":
unittest.main()