launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #25259
[Merge] ~pappacena/launchpad:githosting-copy-and-delete-refs into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:githosting-copy-and-delete-refs into launchpad:master.
Commit message:
Adding to GitHosting the API calls to copy and delete refs
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/390339
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:githosting-copy-and-delete-refs into launchpad:master.
diff --git a/lib/lp/code/errors.py b/lib/lp/code/errors.py
index 85de140..bed7db5 100644
--- a/lib/lp/code/errors.py
+++ b/lib/lp/code/errors.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Errors used in the lp/code modules."""
@@ -34,6 +34,7 @@ __all__ = [
'ClaimReviewFailed',
'DiffNotFound',
'GitDefaultConflict',
+ 'GitReferenceDeletionFault',
'GitRepositoryBlobNotFound',
'GitRepositoryBlobUnsupportedRemote',
'GitRepositoryCreationException',
@@ -493,6 +494,10 @@ class GitRepositoryDeletionFault(Exception):
"""Raised when there is a fault deleting a repository."""
+class GitReferenceDeletionFault(Exception):
+ """Raised when there is a fault deleting a repository's ref."""
+
+
class GitTargetError(Exception):
"""Raised when there is an error determining a Git repository target."""
diff --git a/lib/lp/code/interfaces/githosting.py b/lib/lp/code/interfaces/githosting.py
index 378930a..f6433ed 100644
--- a/lib/lp/code/interfaces/githosting.py
+++ b/lib/lp/code/interfaces/githosting.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interface for communication with the Git hosting service."""
@@ -129,3 +129,21 @@ class IGitHostingClient(Interface):
:param logger: An optional logger.
:return: A binary string with the blob content.
"""
+
+ def copyRefs(self, path, operations, logger=None):
+ """Executes the copy of refs or commits between different
+ repositories.
+
+ :param path: Physical path of the repository on the hosting service.
+ :param operations: A list of RefCopyOperation objects describing
+ source and target of the copy.
+ :param logger: An optional logger.
+ """
+
+ def deleteRef(self, path, ref, logger=None):
+ """Deletes a reference on the given git repository.
+
+ :param path: Physical path of the repository on the hosting service.
+ :param ref: The reference to be delete.
+ :param logger: An optional logger.
+ """
diff --git a/lib/lp/code/model/githosting.py b/lib/lp/code/model/githosting.py
index 94d6538..b957574 100644
--- a/lib/lp/code/model/githosting.py
+++ b/lib/lp/code/model/githosting.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Communication with the Git hosting service."""
@@ -6,6 +6,7 @@
__metaclass__ = type
__all__ = [
'GitHostingClient',
+ 'RefCopyOperation',
]
import base64
@@ -14,7 +15,10 @@ import sys
from lazr.restful.utils import get_current_browser_request
import requests
-from six import reraise
+from six import (
+ ensure_text,
+ reraise,
+ )
from six.moves.urllib.parse import (
quote,
urljoin,
@@ -22,10 +26,13 @@ from six.moves.urllib.parse import (
from zope.interface import implementer
from lp.code.errors import (
+ GitReferenceDeletionFault,
GitRepositoryBlobNotFound,
GitRepositoryCreationFault,
GitRepositoryDeletionFault,
GitRepositoryScanFault,
+ GitTargetError,
+ NoSuchGitReference,
)
from lp.code.interfaces.githosting import IGitHostingClient
from lp.services.config import config
@@ -41,6 +48,18 @@ class RequestExceptionWrapper(requests.RequestException):
"""A non-requests exception that occurred during a request."""
+class RefCopyOperation:
+ """A description of a ref (or commit) copy between repositories.
+
+ This class is just a helper to define copy operations parameters on
+ IGitHostingClient.copyRefs method.
+ """
+ def __init__(self, source_ref, target_repo, target_ref):
+ self.source_ref = source_ref
+ self.target_repo = target_repo
+ self.target_ref = target_ref
+
+
@implementer(IGitHostingClient)
class GitHostingClient:
"""A client for the internal API provided by the Git hosting system."""
@@ -237,3 +256,39 @@ class GitHostingClient:
except Exception as e:
raise GitRepositoryScanFault(
"Failed to get file from Git repository: %s" % unicode(e))
+
+ def copyRefs(self, path, operations, logger=None):
+ """See `IGitHostingClient`."""
+ json_data = {
+ "operations": [{
+ "from": i.source_ref,
+ "to": {"repo": i.target_repo, "ref": i.target_ref}
+ } for i in operations]
+ }
+ try:
+ if logger is not None:
+ logger.info(
+ "Copying refs from %s to %s targets" %
+ (path, len(operations)))
+ url = "/repo/%s/refs-copy" % path
+ self._post(url, json=json_data)
+ except requests.RequestException as e:
+ if (e.response is not None and
+ e.response.status_code == requests.codes.NOT_FOUND):
+ raise GitTargetError(
+ "Could not find repository %s or one of its refs" %
+ ensure_text(path))
+ else:
+ raise GitRepositoryScanFault(
+ "Could not copy refs: HTTP %s" % e.response.status_code)
+
+ def deleteRef(self, path, ref, logger=None):
+ try:
+ if logger is not None:
+ logger.info("Delete from repo %s the ref %s" % (path, ref))
+ url = "/repo/%s/%s" % (path, ref)
+ self._delete(url)
+ except requests.RequestException as e:
+ raise GitReferenceDeletionFault(
+ "Error deleting %s from repo %s: HTTP %s" %
+ (ref, path, e.response.status_code))
diff --git a/lib/lp/code/model/tests/test_githosting.py b/lib/lp/code/model/tests/test_githosting.py
index d966dbe..04f7a8e 100644
--- a/lib/lp/code/model/tests/test_githosting.py
+++ b/lib/lp/code/model/tests/test_githosting.py
@@ -2,7 +2,7 @@
# NOTE: The first line above must stay first; do not move the copyright
# notice to the top. See http://www.python.org/dev/peps/pep-0263/.
#
-# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2016-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Unit tests for `GitHostingClient`.
@@ -37,12 +37,16 @@ from zope.interface import implementer
from zope.security.proxy import removeSecurityProxy
from lp.code.errors import (
+ GitReferenceDeletionFault,
GitRepositoryBlobNotFound,
GitRepositoryCreationFault,
GitRepositoryDeletionFault,
GitRepositoryScanFault,
+ GitTargetError,
+ NoSuchGitReference,
)
from lp.code.interfaces.githosting import IGitHostingClient
+from lp.code.model.githosting import RefCopyOperation
from lp.services.job.interfaces.job import (
IRunnableJob,
JobStatus,
@@ -400,6 +404,46 @@ class TestGitHostingClient(TestCase):
" (256 vs 0)",
self.client.getBlob, "123", "dir/path/file/name")
+ def getCopyRefOperations(self):
+ return [
+ RefCopyOperation("1a2b3c4", "999", "refs/merge/123"),
+ RefCopyOperation("9a8b7c6", "666", "refs/merge/989"),
+ ]
+
+ def test_copyRefs(self):
+ with self.mockRequests("POST", status=202):
+ self.client.copyRefs("123", self.getCopyRefOperations())
+ self.assertRequest("repo/123/refs-copy", {
+ "operations": [
+ {
+ "from": "1a2b3c4",
+ "to": {"repo": "999", "ref": "refs/merge/123"}
+ }, {
+ "from": "9a8b7c6",
+ "to": {"repo": "666", "ref": "refs/merge/989"}
+ }
+ ]
+ }, "POST")
+
+ def test_copyRefs_refs_not_found(self):
+ with self.mockRequests("POST", status=404):
+ self.assertRaisesWithContent(
+ GitTargetError,
+ "Could not find repository 123 or one of its refs",
+ self.client.copyRefs, "123", self.getCopyRefOperations())
+
+ def test_deleteRef(self):
+ with self.mockRequests("DELETE", status=202):
+ self.client.deleteRef("123", "refs/merge/123")
+ self.assertRequest("repo/123/refs/merge/123", method="DELETE")
+
+ def test_deleteRef_refs_request_error(self):
+ with self.mockRequests("DELETE", status=500):
+ self.assertRaisesWithContent(
+ GitReferenceDeletionFault,
+ "Error deleting refs/merge/123 from repo 123: HTTP 500",
+ self.client.deleteRef, "123", "refs/merge/123")
+
def test_works_in_job(self):
# `GitHostingClient` is usable from a running job.
@implementer(IRunnableJob)