gitbetter.gitbetter

  1import os
  2
  3from argshell import ArgShell, ArgShellParser, Namespace, with_parser
  4from pathier import Pathier
  5
  6from gitbetter import git
  7
  8
  9def new_remote_parser() -> ArgShellParser:
 10    parser = ArgShellParser()
 11    parser.add_argument(
 12        "--public",
 13        action="store_true",
 14        help=""" Set the new remote visibility as public. Defaults to private. """,
 15    )
 16    return parser
 17
 18
 19def commit_files_parser() -> ArgShellParser:
 20    parser = ArgShellParser()
 21    parser.add_argument(
 22        "files", type=str, nargs="*", help=""" List of files to stage and commit. """
 23    )
 24    parser.add_argument(
 25        "-m",
 26        "--message",
 27        type=str,
 28        required=True,
 29        help=""" The commit message to use. """,
 30    )
 31    parser.add_argument(
 32        "-r",
 33        "--recursive",
 34        action="store_true",
 35        help=""" If a file name is not found in the current working directory,
 36        search for it in subfolders. This avoids having to type paths to files in subfolders,
 37        but if you have multiple files in different subfolders with the same name that have changes they
 38        will all be staged and committed.""",
 39    )
 40    return parser
 41
 42
 43def amend_files_parser() -> ArgShellParser:
 44    parser = ArgShellParser()
 45    parser.add_argument(
 46        "-f",
 47        "--files",
 48        type=str,
 49        nargs="*",
 50        help=""" List of files to stage and commit. """,
 51    )
 52    parser.add_argument(
 53        "-r",
 54        "--recursive",
 55        action="store_true",
 56        help=""" If a file name is not found in the current working directory,
 57        search for it in subfolders. This avoids having to type paths to files in subfolders,
 58        but if you have multiple files in different subfolders with the same name that have changes they
 59        will all be staged and committed.""",
 60    )
 61    return parser
 62
 63
 64def delete_branch_parser() -> ArgShellParser:
 65    parser = ArgShellParser()
 66    parser.add_argument(
 67        "branch", type=str, help=""" The name of the branch to delete. """
 68    )
 69    parser.add_argument(
 70        "-r",
 71        "--remote",
 72        action="store_true",
 73        help=""" Delete the remote and remote-tracking branches along with the local branch.
 74        By default only the local branch is deleted.""",
 75    )
 76    return parser
 77
 78
 79def recurse_files(filenames: list[str]) -> list[str | Pathier]:
 80    files = []
 81    for filename in filenames:
 82        if not Pathier(filename).exists():
 83            results = list(Pathier.cwd().rglob(f"{filename}"))
 84            if not results:
 85                print(f"WARNING: Could not find any files with name {filename}")
 86            else:
 87                files.extend(results)
 88        else:
 89            files.append(filename)
 90    return files
 91
 92
 93def files_postparser(args: Namespace) -> Namespace:
 94    if args.recursive:
 95        args.files = recurse_files(args.files)
 96    return args
 97
 98
 99class GitBetter(ArgShell):
