gitbetter.git
1import shlex 2import subprocess 3from contextlib import contextmanager 4 5 6class Git: 7 def __init__(self, capture_stdout: bool = False): 8 """If `capture_stdout` is `True`, all functions will return their generated `stdout` as a string. 9 Otherwise, the functions return the call's exit code.""" 10 self.capture_stdout = capture_stdout 11 12 @contextmanager 13 def capture_output(self): 14 self.capture_stdout = True 15 yield self 16 self.capture_stdout = False 17 18 @property 19 def capture_stdout(self) -> bool: 20 """If `True`, member functions will return the generated `stdout` as a string, 21 otherwise they return the command's exit code.""" 22 return self._capture_stdout 23 24 @capture_stdout.setter 25 def capture_stdout(self, should_capture: bool): 26 self._capture_stdout = should_capture 27 28 def _run(self, args: list[str]) -> str | int: 29 if self._capture_stdout: 30 return subprocess.run(args, stdout=subprocess.PIPE, text=True).stdout 31 else: 32 return subprocess.run(args).returncode 33 34 def execute(self, command: str) -> str | int: 35 """Execute git command. 36 37 Equivalent to executing `git {command}` in the shell.""" 38 args = ["git"] + shlex.split(command) 39 return self._run(args) 40 41 def new_repo(self) -> str | int: 42 """Executes `git init -b main`.""" 43 return self.execute("init -b main") 44 45 def loggy(self) -> str | int: 46 """Equivalent to `git log --oneline --name-only --abbrev-commit --graph`.""" 47 return self.execute("log --oneline --name-only --abbrev-commit --graph") 48 49 def status(self) -> str | int: 50 """Execute `git status`.""" 51 return self.execute("status") 52 53 # ======================================Staging/Committing====================================== 54 def commit(self, args: str) -> str | int: 55 """>>> git commit {args}""" 56 return self.execute(f"commit {args}") 57 58 def add(self, files: list[str] | None = None) -> str | int: 59 """Stage a list of files. 60 61 If no files are given (`files=None`), all files will be staged.""" 62 if not files: 63 return self.execute("add .") 64 else: 65 files = " ".join(f'"{file}"' for file in files) # type: ignore 66 return self.execute(f"add {files}") 67 68 def commit_files(self, files: list[str], message: str) -> str | int: 69 """Stage and commit a list of files with commit message `message`.""" 70 return self.add(files) + self.commit(f'-m "{message}"') # type: ignore 71 72 def initcommit(self) -> str | int: 73 """Equivalent to 74 >>> git add . 75 >>> git commit -m "Initial commit" """ 76 return self.add() + self.commit('-m "Initial commit"') # type: ignore 77 78 def amend(self, files: list[str] | None = None) -> str | int: 79 """Stage and commit changes to the previous commit. 80 81 If `files` is `None`, all files will be staged. 82 83 Equivalent to: 84 >>> git add {files} 85 >>> git commit --amend --no-edit 86 """ 87 return self.add(files) + self.commit("--amend --no-edit") # type: ignore 88 89 def tag(self, args: str = "") -> str | int: 90 """Execute the `tag` command with `args`. 91 92 e.g. 93 94 `self.tag("--sort=-committerdate")` 95 96 will list all the tags for this repository in descending commit date.""" 97 return self.execute(f"tag {args}") 98 99 # ==========================================Push/Pull========================================== 100 def add_remote_url(self, url: str, name: str = "origin") -> str | int: 101 """Add remote url to repo.""" 102 return self.execute(f"remote add {name} {url}") 103 104 def push(self, args: str = "") -> str | int: 105 """Equivalent to `git push {args}`.""" 106 return self.execute(f"push {args}") 107 108 def pull(self, args: str = "") -> str | int: 109 """Equivalent to `git pull {args}`.""" 110 return self.execute(f"pull {args}") 111 112 def push_new_branch(self, branch: str) -> str | int: 113 """Push a new branch to origin with tracking. 114 115 Equivalent to `git push -u origin {branch}`.""" 116 return self.push(f"-u origin {branch}") 117 118 def pull_branch(self, branch: str) -> str | int: 119 """Pull `branch` from origin.""" 120 return self.pull(f"origin {branch}") 121 122 # ============================================Checkout/Branches============================================ 123 def branch(self, args: str) -> str | int: 124 """Equivalent to `git branch {args}`.""" 125 return self.execute(f"branch {args}") 126 127 def list_branches(self) -> str | int: 128 """Print a list of branches.""" 129 return self.branch("-vva") 130 131 def checkout(self, args: str) -> str | int: 132 """Equivalent to `git checkout {args}`.""" 133 return self.execute(f"checkout {args}") 134 135 def switch_branch(self, branch_name: str) -> str | int: 136 """Switch to the branch specified by `branch_name`. 137 138 Equivalent to `git checkout {branch_name}`.""" 139 return self.checkout(branch_name) 140 141 def create_new_branch(self, branch_name: str) -> str | int: 142 """Create and switch to a new branch named with `branch_name`. 143 144 Equivalent to `git checkout -b {branch_name} --track`.""" 145 return self.checkout(f"-b {branch_name} --track") 146 147 def delete_branch(self, branch_name: str, local_only: bool = True) -> str | int: 148 """Delete `branch_name` from repo. 149 150 #### :params: 151 152 `local_only`: Only delete the local copy of `branch`, otherwise also delete the remote branch on origin and remote-tracking branch.""" 153 output = self.branch(f"--delete {branch_name}") 154 if not local_only: 155 return output + self.push(f"origin --delete {branch_name}") # type:ignore 156 return output 157 158 def undo(self) -> str | int: 159 """Undo uncommitted changes. 160 161 Equivalent to `git checkout .`.""" 162 return self.checkout(".") 163 164 def merge(self, branch_name: str) -> str | int: 165 """Merge branch `branch_name` with currently active branch.""" 166 return self.execute(f"merge {branch_name}") 167 168 # ===============================Requires GitHub CLI to be installed and configured=============================== 169 170 def create_remote(self, name: str, public: bool = False) -> str | int: 171 """Uses GitHub CLI (must be installed and configured) to create a remote GitHub repo. 172 173 #### :params: 174 175 `name`: The name for the repo. 176 177 `public`: Set to `True` to create the repo as public, otherwise it'll be created as private.""" 178 visibility = "--public" if public else "--private" 179 return self._run(["gh", "repo", "create", name, visibility]) 180 181 def create_remote_from_cwd(self, public: bool = False) -> str | int: 182 """Use GitHub CLI (must be installed and configured) to create a remote GitHub repo from 183 the current working directory repo and add its url as this repo's remote origin. 184 185 #### :params: 186 187 `public`: Create the GitHub repo as a public repo, default is to create it as private.""" 188 visibility = "public" if public else "private" 189 return self._run( 190 ["gh", "repo", "create", "--source", ".", f"--{visibility}", "--push"] 191 ) 192 193 def _change_visibility(self, owner: str, name: str, visibility: str) -> str | int: 194 return self._run( 195 ["gh", "repo", "edit", f"{owner}/{name}", "--visibility", visibility] 196 ) 197 198 def make_private(self, owner: str, name: str) -> str | int: 199 """Uses GitHub CLI (must be installed and configured) to set the repo's visibility to private. 200 201 #### :params: 202 203 `owner`: The repo owner. 204 205 `name`: The name of the repo to edit.""" 206 return self._change_visibility(owner, name, "private") 207 208 def make_public(self, owner: str, name: str) -> str | int: 209 """Uses GitHub CLI (must be installed and configured) to set the repo's visibility to public. 210 211 #### :params: 212 213 `owner`: The repo owner. 214 215 `name`: The name of the repo to edit.""" 216 return self._change_visibility(owner, name, "public") 217 218 def delete_remote(self, owner: str, name: str) -> str | int: 219 """Uses GitHub CLI (must be isntalled and configured) to delete the remote for this repo. 220 221 #### :params: 222 223 `owner`: The repo owner. 224 225 `name`: The name of the remote repo to delete.""" 226 return self._run(["gh", "repo", "delete", f"{owner}/{name}", "--yes"])
7class Git: 8 def __init__(self, capture_stdout: bool = False): 9 """If `capture_stdout` is `True`, all functions will return their generated `stdout` as a string. 10 Otherwise, the functions return the call's exit code.""" 11 self.capture_stdout = capture_stdout 12 13 @contextmanager 14 def capture_output(self): 15 self.capture_stdout = True 16 yield self 17 self.capture_stdout = False 18 19 @property 20 def capture_stdout(self) -> bool: 21 """If `True`, member functions will return the generated `stdout` as a string, 22 otherwise they return the command's exit code.""" 23 return self._capture_stdout 24 25 @capture_stdout.setter 26 def capture_stdout(self, should_capture: bool): 27 self._capture_stdout = should_capture 28 29 def _run(self, args: list[str]) -> str | int: 30 if self._capture_stdout: 31 return subprocess.run(args, stdout=subprocess.PIPE, text=True).stdout 32 else: 33 return subprocess.run(args).returncode 34 35 def execute(self, command: str) -> str | int: 36 """Execute git command. 37 38 Equivalent to executing `git {command}` in the shell.""" 39 args = ["git"] + shlex.split(command) 40 return self._run(args) 41 42 def new_repo(self) -> str | int: 43 """Executes `git init -b main`.""" 44 return self.execute("init -b main") 45 46 def loggy(self) -> str | int: 47 """Equivalent to `git log --oneline --name-only --abbrev-commit --graph`.""" 48 return self.execute("log --oneline --name-only --abbrev-commit --graph") 49 50 def status(self) -> str | int: 51 """Execute `git status`.""" 52 return self.execute("status") 53 54 # ======================================Staging/Committing====================================== 55 def commit(self, args: str) -> str | int: 56 """>>> git commit {args}""" 57 return self.execute(f"commit {args}") 58 59 def add(self, files: list[str] | None = None) -> str | int: 60 """Stage a list of files. 61 62 If no files are given (`files=None`), all files will be staged.""" 63 if not files: 64 return self.execute("add .") 65 else: 66 files = " ".join(f'"{file}"' for file in files) # type: ignore 67 return self.execute(f"add {files}") 68 69 def commit_files(self, files: list[str], message: str) -> str | int: 70 """Stage and commit a list of files with commit message `message`.""" 71 return self.add(files) + self.commit(f'-m "{message}"') # type: ignore 72 73 def initcommit(self) -> str | int: 74 """Equivalent to 75 >>> git add . 76 >>> git commit -m "Initial commit" """ 77 return self.add() + self.commit('-m "Initial commit"') # type: ignore 78 79 def amend(self, files: list[str] | None = None) -> str | int: 80 """Stage and commit changes to the previous commit. 81 82 If `files` is `None`, all files will be staged. 83 84 Equivalent to: 85 >>> git add {files} 86 >>> git commit --amend --no-edit 87 """ 88 return self.add(files) + self.commit("--amend --no-edit") # type: ignore 89 90 def tag(self, args: str = "") -> str | int: 91 """Execute the `tag` command with `args`. 92 93 e.g. 94 95 `self.tag("--sort=-committerdate")` 96 97 will list all the tags for this repository in descending commit date.""" 98 return self.execute(f"tag {args}") 99 100 # ==========================================Push/Pull========================================== 101 def add_remote_url(self, url: str, name: str = "origin") -> str | int: 102 """Add remote url to repo.""" 103 return self.execute(f"remote add {name} {url}") 104 105 def push(self, args: str = "") -> str | int: 106 """Equivalent to `git push {args}`.""" 107 return self.execute(f"push {args}") 108 109 def pull(self, args: str = "") -> str | int: 110 """Equivalent to `git pull {args}`.""" 111 return self.execute(f"pull {args}") 112 113 def push_new_branch(self, branch: str) -> str | int: 114 """Push a new branch to origin with tracking. 115 116 Equivalent to `git push -u origin {branch}`.""" 117 return self.push(f"-u origin {branch}") 118 119 def pull_branch(self, branch: str) -> str | int: 120 """Pull `branch` from origin.""" 121 return self.pull(f"origin {branch}") 122 123 # ============================================Checkout/Branches============================================ 124 def branch(self, args: str) -> str | int: 125 """Equivalent to `git branch {args}`.""" 126 return self.execute(f"branch {args}") 127 128 def list_branches(self) -> str | int: 129 """Print a list of branches.""" 130 return self.branch("-vva") 131 132 def checkout(self, args: str) -> str | int: 133 """Equivalent to `git checkout {args}`.""" 134 return self.execute(f"checkout {args}") 135 136 def switch_branch(self, branch_name: str) -> str | int: 137 """Switch to the branch specified by `branch_name`. 138 139 Equivalent to `git checkout {branch_name}`.""" 140 return self.checkout(branch_name) 141 142 def create_new_branch(self, branch_name: str) -> str | int: 143 """Create and switch to a new branch named with `branch_name`. 144 145 Equivalent to `git checkout -b {branch_name} --track`.""" 146 return self.checkout(f"-b {branch_name} --track") 147 148 def delete_branch(self, branch_name: str, local_only: bool = True) -> str | int: 149 """Delete `branch_name` from repo. 150 151 #### :params: 152 153 `local_only`: Only delete the local copy of `branch`, otherwise also delete the remote branch on origin and remote-tracking branch.""" 154 output = self.branch(f"--delete {branch_name}") 155 if not local_only: 156 return output + self.push(f"origin --delete {branch_name}") # type:ignore 157 return output 158 159 def undo(self) -> str | int: 160 """Undo uncommitted changes. 161 162 Equivalent to `git checkout .`.""" 163 return self.checkout(".") 164 165 def merge(self, branch_name: str) -> str | int: 166 """Merge branch `branch_name` with currently active branch.""" 167 return self.execute(f"merge {branch_name}") 168 169 # ===============================Requires GitHub CLI to be installed and configured=============================== 170 171 def create_remote(self, name: str, public: bool = False) -> str | int: 172 """Uses GitHub CLI (must be installed and configured) to create a remote GitHub repo. 173 174 #### :params: 175 176 `name`: The name for the repo. 177 178 `public`: Set to `True` to create the repo as public, otherwise it'll be created as private.""" 179 visibility = "--public" if public else "--private" 180 return self._run(["gh", "repo", "create", name, visibility]) 181 182 def create_remote_from_cwd(self, public: bool = False) -> str | int: 183 """Use GitHub CLI (must be installed and configured) to create a remote GitHub repo from 184 the current working directory repo and add its url as this repo's remote origin. 185 186 #### :params: 187 188 `public`: Create the GitHub repo as a public repo, default is to create it as private.""" 189 visibility = "public" if public else "private" 190 return self._run( 191 ["gh", "repo", "create", "--source", ".", f"--{visibility}", "--push"] 192 ) 193 194 def _change_visibility(self, owner: str, name: str, visibility: str) -> str | int: 195 return self._run( 196 ["gh", "repo", "edit", f"{owner}/{name}", "--visibility", visibility] 197 ) 198 199 def make_private(self, owner: str, name: str) -> str | int: 200 """Uses GitHub CLI (must be installed and configured) to set the repo's visibility to private. 201 202 #### :params: 203 204 `owner`: The repo owner. 205 206 `name`: The name of the repo to edit.""" 207 return self._change_visibility(owner, name, "private") 208 209 def make_public(self, owner: str, name: str) -> str | int: 210 """Uses GitHub CLI (must be installed and configured) to set the repo's visibility to public. 211 212 #### :params: 213 214 `owner`: The repo owner. 215 216 `name`: The name of the repo to edit.""" 217 return self._change_visibility(owner, name, "public") 218 219 def delete_remote(self, owner: str, name: str) -> str | int: 220 """Uses GitHub CLI (must be isntalled and configured) to delete the remote for this repo. 221 222 #### :params: 223 224 `owner`: The repo owner. 225 226 `name`: The name of the remote repo to delete.""" 227 return self._run(["gh", "repo", "delete", f"{owner}/{name}", "--yes"])
8 def __init__(self, capture_stdout: bool = False): 9 """If `capture_stdout` is `True`, all functions will return their generated `stdout` as a string. 10 Otherwise, the functions return the call's exit code.""" 11 self.capture_stdout = capture_stdout
If capture_stdout
is True
, all functions will return their generated stdout
as a string.
Otherwise, the functions return the call's exit code.
If True
, member functions will return the generated stdout
as a string,
otherwise they return the command's exit code.
35 def execute(self, command: str) -> str | int: 36 """Execute git command. 37 38 Equivalent to executing `git {command}` in the shell.""" 39 args = ["git"] + shlex.split(command) 40 return self._run(args)
Execute git command.
Equivalent to executing git {command}
in the shell.
42 def new_repo(self) -> str | int: 43 """Executes `git init -b main`.""" 44 return self.execute("init -b main")
Executes git init -b main
.
46 def loggy(self) -> str | int: 47 """Equivalent to `git log --oneline --name-only --abbrev-commit --graph`.""" 48 return self.execute("log --oneline --name-only --abbrev-commit --graph")
Equivalent to git log --oneline --name-only --abbrev-commit --graph
.
55 def commit(self, args: str) -> str | int: 56 """>>> git commit {args}""" 57 return self.execute(f"commit {args}")
>>> git commit {args}
59 def add(self, files: list[str] | None = None) -> str | int: 60 """Stage a list of files. 61 62 If no files are given (`files=None`), all files will be staged.""" 63 if not files: 64 return self.execute("add .") 65 else: 66 files = " ".join(f'"{file}"' for file in files) # type: ignore 67 return self.execute(f"add {files}")
Stage a list of files.
If no files are given (files=None
), all files will be staged.
69 def commit_files(self, files: list[str], message: str) -> str | int: 70 """Stage and commit a list of files with commit message `message`.""" 71 return self.add(files) + self.commit(f'-m "{message}"') # type: ignore
Stage and commit a list of files with commit message message
.
73 def initcommit(self) -> str | int: 74 """Equivalent to 75 >>> git add . 76 >>> git commit -m "Initial commit" """ 77 return self.add() + self.commit('-m "Initial commit"') # type: ignore
Equivalent to
>>> git add .
>>> git commit -m "Initial commit"
79 def amend(self, files: list[str] | None = None) -> str | int: 80 """Stage and commit changes to the previous commit. 81 82 If `files` is `None`, all files will be staged. 83 84 Equivalent to: 85 >>> git add {files} 86 >>> git commit --amend --no-edit 87 """ 88 return self.add(files) + self.commit("--amend --no-edit") # type: ignore
Stage and commit changes to the previous commit.
If files
is None
, all files will be staged.
Equivalent to:
>>> git add {files}
>>> git commit --amend --no-edit
90 def tag(self, args: str = "") -> str | int: 91 """Execute the `tag` command with `args`. 92 93 e.g. 94 95 `self.tag("--sort=-committerdate")` 96 97 will list all the tags for this repository in descending commit date.""" 98 return self.execute(f"tag {args}")
Execute the tag
command with args
.
e.g.
self.tag("--sort=-committerdate")
will list all the tags for this repository in descending commit date.
101 def add_remote_url(self, url: str, name: str = "origin") -> str | int: 102 """Add remote url to repo.""" 103 return self.execute(f"remote add {name} {url}")
Add remote url to repo.
105 def push(self, args: str = "") -> str | int: 106 """Equivalent to `git push {args}`.""" 107 return self.execute(f"push {args}")
Equivalent to git push {args}
.
109 def pull(self, args: str = "") -> str | int: 110 """Equivalent to `git pull {args}`.""" 111 return self.execute(f"pull {args}")
Equivalent to git pull {args}
.
113 def push_new_branch(self, branch: str) -> str | int: 114 """Push a new branch to origin with tracking. 115 116 Equivalent to `git push -u origin {branch}`.""" 117 return self.push(f"-u origin {branch}")
Push a new branch to origin with tracking.
Equivalent to git push -u origin {branch}
.
119 def pull_branch(self, branch: str) -> str | int: 120 """Pull `branch` from origin.""" 121 return self.pull(f"origin {branch}")
Pull branch
from origin.
124 def branch(self, args: str) -> str | int: 125 """Equivalent to `git branch {args}`.""" 126 return self.execute(f"branch {args}")
Equivalent to git branch {args}
.
128 def list_branches(self) -> str | int: 129 """Print a list of branches.""" 130 return self.branch("-vva")
Print a list of branches.
132 def checkout(self, args: str) -> str | int: 133 """Equivalent to `git checkout {args}`.""" 134 return self.execute(f"checkout {args}")
Equivalent to git checkout {args}
.
136 def switch_branch(self, branch_name: str) -> str | int: 137 """Switch to the branch specified by `branch_name`. 138 139 Equivalent to `git checkout {branch_name}`.""" 140 return self.checkout(branch_name)
Switch to the branch specified by branch_name
.
Equivalent to git checkout {branch_name}
.
142 def create_new_branch(self, branch_name: str) -> str | int: 143 """Create and switch to a new branch named with `branch_name`. 144 145 Equivalent to `git checkout -b {branch_name} --track`.""" 146 return self.checkout(f"-b {branch_name} --track")
Create and switch to a new branch named with branch_name
.
Equivalent to git checkout -b {branch_name} --track
.
148 def delete_branch(self, branch_name: str, local_only: bool = True) -> str | int: 149 """Delete `branch_name` from repo. 150 151 #### :params: 152 153 `local_only`: Only delete the local copy of `branch`, otherwise also delete the remote branch on origin and remote-tracking branch.""" 154 output = self.branch(f"--delete {branch_name}") 155 if not local_only: 156 return output + self.push(f"origin --delete {branch_name}") # type:ignore 157 return output
Delete branch_name
from repo.
:params:
local_only
: Only delete the local copy of branch
, otherwise also delete the remote branch on origin and remote-tracking branch.
159 def undo(self) -> str | int: 160 """Undo uncommitted changes. 161 162 Equivalent to `git checkout .`.""" 163 return self.checkout(".")
Undo uncommitted changes.
Equivalent to git checkout .
.
165 def merge(self, branch_name: str) -> str | int: 166 """Merge branch `branch_name` with currently active branch.""" 167 return self.execute(f"merge {branch_name}")
Merge branch branch_name
with currently active branch.
171 def create_remote(self, name: str, public: bool = False) -> str | int: 172 """Uses GitHub CLI (must be installed and configured) to create a remote GitHub repo. 173 174 #### :params: 175 176 `name`: The name for the repo. 177 178 `public`: Set to `True` to create the repo as public, otherwise it'll be created as private.""" 179 visibility = "--public" if public else "--private" 180 return self._run(["gh", "repo", "create", name, visibility])
Uses GitHub CLI (must be installed and configured) to create a remote GitHub repo.
:params:
name
: The name for the repo.
public
: Set to True
to create the repo as public, otherwise it'll be created as private.
182 def create_remote_from_cwd(self, public: bool = False) -> str | int: 183 """Use GitHub CLI (must be installed and configured) to create a remote GitHub repo from 184 the current working directory repo and add its url as this repo's remote origin. 185 186 #### :params: 187 188 `public`: Create the GitHub repo as a public repo, default is to create it as private.""" 189 visibility = "public" if public else "private" 190 return self._run( 191 ["gh", "repo", "create", "--source", ".", f"--{visibility}", "--push"] 192 )
Use GitHub CLI (must be installed and configured) to create a remote GitHub repo from the current working directory repo and add its url as this repo's remote origin.
:params:
public
: Create the GitHub repo as a public repo, default is to create it as private.
199 def make_private(self, owner: str, name: str) -> str | int: 200 """Uses GitHub CLI (must be installed and configured) to set the repo's visibility to private. 201 202 #### :params: 203 204 `owner`: The repo owner. 205 206 `name`: The name of the repo to edit.""" 207 return self._change_visibility(owner, name, "private")
Uses GitHub CLI (must be installed and configured) to set the repo's visibility to private.
:params:
owner
: The repo owner.
name
: The name of the repo to edit.
209 def make_public(self, owner: str, name: str) -> str | int: 210 """Uses GitHub CLI (must be installed and configured) to set the repo's visibility to public. 211 212 #### :params: 213 214 `owner`: The repo owner. 215 216 `name`: The name of the repo to edit.""" 217 return self._change_visibility(owner, name, "public")
Uses GitHub CLI (must be installed and configured) to set the repo's visibility to public.
:params:
owner
: The repo owner.
name
: The name of the repo to edit.
219 def delete_remote(self, owner: str, name: str) -> str | int: 220 """Uses GitHub CLI (must be isntalled and configured) to delete the remote for this repo. 221 222 #### :params: 223 224 `owner`: The repo owner. 225 226 `name`: The name of the remote repo to delete.""" 227 return self._run(["gh", "repo", "delete", f"{owner}/{name}", "--yes"])
Uses GitHub CLI (must be isntalled and configured) to delete the remote for this repo.
:params:
owner
: The repo owner.
name
: The name of the remote repo to delete.