Subject: [bug#75859] [PATCH 4/4] gnu: breezy: Update to 3.3.9.

From: Dariqq <[email-masked]>
Date: Sun, 26 Jan 2025 12:27:10 +0000

breezy now depends on a rust compiler and crates so we need to switch to
cargo-build-system to set up rust and then readd all the python phases.

* gnu/packages/version-control.scm (breezy): Update to 3.3.9.
[build-system]: switch to cargo-build-system
[#:phases]: Convert back to python-build-system
Add a phase to install the bash-completion file.
Run the testsuite again.

[native-inputs]: Remove python-docutils, python-testrepository
Add gettext-minimal, python-wrapper, python-setuptools, python-setuptools-rust,
python-setuptools-gettext, python-tomli, python wheel.
Add python testtools and python packaging for tests.
[inputs]: Remove gettext-minimal, python-pygobject.
Add python-merge3, python-pygithub, python-pyyaml, python-tzlocal,
python-urllib3
Replace python-pycrptodome and python-pygpgme with python-gpg

* gnu/packages/patches/breezy-fix-gio.patch: Remove patch
* gnu/local.mk : Deregister it.

Change-Id: I69d6c4491442a9ba93a748137fb2ad810a423abd
---
 gnu/local.mk                              |   1 -
 gnu/packages/patches/breezy-fix-gio.patch | 338 ----------------------
 gnu/packages/version-control.scm          | 122 +++++---
 3 files changed, 89 insertions(+), 372 deletions(-)
 delete mode 100644 gnu/packages/patches/breezy-fix-gio.patch

diff --git a/gnu/local.mk b/gnu/local.mk
index 5091f93eb8..ac29c00c79 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -1052,7 +1052,6 @@ dist_patch_DATA =						\
   %D%/packages/patches/bloomberg-bde-tools-fix-install-path.patch	\
   %D%/packages/patches/boolector-find-googletest.patch	\
   %D%/packages/patches/boost-fix-duplicate-definitions-bug.patch	\
-  %D%/packages/patches/breezy-fix-gio.patch			\
   %D%/packages/patches/byobu-writable-status.patch		\
   %D%/packages/patches/bubblewrap-fix-locale-in-tests.patch	\
   %D%/packages/patches/busybox-add-missing-sha-NI-guard.patch	\
diff --git a/gnu/packages/patches/breezy-fix-gio.patch b/gnu/packages/patches/breezy-fix-gio.patch
deleted file mode 100644
index f70e761555..0000000000
--- a/gnu/packages/patches/breezy-fix-gio.patch
+++ /dev/null
@@ -1,338 +0,0 @@
-This patch combines https://code.launchpad.net/~jelmer/brz/enable-gio/+merge/419150
-and https://bazaar.launchpad.net/~jelmer/brz/fix-gio/revision/7570.
-
-=== modified file 'breezy/transport/gio_transport.py'
---- a/breezy/transport/gio_transport.py	2022-04-09 12:17:41 +0000
-+++ b/breezy/transport/gio_transport.py	2022-04-09 12:33:51 +0000
-@@ -52,11 +52,7 @@
- from ..tests.test_server import TestServer
- 
- try:
--    import glib
--except ImportError as e:
--    raise errors.DependencyNotPresent('glib', e)
--try:
--    import gio
-+    from gi.repository import Gio as gio
- except ImportError as e:
-     raise errors.DependencyNotPresent('gio', e)
- 
-
-@@ -57,6 +57,9 @@
-     raise errors.DependencyNotPresent('gio', e)
- 
- 
-+from gi.repository.GLib import GError
-+
-+
- class GioLocalURLServer(TestServer):
-     """A pretend server for local transports, using file:// urls.
- 
-@@ -81,7 +84,7 @@
-     def __init__(self, transport, relpath):
-         FileStream.__init__(self, transport, relpath)
-         self.gio_file = transport._get_GIO(relpath)
--        self.stream = self.gio_file.create()
-+        self.stream = self.gio_file.create(0, None)
- 
-     def _close(self):
-         self.stream.close()
-@@ -90,7 +93,7 @@
-         try:
-             # Using pump_string_file seems to make things crash
-             osutils.pumpfile(BytesIO(bytes), self.stream)
--        except gio.Error as e:
-+        except GError as e:
-             # self.transport._translate_gio_error(e,self.relpath)
-             raise errors.BzrError(str(e))
- 
-@@ -98,12 +101,12 @@
- class GioStatResult(object):
- 
-     def __init__(self, f):
--        info = f.query_info('standard::size,standard::type')
-+        info = f.query_info('standard::size,standard::type', 0, None)
-         self.st_size = info.get_size()
-         type = info.get_file_type()
--        if (type == gio.FILE_TYPE_REGULAR):
-+        if type == gio.FileType.REGULAR:
-             self.st_mode = stat.S_IFREG
--        elif type == gio.FILE_TYPE_DIRECTORY:
-+        elif type == gio.FileType.DIRECTORY:
-             self.st_mode = stat.S_IFDIR
- 
- 
-@@ -122,7 +125,7 @@
-             user, netloc = netloc.rsplit('@', 1)
-         # Seems it is not possible to list supported backends for GIO
-         # so a hardcoded list it is then.
--        gio_backends = ['dav', 'file', 'ftp', 'obex', 'sftp', 'ssh', 'smb']
-+        gio_backends = ['dav', 'file', 'ftp', 'obex', 'sftp', 'ssh', 'smb', 'http']
-         if scheme not in gio_backends:
-             raise urlutils.InvalidURL(base,
-                                       extra="GIO support is only available for " +
-@@ -138,13 +141,10 @@
-                                            _from_transport=_from_transport)
- 
-     def _relpath_to_url(self, relpath):
--        full_url = urlutils.join(self.url, relpath)
--        if isinstance(full_url, str):
--            raise urlutils.InvalidURL(full_url)
--        return full_url
-+        return urlutils.join(self.url, relpath)
- 
-     def _get_GIO(self, relpath):
--        """Return the ftplib.GIO instance for this object."""
-+        """Return the GIO instance for this object."""
-         # Ensures that a connection is established
-         connection = self._get_connection()
-         if connection is None:
-@@ -152,7 +152,7 @@
-             connection, credentials = self._create_connection()
-             self._set_connection(connection, credentials)
-         fileurl = self._relpath_to_url(relpath)
--        file = gio.File(fileurl)
-+        file = gio.File.new_for_uri(fileurl)
-         return file
- 
-     def _auth_cb(self, op, message, default_user, default_domain, flags):
-@@ -197,7 +197,7 @@
-         try:
-             obj.mount_enclosing_volume_finish(res)
-             self.loop.quit()
--        except gio.Error as e:
-+        except GError as e:
-             self.loop.quit()
-             raise errors.BzrError(
-                 "Failed to mount the given location: " + str(e))
-@@ -209,12 +209,12 @@
-             user, password = credentials
- 
-         try:
--            connection = gio.File(self.url)
-+            connection = gio.File.new_for_uri(self.url)
-             mount = None
-             try:
-                 mount = connection.find_enclosing_mount()
--            except gio.Error as e:
--                if (e.code == gio.ERROR_NOT_MOUNTED):
-+            except GError as e:
-+                if e.code == gio.IOErrorEnum.NOT_MOUNTED:
-                     self.loop = glib.MainLoop()
-                     ui.ui_factory.show_message('Mounting %s using GIO' %
-                                                self.url)
-@@ -227,7 +227,7 @@
-                     m = connection.mount_enclosing_volume(op,
-                                                           self._mount_done_cb)
-                     self.loop.run()
--        except gio.Error as e:
-+        except GError as e:
-             raise errors.TransportError(msg="Error setting up connection:"
-                                         " %s" % str(e), orig_error=e)
-         return connection, (user, password)
-@@ -257,8 +257,8 @@
-             if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
-                 return True
-             return False
--        except gio.Error as e:
--            if e.code == gio.ERROR_NOT_FOUND:
-+        except GError as e:
-+            if e.code == gio.IOErrorEnum.NOT_FOUND:
-                 return False
-             else:
-                 self._translate_gio_error(e, relpath)
-@@ -281,10 +281,10 @@
-             buf = fin.read()
-             fin.close()
-             return BytesIO(buf)
--        except gio.Error as e:
-+        except GError as e:
-             # If we get a not mounted here it might mean
-             # that a bad path has been entered (or that mount failed)
--            if (e.code == gio.ERROR_NOT_MOUNTED):
-+            if e.code == gio.IOErrorEnum.NOT_MOUNTED:
-                 raise errors.PathError(relpath,
-                                        extra='Failed to get file, make sure the path is correct. '
-                                        + str(e))
-@@ -307,19 +307,19 @@
-             closed = True
-             try:
-                 f = self._get_GIO(tmppath)
--                fout = f.create()
-+                fout = f.create(0, None)
-                 closed = False
-                 length = self._pump(fp, fout)
-                 fout.close()
-                 closed = True
-                 self.stat(tmppath)
-                 dest = self._get_GIO(relpath)
--                f.move(dest, flags=gio.FILE_COPY_OVERWRITE)
-+                f.move(dest, flags=gio.FileCopyFlags.OVERWRITE)
-                 f = None
-                 if mode is not None:
-                     self._setmode(relpath, mode)
-                 return length
--            except gio.Error as e:
-+            except GError as e:
-                 self._translate_gio_error(e, relpath)
-         finally:
-             if not closed and fout is not None:
-@@ -335,7 +335,7 @@
-             f = self._get_GIO(relpath)
-             f.make_directory()
-             self._setmode(relpath, mode)
--        except gio.Error as e:
-+        except GError as e:
-             self._translate_gio_error(e, relpath)
- 
-     def open_write_stream(self, relpath, mode=None):
-@@ -369,14 +369,11 @@
-                 f.delete()
-             else:
-                 raise errors.NotADirectory(relpath)
--        except gio.Error as e:
-+        except GError as e:
-             self._translate_gio_error(e, relpath)
-         except errors.NotADirectory as e:
-             # just pass it forward
-             raise e
--        except Exception as e:
--            mutter('failed to rmdir %s: %s' % (relpath, e))
--            raise errors.PathError(relpath)
- 
-     def append_file(self, relpath, file, mode=None):
-         """Append the text in the file-like object into the final
-@@ -392,7 +389,7 @@
-             result = 0
-             fo = self._get_GIO(tmppath)
-             fi = self._get_GIO(relpath)
--            fout = fo.create()
-+            fout = fo.create(0, None)
-             try:
-                 info = GioStatResult(fi)
-                 result = info.st_size
-@@ -400,11 +397,11 @@
-                 self._pump(fin, fout)
-                 fin.close()
-             # This separate except is to catch and ignore the
--            # gio.ERROR_NOT_FOUND for the already existing file.
-+            # gio.IOErrorEnum.NOT_FOUND for the already existing file.
-             # It is valid to open a non-existing file for append.
-             # This is caused by the broken gio append_to...
--            except gio.Error as e:
--                if e.code != gio.ERROR_NOT_FOUND:
-+            except GError as e:
-+                if e.code != gio.IOErrorEnum.NOT_FOUND:
-                     self._translate_gio_error(e, relpath)
-             length = self._pump(file, fout)
-             fout.close()
-@@ -413,9 +410,11 @@
-                 raise errors.BzrError("Failed to append size after "
-                                       "(%d) is not original (%d) + written (%d) total (%d)" %
-                                       (info.st_size, result, length, result + length))
--            fo.move(fi, flags=gio.FILE_COPY_OVERWRITE)
-+            fo.move(
-+                fi, flags=gio.FileCopyFlags.OVERWRITE, cancellable=None,
-+                progress_callback=None)
-             return result
--        except gio.Error as e:
-+        except GError as e:
-             self._translate_gio_error(e, relpath)
- 
-     def _setmode(self, relpath, mode):
-@@ -429,8 +428,8 @@
-             try:
-                 f = self._get_GIO(relpath)
-                 f.set_attribute_uint32(gio.FILE_ATTRIBUTE_UNIX_MODE, mode)
--            except gio.Error as e:
--                if e.code == gio.ERROR_NOT_SUPPORTED:
-+            except GError as e:
-+                if e.code == gio.IOErrorEnum.NOT_SUPPORTED:
-                     # Command probably not available on this server
-                     mutter("GIO Could not set permissions to %s on %s. %s",
-                            oct(mode), self._remote_path(relpath), str(e))
-@@ -444,8 +443,8 @@
-                 mutter("GIO move (rename): %s => %s", rel_from, rel_to)
-             f = self._get_GIO(rel_from)
-             t = self._get_GIO(rel_to)
--            f.move(t)
--        except gio.Error as e:
-+            f.move(t, flags=0, cancellable=None, progress_callback=None)
-+        except GError as e:
-             self._translate_gio_error(e, rel_from)
- 
-     def move(self, rel_from, rel_to):
-@@ -455,8 +454,8 @@
-                 mutter("GIO move: %s => %s", rel_from, rel_to)
-             f = self._get_GIO(rel_from)
-             t = self._get_GIO(rel_to)
--            f.move(t, flags=gio.FILE_COPY_OVERWRITE)
--        except gio.Error as e:
-+            f.move(t, flags=gio.FileCopyFlags.OVERWRITE)
-+        except GError as e:
-             self._translate_gio_error(e, relfrom)
- 
-     def delete(self, relpath):
-@@ -466,7 +465,7 @@
-                 mutter("GIO delete: %s", relpath)
-             f = self._get_GIO(relpath)
-             f.delete()
--        except gio.Error as e:
-+        except GError as e:
-             self._translate_gio_error(e, relpath)
- 
-     def external_url(self):
-@@ -489,11 +488,11 @@
-         try:
-             entries = []
-             f = self._get_GIO(relpath)
--            children = f.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME)
-+            children = f.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_NAME, 0, None)
-             for child in children:
-                 entries.append(urlutils.escape(child.get_name()))
-             return entries
--        except gio.Error as e:
-+        except GError as e:
-             self._translate_gio_error(e, relpath)
- 
-     def iter_files_recursive(self):
-@@ -519,7 +518,7 @@
-                 mutter("GIO stat: %s", relpath)
-             f = self._get_GIO(relpath)
-             return GioStatResult(f)
--        except gio.Error as e:
-+        except GError as e:
-             self._translate_gio_error(e, relpath, extra='error w/ stat')
- 
-     def lock_read(self, relpath):
-@@ -556,21 +555,21 @@
-             mutter("GIO Error: %s %s" % (str(err), path))
-         if extra is None:
-             extra = str(err)
--        if err.code == gio.ERROR_NOT_FOUND:
-+        if err.code == gio.IOErrorEnum.NOT_FOUND:
-             raise errors.NoSuchFile(path, extra=extra)
--        elif err.code == gio.ERROR_EXISTS:
-+        elif err.code == gio.IOErrorEnum.EXISTS:
-             raise errors.FileExists(path, extra=extra)
--        elif err.code == gio.ERROR_NOT_DIRECTORY:
-+        elif err.code == gio.IOErrorEnum.NOT_DIRECTORY:
-             raise errors.NotADirectory(path, extra=extra)
--        elif err.code == gio.ERROR_NOT_EMPTY:
-+        elif err.code == gio.IOErrorEnum.NOT_EMPTY:
-             raise errors.DirectoryNotEmpty(path, extra=extra)
--        elif err.code == gio.ERROR_BUSY:
-+        elif err.code == gio.IOErrorEnum.BUSY:
-             raise errors.ResourceBusy(path, extra=extra)
--        elif err.code == gio.ERROR_PERMISSION_DENIED:
-+        elif err.code == gio.IOErrorEnum.PERMISSION_DENIED:
-             raise errors.PermissionDenied(path, extra=extra)
--        elif err.code == gio.ERROR_HOST_NOT_FOUND:
-+        elif err.code == gio.IOErrorEnum.HOST_NOT_FOUND:
-             raise errors.PathError(path, extra=extra)
--        elif err.code == gio.ERROR_IS_DIRECTORY:
-+        elif err.code == gio.IOErrorEnum.IS_DIRECTORY:
-             raise errors.PathError(path, extra=extra)
-         else:
-             mutter('unable to understand error for path: %s: %s', path, err)
-
diff --git a/gnu/packages/version-control.scm b/gnu/packages/version-control.scm
index 2a95bc79e1..a40de4c812 100644
--- a/gnu/packages/version-control.scm
+++ b/gnu/packages/version-control.scm
@@ -60,6 +60,7 @@
 ;;; Copyright © 2024 Ashish SHUKLA <[email-masked]>
 ;;; Copyright © 2024 Wilko Meyer <[email-masked]>
 ;;; Copyright © 2025 Artyom V. Poptsov <[email-masked]>