100    """GitBetter Shell."""
101
102    intro = "Starting gitbetter...\nEnter 'help' or '?' for command help."
103    prompt = "gitbetter>"
104
105    def do_cd(self, path: str):
106        """Change current working directory to `path`."""
107        os.chdir(path)
108
109    def do_cwd(self, _: str):
110        """Print the current working directory."""
111        print(Pathier.cwd())
112
113    def do_git(self, arg: str):
114        """Directly execute `git {arg}`.
115
116        i.e. You can still do everything directly invoking git can do."""
117        git.execute(arg)
118
119    def do_new_repo(self, _: str):
120        """Create a new git repo in this directory."""
121        git.new_repo()
122
123    def do_new_branch(self, name: str):
124        """Create and switch to a new branch named after the supplied arg."""
125        git.create_new_branch(name)
126
127    @with_parser(new_remote_parser)
128    def do_new_remote(self, args: Namespace):
129        """Create a remote GitHub repository for this repo.
130
131        GitHub CLI must be installed and configured for this to work."""
132        name = Pathier.cwd().stem
133        git.create_remote(name, args.public)
134
135    def do_initcommit(self, _: str):
136        """Stage and commit all files with message "Initial Commit"."""
137        git.initcommit()
138
139    def do_undo(self, _: str):
140        """Undo all uncommitted changes."""
141        git.undo()
142
143    @with_parser(amend_files_parser, [files_postparser])
144    def do_add(self, args: Namespace):
145        """Stage a list of files.
146        If no files are given, all files will be added."""
147        git.add(None if not args.files else args.files)
148
149    def do_commit(self, message: str):
150        """Commit staged files with this message."""
151        git.commit(f'-m "{message}"')
152
153    @with_parser(commit_files_parser, [files_postparser])
154    def do_commitf(self, args: Namespace):
155        """Stage and commit a list of files."""
156        git.commit_files(args.files, args.message)
157
158    def do_commit_all(self, message: str):
159        """Stage and commit all files with this message."""
160        git.add()
161        git.commit(f"-m {message}")
162
163    def do_switch(self, branch_name: str):
164        """Switch to this branch."""
165        git.switch_branch(branch_name)
166
167    def do_add_url(self, url: str):
168        """Add remote url for repo and push repo."""
169        git.add_remote_url(url)
170        git.push("-u origin main")
171
172    def do_push_new(self, branch_name: str):
173        """Push this new branch to origin with -u flag."""
174        git.push_new_branch(branch_name)
175
176    def do_push(self, args: str):
177        """Execute `git push`.
178
179        `args` can be any additional args that `git push` accepts."""
180        git.push(args)
181
182    def do_pull(self, args: str):
183        """Execute `git pull`.
184
185        `args` can be any additional args that `git pull` accepts."""
186        git.pull(args)
187
188    def do_list_branches(self, _: str):
189        """Show local and remote branches."""
190        git.list_branches()
191
192    def do_loggy(self, _: str):
193        """Execute `git --oneline --name-only --abbrev-commit --graph`."""
194        git.loggy()
195
196    def do_merge(self, branch_name: str):
197        """Merge supplied `branch_name` with the currently active branch."""
198        git.merge(branch_name)
199
200    def do_tag(self, tag_id: str):
201        """Tag current commit with `tag_id`."""
202        git.tag(tag_id)
203
204    @with_parser(amend_files_parser, [files_postparser])
205    def do_amend(self, args: Namespace):
206        """Stage files and add to previous commit."""
207        git.amend(args.files)
208
209    @with_parser(delete_branch_parser)
210    def do_delete_branch(self, args: Namespace):
211        """Delete branch."""
212        git.delete_branch(args.branch, not args.remote)
213
214    def do_pull_branch(self, branch: str):
215        """Pull this branch from the origin."""
216        git.pull_branch(branch)
217
218    def do_ignore(self, patterns: str):
219        """Add the list of patterns to `.gitignore`."""
220        patterns = "\n".join(patterns.split())
221        path = Pathier(".gitignore")
222        path.append("\n")
223        path.append(patterns, False)
224
225    def do_make_private(self, owner: str):
226        """Make the GitHub remote for this repo private.
227
228        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
229
230        This repo must exist and GitHub CLI must be installed and configured."""
231        git.make_private(owner, Pathier.cwd().stem)
232
233    def do_make_public(self, owner: str):
234        """Make the GitHub remote for this repo public.
235
236        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
237
238        This repo must exist and GitHub CLI must be installed and configured."""
239        git.make_public(owner, Pathier.cwd().stem)
240
241    def do_delete_github(self, owner: str):
242        """Delete this repo from GitHub.
243
244        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
245
246        GitHub CLI must be installed and configured."""
247        git.delete_remote(owner, Pathier.cwd().stem)
248
249
250def main():
251    GitBetter().cmdloop()
252
253
254if __name__ == "__main__":
255    main()
def new_remote_parser() -> argshell.argshell.ArgShellParser:
10def new_remote_parser() -> ArgShellParser:
11    parser = ArgShellParser()
12    parser.add_argument(
13        "--public",
14        action="store_true",
15        help=""" Set the new remote visibility as public. Defaults to private. """,
16    )
17    return parser
def commit_files_parser() -> argshell.argshell.ArgShellParser:
20def commit_files_parser() -> ArgShellParser:
21    parser = ArgShellParser()
22    parser.add_argument(
23        "files", type=str, nargs="*", help=""" List of files to stage and commit. """
24    )
25    parser.add_argument(
26        "-m",
27        "--message",
28        type=str,
29        required=True,
30        help=""" The commit message to use. """,
31    )
32    parser.add_argument(
33        "-r",
34        "--recursive",
35        action="store_true",
36        help=""" If a file name is not found in the current working directory,
37        search for it in subfolders. This avoids having to type paths to files in subfolders,
38        but if you have multiple files in different subfolders with the same name that have changes they
39        will all be staged and committed.""",
40    )
41    return parser
def amend_files_parser() -> argshell.argshell.ArgShellParser:
44def amend_files_parser() -> ArgShellParser:
45    parser = ArgShellParser()
46    parser.add_argument(
47        "-f",
48        "--files",
49        type=str,
50        nargs="*",
51        help=""" List of files to stage and commit. """,
52    )
53    parser.add_argument(
54        "-r",
55        "--recursive",
56        action="store_true",
57        help=""" If a file name is not found in the current working directory,
58        search for it in subfolders. This avoids having to type paths to files in subfolders,
59        but if you have multiple files in different subfolders with the same name that have changes they
60        will all be staged and committed.""",
61    )
62    return parser
def delete_branch_parser() -> argshell.argshell.ArgShellParser:
65def delete_branch_parser() -> ArgShellParser:
66    parser = ArgShellParser()
67    parser.add_argument(
68        "branch", type=str, help=""" The name of the branch to delete. """
69    )
70    parser.add_argument(
71        "-r",
72        "--remote",
73        action="store_true",
74        help=""" Delete the remote and remote-tracking branches along with the local branch.
75        By default only the local branch is deleted.""",
76    )
77    return parser
def recurse_files(filenames: list[str]) -> list[str | pathier.pathier.Pathier]:
80def recurse_files(filenames: list[str]) -> list[str | Pathier]:
81    files = []
82    for filename in filenames:
83        if not Pathier(filename).exists():
84            results = list(Pathier.cwd().rglob(f"{filename}"))
85            if not results:
86                print(f"WARNING: Could not find any files with name {filename}")
87            else:
88                files.extend(results)
89        else:
90            files.append(filename)
91    return files
def files_postparser(args: argshell.argshell.Namespace) -> argshell.argshell.Namespace:
94def files_postparser(args: Namespace) -> Namespace:
95    if args.recursive:
96        args.files = recurse_files(args.files)
97    return args
class GitBetter(argshell.argshell.ArgShell):
100class GitBetter(ArgShell):
101    """GitBetter Shell."""
102
103    intro = "Starting gitbetter...\nEnter 'help' or '?' for command help."
104    prompt = "gitbetter>"
105
106    def do_cd(self, path: str):
107        """Change current working directory to `path`."""
108        os.chdir(path)
109
110    def do_cwd(self, _: str):
111        """Print the current working directory."""
112        print(Pathier.cwd())
113
114    def do_git(self, arg: str):
115        """Directly execute `git {arg}`.
116
117        i.e. You can still do everything directly invoking git can do."""
118        git.execute(arg)
119
120    def do_new_repo(self, _: str):
121        """Create a new git repo in this directory."""
122        git.new_repo()
123
124    def do_new_branch(self, name: str):
125        """Create and switch to a new branch named after the supplied arg."""
126        git.create_new_branch(name)
127
128    @with_parser(new_remote_parser)
129    def do_new_remote(self, args: Namespace):
130        """Create a remote GitHub repository for this repo.
131
132        GitHub CLI must be installed and configured for this to work."""
133        name = Pathier.cwd().stem
134        git.create_remote(name, args.public)
135
136    def do_initcommit(self, _: str):
137        """Stage and commit all files with message "Initial Commit"."""
138        git.initcommit()
139
140    def do_undo(self, _: str):
141        """Undo all uncommitted changes."""
142        git.undo()
143
144    @with_parser(amend_files_parser, [files_postparser])
145    def do_add(self, args: Namespace):
146        """Stage a list of files.
147        If no files are given, all files will be added."""
148        git.add(None if not args.files else args.files)
149
150    def do_commit(self, message: str):
151        """Commit staged files with this message."""
152        git.commit(f'-m "{message}"')
153
154    @with_parser(commit_files_parser, [files_postparser])
155    def do_commitf(self, args: Namespace):
156        """Stage and commit a list of files."""
157        git.commit_files(args.files, args.message)
158
159    def do_commit_all(self, message: str):
160        """Stage and commit all files with this message."""
161        git.add()
162        git.commit(f"-m {message}")
163
164    def do_switch(self, branch_name: str):
165        """Switch to this branch."""
166        git.switch_branch(branch_name)
167
168    def do_add_url(self, url: str):
169        """Add remote url for repo and push repo."""
170        git.add_remote_url(url)
171        git.push("-u origin main")
172
173    def do_push_new(self, branch_name: str):
174        """Push this new branch to origin with -u flag."""
175        git.push_new_branch(branch_name)
176
177    def do_push(self, args: str):
178        """Execute `git push`.
179
180        `args` can be any additional args that `git push` accepts."""
181        git.push(args)
182
183    def do_pull(self, args: str):
184        """Execute `git pull`.
185
186        `args` can be any additional args that `git pull` accepts."""
187        git.pull(args)
188
189    def do_list_branches(self, _: str):
190        """Show local and remote branches."""
191        git.list_branches()
192
193    def do_loggy(self, _: str):
194        """Execute `git --oneline --name-only --abbrev-commit --graph`."""
195        git.loggy()
196
197    def do_merge(self, branch_name: str):
198        """Merge supplied `branch_name` with the currently active branch."""
199        git.merge(branch_name)
200
201    def do_tag(self, tag_id: str):
202        """Tag current commit with `tag_id`."""
203        git.tag(tag_id)
204
205    @with_parser(amend_files_parser, [files_postparser])
206    def do_amend(self, args: Namespace):
207        """Stage files and add to previous commit."""
208        git.amend(args.files)
209
210    @with_parser(delete_branch_parser)
211    def do_delete_branch(self, args: Namespace):
212        """Delete branch."""
213        git.delete_branch(args.branch, not args.remote)
214
215    def do_pull_branch(self, branch: str):
216        """Pull this branch from the origin."""
217        git.pull_branch(branch)
218
219    def do_ignore(self, patterns: str):
220        """Add the list of patterns to `.gitignore`."""
221        patterns = "\n".join(patterns.split())
222        path = Pathier(".gitignore")
223        path.append("\n")
224        path.append(patterns, False)
225
226    def do_make_private(self, owner: str):
227        """Make the GitHub remote for this repo private.
228
229        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
230
231        This repo must exist and GitHub CLI must be installed and configured."""
232        git.make_private(owner, Pathier.cwd().stem)
233
234    def do_make_public(self, owner: str):
235        """Make the GitHub remote for this repo public.
236
237        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
238
239        This repo must exist and GitHub CLI must be installed and configured."""
240        git.make_public(owner, Pathier.cwd().stem)
241
242    def do_delete_github(self, owner: str):
243        """Delete this repo from GitHub.
244
245        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
246
247        GitHub CLI must be installed and configured."""
248        git.delete_remote(owner, Pathier.cwd().stem)

