Skip to content

Commit 4ed1aa2

Browse files
committed
Add: Extend Git class for deleting tags, reset and pushing refspecs
Allow to delete tags, to push the deleted tags to the remote repo and to reset a local history.
1 parent 86bea78 commit 4ed1aa2

File tree

3 files changed

+129
-18
lines changed

3 files changed

+129
-18
lines changed

pontos/git/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Git,
1010
GitError,
1111
MergeStrategy,
12+
ResetMode,
1213
TagSort,
1314
)
1415
from ._status import Status, StatusEntry
@@ -19,6 +20,7 @@
1920
"Git",
2021
"GitError",
2122
"MergeStrategy",
23+
"ResetMode",
2224
"Status",
2325
"StatusEntry",
2426
"TagSort",

pontos/git/_git.py

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@
44
#
55

66
import subprocess
7-
from enum import Enum
87
from os import PathLike, fspath
98
from pathlib import Path
109
from typing import (
1110
Collection,
1211
Iterable,
1312
Iterator,
14-
List,
1513
Optional,
1614
Sequence,
1715
Union,
1816
)
1917

18+
from pontos.enum import StrEnum
2019
from pontos.errors import PontosError
2120

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

8786

88-
class MergeStrategy(Enum):
87+
class MergeStrategy(StrEnum):
8988
"""
9089
Possible strategies for a merge
9190
@@ -107,7 +106,7 @@ class MergeStrategy(Enum):
107106
SUBTREE = "subtree"
108107

109108

110-
class ConfigScope(Enum):
109+
class ConfigScope(StrEnum):
111110
"""
112111
Possible scopes for git settings
113112
@@ -126,7 +125,7 @@ class ConfigScope(Enum):
126125
WORKTREE = "worktree"
127126

128127

