Skip to content

Commit

Permalink
Add: Extend Git class for deleting tags, reset and pushing refspecs
Browse files Browse the repository at this point in the history
Allow to delete tags, to push the deleted tags to the remote repo and to
reset a local history.
  • Loading branch information
bjoernricks committed Feb 6, 2024
1 parent 86bea78 commit f03cb9a
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 18 deletions.
2 changes: 2 additions & 0 deletions pontos/git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Git,
GitError,
MergeStrategy,
ResetMode,
TagSort,
)
from ._status import Status, StatusEntry
Expand All @@ -19,6 +20,7 @@
"Git",
"GitError",
"MergeStrategy",
"ResetMode",
"Status",
"StatusEntry",
"TagSort",
Expand Down
94 changes: 76 additions & 18 deletions pontos/git/_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@
#

import subprocess
from enum import Enum
from os import PathLike, fspath
from pathlib import Path
from typing import (
Collection,
Iterable,
Iterator,
List,
Optional,
Sequence,
Union,
)

from pontos.enum import StrEnum
from pontos.errors import PontosError

from ._status import StatusEntry, parse_git_status
Expand Down Expand Up @@ -85,7 +84,7 @@ def exec_git(
raise GitError(e.returncode, e.cmd, e.output, e.stderr) from None


class MergeStrategy(Enum):
class MergeStrategy(StrEnum):
"""
Possible strategies for a merge
Expand All @@ -107,7 +106,7 @@ class MergeStrategy(Enum):
SUBTREE = "subtree"


class ConfigScope(Enum):
class ConfigScope(StrEnum):
"""
Possible scopes for git settings
Expand All @@ -126,7 +125,7 @@ class ConfigScope(Enum):
WORKTREE = "worktree"


class TagSort(Enum):
class TagSort(StrEnum):
"""
Sorting for git tags
Expand All @@ -137,6 +136,14 @@ class TagSort(Enum):
VERSION = "version:refname"


class ResetMode(StrEnum):
SOFT = "soft"
MIXED = "mixed"
HARD = "hard"
MERGE = "merge"
KEEP = "keep"


class Git:
"""
Run git commands as subprocesses
Expand Down Expand Up @@ -231,7 +238,7 @@ def rebase(
if strategy == MergeStrategy.ORT_OURS:
args.extend(["--strategy", "ort", "-X", "ours"])
else:
args.extend(["--strategy", strategy.value])
args.extend(["--strategy", str(strategy)])

if onto:
args.extend(["--onto", onto])
Expand Down Expand Up @@ -274,32 +281,43 @@ def clone(

def push(
self,
refspec: Optional[Union[str, Iterable[str]]] = None,
*,
remote: Optional[str] = None,
branch: Optional[str] = None,
follow_tags: bool = False,
force: Optional[bool] = None,
delete: Optional[bool] = None,
) -> None:
"""
Push changes to remote repository
Args:
refspec: Refs to push
remote: Push changes to the named remote
branch: Branch to push. Will only be considered in combination with
a remote.
branch: Branch to push. Will only be considered in
combination with a remote. Deprecated, use refs instead.
follow_tags: Push all tags pointing to a commit included in the to
be pushed branch.
be pushed branch.
force: Force push changes.
delete: Delete remote refspec
"""
args = ["push"]
if follow_tags:
args.append("--follow-tags")
if force:
args.append("--force")
if delete:
args.append("--delete")
if remote:
args.append(remote)
if branch:
args.append(branch)
if refspec:
if isinstance(refspec, str):
args.append(refspec)
else:
args.extend(refspec)

self.exec(*args)

Expand All @@ -308,7 +326,7 @@ def config(
key: str,
value: Optional[str] = None,
*,
scope: Optional[ConfigScope] = None,
scope: Optional[Union[ConfigScope, str]] = None,
) -> str:
"""
Get and set a git config
Expand All @@ -320,7 +338,7 @@ def config(
"""
args = ["config"]
if scope:
args.append(f"--{scope.value}")
args.append(f"--{scope}")

args.append(key)

Expand All @@ -329,7 +347,7 @@ def config(

return self.exec(*args)

def cherry_pick(self, commits: Union[str, List[str]]) -> None:
def cherry_pick(self, commits: Union[str, list[str]]) -> None:
"""
Apply changes of a commit(s) to the current branch
Expand All @@ -348,10 +366,10 @@ def cherry_pick(self, commits: Union[str, List[str]]) -> None:
def list_tags(
self,
*,
sort: Optional[TagSort] = None,
sort: Optional[Union[TagSort, str]] = None,
tag_name: Optional[str] = None,
sort_suffix: Optional[List[str]] = None,
) -> List[str]:
sort_suffix: Optional[list[str]] = None,
) -> list[str]:
"""
List all available tags
Expand All @@ -370,7 +388,7 @@ def list_tags(
args.extend(["-c", f"versionsort.suffix={suffix}"])

args.extend(["tag", "-l"])
args.append(f"--sort={sort.value}")
args.append(f"--sort={sort}")
else:
args = ["tag", "-l"]

Expand Down Expand Up @@ -463,6 +481,19 @@ def tag(

self.exec(*args)

def delete_tag(
self,
tag: str,
) -> None:
"""
Delete a Tag
Args:
tag: Tag name to delete
"""
args = ["tag", "-d", tag]
self.exec(*args)

def fetch(
self,
remote: Optional[str] = None,
Expand Down Expand Up @@ -538,7 +569,7 @@ def log(
*log_args: str,
oneline: Optional[bool] = None,
format: Optional[str] = None,
) -> List[str]:
) -> list[str]:
"""
Get log of a git repository
Expand Down Expand Up @@ -614,7 +645,7 @@ def rev_list(
*commit: str,
max_parents: Optional[int] = None,
abbrev_commit: Optional[bool] = False,
) -> List[str]:
) -> list[str]:
"""
Lists commit objects in reverse chronological order
Expand Down Expand Up @@ -693,3 +724,30 @@ def status(

output = self.exec(*args)
return parse_git_status(output)

def reset(
self,
commit,
*,
mode: Union[ResetMode, str],
) -> None:
"""
Reset the git history
Args:
commit: Git reference to reset the checked out tree to
mode: The reset mode to use
Examples:
This will "list all the commits which are reachable from foo or
bar, but not from baz".
.. code-block:: python
from pontos.git import Git, ResetMode
git = Git()
git.reset("HEAD^", mode=ResetMode.HARD)
"""
args = ["reset", f"--{mode}", commit]
self.exec(*args)
51 changes: 51 additions & 0 deletions tests/git/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
Git,
GitError,
MergeStrategy,
ResetMode,
Status,
TagSort,
)
Expand Down Expand Up @@ -193,6 +194,40 @@ def test_push_with_force_false(self, exec_git_mock):

exec_git_mock.assert_called_once_with("push", cwd=None)

@patch("pontos.git._git.exec_git")
def test_push_branch_with_delete(self, exec_git_mock):
git = Git()
git.push(delete=True, branch="v1.2.3", remote="origin")

exec_git_mock.assert_called_once_with(
"push", "--delete", "origin", "v1.2.3", cwd=None
)

@patch("pontos.git._git.exec_git")
def test_push_refspec_with_delete(self, exec_git_mock):
git = Git()
git.push("v1.2.3", delete=True)

exec_git_mock.assert_called_once_with(
"push", "--delete", "v1.2.3", cwd=None
)

@patch("pontos.git._git.exec_git")
def test_push_refspec(self, exec_git_mock):
git = Git()
git.push("v1.2.3")

exec_git_mock.assert_called_once_with("push", "v1.2.3", cwd=None)

@patch("pontos.git._git.exec_git")
def test_push_refspecs(self, exec_git_mock):
git = Git()
git.push(["v1.2.3", "main"])

exec_git_mock.assert_called_once_with(
"push", "v1.2.3", "main", cwd=None
)

@patch("pontos.git._git.exec_git")
def test_config_get(self, exec_git_mock):
git = Git()
Expand Down Expand Up @@ -757,6 +792,22 @@ def test_show_with_no_patch(self, exec_git_mock: MagicMock):

self.assertEqual(show, content.strip())

@patch("pontos.git._git.exec_git")
def test_delete_tag(self, exec_git_mock):
git = Git()
git.delete_tag("v1.2.3")

exec_git_mock.assert_called_once_with("tag", "-d", "v1.2.3", cwd=None)

@patch("pontos.git._git.exec_git")
def test_reset_mixed(self, exec_git_mock):
git = Git()
git.reset("c1234", mode=ResetMode.MIXED)

exec_git_mock.assert_called_once_with(
"reset", "--mixed", "c1234", cwd=None
)


class GitExtendedTestCase(unittest.TestCase):
def test_semantic_list_tags(self):
Expand Down

0 comments on commit f03cb9a

Please sign in to comment.