GitBetter Shell.

def do_cd(self, path: str):
106    def do_cd(self, path: str):
107        """Change current working directory to `path`."""
108        os.chdir(path)

Change current working directory to path.

def do_cwd(self, _: str):
110    def do_cwd(self, _: str):
111        """Print the current working directory."""
112        print(Pathier.cwd())

Print the current working directory.

def do_git(self, arg: str):
114    def do_git(self, arg: str):
115        """Directly execute `git {arg}`.
116
117        i.e. You can still do everything directly invoking git can do."""
118        git.execute(arg)

Directly execute git {arg}.

i.e. You can still do everything directly invoking git can do.

def do_new_repo(self, _: str):
120    def do_new_repo(self, _: str):
121        """Create a new git repo in this directory."""
122        git.new_repo()

Create a new git repo in this directory.

def do_new_branch(self, name: str):
124    def do_new_branch(self, name: str):
125        """Create and switch to a new branch named after the supplied arg."""
126        git.create_new_branch(name)

Create and switch to a new branch named after the supplied arg.

@with_parser(new_remote_parser)
def do_new_remote(self, args: argshell.argshell.Namespace):
128    @with_parser(new_remote_parser)
129    def do_new_remote(self, args: Namespace):
130        """Create a remote GitHub repository for this repo.
131
132        GitHub CLI must be installed and configured for this to work."""
133        name = Pathier.cwd().stem
134        git.create_remote(name, args.public)