129-
class TagSort(Enum):
128+
class TagSort(StrEnum):
130129
"""
131130
Sorting for git tags
132131
@@ -137,6 +136,14 @@ class TagSort(Enum):
137136
VERSION = "version:refname"
138137

139138

139+
class ResetMode(StrEnum):
140+
SOFT = "soft"
141+
MIXED = "mixed"
142+
HARD = "hard"
143+
MERGE = "merge"
144+
KEEP = "keep"
145+
146+
140147
class Git:
141148
"""
142149
Run git commands as subprocesses
@@ -231,7 +238,7 @@ def rebase(
231238
if strategy == MergeStrategy.ORT_OURS:
232239
args.extend(["--strategy", "ort", "-X", "ours"])
233240
else:
234-
args.extend(["--strategy", strategy.value])
241+
args.extend(["--strategy", str(strategy)])
235242

236243
if onto:
237244
args.extend(["--onto", onto])
@@ -274,32 +281,43 @@ def clone(
274281

275282
def push(
276283
self,
284+
refspec: Optional[Union[str, Iterable[str]]] = None,
277285
*,
278286
remote: Optional[str] = None,
279287
branch: Optional[str] = None,
280288
follow_tags: bool = False,
281289
force: Optional[bool] = None,
290+
delete: Optional[bool] = None,
282291
) -> None:
283292
"""
284293
Push changes to remote repository
285294
286295
Args:
296+
refspec: Refs to push
287297
remote: Push changes to the named remote
288-
branch: Branch to push. Will only be considered in combination with
289-
a remote.
298+
branch: Branch to push. Will only be considered in
299+
combination with a remote. Deprecated, use refs instead.
290300
follow_tags: Push all tags pointing to a commit included in the to
291-
be pushed branch.
301+
be pushed branch.
292302
force: Force push changes.
303+
delete: Delete remote refspec
293304
"""
294305
args = ["push"]
295306
if follow_tags:
296307
args.append("--follow-tags")
297308
if force:
298309
args.append("--force")
310+
if delete:
311+
args.append("--delete")
299312
if remote:
300313
args.append(remote)
301314
if branch:
302315
args.append(branch)
316+
if refspec:
317+
if isinstance(refspec, str):
318+
args.append(refspec)
319+
else:
320+
args.extend(refspec)
303321

304322
self.exec(*args)
305323

@@ -308,7 +326,7 @@ def config(
308326
key: str,
309327
value: Optional[str] = None,
310328
*,
311-
scope: Optional[ConfigScope] = None,
329+
scope: Optional[Union[ConfigScope, str]] = None,
312330
) -> str:
313331
"""
314332
Get and set a git config
@@ -320,7 +338,7 @@ def config(
320338
"""
321339
args = ["config"]
322340
if scope:
323-
args.append(f"--{scope.value}")
341+
args.append(f"--{scope}")
324342

325343
args.append(key)
326344

@@ -329,7 +347,7 @@ def config(
329347

330348
return self.exec(*args)
331349

332-
def cherry_pick(self, commits: Union[str, List[str]]) -> None:
350+
def cherry_pick(self, commits: Union[str, list[str]]) -> None:
333351
"""
334352
Apply changes of a commit(s) to the current branch
335353
@@ -348,10 +366,10 @@ def cherry_pick(self, commits: Union[str, List[str]]) -> None:
348366
def list_tags(
349367
self,
350368
*,
351-
sort: Optional[TagSort] = None,
369+
sort: Optional[Union[TagSort, str]] = None,
352370
tag_name: Optional[str] = None,
353-
sort_suffix: Optional[List[str]] = None,
354-
) -> List[str]:
371+
sort_suffix: Optional[list[str]] = None,
372+
) -> list[str]:
355373
"""
356374
List all available tags
357375
@@ -370,7 +388,7 @@ def list_tags(
370388
args.extend(["-c", f"versionsort.suffix={suffix}"])
371389

372390
args.extend(["tag", "-l"])
373-
args.append(f"--sort={sort.value}")
391+
args.append(f"--sort={sort}")
374392
else:
375393
args = ["tag", "-l"]
376394

@@ -463,6 +481,19 @@ def tag(
463481

464482
self.exec(*args)
465483

484+
def delete_tag(
485+
self,
486+
tag: str,
487+
) -> None:
488+
"""
489+
Delete a Tag
490+
491+
Args:
492+
tag: Tag name to delete
493+
"""
494+
args = ["tag", "-d", tag]
495+
self.exec(*args)
496+
466497
def fetch(
467498
self,
468499
remote: Optional[str] = None,
@@ -538,7 +569,7 @@ def log(
538569
*log_args: str,
539570
oneline: Optional[bool] = None,
540571
format: Optional[str] = None,
541-
) -> List[str]:
572+
) -> list[str]:
542573
"""
543574
Get log of a git repository
544575
@@ -614,7 +645,7 @@ def rev_list(
614645
*commit: str,
615646
max_parents: Optional[int] = None,
616647
abbrev_commit: Optional[bool] = False,
617-
) -> List[str]:
648+
) -> list[str]:
618649
"""
619650
Lists commit objects in reverse chronological order
620651
@@ -693,3 +724,30 @@ def status(
693724

694725
output = self.exec(*args)
695726
return parse_git_status(output)
727+
728+
def reset(
729+
self,
730+
commit,
731+
*,
732+
mode: Union[ResetMode, str],
733+
) -> None:
734+
"""
735+
Reset the git history
736+
737+
Args:
738+
commit: Git reference to reset the checked out tree to
739+
mode: The reset mode to use
740+
741+
Examples:
742+
This will "list all the commits which are reachable from foo or
743+
bar, but not from baz".
744+
745+
.. code-block:: python
746+
747+
from pontos.git import Git, ResetMode
748+
749+
git = Git()
750+
git.reset("HEAD^", mode=ResetMode.HARD)
751+
"""
752+
args = ["reset", f"--{mode}", commit]
753+
self.exec(*args)

tests/git/test_git.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Git,
1414
GitError,
1515
MergeStrategy,
16+
ResetMode,
1617
Status,
1718
TagSort,
1819
)
@@ -193,6 +194,40 @@ def test_push_with_force_false(self, exec_git_mock):
193194

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

197+
@patch("pontos.git._git.exec_git")
198+
def test_push_branch_with_delete(self, exec_git_mock):
199+
git = Git()
200+
git.push(delete=True, branch="v1.2.3", remote="origin")
201+
202+
exec_git_mock.assert_called_once_with(
203+
"push", "--delete", "origin", "v1.2.3", cwd=None
204+
)
205+
206+
@patch("pontos.git._git.exec_git")
207+
def test_push_refspec_with_delete(self, exec_git_mock):
208+
git = Git()
209+
git.push("v1.2.3", delete=True)
210+
211+
exec_git_mock.assert_called_once_with(
212+
"push", "--delete", "v1.2.3", cwd=None
213+
)
214+
215+
@patch("pontos.git._git.exec_git")
216+
def test_push_refspec(self, exec_git_mock):
217+
git = Git()
218+
git.push("v1.2.3")
219+
220+
exec_git_mock.assert_called_once_with("push", "v1.2.3", cwd=None)
221+
222+
@patch("pontos.git._git.exec_git")
223+
def test_push_refspecs(self, exec_git_mock):
224+
git = Git()
225+
git.push(["v1.2.3", "main"])
226+
227+
exec_git_mock.assert_called_once_with(
228+
"push", "v1.2.3", "main", cwd=None
229+
)
230+
196231
@patch("pontos.git._git.exec_git")
197232
def test_config_get(self, exec_git_mock):
198233
git = Git()
@@ -757,6 +792,22 @@ def test_show_with_no_patch(self, exec_git_mock: MagicMock):
757792

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

795+
@patch("pontos.git._git.exec_git")
796+
def test_delete_tag(self, exec_git_mock):
797+
git = Git()
798+
git.delete_tag("v1.2.3")
799+
800+
exec_git_mock.assert_called_once_with("tag", "-d", "v1.2.3", cwd=None)
801+
802+
@patch("pontos.git._git.exec_git")
803+
def test_reset_mixed(self, exec_git_mock):
804+
git = Git()
805+
git.reset("c1234", mode=ResetMode.MIXED)
806+
807+
exec_git_mock.assert_called_once_with(
808+
"reset", "--mixed", "c1234", cwd=None
809+
)
810+
760811

761812
class GitExtendedTestCase(unittest.TestCase):
762813
def test_semantic_list_tags(self):

0 commit comments

Comments
 (0)