From 5b64ba453e8c8c8e68dc049126fe805b328bf5b5 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Mar 04 2020 17:05:17 +0000 Subject: Introduce a new API endpoint to add git tags to a project remotely This allows third party application (or contributors) to add git tags to a project without having direct commit to said project. Signed-off-by: Pierre-Yves Chibon --- diff --git a/pagure/api/project.py b/pagure/api/project.py index 2a5dd8d..7312b7a 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -344,6 +344,107 @@ def api_git_tags(repo, username=None, namespace=None): return jsonout +@API.route("//git/tags", methods=["POST"]) +@API.route("///git/tags", methods=["POST"]) +@API.route("/fork///git/tags", methods=["POST"]) +@API.route("/fork////git/tags", methods=["POST"]) +@api_login_required(acls=["modify_project", "tag_project"]) +@api_method +def api_new_git_tags(repo, username=None, namespace=None): + """ + Create new git tags + ------------------- + Create a new tag on the project Git repository. + + :: + + POST /api/0//git/tags + POST /api/0///git/tags + + :: + + POST /api/0/fork///git/tags + POST /api/0/fork////git/tags + + Parameters + ^^^^^^^^^^ + + +-----------------+----------+---------------+--------------------------+ + | Key | Type | Optionality | Description | + +=================+==========+===============+==========================+ + | ``tagname`` | string | Mandatory | | Name of the tag to | + | | | | create in the git repo | + +-----------------+----------+---------------+--------------------------+ + | ``commit_hash`` | string | Mandatory | | Hash of the commit/ | + | | | | reference to tag | + +-----------------+----------+---------------+--------------------------+ + | ``message`` | string | Optional | | Message to include in | + | | | | the annotation of the | + | | | | git tag | + +-----------------+----------+---------------+--------------------------+ + | ``with_commits``| string | Optional | | Include the commit hash| + | | | | corresponding to the | + | | | | tags found in the repo | + | | | | in the data returned | + +-----------------+----------+---------------+--------------------------+ + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "total_tags": 2, + "tags": ["0.0.1", "0.0.2"], + } + + + { + "total_tags": 2, + "tags": { + "0.0.1": "bb8fa2aa199da08d6085e1c9badc3d83d188d38c", + "0.0.2": "d16fe107eca31a1bdd66fb32c6a5c568e45b627e" + }, + } + + """ + repo = _get_repo(repo, username, namespace) + _check_token(repo, project_token=False) + + with_commits = pagure.utils.is_true( + flask.request.values.get("with_commits", False) + ) + + form = pagure.forms.AddGitTagForm(csrf_enabled=False) + if form.validate_on_submit(): + user_obj = pagure.lib.query.get_user( + flask.g.session, flask.g.fas_user.username + ) + try: + pagure.lib.git.new_git_tag( + project=repo, + tagname=form.tagname.data, + target=form.commit_hash.data, + user=user_obj, + message=form.message.data, + ) + except GitError as err: + _log.exception(err) + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EGITERROR, error=str(err) + ) + + else: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors + ) + + tags = pagure.lib.git.get_git_tags(repo, with_commits=with_commits) + + jsonout = flask.jsonify({"total_tags": len(tags), "tags": tags}) + return jsonout + + @API.route("//watchers") @API.route("///watchers") @API.route("/fork///watchers") diff --git a/pagure/default_config.py b/pagure/default_config.py index ca24744..282ff87 100644 --- a/pagure/default_config.py +++ b/pagure/default_config.py @@ -353,6 +353,7 @@ ACLS = { ), "update_watch_status": "Update the watch status on a project", "pull_request_rebase": "Rebase a pull-request", + "tag_project": "Allows adding git tags to a project", } # List of ACLs which a regular user is allowed to associate to an API token @@ -385,6 +386,7 @@ ADMIN_API_ACLS = [ "generate_acls_project", "commit_flag", "create_branch", + "tag_project", ] # List of the type of CI service supported by this pagure instance diff --git a/pagure/forms.py b/pagure/forms.py index ee6e01b..a41a278 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -931,3 +931,18 @@ class TriggerCIPRForm(PagureForm): comment = wtforms.SelectField( "comment", [wtforms.validators.Required()], choices=[] ) + + +class AddGitTagForm(PagureForm): + """ Form to create a new git tag. """ + + tagname = wtforms.StringField( + 'Name of the tag*', + [wtforms.validators.DataRequired()], + ) + commit_hash = wtforms.StringField( + "Hash of the commit to tag", [wtforms.validators.DataRequired()] + ) + message = wtforms.TextAreaField( + "Annotation message", [wtforms.validators.Optional()] + ) diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 0199dcf..4a5a400 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -2352,6 +2352,36 @@ def get_git_tags(project, with_commits=False): return tags +def new_git_tag(project, tagname, target, user, message=None): + """ Create a new git tag in the git repositorie of the specified project. + + :arg project: the project in which we want to create a git tag + :type project: pagure.lib.model.Project + :arg tagname: the name of the tag to create + :type tagname: str + :arg user: the user creating the tag + :type user: pagure.lib.model.User + :kwarg message: the message to include in the annotation of the tag + :type message: str or None + """ + repopath = pagure.utils.get_repo_path(project) + repo_obj = PagureRepo(repopath) + + target_obj = repo_obj.get(target) + if not target_obj: + raise pygit2.GitError("Unknown target: %s" % target) + + tag = repo_obj.create_tag( + tagname, + target, + target_obj.type, + pygit2.Signature(user.fullname, user.default_email), + message, + ) + + return tag + + def get_git_tags_objects(project): """ Returns the list of references of the tags created in the git repositorie the specified project. diff --git a/tests/__init__.py b/tests/__init__.py index e3fd799..165c37d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -688,26 +688,35 @@ def create_projects_git(folder, bare=False): return repos -def create_tokens(session, user_id=1, project_id=1): +def create_tokens(session, user_id=1, project_id=1, suffix=None): """ Create some tokens for the project in the database. """ + token = "aaabbbcccddd" + if suffix: + token += suffix item = pagure.lib.model.Token( - id="aaabbbcccddd", + id=token, user_id=user_id, project_id=project_id, expiration=datetime.utcnow() + timedelta(days=30), ) session.add(item) + token = "foo_token" + if suffix: + token += suffix item = pagure.lib.model.Token( - id="foo_token", + id=token, user_id=user_id, project_id=project_id, expiration=datetime.utcnow() + timedelta(days=30), ) session.add(item) + token = "expired_token" + if suffix: + token += suffix item = pagure.lib.model.Token( - id="expired_token", + id=token, user_id=user_id, project_id=project_id, expiration=datetime.utcnow() - timedelta(days=1), diff --git a/tests/test_pagure_flask_api_project_git_tags.py b/tests/test_pagure_flask_api_project_git_tags.py new file mode 100644 index 0000000..bfd90db --- /dev/null +++ b/tests/test_pagure_flask_api_project_git_tags.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +""" + Authors: + Pierre-Yves Chibon +""" + +from __future__ import unicode_literals, absolute_import + +import json +import sys +import os + +import pygit2 + +sys.path.insert( + 0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") +) + +import tests +import pagure.lib.query + + +class PagureFlaskApiProjectGitTagstests(tests.Modeltests): + """ Tests for the flask API of pagure for creating new git tags """ + + maxDiff = None + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskApiProjectGitTagstests, self).setUp() + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, "repos"), bare=True) + tests.add_commit_git_repo( + folder=os.path.join(self.path, "repos", "test.git") + ) + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + # token for user = pingou (user_id = 1) + self.headers = {"Authorization": "token aaabbbcccddd"} + + def test_api_new_git_tags_no_project(self): + """ Test the api_new_git_tags function. """ + output = self.app.post("/api/0/foo/git/tags", headers=self.headers) + self.assertEqual(output.status_code, 404) + expected_rv = { + "error": "Project not found", + "error_code": "ENOPROJECT", + } + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual(data, expected_rv) + + def test_api_new_git_tags_invalid_auth(self): + """ Test the api_new_git_tags function. """ + headers = self.headers + headers["Authorization"] += "foo" + output = self.app.post("/api/0/foo/git/tags", headers=headers) + self.assertEqual(output.status_code, 401) + expected_rv = { + "error": "Project not found", + "error_code": "EINVALIDTOK", + } + data = json.loads(output.get_data(as_text=True)) + self.assertEqual( + sorted(data.keys()), ["error", "error_code", "errors"] + ) + self.assertEqual( + pagure.api.APIERROR.EINVALIDTOK.name, data["error_code"] + ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data["error"]) + self.assertEqual("Invalid token", data["errors"]) + + def test_api_new_git_tag(self): + """ Test the api_new_git_tags function. """ + + # Before + output = self.app.get("/api/0/test/git/tags") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], []) + self.assertEqual(data["total_tags"], 0) + + # Add a tag so that we can list it + repo = pygit2.Repository(os.path.join(self.path, "repos", "test.git")) + latest_commit = repo.revparse_single("HEAD") + data = { + "tagname": "test-tag-no-message", + "commit_hash": latest_commit.oid.hex, + "message": None, + } + + output = self.app.post( + "/api/0/test/git/tags", headers=self.headers, data=data + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], ["test-tag-no-message"]) + self.assertEqual(data["total_tags"], 1) + + output = self.app.get("/api/0/test/git/tags?with_commits=t") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual( + data["tags"], {"test-tag-no-message": latest_commit.oid.hex} + ) + self.assertEqual(data["total_tags"], 1) + + def test_api_new_git_tag_with_commits(self): + """ Test the api_new_git_tags function. """ + + # Before + output = self.app.get("/api/0/test/git/tags") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], []) + self.assertEqual(data["total_tags"], 0) + + # Add a tag so that we can list it + repo = pygit2.Repository(os.path.join(self.path, "repos", "test.git")) + latest_commit = repo.revparse_single("HEAD") + data = { + "tagname": "test-tag-no-message", + "commit_hash": latest_commit.oid.hex, + "message": None, + "with_commits": True, + } + + output = self.app.post( + "/api/0/test/git/tags", headers=self.headers, data=data + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual( + data["tags"], {"test-tag-no-message": latest_commit.oid.hex} + ) + self.assertEqual(data["total_tags"], 1) + + def test_api_new_git_tag_with_message(self): + """ Test the api_new_git_tags function. """ + + # Before + output = self.app.get("/api/0/test/git/tags") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], []) + self.assertEqual(data["total_tags"], 0) + + # Add a tag so that we can list it + repo = pygit2.Repository(os.path.join(self.path, "repos", "test.git")) + latest_commit = repo.revparse_single("HEAD") + data = { + "tagname": "test-tag-no-message", + "commit_hash": latest_commit.oid.hex, + "message": "This is a long annotation\nover multiple lines\n for testing", + } + + output = self.app.post( + "/api/0/test/git/tags", headers=self.headers, data=data + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], ["test-tag-no-message"]) + self.assertEqual(data["total_tags"], 1) + + def test_api_new_git_tag_user_no_access(self): + """ Test the api_new_git_tags function. """ + + tests.create_tokens( + self.session, user_id=2, project_id=2, suffix="foo" + ) + tests.create_tokens_acl(self.session, token_id="aaabbbcccdddfoo") + # token for user = foo (user_id = 2) + headers = {"Authorization": "token aaabbbcccdddfoo"} + + # Before + output = self.app.get("/api/0/test/git/tags") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], []) + self.assertEqual(data["total_tags"], 0) + + # Add a tag so that we can list it + repo = pygit2.Repository(os.path.join(self.path, "repos", "test.git")) + latest_commit = repo.revparse_single("HEAD") + data = { + "tagname": "test-tag-no-message", + "commit_hash": latest_commit.oid.hex, + "message": "This is a long annotation\nover multiple lines\n for testing", + } + + output = self.app.post( + "/api/0/test/git/tags", headers=headers, data=data + ) + self.assertEqual(output.status_code, 401) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["error", "error_code"]) + self.assertEqual( + pagure.api.APIERROR.EINVALIDTOK.name, data["error_code"] + ) + self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data["error"]) + + def test_api_new_git_tag_user_global_token(self): + """ Test the api_new_git_tags function. """ + + tests.create_tokens( + self.session, user_id=2, project_id=None, suffix="foo" + ) + tests.create_tokens_acl(self.session, token_id="aaabbbcccdddfoo") + # token for user = foo (user_id = 2) + headers = {"Authorization": "token aaabbbcccdddfoo"} + + # Before + output = self.app.get("/api/0/test/git/tags") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], []) + self.assertEqual(data["total_tags"], 0) + + # Add a tag so that we can list it + repo = pygit2.Repository(os.path.join(self.path, "repos", "test.git")) + latest_commit = repo.revparse_single("HEAD") + data = { + "tagname": "test-tag-no-message", + "commit_hash": latest_commit.oid.hex, + "message": "This is a long annotation\nover multiple lines\n for testing", + } + + output = self.app.post( + "/api/0/test/git/tags", headers=headers, data=data + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertEqual(sorted(data.keys()), ["tags", "total_tags"]) + self.assertEqual(data["tags"], ["test-tag-no-message"]) + self.assertEqual(data["total_tags"], 1) diff --git a/tests/test_pagure_lib.py b/tests/test_pagure_lib.py index d1b8048..b575a6d 100644 --- a/tests/test_pagure_lib.py +++ b/tests/test_pagure_lib.py @@ -5650,6 +5650,7 @@ foo bar "pull_request_rebase", "pull_request_subscribe", "pull_request_update", + "tag_project", "update_watch_status", ], )