From e7286c64604b038d4ea690ecbeeea1e6f5844e49 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jul 29 2020 11:59:59 +0000 Subject: Add API endpoint to set the default git branch and expose it in an existing endpoint This commit adds a "default" field to api/0///git/branches which returns the name of the default branch and potentially its HEAD commit hash. It also adds a new API endpoint allowing to change this default branch to any other existing branch. Signed-off-by: Pierre-Yves Chibon --- diff --git a/pagure/api/project.py b/pagure/api/project.py index 8ad432e..5788fd1 100644 --- a/pagure/api/project.py +++ b/pagure/api/project.py @@ -660,13 +660,17 @@ def api_git_branches(repo, username=None, namespace=None): { "total_branches": 2, - "branches": ["master", "dev"] + "branches": ["main", "dev"] + "default": "main" } { "total_branches": 2, + "default": { + "main": "16ae2a4df107658b52750063ae203f978cf02ff7", + } "branches": { - "master": "16ae2a4df107658b52750063ae203f978cf02ff7", + "main": "16ae2a4df107658b52750063ae203f978cf02ff7", "dev": "8351c460167a41defc393f5b6c1d51fe1b3b82b8" } } @@ -680,10 +684,105 @@ def api_git_branches(repo, username=None, namespace=None): repo = _get_repo(repo, username, namespace) branches = pagure.lib.git.get_git_branches(repo, with_commits=with_commits) + default_name = default_commit = None + try: + default_name, default_commit = pagure.lib.git.get_default_git_branches( + repo + ) + except pygit2.GitError: + pass - return flask.jsonify( - {"total_branches": len(branches), "branches": branches} - ) + output = { + "total_branches": len(branches), + "branches": branches, + "default": {}, + } + if with_commits: + if default_name: + output["default"] = {default_name: default_commit} + else: + output["default"] = default_name + + return flask.jsonify(output) + + +@API.route("//git/branches", methods=["POST"]) +@API.route("///git/branches", methods=["POST"]) +@API.route("/fork///git/branches", methods=["POST"]) +@API.route( + "/fork////git/branches", methods=["POST"] +) +@api_login_required(acls=["modify_project"]) +@api_method +def api_set_git_default_branch(repo, username=None, namespace=None): + """ + Set the default git branch + -------------------------- + Set the default git branch of the git repository + + :: + + POST /api/0//git/branches + POST /api/0///git/branches + + :: + + POST /api/0/fork///git/branches + POST /api/0/fork////git/branches + + Parameters + ^^^^^^^^^^ + + +-----------------+----------+---------------+--------------------------+ + | Key | Type | Optionality | Description | + +=================+==========+===============+==========================+ + | ``branch_name`` | string | Mandatory | | Name of the git branch | + | | | | to be made the default | + | | | | branch of the git repo | + +-----------------+----------+---------------+--------------------------+ + | ``with_commits``| boolean | Optional | | Include the commit hash| + | | | | corresponding to the | + | | | | HEAD of each branch | + +-----------------+----------+---------------+--------------------------+ + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "total_branches": 2, + "branches": ["main", "dev"] + "default": "main" + } + + { + "total_branches": 2, + "default": { + "main": "16ae2a4df107658b52750063ae203f978cf02ff7", + } + "branches": { + "main": "16ae2a4df107658b52750063ae203f978cf02ff7", + "dev": "8351c460167a41defc393f5b6c1d51fe1b3b82b8" + } + } + + """ + + branch_name = flask.request.values.get("branch_name") + + repo = _get_repo(repo, username, namespace) + _check_token(repo, project_token=False) + + try: + pagure.lib.git.git_set_ref_head(project=repo, branch=branch_name) + except Exception as err: + _log.exception(err) + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EGITERROR, error=str(err) + ) + + return api_git_branches(repo.name, username=username, namespace=namespace) @API.route("//tree") diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 53ef0b6..9a483a4 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -2608,6 +2608,19 @@ def get_git_branches(project, with_commits=False): return branches +def get_default_git_branches(project): + """ Return a tuple of the default branchname and its head commit hash + :arg project: The Project instance to get the branches for + """ + repo_path = pagure.utils.get_repo_path(project) + repo_obj = PagureRepo(repo_path) + branchname = repo_obj.head.shorthand + branch = repo_obj.lookup_branch(branchname) + commit = branch.peel(pygit2.Commit) + + return branchname, commit.oid.hex + + def new_git_branch( username, project, branch, from_branch=None, from_commit=None ): diff --git a/tests/test_pagure_flask_api_project.py b/tests/test_pagure_flask_api_project.py index 3cf2d01..0921212 100644 --- a/tests/test_pagure_flask_api_project.py +++ b/tests/test_pagure_flask_api_project.py @@ -117,93 +117,6 @@ class PagureFlaskApiProjecttests(tests.Modeltests): shutil.rmtree(newpath) - def test_api_git_branches(self): - """ Test the api_git_branches method of the flask api. """ - # Create a git repo to add branches to - tests.create_projects(self.session) - repo_path = os.path.join(self.path, "repos", "test.git") - tests.add_content_git_repo(repo_path) - new_repo_path = tempfile.mkdtemp(prefix="pagure-api-git-branches-test") - clone_repo = pygit2.clone_repository(repo_path, new_repo_path) - - # Create two other branches based on master - for branch in ["pats-win-49", "pats-win-51"]: - clone_repo.create_branch(branch, clone_repo.head.peel()) - refname = "refs/heads/{0}:refs/heads/{0}".format(branch) - PagureRepo.push(clone_repo.remotes[0], refname) - - # Check that the branches show up on the API - output = self.app.get("/api/0/test/git/branches") - # Delete the cloned git repo after the API call - shutil.rmtree(new_repo_path) - - # Verify the API data - self.assertEqual(output.status_code, 200) - data = json.loads(output.get_data(as_text=True)) - self.assertDictEqual( - data, - { - "branches": ["master", "pats-win-49", "pats-win-51"], - "total_branches": 3, - }, - ) - - def test_api_git_branches_with_commits(self): - """ Test the api_git_branches method of the flask api with with_commits=True. """ - # Create a git repo to add branches to - tests.create_projects(self.session) - repo_path = os.path.join(self.path, "repos", "test.git") - tests.add_content_git_repo(repo_path) - new_repo_path = tempfile.mkdtemp(prefix="pagure-api-git-branches-test") - clone_repo = pygit2.clone_repository(repo_path, new_repo_path) - - # Create two other branches based on master - for branch in ["pats-win-49", "pats-win-51"]: - clone_repo.create_branch(branch, clone_repo.head.peel()) - refname = "refs/heads/{0}:refs/heads/{0}".format(branch) - PagureRepo.push(clone_repo.remotes[0], refname) - - # Check that the branches show up on the API - output = self.app.get("/api/0/test/git/branches?with_commits=true") - # Delete the cloned git repo after the API call - shutil.rmtree(new_repo_path) - - # Get the commit hex - repo_obj = pygit2.Repository( - os.path.join(self.path, "repos", "test.git") - ) - commit = repo_obj[repo_obj.head.target] - - # Verify the API data - self.assertEqual(output.status_code, 200) - data = json.loads(output.get_data(as_text=True)) - self.assertDictEqual( - data, - { - "branches": { - "master": commit.hex, - "pats-win-49": commit.hex, - "pats-win-51": commit.hex, - }, - "total_branches": 3, - }, - ) - - def test_api_git_branches_empty_repo(self): - """ Test the api_git_branches method of the flask api when the repo is - empty. - """ - # Create a git repo without any branches - tests.create_projects(self.session) - repo_base_path = os.path.join(self.path, "repos") - tests.create_projects_git(repo_base_path) - - # Check that no branches show up on the API - output = self.app.get("/api/0/test/git/branches") - self.assertEqual(output.status_code, 200) - data = json.loads(output.get_data(as_text=True)) - self.assertDictEqual(data, {"branches": [], "total_branches": 0}) - def test_api_git_branches_no_repo(self): """ Test the api_git_branches method of the flask api when there is no repo on a project. @@ -4573,5 +4486,210 @@ class PagureFlaskApiProjectCommitInfotests(tests.Modeltests): self.assertEqual(pagure.api.APIERROR.ENOCOMMIT.value, data["error"]) +class PagureFlaskApiProjectGitBranchestests(tests.Modeltests): + """ Tests for the flask API of pagure for git branches + """ + + maxDiff = None + + def setUp(self): + """ Set up the environnment, ran before every tests. """ + super(PagureFlaskApiProjectGitBranchestests, self).setUp() + + tests.create_projects(self.session) + repo_path = os.path.join(self.path, "repos") + self.git_path = os.path.join(repo_path, "test.git") + tests.create_projects_git(repo_path, bare=True) + tests.add_content_git_repo(self.git_path) + + tests.create_tokens(self.session, project_id=None) + # Set a default ACL to avoid get all rights set on + tests.create_tokens_acl(self.session, "foo_token", "modify_project") + tests.create_tokens_acl(self.session, "aaabbbcccddd", "create_branch") + + # Add a couple of branches to the test project + repo_obj = pygit2.Repository(self.git_path) + self.commit = repo_obj.revparse_single("HEAD") + + new_repo_path = os.path.join(self.path, "lcl_forks") + clone_repo = pygit2.clone_repository(self.git_path, new_repo_path) + + # Create two other branches based on master + for branch in ["pats-win-49", "pats-win-51"]: + clone_repo.create_branch(branch, clone_repo.head.peel()) + refname = "refs/heads/{0}:refs/heads/{0}".format(branch) + PagureRepo.push(clone_repo.remotes[0], refname) + + def test_api_git_branches(self): + """ Test the api_git_branches method of the flask api. """ + # Check that the branches show up on the API + output = self.app.get("/api/0/test/git/branches") + + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "branches": ["master", "pats-win-49", "pats-win-51"], + "default": "master", + "total_branches": 3, + }, + ) + + def test_api_git_branches_with_commits(self): + """ Test the api_git_branches method of the flask api with with_commits=True. """ + # Check that the branches show up on the API + output = self.app.get("/api/0/test/git/branches?with_commits=true") + + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "branches": { + "master": self.commit.hex, + "pats-win-49": self.commit.hex, + "pats-win-51": self.commit.hex, + }, + "default": {"master": self.commit.hex,}, + "total_branches": 3, + }, + ) + + def test_api_git_branches_empty_repo(self): + """ Test the api_git_branches method of the flask api when the repo is + empty. + """ + # Check that no branches show up on the API + output = self.app.get("/api/0/test2/git/branches") + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, {"branches": [], "default": None, "total_branches": 0} + ) + + def test_api_set_git_default_branch(self): + """ Test the api_git_branches method of the flask api. """ + headers = {"Authorization": "token foo_token"} + data = {"branch_name": "pats-win-49"} + output = self.app.post( + "/api/0/test/git/branches", data=data, headers=headers + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "branches": ["master", "pats-win-49", "pats-win-51"], + "default": "pats-win-49", + "total_branches": 3, + }, + ) + + def test_api_set_git_default_branch_with_commits_form(self): + """ Test the api_git_branches method of the flask api with with_commits=True. """ + headers = {"Authorization": "token foo_token"} + data = {"branch_name": "pats-win-49", "with_commits": True} + output = self.app.post( + "/api/0/test/git/branches", data=data, headers=headers + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "branches": { + "master": self.commit.hex, + "pats-win-49": self.commit.hex, + "pats-win-51": self.commit.hex, + }, + "default": {"pats-win-49": self.commit.hex,}, + "total_branches": 3, + }, + ) + + def test_api_set_git_default_branch_with_commits_url(self): + """ Test the api_git_branches method of the flask api with with_commits=True. """ + headers = {"Authorization": "token foo_token"} + data = {"branch_name": "pats-win-49"} + output = self.app.post( + "/api/0/test/git/branches?with_commits=1", + data=data, + headers=headers, + ) + self.assertEqual(output.status_code, 200) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "branches": { + "master": self.commit.hex, + "pats-win-49": self.commit.hex, + "pats-win-51": self.commit.hex, + }, + "default": {"pats-win-49": self.commit.hex,}, + "total_branches": 3, + }, + ) + + def test_api_set_git_default_branch_invalid_branch(self): + """ Test the api_git_branches method of the flask api with with_commits=True. """ + headers = {"Authorization": "token foo_token"} + data = {"branch_name": "main"} + output = self.app.post( + "/api/0/test/git/branches?with_commits=1", + data=data, + headers=headers, + ) + self.assertEqual(output.status_code, 400) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "error": "An error occurred during a git operation", + "error_code": "EGITERROR", + }, + ) + + def test_api_set_git_default_branch_invalid_token(self): + """ Test the api_git_branches method of the flask api with with_commits=True. """ + headers = {"Authorization": "token aaabbbcccddd"} + data = {"branch_name": "main"} + output = self.app.post( + "/api/0/test/git/branches", data=data, headers=headers, + ) + self.assertEqual(output.status_code, 401) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "error": "Invalid or expired token. Please visit " + "http://localhost.localdomain/settings#nav-api-tab to get or renew " + "your API token.", + "error_code": "EINVALIDTOK", + "errors": "Missing ACLs: modify_project", + }, + ) + + def test_api_set_git_default_branch_empty_repo(self): + """ Test the api_git_branches method of the flask api when the repo is + empty. + """ + headers = {"Authorization": "token foo_token"} + data = {"branch_name": "main"} + output = self.app.post( + "/api/0/test2/git/branches", data=data, headers=headers + ) + self.assertEqual(output.status_code, 400) + data = json.loads(output.get_data(as_text=True)) + self.assertDictEqual( + data, + { + "error": "An error occurred during a git operation", + "error_code": "EGITERROR", + }, + ) + + if __name__ == "__main__": unittest.main(verbosity=2)