Skip to content

Commit 2ad7f4a

Browse files
committed
cmd(git) Git.branches (including management query)
1 parent 43b41ab commit 2ad7f4a

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed

src/libvcs/cmd/git.py

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import typing as t
99
from collections.abc import Sequence
1010

11+
from libvcs._internal.query_list import QueryList
1112
from libvcs._internal.run import ProgressCallbackProtocol, run
1213
from libvcs._internal.types import StrOrBytesPath, StrPath
1314

@@ -23,6 +24,7 @@ class Git:
2324
submodule: GitSubmoduleCmd
2425
remote: GitRemoteCmd
2526
stash: GitStashCmd
27+
branch: GitBranchManager
2628

2729
def __init__(
2830
self,
@@ -83,6 +85,7 @@ def __init__(
8385
self.submodule = GitSubmoduleCmd(path=self.path, cmd=self)
8486
self.remote = GitRemoteCmd(path=self.path, cmd=self)
8587
self.stash = GitStashCmd(path=self.path, cmd=self)
88+
self.branches = GitBranchManager(path=self.path, cmd=self)
8689

8790
def __repr__(self) -> str:
8891
"""Representation of Git repo command object."""
@@ -2950,3 +2953,315 @@ def save(
29502953
check_returncode=check_returncode,
29512954
log_in_real_time=log_in_real_time,
29522955
)
2956+
2957+
2958+
GitBranchCommandLiteral = t.Literal[
2959+
# "create", # checkout -b
2960+
# "checkout", # checkout
2961+
"--list",
2962+
"move", # branch -m, or branch -M with force
2963+
"copy", # branch -c, or branch -C with force
2964+
"delete", # branch -d, or branch -D /ith force
2965+
"set_upstream",
2966+
"unset_upstream",
2967+
"track",
2968+
"no_track",
2969+
"edit_description",
2970+
]
2971+
2972+
2973+
class GitBranchCmd:
2974+
"""Run commands directly against a git branch for a git repo."""
2975+
2976+
branch_name: str
2977+
2978+
def __init__(
2979+
self,
2980+
*,
2981+
path: StrPath,
2982+
branch_name: str,
2983+
cmd: Git | None = None,
2984+
) -> None:
2985+
"""Lite, typed, pythonic wrapper for git-branch(1).
2986+
2987+
Parameters
2988+
----------
2989+
path :
2990+
Operates as PATH in the corresponding git subcommand.
2991+
branch_name:
2992+
Name of branch.
2993+
2994+
Examples
2995+
--------
2996+
>>> GitBranchCmd(path=tmp_path, branch_name='master')
2997+
<GitBranchCmd path=... branch_name=master>
2998+
2999+
>>> GitBranchCmd(path=tmp_path, branch_name="master").run(quiet=True)
3000+
'fatal: not a git repository (or any of the parent directories): .git'
3001+
3002+
>>> GitBranchCmd(
3003+
... path=git_local_clone.path, branch_name="master").run(quiet=True)
3004+
'* master'
3005+
"""
3006+
#: Directory to check out
3007+
self.path: pathlib.Path
3008+
if isinstance(path, pathlib.Path):
3009+
self.path = path
3010+
else:
3011+
self.path = pathlib.Path(path)
3012+
3013+
self.cmd = cmd if isinstance(cmd, Git) else Git(path=self.path)
3014+
3015+
self.branch_name = branch_name
3016+
3017+
def __repr__(self) -> str:
3018+
"""Representation of git branch command object."""
3019+
return f"<GitBranchCmd path={self.path} branch_name={self.branch_name}>"
3020+
3021+
def run(
3022+
self,
3023+
command: GitBranchCommandLiteral | None = None,
3024+
local_flags: list[str] | None = None,
3025+
*,
3026+
quiet: bool | None = None,
3027+
cached: bool | None = None, # Only when no command entered and status
3028+
# Pass-through to run()
3029+
log_in_real_time: bool = False,
3030+
check_returncode: bool | None = None,
3031+
**kwargs: t.Any,
3032+
) -> str:
3033+
"""Run a command against a git repository's branch.
3034+
3035+
Wraps `git branch <https://git-scm.com/docs/git-branch>`_.
3036+
3037+
Examples
3038+
--------
3039+
>>> GitBranchCmd(path=git_local_clone.path, branch_name='master').run()
3040+
'* master'
3041+
"""
3042+
local_flags = local_flags if isinstance(local_flags, list) else []
3043+
if command is not None:
3044+
local_flags.insert(0, command)
3045+
3046+
if quiet is True:
3047+
local_flags.append("--quiet")
3048+
if cached is True:
3049+
local_flags.append("--cached")
3050+
3051+
return self.cmd.run(
3052+
["branch", *local_flags],
3053+
check_returncode=check_returncode,
3054+
log_in_real_time=log_in_real_time,
3055+
)
3056+
3057+
def checkout(self) -> str:
3058+
"""Git branch checkout.
3059+
3060+
Examples
3061+
--------
3062+
>>> GitBranchCmd(path=git_local_clone.path, branch_name='master').checkout()
3063+
"Your branch is up to date with 'origin/master'."
3064+
"""
3065+
return self.cmd.run(
3066+
[
3067+
"checkout",
3068+
*[self.branch_name],
3069+
],
3070+
)
3071+
3072+
def create(self) -> str:
3073+
"""Create a git branch.
3074+
3075+
Examples
3076+
--------
3077+
>>> GitBranchCmd(path=git_local_clone.path, branch_name='master').create()
3078+
"fatal: a branch named 'master' already exists"
3079+
"""
3080+
return self.cmd.run(
3081+
[
3082+
"checkout",
3083+
*["-b", self.branch_name],
3084+
],
3085+
# Pass-through to run()
3086+
check_returncode=False,
3087+
)
3088+
3089+
3090+
class GitBranchManager:
3091+
"""Run commands directly related to git branches of a git repo."""
3092+
3093+
branch_name: str
3094+
3095+
def __init__(
3096+
self,
3097+
*,
3098+
path: StrPath,
3099+
cmd: Git | None = None,
3100+
) -> None:
3101+
"""Wrap some of git-branch(1), git-checkout(1), manager.
3102+
3103+
Parameters
3104+
----------
3105+
path :
3106+
Operates as PATH in the corresponding git subcommand.
3107+
3108+
Examples
3109+
--------
3110+
>>> GitBranchManager(path=tmp_path)
3111+
<GitBranchManager path=...>
3112+
3113+
>>> GitBranchManager(path=tmp_path).run(quiet=True)
3114+
'fatal: not a git repository (or any of the parent directories): .git'
3115+
3116+
>>> GitBranchManager(
3117+
... path=git_local_clone.path).run(quiet=True)
3118+
'* master'
3119+
"""
3120+
#: Directory to check out
3121+
self.path: pathlib.Path
3122+
if isinstance(path, pathlib.Path):
3123+
self.path = path
3124+
else:
3125+
self.path = pathlib.Path(path)
3126+
3127+
self.cmd = cmd if isinstance(cmd, Git) else Git(path=self.path)
3128+
3129+
def __repr__(self) -> str:
3130+
"""Representation of git branch manager object."""
3131+
return f"<GitBranchManager path={self.path}>"
3132+
3133+
def run(
3134+
self,
3135+
command: GitBranchCommandLiteral | None = None,
3136+
local_flags: list[str] | None = None,
3137+
*,
3138+
quiet: bool | None = None,
3139+
cached: bool | None = None, # Only when no command entered and status
3140+
# Pass-through to run()
3141+
log_in_real_time: bool = False,
3142+
check_returncode: bool | None = None,
3143+
**kwargs: t.Any,
3144+
) -> str:
3145+
"""Run a command against a git repository's branches.
3146+
3147+
Wraps `git branch <https://git-scm.com/docs/git-branch>`_.
3148+
3149+
Examples
3150+
--------
3151+
>>> GitBranchManager(path=git_local_clone.path).run()
3152+
'* master'
3153+
"""
3154+
local_flags = local_flags if isinstance(local_flags, list) else []
3155+
if command is not None:
3156+
local_flags.insert(0, command)
3157+
3158+
if quiet is True:
3159+
local_flags.append("--quiet")
3160+
if cached is True:
3161+
local_flags.append("--cached")
3162+
3163+
return self.cmd.run(
3164+
["branch", *local_flags],
3165+
check_returncode=check_returncode,
3166+
log_in_real_time=log_in_real_time,
3167+
)
3168+
3169+
def checkout(self, *, branch: str) -> str:
3170+
"""Git branch checkout.
3171+
3172+
Examples
3173+
--------
3174+
>>> GitBranchManager(path=git_local_clone.path).checkout(branch='master')
3175+
"Your branch is up to date with 'origin/master'."
3176+
"""
3177+
return self.cmd.run(
3178+
[
3179+
"checkout",
3180+
*[branch],
3181+
],
3182+
)
3183+
3184+
def create(self, *, branch: str) -> str:
3185+
"""Create a git branch.
3186+
3187+
Examples
3188+
--------
3189+
>>> GitBranchManager(path=git_local_clone.path).create(branch='master')
3190+
"fatal: a branch named 'master' already exists"
3191+
"""
3192+
return self.cmd.run(
3193+
[
3194+
"checkout",
3195+
*["-b", branch],
3196+
],
3197+
# Pass-through to run()
3198+
check_returncode=False,
3199+
)
3200+
3201+
def _ls(self) -> list[str]:
3202+
"""List branches.
3203+
3204+
Examples
3205+
--------
3206+
>>> GitBranchManager(path=git_local_clone.path)._ls()
3207+
['* master']
3208+
"""
3209+
return self.run(
3210+
"--list",
3211+
).splitlines()
3212+
3213+
def ls(self) -> QueryList[GitBranchCmd]:
3214+
"""List branches.
3215+
3216+
Examples
3217+
--------
3218+
>>> GitBranchManager(path=git_local_clone.path).ls()
3219+
[<GitBranchCmd path=... branch_name=master>]
3220+
"""
3221+
return QueryList(
3222+
[
3223+
GitBranchCmd(path=self.path, branch_name=branch_name.lstrip("* "))
3224+
for branch_name in self._ls()
3225+
],
3226+
)
3227+
3228+
def get(self, *args: t.Any, **kwargs: t.Any) -> GitBranchCmd | None:
3229+
"""Get branch via filter lookup.
3230+
3231+
Examples
3232+
--------
3233+
>>> GitBranchManager(
3234+
... path=git_local_clone.path
3235+
... ).get(branch_name='master')
3236+
<GitBranchCmd path=... branch_name=master>
3237+
3238+
>>> GitBranchManager(
3239+
... path=git_local_clone.path
3240+
... ).get(branch_name='unknown')
3241+
Traceback (most recent call last):
3242+
exec(compile(example.source, filename, "single",
3243+
...
3244+
return self.ls().get(*args, **kwargs)
3245+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
3246+
File "..._internal/query_list.py", line ..., in get
3247+
raise ObjectDoesNotExist
3248+
libvcs._internal.query_list.ObjectDoesNotExist
3249+
"""
3250+
return self.ls().get(*args, **kwargs)
3251+
3252+
def filter(self, *args: t.Any, **kwargs: t.Any) -> list[GitBranchCmd]:
3253+
"""Get branches via filter lookup.
3254+
3255+
Examples
3256+
--------
3257+
>>> GitBranchManager(
3258+
... path=git_local_clone.path
3259+
... ).filter(branch_name__contains='master')
3260+
[<GitBranchCmd path=... branch_name=master>]
3261+
3262+
>>> GitBranchManager(
3263+
... path=git_local_clone.path
3264+
... ).filter(branch_name__contains='unknown')
3265+
[]
3266+
"""
3267+
return self.ls().filter(*args, **kwargs)

0 commit comments

Comments
 (0)