Create a remote GitHub repository for this repo.

GitHub CLI must be installed and configured for this to work.

def do_initcommit(self, _: str):
136    def do_initcommit(self, _: str):
137        """Stage and commit all files with message "Initial Commit"."""
138        git.initcommit()

Stage and commit all files with message "Initial Commit".

def do_undo(self, _: str):
140    def do_undo(self, _: str):
141        """Undo all uncommitted changes."""
142        git.undo()

Undo all uncommitted changes.

@with_parser(amend_files_parser, [files_postparser])
def do_add(self, args: argshell.argshell.Namespace):
144    @with_parser(amend_files_parser, [files_postparser])
145    def do_add(self, args: Namespace):
146        """Stage a list of files.
147        If no files are given, all files will be added."""
148        git.add(None if not args.files else args.files)

Stage a list of files. If no files are given, all files will be added.

def do_commit(self, message: str):
150    def do_commit(self, message: str):
151        """Commit staged files with this message."""
152        git.commit(f'-m "{message}"')

Commit staged files with this message.

@with_parser(commit_files_parser, [files_postparser])
def do_commitf(self, args: argshell.argshell.Namespace):
154    @with_parser(commit_files_parser, [files_postparser])
155    def do_commitf(self, args: Namespace):
156        """Stage and commit a list of files."""
157        git.commit_files(args.files, args.message)