+;;; Copyright © 2025 Dariqq <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -180,7 +181,7 @@ (define-module (gnu packages version-control)
 (define-public breezy
   (package
     (name "breezy")
-    (version "3.2.2")
+    (version "3.3.9")
     (source
      (origin
        (method url-fetch)
@@ -192,51 +193,106 @@ (define-public breezy
        (snippet '(for-each delete-file (find-files "." "\\pyx.c$")))
        (sha256
         (base32
-         "1md4b6ajawf5h50fqizmjj0g833ihc674dh7fn0mvl4d412nwyhq"))
-       (patches (search-patches "breezy-fix-gio.patch"))))
-    (build-system python-build-system)
+         "1n6mqd1iy50537kb4lsr52289yyr1agmkxpchxlhb9682zr8nn62"))))
+    (build-system cargo-build-system)
     (arguments
      (list
-      #:tests? #f                       ;FIXME: the test suite hangs
+      #:cargo-inputs (list rust-lazy-static-1
+                           rust-pyo3-0.22
+                           rust-regex-1)
+      #:install-source? #f
+      #:modules
+      '((guix build cargo-build-system)
+        ((guix build python-build-system) #:prefix py:)
+        (guix build utils))
+      #:imported-modules
+      `(,@%cargo-build-system-modules
+        ,@%python-build-system-modules)
       #:phases
       #~(modify-phases %standard-phases
+          (add-after 'unpack 'ensure-no-mtimes-pre-1980
+            (assoc-ref py:%standard-phases 'ensure-no-mtimes-pre-1980))
+          (add-after 'ensure-no-mtimes-pre-1980 'enable-bytecode-determinism
+            (assoc-ref py:%standard-phases 'enable-bytecode-determinism))
+          (add-after 'enable-bytecode-determinism 'ensure-no-cythonized-files
+            (assoc-ref py:%standard-phases 'ensure-no-cythonized-files))
           (add-after 'unpack 'patch-test-shebangs
             (lambda _
               (substitute* (append (find-files "breezy/bzr/tests")
                                    (find-files "breezy/tests"))
                 (("#!/bin/sh")
                  (format #f "#!~a" (which "sh"))))))
-          (replace 'check
+          (add-before 'build 'adjust-for-python-3.10
+            (lambda _
+              (substitute* '("breezy/doc_generate/__init__.py"
+                             "breezy/tests/test_selftest.py")
+                ;; AttributeError: module 'datetime' has no attribute 'UTC'
+                ;; This only works for python >= 3.11
+                (("datetime.UTC") "datetime.timezone.utc"))))
+          (replace 'build
+            (assoc-ref py:%standard-phases 'build))
+          (delete 'check)             ;moved after the install phase
+          (replace 'install
+            (assoc-ref py:%standard-phases 'install))
+          (add-after 'install 'add-install-to-pythonpath
+            (assoc-ref py:%standard-phases 'add-install-to-pythonpath))
+          (add-after 'add-install-to-pythonpath 'add-install-to-path
+            (assoc-ref py:%standard-phases 'add-install-to-path))
+          (add-after 'add-install-to-path 'install-completion
+            (lambda* (#:key outputs #:allow-other-keys)
+              (let* ((out  (assoc-ref outputs "out"))
+                     (bash (string-append out "/share/bash-completion"
+                                          "/completions")))
+                (install-file "contrib/bash/brz" bash))))
+          (add-after 'add-install-to-path 'wrap
+            (assoc-ref py:%standard-phases 'wrap))
+          (add-after 'wrap 'check
             (lambda* (#:key tests? #:allow-other-keys)
               (when tests?
-                ;; The test_read_bundle tests fails with "TypeError: a
-                ;; bytes-like object is required, not '_ResultTuple'" (see:
-                ;; https://bugs.launchpad.net/brz/+bug/1968415/comments/4).
-                (substitute* "breezy/bzr/tests/__init__.py"
-                  (("'test_read_bundle'," all)
-                   (string-append "# " all)))
                 (setenv "BZR_EDITOR" "nano")
-                (setenv "HOME" "/tmp")
-                (invoke "testr" "init")
-                (invoke "testr" "run")))))))
-    (native-inputs
-     (list nano                         ;for tests
-           python-cython
-           python-docutils
-           python-subunit
-           python-testrepository))
-    (inputs
-     (list gettext-minimal
-           python-configobj
-           python-dulwich
-           python-fastbencode
-           python-fastimport
-           python-launchpadlib
-           python-paramiko
-           python-patiencediff
-           python-pycryptodome
-           python-pygobject
-           python-pygpgme))
+                (invoke "brz" "selftest" "--verbose" "--parallel=fork"
+                        ;; This test hangs
+                        "-x" "breezy.tests.blackbox.test_serve"
+                        ;; No GnuPG key results for pattern: [email-masked]
+                        "-x" "breezy.tests.test_gpg"
+                        ;; compgen: command not found
+                        "-x" "bash_completion"
+                        ;; No such file or directory: '/etc/mtab'
+                        "-x" "breezy.tests.blackbox.test_diff.TestExternalDiff.test_external_diff"
+                        ;; Value "/etc/ssl/certs/ca-certificates.crt" is not valid for "ssl.ca_certs"
+                        "-x" "breezy.tests.test_https_urllib.CaCertsConfigTests.test_default_exists"
+                        ;; Unknown Failure
+                        "-x" "breezy.tests.test_plugins.TestLoadPluginAt.test_compiled_loaded"
+                        "-x" "breezy.tests.test_plugins.TestPlugins.test_plugin_get_path_pyc_only"
+                        "-x" "breezy.tests.test_selftest.TestActuallyStartBzrSubprocess.test_start_and_stop_bzr_subprocess_send_signal"))))
+          (add-before 'strip 'rename-pth-file
+            (assoc-ref py:%standard-phases 'rename-pth-file)))))
+    (native-inputs (list gettext-minimal
+                         python-wrapper
+                         python-cython
+                         python-setuptools
+                         python-setuptools-gettext
+                         python-setuptools-rust
+                         python-tomli
+                         python-wheel
+                         ;; tests
+                         nano
+                         python-testtools
+                         python-packaging
+                         python-subunit))
+    (inputs (list python-configobj
+                  python-dulwich
+                  python-fastbencode
+                  python-fastimport
+                  python-launchpadlib
+                  python-merge3
+                  python-paramiko
+                  python-gpg
+                  python-patiencediff
+                  python-pygithub
+                  python-pyyaml
+                  python-tzlocal
+                  python-urllib3))
     (home-page "https://www.breezy-vcs.org/")
     (synopsis "Decentralized revision control system")
     (description
-- 
2.47.1