From 861a3626220550e9a14fa3e9c00fc1fd26813dc6 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 16 Apr 2024 16:18:50 +0200 Subject: [PATCH 1/7] Move run_pager() and get_default_pager() from 'core' to 'output' module --- osc/core.py | 47 ++------------------------------------ osc/output/__init__.py | 2 ++ osc/output/output.py | 52 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/osc/core.py b/osc/core.py index a20fc4dcc0..ccc60caa30 100644 --- a/osc/core.py +++ b/osc/core.py @@ -77,6 +77,8 @@ from .obs_scm.store import store_write_last_buildroot from .obs_scm.store import store_write_project from .obs_scm.store import store_write_string +from .output import get_default_pager +from .output import run_pager from .output import sanitize_text from .util import xdg from .util.helper import decode_list, decode_it, raw_input, _html_escape @@ -1915,16 +1917,6 @@ def get_default_editor(): return 'vi' -def get_default_pager(): - system = platform.system() - if system == 'Linux': - dist = _get_linux_distro() - if dist == 'debian': - return 'pager' - return 'less' - return 'more' - - def format_diff_line(line): if line.startswith(b"+++") or line.startswith(b"---") or line.startswith(b"Index:"): line = b"\x1b[1m" + line + b"\x1b[0m" @@ -1943,41 +1935,6 @@ def highlight_diff(diff): return diff -def run_pager(message, tmp_suffix=''): - if not message: - return - - if not sys.stdout.isatty(): - if isinstance(message, str): - print(message) - else: - sys.stdout.buffer.write(message) - else: - tmpfile = tempfile.NamedTemporaryFile(suffix=tmp_suffix) - if isinstance(message, str): - tmpfile.write(bytes(message, 'utf-8')) - else: - tmpfile.write(message) - tmpfile.flush() - - env = os.environ.copy() - - pager = os.getenv("PAGER", default="").strip() - pager = pager or get_default_pager() - - # LESS env is not always set and we need -R to display escape sequences properly - less_opts = os.getenv("LESS", default="") - if "-R" not in less_opts: - less_opts += " -R" - env["LESS"] = less_opts - - cmd = shlex.split(pager) + [tmpfile.name] - try: - run_external(*cmd, env=env) - finally: - tmpfile.close() - - def run_editor(filename): cmd = _editor_command() cmd.append(filename) diff --git a/osc/output/__init__.py b/osc/output/__init__.py index 254feb8635..720a265f08 100644 --- a/osc/output/__init__.py +++ b/osc/output/__init__.py @@ -1,6 +1,8 @@ from .key_value_table import KeyValueTable from .input import get_user_input +from .output import get_default_pager from .output import print_msg +from .output import run_pager from .output import sanitize_text from .output import safe_print from .output import safe_write diff --git a/osc/output/output.py b/osc/output/output.py index 63b46a46ab..d646cd2270 100644 --- a/osc/output/output.py +++ b/osc/output/output.py @@ -1,6 +1,9 @@ import os +import platform import re +import shlex import sys +import tempfile from typing import Dict from typing import Optional from typing import TextIO @@ -147,3 +150,52 @@ def safe_write(file: TextIO, text: Union[str, bytes], *, add_newline: bool = Fal file.write(text) if add_newline: file.write(os.linesep) + + +def get_default_pager(): + from ..core import _get_linux_distro + + system = platform.system() + if system == 'Linux': + dist = _get_linux_distro() + if dist == 'debian': + return 'pager' + return 'less' + return 'more' + + +def run_pager(message: Union[bytes, str], tmp_suffix: str = ""): + from ..core import run_external + + if not message: + return + + if not sys.stdout.isatty(): + if isinstance(message, str): + print(message) + else: + sys.stdout.buffer.write(message) + else: + tmpfile = tempfile.NamedTemporaryFile(suffix=tmp_suffix) + if isinstance(message, str): + tmpfile.write(bytes(message, 'utf-8')) + else: + tmpfile.write(message) + tmpfile.flush() + + env = os.environ.copy() + + pager = os.getenv("PAGER", default="").strip() + pager = pager or get_default_pager() + + # LESS env is not always set and we need -R to display escape sequences properly + less_opts = os.getenv("LESS", default="") + if "-R" not in less_opts: + less_opts += " -R" + env["LESS"] = less_opts + + cmd = shlex.split(pager) + [tmpfile.name] + try: + run_external(*cmd, env=env) + finally: + tmpfile.close() From d2503fbf49e144e4725bcb6a0495e38ff6525dc7 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 16 Apr 2024 16:31:27 +0200 Subject: [PATCH 2/7] Modernize output.run_pager() --- osc/output/output.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/osc/output/output.py b/osc/output/output.py index d646cd2270..14455cfdf5 100644 --- a/osc/output/output.py +++ b/osc/output/output.py @@ -170,17 +170,12 @@ def run_pager(message: Union[bytes, str], tmp_suffix: str = ""): if not message: return - if not sys.stdout.isatty(): - if isinstance(message, str): - print(message) - else: - sys.stdout.buffer.write(message) - else: - tmpfile = tempfile.NamedTemporaryFile(suffix=tmp_suffix) - if isinstance(message, str): - tmpfile.write(bytes(message, 'utf-8')) - else: - tmpfile.write(message) + if not tty.IS_INTERACTIVE: + safe_write(sys.stdout, message) + return + + with tempfile.NamedTemporaryFile(suffix=tmp_suffix) as tmpfile: + safe_write(tmpfile, message) tmpfile.flush() env = os.environ.copy() @@ -195,7 +190,4 @@ def run_pager(message: Union[bytes, str], tmp_suffix: str = ""): env["LESS"] = less_opts cmd = shlex.split(pager) + [tmpfile.name] - try: - run_external(*cmd, env=env) - finally: - tmpfile.close() + run_external(*cmd, env=env) From d1111e23a1baac9b93715f1abdc250b65e257f5d Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 16 Apr 2024 16:50:10 +0200 Subject: [PATCH 3/7] Fix output.safe_write() in connection with NamedTemporaryFile Fixes: AttributeError: '_io.BufferedRandom' object has no attribute 'buffer' --- osc/output/output.py | 15 +++++++++++---- tests/test_output.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/osc/output/output.py b/osc/output/output.py index 14455cfdf5..fe90b665a5 100644 --- a/osc/output/output.py +++ b/osc/output/output.py @@ -143,9 +143,15 @@ def safe_write(file: TextIO, text: Union[str, bytes], *, add_newline: bool = Fal """ text = sanitize_text(text) if isinstance(text, bytes): - file.buffer.write(text) - if add_newline: - file.buffer.write(os.linesep.encode("utf-8")) + if hasattr(file, "buffer"): + file.buffer.write(text) + if add_newline: + file.buffer.write(os.linesep.encode("utf-8")) + else: + # file has no "buffer" attribute, let's try to write the bytes directly + file.write(text) + if add_newline: + file.write(os.linesep.encode("utf-8")) else: file.write(text) if add_newline: @@ -174,7 +180,8 @@ def run_pager(message: Union[bytes, str], tmp_suffix: str = ""): safe_write(sys.stdout, message) return - with tempfile.NamedTemporaryFile(suffix=tmp_suffix) as tmpfile: + mode = "w+b" if isinstance(message, bytes) else "w+" + with tempfile.NamedTemporaryFile(mode=mode, suffix=tmp_suffix) as tmpfile: safe_write(tmpfile, message) tmpfile.flush() diff --git a/tests/test_output.py b/tests/test_output.py index 33971392c3..9800a499e7 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,10 +1,12 @@ import contextlib import io +import tempfile import unittest import osc.conf from osc.output import KeyValueTable from osc.output import print_msg +from osc.output import safe_write from osc.output import sanitize_text from osc.output import tty @@ -238,5 +240,23 @@ def test_osc_escape_sequences_bytes(self): self.assertEqual(sanitized, b"0;this is the window title") +class TestSafeWrite(unittest.TestCase): + def test_string_to_file(self): + with tempfile.NamedTemporaryFile(mode="w+") as f: + safe_write(f, "string") + + def test_bytes_to_file(self): + with tempfile.NamedTemporaryFile(mode="wb+") as f: + safe_write(f, b"bytes") + + def test_string_to_stringio(self): + with io.StringIO() as f: + safe_write(f, "string") + + def test_bytes_to_bytesio(self): + with io.BytesIO() as f: + safe_write(f, b"bytes") + + if __name__ == "__main__": unittest.main() From dc7efaa6deda42b27354a0a7808bab05689aa860 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Tue, 16 Apr 2024 16:56:00 +0200 Subject: [PATCH 4/7] Add output.pipe_to_pager() that pipes lines to a pager without creating an intermediate temporary file --- osc/output/__init__.py | 1 + osc/output/output.py | 54 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/osc/output/__init__.py b/osc/output/__init__.py index 720a265f08..c12d837007 100644 --- a/osc/output/__init__.py +++ b/osc/output/__init__.py @@ -1,6 +1,7 @@ from .key_value_table import KeyValueTable from .input import get_user_input from .output import get_default_pager +from .output import pipe_to_pager from .output import print_msg from .output import run_pager from .output import sanitize_text diff --git a/osc/output/output.py b/osc/output/output.py index fe90b665a5..50aa98b598 100644 --- a/osc/output/output.py +++ b/osc/output/output.py @@ -2,9 +2,11 @@ import platform import re import shlex +import subprocess import sys import tempfile from typing import Dict +from typing import List from typing import Optional from typing import TextIO from typing import Union @@ -170,6 +172,25 @@ def get_default_pager(): return 'more' +def get_pager(): + """ + Return (pager, env) where + ``pager`` is a list with parsed pager command + ``env`` is copy of os.environ() with added variables specific to the pager + """ + env = os.environ.copy() + pager = os.getenv("PAGER", default="").strip() + pager = pager or get_default_pager() + + # LESS env is not always set and we need -R to display escape sequences properly + less_opts = os.getenv("LESS", default="") + if "-R" not in less_opts: + less_opts += " -R" + env["LESS"] = less_opts + + return shlex.split(pager), env + + def run_pager(message: Union[bytes, str], tmp_suffix: str = ""): from ..core import run_external @@ -185,16 +206,29 @@ def run_pager(message: Union[bytes, str], tmp_suffix: str = ""): safe_write(tmpfile, message) tmpfile.flush() - env = os.environ.copy() + pager, env = get_pager() + cmd = pager + [tmpfile.name] + run_external(*cmd, env=env) - pager = os.getenv("PAGER", default="").strip() - pager = pager or get_default_pager() - # LESS env is not always set and we need -R to display escape sequences properly - less_opts = os.getenv("LESS", default="") - if "-R" not in less_opts: - less_opts += " -R" - env["LESS"] = less_opts +def pipe_to_pager(lines: Union[List[bytes], List[str]], *, add_newlines=False): + """ + Pipe ``lines`` to the pager. + If running in a non-interactive terminal, print the data instead. + Add a newline after each line if ``add_newlines`` is ``True``. + """ + if not tty.IS_INTERACTIVE: + for line in lines: + safe_write(sys.stdout, line, add_newline=add_newlines) + return - cmd = shlex.split(pager) + [tmpfile.name] - run_external(*cmd, env=env) + pager, env = get_pager() + with subprocess.Popen(pager, stdin=subprocess.PIPE, encoding="utf-8", env=env) as proc: + try: + for line in lines: + safe_write(proc.stdin, line, add_newline=add_newlines) + proc.stdin.flush() + proc.stdin.close() + except BrokenPipeError: + pass + proc.wait() From eb2678e0c78381e5a0c1645ecfb7e296e26d7171 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 17 Apr 2024 10:33:08 +0200 Subject: [PATCH 5/7] Fix XmlModel to load an empty string in an int field as None --- osc/util/models.py | 2 ++ tests/test_models_xmlmodel.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/osc/util/models.py b/osc/util/models.py index 0777b535d7..be41e5b924 100644 --- a/osc/util/models.py +++ b/osc/util/models.py @@ -594,6 +594,8 @@ def value_from_string(field, value): return value if field.origin_type is int: + if not value or not value.strip(): + return None value = int(value) return value diff --git a/tests/test_models_xmlmodel.py b/tests/test_models_xmlmodel.py index 665db5e229..4f63a0e40a 100644 --- a/tests/test_models_xmlmodel.py +++ b/tests/test_models_xmlmodel.py @@ -185,6 +185,38 @@ class ParentModel(XmlModel): self.assertEqual(m.child[1]._apiurl, apiurl) self.assertEqual(m.child[2]._apiurl, apiurl) + def test_empty_int_optional(self): + class TestModel(XmlModel): + XML_TAG = "model" + num_attr: Optional[int] = Field(xml_attribute=True) + num_elem: Optional[int] = Field() + + data = textwrap.dedent( + """ + + + + """ + ).strip() + m = TestModel.from_string(data) + self.assertEqual(m.num_attr, None) + self.assertEqual(m.num_elem, None) + + def test_empty_int(self): + class TestModel(XmlModel): + XML_TAG = "model" + num_attr: int = Field(xml_attribute=True) + num_elem: int = Field() + + data = textwrap.dedent( + """ + + + + """ + ).strip() + self.assertRaises(TypeError, TestModel.from_string, data) + if __name__ == "__main__": unittest.main() From 89f74665d163808b2382ffb4973ceb31b7486f43 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 17 Apr 2024 10:51:46 +0200 Subject: [PATCH 6/7] Add Package.get_revision_list() for listing commit log --- osc/obs_api/package.py | 18 +++++++++++++++++ osc/obs_api/package_revision.py | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 osc/obs_api/package_revision.py diff --git a/osc/obs_api/package.py b/osc/obs_api/package.py index 8c2967d714..9a4c7b5226 100644 --- a/osc/obs_api/package.py +++ b/osc/obs_api/package.py @@ -3,6 +3,7 @@ from .flag import Flag from .group_role import GroupRole from .package_devel import PackageDevel +from .package_revision import PackageRevision from .person_role import PersonRole from .simple_flag import SimpleFlag from .status import Status @@ -126,3 +127,20 @@ def cmd_release( } response = cls.xml_request("POST", apiurl, url_path, url_query) return Status.from_file(response, apiurl=apiurl) + + @classmethod + def get_revision_list(cls, apiurl: str, project: str, package: str, deleted: Optional[bool] = None, meta: Optional[bool] = None): + from xml.etree import ElementTree as ET + + url_path = ["source", project, package, "_history"] + url_query = { + "meta": meta, + "deleted": deleted, + } + response = cls.xml_request("GET", apiurl, url_path, url_query) + root = ET.parse(response).getroot() + assert root.tag == "revisionlist" + result = [] + for node in root: + result.append(PackageRevision.from_xml(node, apiurl=apiurl)) + return result diff --git a/osc/obs_api/package_revision.py b/osc/obs_api/package_revision.py new file mode 100644 index 0000000000..a9663d1f38 --- /dev/null +++ b/osc/obs_api/package_revision.py @@ -0,0 +1,35 @@ +from ..util.models import * # pylint: disable=wildcard-import,unused-wildcard-import + + +class PackageRevision(XmlModel): + XML_TAG = "revision" + + rev: int = Field( + xml_attribute=True, + ) + + vrev: Optional[int] = Field( + xml_attribute=True, + ) + + srcmd5: str = Field( + ) + + version: str = Field( + ) + + time: int = Field( + ) + + user: str = Field( + ) + + comment: Optional[str] = Field( + ) + + requestid: Optional[int] = Field( + ) + + def get_time_str(self): + import time + return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(self.time)) From 8ee02dd098682057a6ab23bfeb0cce3b5b6819a0 Mon Sep 17 00:00:00 2001 From: Daniel Mach Date: Wed, 17 Apr 2024 10:57:10 +0200 Subject: [PATCH 7/7] Improve 'log' command: produce proper CSV and XML outputs, add -p/--patch option for the text output --- behave/features/log.feature | 114 ++++++++++++++++++++++++++ osc/commandline.py | 13 +-- osc/core.py | 154 ++++++++++++++++++++---------------- 3 files changed, 207 insertions(+), 74 deletions(-) create mode 100644 behave/features/log.feature diff --git a/behave/features/log.feature b/behave/features/log.feature new file mode 100644 index 0000000000..13a187aaa0 --- /dev/null +++ b/behave/features/log.feature @@ -0,0 +1,114 @@ +Feature: `osc log` command + + +Scenario: Run `osc log` on a package + Given I execute osc with args "log test:factory/test-pkgA" + Then the exit code is 0 + And stdout matches + """ + ---------------------------------------------------------------------------- + r3 | Admin | ....-..-.. ..:..:.. | dc997133b8ddfaf084b471b05c2643b3 | 3 | + + Version 3 + ---------------------------------------------------------------------------- + r2 | Admin | ....-..-.. ..:..:.. | 0ea55feb9cdd741ba7f523ed58a4f099 | 2 | + + Version 2 + ---------------------------------------------------------------------------- + r1 | Admin | ....-..-.. ..:..:.. | e675755e79e0d69483d311e96d6b719e | 1 | + + Initial commit + ---------------------------------------------------------------------------- + """ + + +Scenario: Run `osc log` on single revision of a package + Given I execute osc with args "log test:factory/test-pkgA --revision=2" + Then the exit code is 0 + And stdout matches + """ + ---------------------------------------------------------------------------- + r2 | Admin | ....-..-.. ..:..:.. | 0ea55feb9cdd741ba7f523ed58a4f099 | 2 | + + Version 2 + ---------------------------------------------------------------------------- + """ + + +Scenario: Run `osc log` on revision range of a package + Given I execute osc with args "log test:factory/test-pkgA --revision=1:2" + Then the exit code is 0 + And stdout matches + """ + ---------------------------------------------------------------------------- + r2 | Admin | ....-..-.. ..:..:.. | 0ea55feb9cdd741ba7f523ed58a4f099 | 2 | + + Version 2 + ---------------------------------------------------------------------------- + r1 | Admin | ....-..-.. ..:..:.. | e675755e79e0d69483d311e96d6b719e | 1 | + + Initial commit + ---------------------------------------------------------------------------- + """ + + +@wip +Scenario: Run `osc log --patch` on revision range of a package + Given I execute osc with args "log test:factory/test-pkgA --revision=1:2 --patch" + Then the exit code is 0 + And stdout matches + """ + ---------------------------------------------------------------------------- + r2 \| Admin \| ....-..-.. ..:..:.. \| 0ea55feb9cdd741ba7f523ed58a4f099 \| 2 \| + + Version 2 + + + changes files: + -------------- + --- test-pkgA.changes + \+\+\+ test-pkgA.changes + @@ -2 \+2 @@ + -Tue Jan 4 11:22:33 UTC 2022 - Geeko Packager + \+Mon Jan 3 11:22:33 UTC 2022 - Geeko Packager + @@ -4 \+4 @@ + -- Release upstream version 2 + \+- Release upstream version 1 + + spec files: + ----------- + --- test-pkgA.spec + \+\+\+ test-pkgA.spec + @@ -1,5 \+1,5 @@ + Name: test-pkgA + -Version: 2 + \+Version: 1 + Release: 1 + License: GPL-2.0 + Summary: Test package + + ---------------------------------------------------------------------------- + r1 \| Admin \| ....-..-.. ..:..:.. \| e675755e79e0d69483d311e96d6b719e \| 1 \| + + Initial commit + + + changes files: + -------------- + + \+\+\+\+\+\+ deleted changes files: + --- test-pkgA.changes + + old: + ---- + test-pkgA.changes + test-pkgA.spec + + spec files: + ----------- + + \+\+\+\+\+\+ deleted spec files: + --- test-pkgA.spec + + + """ diff --git a/osc/commandline.py b/osc/commandline.py index 07466d0ef0..5015d5b27f 100644 --- a/osc/commandline.py +++ b/osc/commandline.py @@ -39,6 +39,7 @@ from .grabber import OscFileGrabber from .meter import create_text_meter from .output import get_user_input +from .output import pipe_to_pager from .util import cpio, rpmquery, safewriter from .util.helper import _html_escape, format_table @@ -7661,13 +7662,15 @@ def do_jobhistory(self, subcmd, opts, *args): @cmdln.option('-r', '--revision', metavar='rev', help='show log of the specified revision') + @cmdln.option("-p", "--patch", action="store_true", + help='show patch for each revision; NOTE: use this option carefully because it loads patches on demand in a pager') @cmdln.option('', '--csv', action='store_true', - help='generate output in CSV (separated by |)') + help='generate output in CSV') @cmdln.option('', '--xml', action='store_true', help='generate output in XML') - @cmdln.option('-D', '--deleted', action='store_true', + @cmdln.option('-D', '--deleted', action='store_true', default=None, help='work on deleted package') - @cmdln.option('-M', '--meta', action='store_true', + @cmdln.option('-M', '--meta', action='store_true', default=None, help='checkout out meta data instead of sources') def do_log(self, subcmd, opts, *args): """ @@ -7695,8 +7698,8 @@ def do_log(self, subcmd, opts, *args): if opts.xml: format = 'xml' - log = '\n'.join(get_commitlog(apiurl, project, package, rev, format, opts.meta, opts.deleted, rev_upper)) - run_pager(log) + lines = get_commitlog(apiurl, project, package, rev, format, opts.meta, opts.deleted, rev_upper, patch=opts.patch) + pipe_to_pager(lines, add_newlines=True) @cmdln.option('-v', '--verbose', action='store_true', help='verbose run of local services for debugging purposes') diff --git a/osc/core.py b/osc/core.py index ccc60caa30..30d75a524e 100644 --- a/osc/core.py +++ b/osc/core.py @@ -6,6 +6,7 @@ import codecs import copy +import csv import datetime import difflib import errno @@ -4657,82 +4658,97 @@ def print_jobhistory(apiurl: str, prj: str, current_package: str, repository: st def get_commitlog( - apiurl: str, prj: str, package: str, revision, format="text", meta=False, deleted=False, revision_upper=None + apiurl: str, + prj: str, + package: str, + revision: Optional[str], + format: str = "text", + meta: Optional[bool] = None, + deleted: Optional[bool] = None, + revision_upper: Optional[str] = None, + patch: Optional[bool] = None, ): if package is None: package = "_project" - query = {} - if deleted: - query['deleted'] = 1 - if meta: - query['meta'] = 1 + from . import obs_api + revision_list = obs_api.Package.get_revision_list(apiurl, prj, package, deleted=deleted, meta=meta) - u = makeurl(apiurl, ['source', prj, package, '_history'], query) - f = http_GET(u) - root = ET.parse(f).getroot() + # TODO: consider moving the following block to Package.get_revision_list() + # keep only entries matching the specified revision + if not revision_is_empty(revision): + if isinstance(revision, str) and len(revision) == 32: + # revision is srcmd5 + revision_list = [i for i in revision_list if i.srcmd5 == revision] + else: + revision = int(revision) + if revision_is_empty(revision_upper): + revision_list = [i for i in revision_list if i.rev == revision] + else: + revision_upper = int(revision_upper) + revision_list = [i for i in revision_list if i.rev <= revision_upper and i.rev >= revision] + + if format == "csv": + f = io.StringIO() + writer = csv.writer(f, dialect="unix") + for revision in reversed(revision_list): + writer.writerow( + ( + revision.rev, + revision.user, + revision.get_time_str(), + revision.srcmd5, + revision.comment, + revision.requestid, + ) + ) + f.seek(0) + yield from f.read().splitlines() + return - r = [] - if format == 'xml': - r.append('') - r.append('') - revisions = root.findall('revision') - revisions.reverse() - for node in revisions: - srcmd5 = node.find('srcmd5').text - try: - rev = int(node.get('rev')) - # vrev = int(node.get('vrev')) # what is the meaning of vrev? - try: - if not revision_is_empty(revision) and revision_upper is not None: - if rev > int(revision_upper) or rev < int(revision): - continue - elif not revision_is_empty(revision) and rev != int(revision): - continue - except ValueError: - if revision != srcmd5: - continue - except ValueError: - # this part should _never_ be reached but... - return ['an unexpected error occured - please file a bug'] - version = node.find('version').text - user = node.find('user').text - try: - comment = node.find('comment').text.encode(locale.getpreferredencoding(), 'replace') - except: - comment = b'' - try: - requestid = node.find('requestid').text.encode(locale.getpreferredencoding(), 'replace') - except: - requestid = "" - t = time.gmtime(int(node.find('time').text)) - t = time.strftime('%Y-%m-%d %H:%M:%S', t) + if format == "xml": + root = ET.Element("log") + for revision in reversed(revision_list): + entry = ET.SubElement(root, "logentry") + entry.attrib["revision"] = str(revision.rev) + entry.attrib["srcmd5"] = revision.srcmd5 + ET.SubElement(entry, "author").text = revision.user + ET.SubElement(entry, "date").text = revision.get_time_str() + ET.SubElement(entry, "requestid").text = str(revision.requestid) if revision.requestid else "" + ET.SubElement(entry, "msg").text = revision.comment or "" + xmlindent(root) + yield from ET.tostring(root, encoding="utf-8").decode("utf-8").splitlines() + return - if format == 'csv': - s = '%s|%s|%s|%s|%s|%s|%s' % (rev, user, t, srcmd5, version, - decode_it(comment).replace('\\', '\\\\').replace('\n', '\\n').replace('|', '\\|'), requestid) - r.append(s) - elif format == 'xml': - r.append('') - r.append(f'{user}') - r.append(f'{t}') - r.append(f'{requestid}') - r.append(f'{_private.api.xml_escape(decode_it(comment))}') - r.append('') - else: - if requestid: - requestid = decode_it(b"rq" + requestid) - s = '-' * 76 + \ - f'\nr{rev} | {user} | {t} | {srcmd5} | {version} | {requestid}\n' + \ - '\n' + decode_it(comment) - r.append(s) - - if format not in ['csv', 'xml']: - r.append('-' * 76) - if format == 'xml': - r.append('') - return r + if format == "text": + for revision in reversed(revision_list): + entry = ( + f"r{revision.rev}", + revision.user, + revision.get_time_str(), + revision.srcmd5, + revision.version, + f"rq{revision.requestid}" if revision.requestid else "" + ) + yield 76 * "-" + yield " | ".join(entry) + yield "" + yield revision.comment or "" + yield "" + if patch: + rdiff = server_diff_noex( + apiurl, + prj, + package, + revision.rev, + prj, + package, + revision.rev - 1, + ) + yield highlight_diff(rdiff).decode("utf-8", errors="replace") + return + + raise ValueError(f"Invalid format: {format}") def runservice(apiurl: str, prj: str, package: str):