Skip to content

Commit

Permalink
Add: New python module for git usage
Browse files Browse the repository at this point in the history
Add a new module and class for providing an API for git.
  • Loading branch information
bjoernricks committed Jan 6, 2022
1 parent 1bf12dc commit ef58adb
Show file tree
Hide file tree
Showing 4 changed files with 383 additions and 0 deletions.
18 changes: 18 additions & 0 deletions pontos/git/__init__.py
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
199 changes: 199 additions & 0 deletions pontos/git/git.py
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)
16 changes: 16 additions & 0 deletions tests/git/__init__.py
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/>.
150 changes: 150 additions & 0 deletions tests/git/test_git.py
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)

0 comments on commit ef58adb

Please sign in to comment.