#4757 Introduce a new API endpoint to add git tags to a project remotely
Merged 4 years ago by pingou. Opened 4 years ago by pingou.

file modified
+101
@@ -344,6 +344,107 @@ 

      return jsonout

  

  

+ @API.route("/<repo>/git/tags", methods=["POST"])

+ @API.route("/<namespace>/<repo>/git/tags", methods=["POST"])

+ @API.route("/fork/<username>/<repo>/git/tags", methods=["POST"])

+ @API.route("/fork/<username>/<namespace>/<repo>/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/<repo>/git/tags

+         POST /api/0/<namespace>/<repo>/git/tags

+ 

+     ::

+ 

+         POST /api/0/fork/<username>/<repo>/git/tags

+         POST /api/0/fork/<username>/<namespace>/<repo>/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("/<repo>/watchers")

  @API.route("/<namespace>/<repo>/watchers")

  @API.route("/fork/<username>/<repo>/watchers")

@@ -353,6 +353,7 @@ 

      ),

      "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 @@ 

      "generate_acls_project",

      "commit_flag",

      "create_branch",

+     "tag_project",

  ]

  

  # List of the type of CI service supported by this pagure instance

file modified
+15
@@ -931,3 +931,18 @@ 

      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<span class="error">*</span>',

+         [wtforms.validators.DataRequired()],

+     )

+     commit_hash = wtforms.StringField(

+         "Hash of the commit to tag", [wtforms.validators.DataRequired()]

+     )

+     message = wtforms.TextAreaField(

+         "Annotation message", [wtforms.validators.Optional()]

+     )

file modified
+30
@@ -2352,6 +2352,36 @@ 

      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.

file modified
+13 -4
@@ -688,26 +688,35 @@ 

      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),

@@ -0,0 +1,245 @@ 

+ # -*- coding: utf-8 -*-

+ 

+ """

+  Authors:

+   Pierre-Yves Chibon <pingou@pingoured.fr>

+ """

+ 

+ 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)

@@ -5650,6 +5650,7 @@ 

                  "pull_request_rebase",

                  "pull_request_subscribe",

                  "pull_request_update",

+                 "tag_project",

                  "update_watch_status",

              ],

          )

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 pingou@pingoured.fr

@pingou typo in commit message: remotly -> remotely

rebased onto 270587bfe799a77b6e3bd0828036fb2b5e96aa0d

4 years ago

rebased onto 270587bfe799a77b6e3bd0828036fb2b5e96aa0d

4 years ago

@pingou typo in commit message: remotly -> remotely

Fixed, thanks

@pingou should we be concerned that this is a fully synchronous API call, as opposed to something that is handled in a celery queue?

(This is also partly a broader question about a lot of our APIs...)

@pingou should we be concerned that this is a fully synchronous API call, as opposed to something that is handled in a celery queue?

I'm not seeing anything that would concern me, what do you have in mind?

you have my :thumbsup: if jenkins is happy :)

rebased onto 8938e6013e328a1f99fa13525e97ee445fe702cd

4 years ago

rebased onto 5b64ba4

4 years ago

@pingou My general concern is that I don't want the API being hammered by automation in such a way that the frontend gets knocked out regularly during high numbers of builds in parallel.

Your fear DDoS by API calls, this is plausible and would affect every endpoints, not just this one indeed.

I don't really like the idea of moving this to be async, or we make it optional maybe?

@pingou I think we need a way to optionally make API calls handled async, as unpleasant as it sounds. But this PR in itself is fine as-is. The work to do that can be done separately.

Thanks for the review folks

@ngompa I agree, we may want to consider having the API be async on demand, but we can do this in a separate PR.

Pull-Request has been merged by pingou

4 years ago