Stage and commit a list of files.

def do_commit_all(self, message: str):
159    def do_commit_all(self, message: str):
160        """Stage and commit all files with this message."""
161        git.add()
162        git.commit(f"-m {message}")

Stage and commit all files with this message.

def do_switch(self, branch_name: str):
164    def do_switch(self, branch_name: str):
165        """Switch to this branch."""
166        git.switch_branch(branch_name)

Switch to this branch.

def do_add_url(self, url: str):
168    def do_add_url(self, url: str):
169        """Add remote url for repo and push repo."""
170        git.add_remote_url(url)
171        git.push("-u origin main")

Add remote url for repo and push repo.

def do_push_new(self, branch_name: str):
173    def do_push_new(self, branch_name: str):
174        """Push this new branch to origin with -u flag."""
175        git.push_new_branch(branch_name)

Push this new branch to origin with -u flag.

def do_push(self, args: str):
177    def do_push(self, args: str):
178        """Execute `git push`.
179
180        `args` can be any additional args that `git push` accepts."""
181        git.push(args)

Execute git push.

args can be any additional args that git push accepts.

def do_pull(self, args: str):
183    def do_pull(self, args: str):
184        """Execute `git pull`.
185
186        `args` can be any additional args that `git pull` accepts."""
187        git.pull(args)

