88import typing as t
99from collections .abc import Sequence
1010
11+ from libvcs ._internal .query_list import QueryList
1112from libvcs ._internal .run import ProgressCallbackProtocol , run
1213from 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