-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add: New python module for git usage
Add a new module and class for providing an API for git.
- Loading branch information
1 parent
1bf12dc
commit ef58adb
Showing
4 changed files
with
383 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Copyright (C) 2022 Greenbone Networks GmbH | ||
# | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
from .git import Git, GitError |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
# Copyright (C) 2022 Greenbone Networks GmbH | ||
# | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import subprocess | ||
|
||
from pathlib import Path | ||
from typing import Optional | ||
|
||
|
||
class GitError(subprocess.CalledProcessError): | ||
""" | ||
Error raised while executing a git command | ||
""" | ||
|
||
def __str__(self) -> str: | ||
cmd = " ".join(self.cmd) | ||
return ( | ||
f"Git command '{cmd}' returned " | ||
f"non-zero exit status {str(self.returncode)}" | ||
) | ||
|
||
|
||
def _exec_git( | ||
*args: str, ignore_errors: Optional[bool] = False, cwd: Optional[str] = None | ||
) -> str: | ||
""" | ||
Internal module function to abstract calling git via subprocess | ||
""" | ||
try: | ||
cmd_args = ["git"] | ||
cmd_args.extend(args) | ||
output = subprocess.check_output(cmd_args, cwd=cwd) | ||
return output.decode() | ||
except subprocess.CalledProcessError as e: | ||
if ignore_errors: | ||
return "" | ||
raise GitError(e.returncode, e.cmd, e.output, e.stderr) from None | ||
|
||
|
||
class Git: | ||
""" | ||
Run git commands as subprocesses | ||
""" | ||
|
||
def __init__(self, cwd: Optional[Path] = None) -> None: | ||
""" | ||
Create a new Git instance | ||
Args: | ||
cwd: Set the current working directory for the git commands | ||
""" | ||
self._cwd = cwd.absolute() if cwd else None | ||
|
||
@property | ||
def cwd(self) -> Path: | ||
""" | ||
Get the current working directory as Path | ||
""" | ||
return self._cwd | ||
|
||
@cwd.setter | ||
def cwd(self, cwd: Path): | ||
""" | ||
Set the current working directory for all following git commands | ||
""" | ||
self._cwd = cwd.absolute() | ||
|
||
def init(self, *, bare: Optional[bool] = False): | ||
""" | ||
Init a git repository | ||
Args: | ||
bare: Wether to create a `bare` repository or not. | ||
Defaults to false. | ||
""" | ||
args = ["init"] | ||
if bare: | ||
args.append("--bare") | ||
_exec_git(*args, cwd=self._cwd) | ||
|
||
def create_branch(self, branch: str, *, start_point: Optional[str] = None): | ||
""" | ||
Create a new branch | ||
Args: | ||
branch: Name of the branch to be created | ||
start_point: An optional git reference (branch, tag, sha, ...) from | ||
where to start the branch | ||
""" | ||
args = ["checkout", "-b", branch] | ||
if start_point: | ||
args.append(start_point) | ||
|
||
_exec_git(*args, cwd=self._cwd) | ||
|
||
def rebase( | ||
self, | ||
base: str, | ||
*, | ||
head: Optional[str] = None, | ||
onto: Optional[str] = None, | ||
): | ||
""" | ||
Rebase a branch | ||
Args: | ||
base: Apply changes of this branch | ||
head: Apply changes on this branch. If not set the current branch is | ||
used. | ||
onto: Apply changes on top of this branch | ||
""" | ||
args = ["rebase"] | ||
|
||
if onto: | ||
args.extend(["--onto", onto]) | ||
|
||
args.append(base) | ||
|
||
if head: | ||
args.append(head) | ||
|
||
_exec_git(*args, cwd=self._cwd) | ||
|
||
def clone( | ||
self, | ||
repo_url: str, | ||
destination: Path, | ||
*, | ||
branch: Optional[str] = None, | ||
remote: Optional[str] = None, | ||
): | ||
""" | ||
Clone a repository | ||
Args: | ||
repo_url: URL of the repo to clone | ||
destination: Where to checkout the clone | ||
branch: Branch to checkout. By default the default branch is used. | ||
remote: Store repo url under this remote name | ||
""" | ||
args = ["clone"] | ||
if remote: | ||
args.extend(["-o", remote]) | ||
if branch: | ||
args.extend(["-b", branch]) | ||
args.extend([repo_url, str(destination.absolute())]) | ||
|
||
_exec_git( | ||
*args, | ||
cwd=self._cwd, | ||
) | ||
|
||
def push( | ||
self, *, remote: Optional[str] = None, branch: Optional[str] = None | ||
): | ||
""" | ||
Push changes to remote repository | ||
Args: | ||
remote: Push changes to the named remote | ||
branch: Branch to push. Will only be considered in combination with | ||
a remote. | ||
""" | ||
args = ["push"] | ||
if remote: | ||
args.append(remote) | ||
if branch: | ||
args.append(branch) | ||
|
||
_exec_git(*args, cwd=self._cwd) | ||
|
||
def config(self, key: str, value: str): | ||
""" | ||
Set a (local) git config | ||
""" | ||
_exec_git("config", key, value, cwd=self._cwd) | ||
|
||
def cherry_pick(self, commit: str): | ||
""" | ||
Apply change of a commit to the current branch | ||
Args: | ||
commit: Git reference (e.g. sha) of the commit | ||
""" | ||
_exec_git("cherry-pick", commit, cwd=self._cwd) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Copyright (C) 2022 Greenbone Networks GmbH | ||
# | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# Copyright (C) 2022 Greenbone Networks GmbH | ||
# | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import unittest | ||
|
||
from pathlib import Path | ||
from unittest.mock import patch | ||
|
||
from pontos.git import Git | ||
|
||
|
||
class GitTestCase(unittest.TestCase): | ||
@patch("pontos.git.git._exec_git") | ||
def test_clone(self, exec_git_mock): | ||
git = Git() | ||
git.clone("http://foo/foo.git", Path("/bar")) | ||
|
||
exec_git_mock.assert_called_once_with( | ||
"clone", "http://foo/foo.git", "/bar", cwd=None | ||
) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_clone_with_remote(self, exec_git_mock): | ||
git = Git() | ||
git.clone("http://foo/foo.git", Path("/bar"), remote="foo") | ||
|
||
exec_git_mock.assert_called_once_with( | ||
"clone", "-o", "foo", "http://foo/foo.git", "/bar", cwd=None | ||
) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_clone_with_branch(self, exec_git_mock): | ||
git = Git() | ||
git.clone("http://foo/foo.git", Path("/bar"), branch="foo") | ||
|
||
exec_git_mock.assert_called_once_with( | ||
"clone", "-b", "foo", "http://foo/foo.git", "/bar", cwd=None | ||
) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_init(self, exec_git_mock): | ||
git = Git() | ||
git.init() | ||
|
||
exec_git_mock.assert_called_once_with("init", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_init_bare(self, exec_git_mock): | ||
git = Git() | ||
git.init(bare=True) | ||
|
||
exec_git_mock.assert_called_once_with("init", "--bare", cwd=None) | ||
|
||
def test_cwd(self): | ||
git = Git() | ||
|
||
self.assertIsNone(git.cwd) | ||
|
||
new_cwd = Path("foo") | ||
git.cwd = new_cwd | ||
|
||
self.assertEqual(git.cwd, new_cwd.absolute()) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_create_branch(self, exec_git_mock): | ||
git = Git() | ||
git.create_branch("foo") | ||
|
||
exec_git_mock.assert_called_once_with("checkout", "-b", "foo", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_create_branch_with_starting_point(self, exec_git_mock): | ||
git = Git() | ||
git.create_branch("foo", start_point="bar") | ||
|
||
exec_git_mock.assert_called_once_with( | ||
"checkout", "-b", "foo", "bar", cwd=None | ||
) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_rebase(self, exec_git_mock): | ||
git = Git() | ||
git.rebase("foo") | ||
|
||
exec_git_mock.assert_called_once_with("rebase", "foo", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_rebase_with_head(self, exec_git_mock): | ||
git = Git() | ||
git.rebase("foo", head="bar") | ||
|
||
exec_git_mock.assert_called_once_with("rebase", "foo", "bar", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_rebase_with_head_and_onto(self, exec_git_mock): | ||
git = Git() | ||
git.rebase("foo", head="bar", onto="staging") | ||
|
||
exec_git_mock.assert_called_once_with( | ||
"rebase", "--onto", "staging", "foo", "bar", cwd=None | ||
) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_push(self, exec_git_mock): | ||
git = Git() | ||
git.push() | ||
|
||
exec_git_mock.assert_called_once_with("push", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_push_with_remote(self, exec_git_mock): | ||
git = Git() | ||
git.push(remote="foo") | ||
|
||
exec_git_mock.assert_called_once_with("push", "foo", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_push_with_remote_and_branch(self, exec_git_mock): | ||
git = Git() | ||
git.push(remote="foo", branch="bar") | ||
|
||
exec_git_mock.assert_called_once_with("push", "foo", "bar", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_config(self, exec_git_mock): | ||
git = Git() | ||
git.config("foo", "bar") | ||
|
||
exec_git_mock.assert_called_once_with("config", "foo", "bar", cwd=None) | ||
|
||
@patch("pontos.git.git._exec_git") | ||
def test_cherry_pick(self, exec_git_mock): | ||
git = Git() | ||
git.cherry_pick("foo") | ||
|
||
exec_git_mock.assert_called_once_with("cherry-pick", "foo", cwd=None) |