Execute git pull.

args can be any additional args that git pull accepts.

def do_list_branches(self, _: str):
189    def do_list_branches(self, _: str):
190        """Show local and remote branches."""
191        git.list_branches()

Show local and remote branches.

def do_loggy(self, _: str):
193    def do_loggy(self, _: str):
194        """Execute `git --oneline --name-only --abbrev-commit --graph`."""
195        git.loggy()

Execute git --oneline --name-only --abbrev-commit --graph.

def do_merge(self, branch_name: str):
197    def do_merge(self, branch_name: str):
198        """Merge supplied `branch_name` with the currently active branch."""
199        git.merge(branch_name)

Merge supplied branch_name with the currently active branch.

def do_tag(self, tag_id: str):
201    def do_tag(self, tag_id: str):
202        """Tag current commit with `tag_id`."""
203        git.tag(tag_id)

Tag current commit with tag_id.

@with_parser(amend_files_parser, [files_postparser])
def do_amend(self, args: argshell.argshell.Namespace):
205    @with_parser(amend_files_parser, [files_postparser])
206    def do_amend(self, args: Namespace):
207        """Stage files and add to previous commit."""
208        git.amend(args.files)

Stage files and add to previous commit.

@with_parser(delete_branch_parser)
def do_delete_branch(self, args: argshell.argshell.Namespace):
210    @with_parser(delete_branch_parser)
211    def do_delete_branch(self, args: Namespace):
212        """Delete branch."""
213        git.delete_branch(args.branch, not args.remote)

Delete branch.

def do_pull_branch(self, branch: str):
215    def do_pull_branch(self, branch: str):
216        """Pull this branch from the origin."""
217        git.pull_branch(branch)

Pull this branch from the origin.

def do_ignore(self, patterns: str):
219    def do_ignore(self, patterns: str):
220        """Add the list of patterns to `.gitignore`."""
221        patterns = "\n".join(patterns.split())
222        path = Pathier(".gitignore")
223        path.append("\n")
224        path.append(patterns, False)

Add the list of patterns to .gitignore.

def do_make_private(self, owner: str):
226    def do_make_private(self, owner: str):
227        """Make the GitHub remote for this repo private.
228
229        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
230
231        This repo must exist and GitHub CLI must be installed and configured."""
232        git.make_private(owner, Pathier.cwd().stem)

Make the GitHub remote for this repo private.

Expects an argument for the repo owner, i.e. the OWNER in github.com/{OWNER}/{repo-name}

This repo must exist and GitHub CLI must be installed and configured.

def do_make_public(self, owner: str):
234    def do_make_public(self, owner: str):
235        """Make the GitHub remote for this repo public.
236
237        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
238
239        This repo must exist and GitHub CLI must be installed and configured."""
240        git.make_public(owner, Pathier.cwd().stem)

Make the GitHub remote for this repo public.

Expects an argument for the repo owner, i.e. the OWNER in github.com/{OWNER}/{repo-name}

This repo must exist and GitHub CLI must be installed and configured.

def do_delete_github(self, owner: str):
242    def do_delete_github(self, owner: str):
243        """Delete this repo from GitHub.
244
245        Expects an argument for the repo owner, i.e. the `OWNER` in `github.com/{OWNER}/{repo-name}`
246
247        GitHub CLI must be installed and configured."""
248        git.delete_remote(owner, Pathier.cwd().stem)

Delete this repo from GitHub.

Expects an argument for the repo owner, i.e. the OWNER in github.com/{OWNER}/{repo-name}

GitHub CLI must be installed and configured.

Inherited Members
cmd.Cmd
Cmd
precmd
postcmd
preloop
postloop
parseline
onecmd
emptyline
default
completedefault
completenames
complete
get_names
complete_help
print_topics
columnize
argshell.argshell.ArgShell
do_quit
do_help
cmdloop
def main():
251def main():
252    GitBetter().cmdloop()