launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #30531
[Merge] ~bowenfan/turnip:SN2052-add_ref_creation_endpoint_to_turnip_api into turnip:master
Bowen Fan has proposed merging ~bowenfan/turnip:SN2052-add_ref_creation_endpoint_to_turnip_api into turnip:master.
Commit message:
Add ref creation POST endpoint to turnip-api
The `collection_post` endpoint is mounted at `/repo/{name}/refs`.
It will read the json request body to dispatch logic layer functions to
either create a tag or branch. The endpoint supports forced
overwrites and will pass the param down to pygit2 if specified.
`write_lightweight_tag` takes a tag name and writes it to a given
commit sha1. Annotated tags are not supported here: it is not
straightforward to support signatures as the endpoint is internal
and unauthed.
`create_branch` does the same for a branch name. Turnip repos
are all bare and therefore we do not have to worry about being
checked out on a branch we are about to overwrite.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~bowenfan/turnip/+git/turnip/+merge/452540
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~bowenfan/turnip:SN2052-add_ref_creation_endpoint_to_turnip_api into turnip:master.
diff --git a/turnip/api/store.py b/turnip/api/store.py
index ae897c2..ac88c65 100644
--- a/turnip/api/store.py
+++ b/turnip/api/store.py
@@ -17,8 +17,16 @@ from pygit2 import (
GIT_OBJ_TAG,
GIT_REF_OID,
GIT_SORT_TOPOLOGICAL,
+)
+
+# Some irony in that this error name already exists and is used to signify
+# an existing repo. It inherits from GitError. pygit2's AlreadyExistsError
+# is more appropriate for objects (e.g. references) and we rename it thus.
+from pygit2 import AlreadyExistsError as ObjectAlreadyExistsError
+from pygit2 import (
GitError,
IndexEntry,
+ InvalidSpecError,
Oid,
Repository,
init_repository,
@@ -555,6 +563,43 @@ def get_ref(repo_store, repo_name, ref):
return ref_obj
+def write_lightweight_tag(
+ repo_store, repo_name, tag_name, commit_sha1, force=False
+):
+ """Write a lightweight tag referencing a given commit sha1.
+
+ :param tag_name: Name of the git tag to write.
+ :param commit_sha1: SHA1 hash of the commit to tag.
+ :param force: If True, force overwrite the original named tag
+ (if any) to point to the new commit.
+ """
+ ref_base = "refs/tags/"
+ ref_name = ref_base + tag_name
+ with open_repo(repo_store, repo_name) as repo:
+ try:
+ repo.create_reference(ref_name, commit_sha1, force)
+ except (ObjectAlreadyExistsError, InvalidSpecError, GitError):
+ raise
+
+
+def create_branch(
+ repo_store, repo_name, branch_name, commit_sha1, force=False
+):
+ """Create a branch and set a given commit sha1 as its tip.
+
+ :param branch_name: Name of the git branch.
+ :param commit_sha1: SHA1 hash of the commit to set as branch tip.
+ :param force: If True, force reset the original named branch
+ (if any) to the given commit. Can cause history loss.
+ """
+ with open_repo(repo_store, repo_name) as repo:
+ try:
+ commit = get_commit(repo_store, repo_name, commit_sha1, repo=repo)
+ repo.create_branch(branch_name, commit, force)
+ except (ObjectAlreadyExistsError, InvalidSpecError, GitError):
+ raise
+
+
def get_common_ancestor_diff(
repo_store, repo_name, sha1_target, sha1_source, context_lines=3
):
diff --git a/turnip/api/tests/test_api.py b/turnip/api/tests/test_api.py
index 1513618..f49f46f 100644
--- a/turnip/api/tests/test_api.py
+++ b/turnip/api/tests/test_api.py
@@ -241,6 +241,220 @@ class ApiTestCase(TestCase, ApiRepoStoreMixin):
resp = self.app.get("/repo/1/refs", expect_errors=True)
self.assertEqual(404, resp.status_code)
+ def test_repo_write_lightweight_tag(self):
+ factory = RepoFactory(self.repo_store)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+
+ body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "lightweight_tag",
+ "ref_name": "1701",
+ }
+ resp = self.app.post_json(f"/repo/{self.repo_path}/refs", body)
+ self.assertEqual(201, resp.status_code)
+
+ def test_repo_create_branch(self):
+ factory = RepoFactory(self.repo_store)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+
+ body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "branch",
+ "ref_name": "new-feature",
+ }
+ resp = self.app.post_json(f"/repo/{self.repo_path}/refs", body)
+ self.assertEqual(201, resp.status_code)
+
+ def test_repo_force_overwrite_ref(self):
+ factory = RepoFactory(self.repo_store)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+ factory.add_tag("1701", "", commit_oid)
+ factory.add_branch("new-feature", commit_oid)
+
+ tag_body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "lightweight_tag",
+ "ref_name": "1701",
+ "force": True,
+ }
+ tag_resp = self.app.post_json(f"/repo/{self.repo_path}/refs", tag_body)
+ self.assertEqual(201, tag_resp.status_code)
+
+ branch_body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "branch",
+ # factory.add_branch prefixes 'branch-'
+ "ref_name": "branch-new-feature",
+ "force": True,
+ }
+ branch_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", branch_body
+ )
+ self.assertEqual(201, branch_resp.status_code)
+
+ def test_request_body_api_validation(self):
+ factory = RepoFactory(self.repo_store)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+
+ missing_sha_1 = {
+ "ref_type": "lightweight_tag",
+ "ref_name": "1701",
+ }
+ resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", missing_sha_1, expect_errors=True
+ )
+ self.assertEqual(400, resp.status_code)
+ self.assertIn(
+ "commit_sha1 for ref target is missing",
+ resp.text,
+ )
+
+ bad_ref_type = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "invalid_type",
+ "ref_name": "1701",
+ }
+ resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", bad_ref_type, expect_errors=True
+ )
+ self.assertEqual(400, resp.status_code)
+ self.assertIn(
+ "must specify ref type (lightweight_tag or branch)",
+ resp.text,
+ )
+
+ missing_ref_name = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "branch",
+ }
+ resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs",
+ missing_ref_name,
+ expect_errors=True,
+ )
+ self.assertEqual(400, resp.status_code)
+ self.assertIn(
+ "must specify ref name",
+ resp.text,
+ )
+
+ bad_force_optiom = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "lightweight_tag",
+ "ref_name": "1701",
+ "force": "y",
+ }
+ resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs",
+ bad_force_optiom,
+ expect_errors=True,
+ )
+ self.assertEqual(400, resp.status_code)
+ self.assertIn(
+ "'force' must be a json boolean",
+ resp.text,
+ )
+
+ def test_repo_write_invalid_ref_name(self):
+ factory = RepoFactory(self.repo_store)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+
+ tag_body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "lightweight_tag",
+ "ref_name": "?*",
+ }
+ tag_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", tag_body, expect_errors=True
+ )
+ self.assertEqual(400, tag_resp.status_code)
+ self.assertIn(
+ "ensure tag or branch name is valid per git-check-ref-format",
+ tag_resp.text,
+ )
+
+ branch_body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "branch",
+ "ref_name": "?*",
+ }
+ branch_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", branch_body, expect_errors=True
+ )
+ self.assertEqual(400, branch_resp.status_code)
+ self.assertIn(
+ "ensure tag or branch name is valid per git-check-ref-format",
+ branch_resp.text,
+ )
+
+ def test_repo_ref_already_exists(self):
+ factory = RepoFactory(self.repo_store)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+ factory.add_tag("1701", "", commit_oid)
+ factory.add_branch("new-feature", commit_oid)
+
+ tag_body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "lightweight_tag",
+ "ref_name": "1701",
+ }
+ tag_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", tag_body, expect_errors=True
+ )
+ self.assertEqual(409, tag_resp.status_code)
+ self.assertIn(
+ "tag or branch name already exists",
+ tag_resp.text,
+ )
+
+ branch_body = {
+ "commit_sha1": commit_oid.hex,
+ "ref_type": "branch",
+ # factory.add_branch prefixes 'branch-'
+ "ref_name": "branch-new-feature",
+ }
+ branch_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", branch_body, expect_errors=True
+ )
+ self.assertEqual(409, branch_resp.status_code)
+ self.assertIn(
+ "tag or branch name already exists",
+ branch_resp.text,
+ )
+
+ def test_repo_nonexistent_commit(self):
+ factory = RepoFactory(self.repo_store)
+
+ tag_body = {
+ "commit_sha1": factory.nonexistent_oid(),
+ "ref_type": "lightweight_tag",
+ "ref_name": "1701",
+ }
+ tag_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", tag_body, expect_errors=True
+ )
+ self.assertEqual(404, tag_resp.status_code)
+ self.assertIn(
+ "no commit found for commit_sha1 given",
+ tag_resp.text,
+ )
+
+ branch_body = {
+ "commit_sha1": factory.nonexistent_oid(),
+ "ref_type": "branch",
+ # factory.add_branch prefixes 'branch-'
+ "ref_name": "branch-new-feature",
+ "force": True,
+ }
+ branch_resp = self.app.post_json(
+ f"/repo/{self.repo_path}/refs", branch_body, expect_errors=True
+ )
+ self.assertEqual(404, branch_resp.status_code)
+ self.assertIn(
+ "no commit found for commit_sha1 given",
+ branch_resp.text,
+ )
+
def test_ignore_non_unicode_refs(self):
"""Ensure non-unicode refs are dropped from ref collection."""
factory = RepoFactory(self.repo_store)
diff --git a/turnip/api/tests/test_store.py b/turnip/api/tests/test_store.py
index 47a97da..c929012 100644
--- a/turnip/api/tests/test_store.py
+++ b/turnip/api/tests/test_store.py
@@ -8,6 +8,8 @@ import uuid
import pygit2
import yaml
+
+from pygit2 import AlreadyExistsError, GitError, InvalidSpecError
from fixtures import EnvironmentVariable, MonkeyPatch, TempDir
from testtools import TestCase
@@ -391,6 +393,145 @@ class InitTestCase(TestCase):
packed_refs, os.path.join(too_path, "turnip-subordinate")
)
+ def test_write_lightweight_tag(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+ commit_sha1 = commit_oid.hex
+ tag_name = "1701"
+
+ store.write_lightweight_tag(
+ self.repo_store, repo_path, tag_name, commit_sha1
+ )
+ self.assertAdvertisedRefs(
+ [("refs/tags/1701", commit_sha1)], [], repo_path
+ )
+
+ def test_create_branch(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+ commit_sha1 = commit_oid.hex
+ branch_name = "new-feature"
+
+ store.create_branch(
+ self.repo_store, repo_path, branch_name, commit_sha1
+ )
+ self.assertAdvertisedRefs(
+ [("refs/heads/new-feature", commit_sha1)], [], repo_path
+ )
+
+ def test_force_overwrite_tag(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+ first_commit_oid = factory.add_commit("foo", "foobar.txt")
+ first_commit_sha1 = first_commit_oid.hex
+ tag_name = "1701"
+
+ store.write_lightweight_tag(
+ self.repo_store, repo_path, tag_name, first_commit_sha1
+ )
+ self.assertAdvertisedRefs(
+ [("refs/tags/1701", first_commit_sha1)], [], repo_path
+ )
+
+ second_commit_oid = factory.add_commit("bar", "barbaz.txt")
+ second_commit_sha1 = second_commit_oid.hex
+ store.write_lightweight_tag(
+ self.repo_store, repo_path, tag_name, second_commit_sha1, True
+ )
+ self.assertAdvertisedRefs(
+ [("refs/tags/1701", second_commit_sha1)], # in refs
+ [("refs/tags/1701", first_commit_sha1)], # not in refs
+ repo_path,
+ )
+
+ def test_force_overwrite_branch(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+ first_commit_oid = factory.add_commit("foo", "foobar.txt")
+ first_commit_sha1 = first_commit_oid.hex
+ branch_name = "new-feature"
+
+ store.create_branch(
+ self.repo_store, repo_path, branch_name, first_commit_sha1
+ )
+ self.assertAdvertisedRefs(
+ [("refs/heads/new-feature", first_commit_sha1)],
+ [],
+ repo_path,
+ )
+
+ second_commit_oid = factory.add_commit("bar", "barbaz.txt")
+ second_commit_sha1 = second_commit_oid.hex
+ store.create_branch(
+ self.repo_store, repo_path, branch_name, second_commit_sha1, True
+ )
+ self.assertAdvertisedRefs(
+ [("refs/heads/new-feature", second_commit_sha1)], # in refs
+ [("refs/heads/new-feature", first_commit_sha1)], # not in refs
+ repo_path,
+ )
+
+ def test_write_invalid_ref_name(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+ commit_sha1 = commit_oid.hex
+ invalid_ref_name = "?*"
+
+ self.assertRaises(
+ InvalidSpecError,
+ store.write_lightweight_tag,
+ *[self.repo_store, repo_path, invalid_ref_name, commit_sha1],
+ )
+
+ self.assertRaises(
+ InvalidSpecError,
+ store.create_branch,
+ *[self.repo_store, repo_path, invalid_ref_name, commit_sha1],
+ )
+
+ def test_ref_already_exists(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+ commit_oid = factory.add_commit("foo", "foobar.txt")
+ factory.add_tag("1701", "", commit_oid)
+ factory.add_branch("new-feature", commit_oid)
+ commit_sha1 = commit_oid.hex
+
+ self.assertRaises(
+ AlreadyExistsError,
+ store.write_lightweight_tag,
+ *[self.repo_store, repo_path, "1701", commit_sha1],
+ )
+
+ self.assertRaises(
+ AlreadyExistsError,
+ store.create_branch,
+ *[self.repo_store, repo_path, "branch-new-feature", commit_sha1],
+ )
+
+ def test_nonexistent_commit(self):
+ repo_path = os.path.join(self.repo_store, uuid.uuid1().hex)
+ factory = RepoFactory(repo_path)
+
+ self.assertRaises(
+ GitError,
+ store.write_lightweight_tag,
+ *[self.repo_store, repo_path, "1701", factory.nonexistent_oid()],
+ )
+ self.assertRaises(
+ GitError,
+ store.create_branch,
+ *[
+ self.repo_store,
+ repo_path,
+ "new-feature",
+ factory.nonexistent_oid(),
+ ],
+ )
+
def test_fetch_refs(self):
celery_fixture = CeleryWorkerFixture()
self.useFixture(celery_fixture)
diff --git a/turnip/api/views.py b/turnip/api/views.py
index 5014634..4fda65a 100644
--- a/turnip/api/views.py
+++ b/turnip/api/views.py
@@ -7,7 +7,7 @@ import re
import pyramid.httpexceptions as exc
from cornice.resource import resource
from cornice.validators import extract_cstruct
-from pygit2 import GitError
+from pygit2 import AlreadyExistsError, GitError, InvalidSpecError
from pyramid.response import Response
from turnip.api import store
@@ -265,6 +265,75 @@ class RefAPI(BaseAPI):
return exc.HTTPNotFound() # 404
@validate_path
+ def collection_post(self, repo_store, repo_name):
+ """Create a lightweight tag or a branch against a commit."""
+ json_data = extract_cstruct(self.request)["body"]
+ commit_sha1 = json_data.get("commit_sha1")
+ ref_type = json_data.get("ref_type")
+ ref_name = json_data.get("ref_name")
+ force = json_data.get("force", False)
+
+ if not commit_sha1:
+ self.request.errors.add(
+ "body", "commit_sha1", "commit_sha1 for ref target is missing"
+ )
+ return
+ if not ref_type or ref_type not in {"lightweight_tag", "branch"}:
+ self.request.errors.add(
+ "body",
+ "ref_type",
+ "must specify ref type (lightweight_tag or branch)",
+ )
+ return
+ if not ref_name:
+ self.request.errors.add(
+ "body", "ref_name", "must specify ref name"
+ )
+ return
+ # Do not accept truthy values (e.g. {"force": "no"}) to avoid confusion
+ if force and not isinstance(force, bool):
+ self.request.errors.add(
+ "body", "force", "'force' must be a json boolean"
+ )
+ return
+
+ try:
+ if ref_type == "lightweight_tag":
+ store.write_lightweight_tag(
+ repo_store, repo_name, ref_name, commit_sha1, force
+ )
+ elif ref_type == "branch":
+ store.create_branch(
+ repo_store, repo_name, ref_name, commit_sha1, force
+ )
+ except AlreadyExistsError:
+ self.request.errors.add(
+ "body",
+ "ref_name",
+ "tag or branch name already exists",
+ )
+ self.request.errors.status = 409
+ return
+ except InvalidSpecError:
+ self.request.errors.add(
+ "body",
+ "ref_name",
+ "ensure tag or branch name is valid per git-check-ref-format",
+ )
+ return
+ except GitError:
+ # This diverges from the generic Resource Not Found for get_ref,
+ # but as we are dealing with multiple object types here it would
+ # be helpful to raise a more specific error.
+ self.request.errors.add(
+ "body", "commit_sha1", "no commit found for commit_sha1 given"
+ )
+ self.request.errors.status = 404
+ return
+ else:
+ return Response(status=201)
+
+ @validate_path
def get(self, repo_store, repo_name):
ref = "refs/" + self.request.matchdict["ref"]
try:
Follow ups