diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4a4cd4b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3 + +WORKDIR /usr/src/git-sim + +RUN apt update + +RUN apt -y install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg + +RUN pip3 install manim + +RUN pip3 install git-sim + +ENTRYPOINT [ "git-sim" ] \ No newline at end of file diff --git a/README.md b/README.md index 7014a1d..bc8ec35 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,12 @@ Example: `$ git-sim merge ` - Supported commands: `log`, `status`, `add`, `restore`, `commit`, `stash`, `branch`, `tag`, `reset`, `revert`, `merge`, `rebase`, `cherry-pick` - Generate an animated video (.mp4) instead of a static image using the `--animate` flag (note: significant performance slowdown, it is recommended to use `--low-quality` to speed up testing and remove when ready to generate presentation-quality video) - Choose between dark mode (default) and light mode +- Specify output formats of either jpg, png, mp4, or webm - Animation only: Add custom branded intro/outro sequences if desired - Animation only: Speed up or slow down animation speed as desired ## Quickstart +Note: If you prefer to install git-sim with Docker, skip steps (1) and (2) here and jump to the [Docker installation](#docker-installation) section below, then come back here to step (3). 1) **Install Manim and its dependencies for your OS / environment:** - [Install Manim on Windows](https://docs.manim.community/en/stable/installation/windows.html) @@ -61,13 +63,41 @@ $ git-sim [global options] [subcommand options] 5) Simulated output will be created as a `.jpg` file. Output files are named using the subcommand executed combined with a timestamp, and by default are stored in a subdirectory called `git-sim_media/`. The location of this subdirectory is customizable using the command line flag `--media-dir=path/to/output`. Note that when the `--animate` global flag is used, render times will be much longer and a `.mp4` video output file will be produced. -6) See global help for list of global options/flags and subcommands: +6) For convenience, environment variables can be set for any global command-line option available in git-sim. All environment variables start with `git_sim_` followed by the name of the option. + +For example, the `--media-dir` option can be set as an environment variable like: + +```console +$ export git_sim_media_dir=~/Desktop +``` + +Similarly, the `--speed` option can be set like: + +```console +$ export git_sim_speed=2 +``` + +Boolean flags can be set like: + +```console +$ export git_sim_light_mode=true +``` + +In general: + +```console +$ export git_sim_option_name=option_value +``` + +Explicitly specifying options at the command-line takes precedence over the corresponding environment variable values. + +7) See global help for list of global options/flags and subcommands: ```console $ git-sim -h ``` -7) See subcommand help for list of options/flags for a specific subcommand: +8) See subcommand help for list of options/flags for a specific subcommand: ```console $ git-sim -h @@ -89,14 +119,15 @@ The `[global options]` apply to the overarching `git-sim` simulation itself, inc `--light-mode`: Use a light mode color scheme instead of default dark mode. `--animate`: Instead of outputting a static image, animate the Git command behavior in a .mp4 video. -`--disable-auto-open, -d`: Disable the automatic opening of the image/video file after generation. +`-d`: Disable the automatic opening of the image/video file after generation. Useful to avoid errors in console mode with no GUI. `--reverse, -r`: Display commit history in the reverse direction. -`--img-format`: Output format for the image file, i.e. `jpg` or `png`. Default output format is `jpg`. +`--img-format`: Output format for the image file, i.e. `jpg` or `png`. Default output format is `jpg`. +`--stdout`: Write raw image data to stdout while suppressing all other program output. Animation-only global options (to be used in conjunction with `--animate`): `--video-format`: Output format for the video file, i.e. `mp4` or `webm`. Default output format is `mp4`. -`--speed=n`: Set the multiple of animation speed of the output simulation, `n` can be an integer or float, default is 1. +`--speed=n`: Set the multiple of animation speed of the output simulation, `n` can be an integer or float, default is 1.5. `--low-quality`: Render the animation in low quality to speed up creation time, recommended for non-presentation use. `--show-intro`: Add an intro sequence with custom logo and title. `--show-outro`: Add an outro sequence with custom logo and text. @@ -361,7 +392,7 @@ Optionally, set the environment variable `git_sim_media_dir` to set a global def $ export git_sim_media_dir=path/to/media/directory $ git-sim status ``` -Note: `--media-dir` takes precedence over the environment variable. If you set the environment and still provide the argument, you'll find the media in the path provided by `--media-dir`. +Note: `--media-dir` takes precedence over the environment variable. If you set the environment variable and still provide the argument, you'll find the media in the path provided by `--media-dir`. Generate output video in low quality to speed up rendering time (useful for repeated testing, must include `--animate`): @@ -376,6 +407,32 @@ See **Quickstart** section for details on installing manim and other dependencie $ pip3 install git-sim ``` +## Docker installation + +1) Clone down the git-sim repository: + +```console +$ git clone https://github.com/initialcommit-com/git-sim.git +``` + +2) Browse into the `git-sim` folder and build the Docker image: + +```console +$ docker build -t git-sim . +``` + +3) Run git-sim commands as follows: + - Windows: `docker run --rm -v %cd%:/usr/src/git-sim git-sim [global options] [subcommand options]` + - MacOS / Linux: `docker run --rm -v $(pwd):/usr/src/git-sim git-sim [global options] [subcommand options]` + +Optional: On MacOS / Linux / or GitBash in Windows, create an alias for the long docker command so your can run it as a normal `git-sim` command. To do so add the following line to your `.bashrc` or equivalent, then restart your terminal: + +```bash +git-sim() { docker run --rm -v $(pwd):/usr/src/git-sim git-sim "$@" } +``` + +This will enable you to run git-sim subcommands as [described above](#commands). + ## Learn More Learn more about this tool on the [git-sim project page](https://initialcommit.com/tools/git-sim). diff --git a/git_sim/__main__.py b/git_sim/__main__.py index 93291a4..17f970d 100644 --- a/git_sim/__main__.py +++ b/git_sim/__main__.py @@ -1,372 +1,170 @@ -import argparse -import datetime -import os import pathlib -import subprocess +import typer +import os import sys +import datetime import time -from argparse import Namespace -from typing import Type - -import cv2 import git -from manim import WHITE, config -from manim.utils.file_ops import open_file as open_media_file - -from git_sim.git_sim_add import GitSimAdd -from git_sim.git_sim_base_command import GitSimBaseCommand -from git_sim.git_sim_branch import GitSimBranch -from git_sim.git_sim_cherrypick import GitSimCherryPick -from git_sim.git_sim_commit import GitSimCommit -from git_sim.git_sim_log import GitSimLog -from git_sim.git_sim_merge import GitSimMerge -from git_sim.git_sim_rebase import GitSimRebase -from git_sim.git_sim_reset import GitSimReset -from git_sim.git_sim_restore import GitSimRestore -from git_sim.git_sim_revert import GitSimRevert -from git_sim.git_sim_stash import GitSimStash -from git_sim.git_sim_status import GitSimStatus -from git_sim.git_sim_tag import GitSimTag - -def get_scene_for_command(args: Namespace) -> Type[GitSimBaseCommand]: - - if args.subcommand == "log": - return GitSimLog - elif args.subcommand == "status": - return GitSimStatus - elif args.subcommand == "add": - return GitSimAdd - elif args.subcommand == "restore": - return GitSimRestore - elif args.subcommand == "commit": - return GitSimCommit - elif args.subcommand == "stash": - return GitSimStash - elif args.subcommand == "branch": - return GitSimBranch - elif args.subcommand == "tag": - return GitSimTag - elif args.subcommand == "reset": - return GitSimReset - elif args.subcommand == "revert": - return GitSimRevert - elif args.subcommand == "merge": - return GitSimMerge - elif args.subcommand == "rebase": - return GitSimRebase - elif args.subcommand == "cherry-pick": - return GitSimCherryPick - - raise NotImplementedError(f"command '{args.subcommand}' is not yet implemented.") - - -def main(): - parser = argparse.ArgumentParser( - "git-sim", formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - parser.add_argument( - "--title", - help="Custom title to display at the beginning of the animation", - type=str, - default="Git Sim, by initialcommit.com", - ) - parser.add_argument( - "--logo", +import git_sim.add +import git_sim.branch +import git_sim.cherrypick +import git_sim.commit +import git_sim.log +import git_sim.merge +import git_sim.rebase +import git_sim.reset +import git_sim.restore +import git_sim.revert +import git_sim.stash +import git_sim.status +import git_sim.tag +from git_sim.settings import ImgFormat, VideoFormat, settings +from manim import config, WHITE + +app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}) + + +@app.callback(no_args_is_help=True) +def main( + ctx: typer.Context, + animate: bool = typer.Option( + settings.animate, + help="Animate the simulation and output as an mp4 video", + ), + auto_open: bool = typer.Option( + settings.auto_open, + "--auto-open", + " /-d", + help="Enable / disable the automatic opening of the image/video file after generation", + ), + img_format: ImgFormat = typer.Option( + settings.img_format, + help="Output format for the image files.", + ), + light_mode: bool = typer.Option( + settings.light_mode, + "--light-mode", + help="Enable light-mode with white background", + ), + logo: pathlib.Path = typer.Option( + settings.logo, help="The path to a custom logo to use in the animation intro/outro", - type=str, - default=os.path.join(str(pathlib.Path(__file__).parent.resolve()), "logo.png"), - ) - parser.add_argument( - "--outro-top-text", - help="Custom text to display above the logo during the outro", - type=str, - default="Thanks for using Initial Commit!", - ) - parser.add_argument( - "--outro-bottom-text", - help="Custom text to display below the logo during the outro", - type=str, - default="Learn more at initialcommit.com", - ) - parser.add_argument( - "--show-intro", - help="Add an intro sequence with custom logo and title", - action="store_true", - ) - parser.add_argument( - "--show-outro", - help="Add an outro sequence with custom logo and text", - action="store_true", - ) - parser.add_argument( - "--media-dir", - help="The path to output the animation data and video file", - type=str, - default=".", - ) - parser.add_argument( + ), + low_quality: bool = typer.Option( + settings.low_quality, "--low-quality", help="Render output video in low quality, useful for faster testing", - action="store_true", - ) - parser.add_argument( - "--light-mode", - help="Enable light-mode with white background", - action="store_true", - ) - parser.add_argument( - "--speed", - help="A multiple of the standard 1x animation speed (ex: 2 = twice as fast, 0.5 = half as fast)", - type=float, - default=1.5, - ) - parser.add_argument( - "--animate", - help="Animate the simulation and output as an mp4 video", - action="store_true", - ) - parser.add_argument( - "--max-branches-per-commit", + ), + max_branches_per_commit: int = typer.Option( + settings.max_branches_per_commit, help="Maximum number of branch labels to display for each commit", - type=int, - default=1, - ) - parser.add_argument( - "--max-tags-per-commit", + ), + max_tags_per_commit: int = typer.Option( + settings.max_tags_per_commit, help="Maximum number of tags to display for each commit", - type=int, - default=1, - ) - parser.add_argument( - "-d", - "--disable-auto-open", - help="Disable the automatic opening of the image/video file after generation", - action="store_true", - ) - parser.add_argument( - "-r", + ), + media_dir: pathlib.Path = typer.Option( + settings.media_dir, + help="The path to output the animation data and video file", + ), + outro_bottom_text: str = typer.Option( + settings.outro_bottom_text, + help="Custom text to display below the logo during the outro", + ), + outro_top_text: str = typer.Option( + settings.outro_top_text, + help="Custom text to display above the logo during the outro", + ), + reverse: bool = typer.Option( + settings.reverse, "--reverse", + "-r", help="Display commit history in the reverse direction", - action="store_true", - ) - parser.add_argument( - "--video-format", - help="Output format for the animation files. Supports mp4 (default) and webm", - type=str, - default="mp4", - choices=["mp4", "webm"], - ) - parser.add_argument( - "--img-format", - help="Output format for the image files. Supports jpg (default) and png", - type=str, - default="jpg", - choices=["jpg", "png"], - ) - - subparsers = parser.add_subparsers(dest="subcommand", help="subcommand help") - - log = subparsers.add_parser("log", help="log -h") - log.add_argument( - "--commits", - help="The number of commits to display in the simulated log output", - type=int, - default=5, - choices=range(1, 13), - ) - - status = subparsers.add_parser("status", help="status -h") - - add = subparsers.add_parser("add", help="add -h") - add.add_argument( - "name", - nargs="+", - help="The names of one or more files to add to Git's staging area", - type=str, - ) - - restore = subparsers.add_parser("restore", help="restore -h") - restore.add_argument( - "name", nargs="+", help="The names of one or more files to restore", type=str - ) - - commit = subparsers.add_parser("commit", help="commit -h") - commit.add_argument( - "-m", - "--message", - help="The commit message of the new commit", - type=str, - default="New commit", - ) - commit.add_argument( - "--amend", - help="Amend the last commit message, must be used with the -m flag", - action="store_true", - ) - - stash = subparsers.add_parser("stash", help="stash -h") - stash.add_argument( - "name", nargs="*", help="The name of the file to stash changes for", type=str - ) - - branch = subparsers.add_parser("branch", help="branch -h") - branch.add_argument("name", help="The name of the new branch", type=str) - - tag = subparsers.add_parser("tag", help="tag -h") - tag.add_argument("name", help="The name of the new tag", type=str) - - reset = subparsers.add_parser("reset", help="reset -h") - reset.add_argument( - "commit", - nargs="?", - help="The ref (branch/tag), or commit ID to simulate reset to", - type=str, - default="HEAD", - ) - reset.add_argument( - "--mode", - help="Either mixed (default), soft, or hard", - type=str, - default="default", - ) - reset.add_argument( - "--soft", - help="Simulate a soft reset, shortcut for --mode=soft", - action="store_true", - ) - reset.add_argument( - "--mixed", - help="Simulate a mixed reset, shortcut for --mode=mixed", - action="store_true", - ) - reset.add_argument( - "--hard", - help="Simulate a soft reset, shortcut for --mode=hard", - action="store_true", - ) - - revert = subparsers.add_parser("revert", help="revert -h") - revert.add_argument( - "commit", - nargs="?", - help="The ref (branch/tag), or commit ID to simulate revert", - type=str, - default="HEAD", - ) - - merge = subparsers.add_parser("merge", help="merge -h") - merge.add_argument( - "branch", - nargs=1, - type=str, - help="The name of the branch to merge into the active checked-out branch", - ) - merge.add_argument( - "--no-ff", - help="Simulate creation of a merge commit in all cases, even when the merge could instead be resolved as a fast-forward", - action="store_true", - ) - - rebase = subparsers.add_parser("rebase", help="rebase -h") - rebase.add_argument( - "branch", - nargs=1, - type=str, - help="The branch to simulate rebasing the checked-out commit onto", - ) - - cherrypick = subparsers.add_parser("cherry-pick", help="cherry-pick -h") - cherrypick.add_argument( - "commit", - nargs=1, - type=str, - help="The ref (branch/tag), or commit ID to simulate cherry-pick onto active branch", - ) - cherrypick.add_argument( - "-e", - "--edit", - help="Specify a new commit message for the cherry-picked commit", - type=str, - ) - - if len(sys.argv) == 1: - parser.print_help() - sys.exit(1) - - args = parser.parse_args() + ), + show_intro: bool = typer.Option( + settings.show_intro, + help="Add an intro sequence with custom logo and title", + ), + show_outro: bool = typer.Option( + settings.show_outro, + help="Add an outro sequence with custom logo and text", + ), + speed: float = typer.Option( + settings.speed, + help="A multiple of the standard 1x animation speed (ex: 2 = twice as fast, 0.5 = half as fast)", + ), + title: str = typer.Option( + settings.title, + help="Custom title to display at the beginning of the animation", + ), + video_format: VideoFormat = typer.Option( + settings.video_format.value, + help="Output format for the animation files.", + case_sensitive=False, + ), + stdout: bool = typer.Option( + settings.stdout, + help="Write raw image data to stdout while suppressing all other program output", + ), +): + settings.animate = animate + settings.auto_open = auto_open + settings.img_format = img_format + settings.light_mode = light_mode + settings.logo = logo + settings.low_quality = low_quality + settings.max_branches_per_commit = max_branches_per_commit + settings.max_tags_per_commit = max_tags_per_commit + settings.media_dir = os.path.join(os.path.expanduser(media_dir), "git-sim_media") + settings.outro_bottom_text = outro_bottom_text + settings.outro_top_text = outro_top_text + settings.reverse = reverse + settings.show_intro = show_intro + settings.show_outro = show_outro + settings.speed = speed + settings.title = title + settings.video_format = video_format + settings.stdout = stdout if sys.platform == "linux" or sys.platform == "darwin": - repo_name = git.Repo(search_parent_directories=True).working_tree_dir.split( - "/" - )[-1] + repo_name = git.repo.Repo( + search_parent_directories=True + ).working_tree_dir.split("/")[-1] elif sys.platform == "win32": - repo_name = git.Repo(search_parent_directories=True).working_tree_dir.split( - "\\" - )[-1] + repo_name = git.repo.Repo( + search_parent_directories=True + ).working_tree_dir.split("\\")[-1] - config.media_dir = os.path.join(os.path.expanduser(args.media_dir), "git-sim_media") - config.verbosity = "ERROR" + settings.media_dir = os.path.join(settings.media_dir, repo_name) - # If the env variable is set and no argument provided, use the env variable value - if os.getenv("git_sim_media_dir") and args.media_dir == ".": - config.media_dir = os.path.join( - os.path.expanduser(os.getenv("git_sim_media_dir")), - "git-sim_media", - repo_name, - ) + config.media_dir = settings.media_dir + config.verbosity = "ERROR" - if args.low_quality: + if settings.low_quality: config.quality = "low_quality" - if args.light_mode: + if settings.light_mode: config.background_color = WHITE t = datetime.datetime.fromtimestamp(time.time()).strftime("%m-%d-%y_%H-%M-%S") - config.output_file = "git-sim-" + args.subcommand + "_" + t + ".mp4" - - scene_class = get_scene_for_command(args=args) - scene = scene_class(args=args) - scene.render() - - if args.video_format == "webm": - webm_file_path = str(scene.renderer.file_writer.movie_file_path)[:-3] + "webm" - cmd = f"ffmpeg -y -i {scene.renderer.file_writer.movie_file_path} -hide_banner -loglevel error -c:v libvpx-vp9 -crf 50 -b:v 0 -b:a 128k -c:a libopus {webm_file_path}" - print("Converting video output to .webm format...") - # Start ffmpeg conversion - p = subprocess.Popen(cmd, shell=True) - p.wait() - # if the conversion is successful, delete the .mp4 - if os.path.exists(webm_file_path): - os.remove(scene.renderer.file_writer.movie_file_path) - scene.renderer.file_writer.movie_file_path = webm_file_path + config.output_file = "git-sim-" + ctx.invoked_subcommand + "_" + t + ".mp4" - if not args.animate: - video = cv2.VideoCapture(str(scene.renderer.file_writer.movie_file_path)) - success, image = video.read() - if success: - image_file_name = ( - "git-sim-" + args.subcommand + "_" + t + "." + args.img_format - ) - image_file_path = os.path.join( - os.path.join(config.media_dir, "images"), image_file_name - ) - cv2.imwrite(image_file_path, image) - print("Output image location:", image_file_path) - else: - print("Output video location:", scene.renderer.file_writer.movie_file_path) - if not args.disable_auto_open: - try: - if not args.animate: - open_media_file(image_file_path) - else: - open_media_file(scene.renderer.file_writer.movie_file_path) - except FileNotFoundError: - print( - "Error automatically opening media, please manually open the image or video file to view." - ) +app.command()(git_sim.add.add) +app.command()(git_sim.branch.branch) +app.command()(git_sim.cherrypick.cherry_pick) +app.command()(git_sim.commit.commit) +app.command()(git_sim.log.log) +app.command()(git_sim.merge.merge) +app.command()(git_sim.rebase.rebase) +app.command()(git_sim.reset.reset) +app.command()(git_sim.restore.restore) +app.command()(git_sim.revert.revert) +app.command()(git_sim.stash.stash) +app.command()(git_sim.status.status) +app.command()(git_sim.tag.tag) if __name__ == "__main__": - main() + app() diff --git a/git_sim/git_sim_add.py b/git_sim/add.py similarity index 67% rename from git_sim/git_sim_add.py rename to git_sim/add.py index 0dc2e52..0d15e80 100644 --- a/git_sim/git_sim_add.py +++ b/git_sim/add.py @@ -1,35 +1,39 @@ import sys -from argparse import Namespace - import git import manim as m +import typer + +from typing import List +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimAdd(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.maxrefs = 2 +class Add(GitSimBaseCommand): + def __init__(self, files: List[str]): + super().__init__() self.hide_first_tag = True self.allow_no_commits = True + self.files = files try: self.selected_branches.append(self.repo.active_branch.name) except TypeError: pass - for name in self.args.name: - if name not in [x.a_path for x in self.repo.index.diff(None)] + [ + for file in self.files: + if file not in [x.a_path for x in self.repo.index.diff(None)] + [ z for z in self.repo.untracked_files ]: - print("git-sim error: No modified file with name: '" + name + "'") + print(f"git-sim error: No modified file with name: '{file}'") sys.exit() def construct(self): - print( - "Simulating: git " + self.args.subcommand + " " + " ".join(self.args.name) - ) + if not settings.stdout: + print( + f"{settings.INFO_STRING} {type(self).__name__.lower()} {' '.join(self.files)}" + ) self.show_intro() self.get_commits() @@ -49,12 +53,11 @@ def populate_zones( firstColumnArrowMap, secondColumnArrowMap, ): - for x in self.repo.index.diff(None): if "git-sim_media" not in x.a_path: secondColumnFileNames.add(x.a_path) - for name in self.args.name: - if name == x.a_path: + for file in self.files: + if file == x.a_path: thirdColumnFileNames.add(x.a_path) secondColumnArrowMap[x.a_path] = m.Arrow( stroke_width=3, color=self.fontColor @@ -71,9 +74,19 @@ def populate_zones( for z in self.repo.untracked_files: if "git-sim_media" not in z: firstColumnFileNames.add(z) - for name in self.args.name: - if name == z: + for file in self.files: + if file == z: thirdColumnFileNames.add(z) firstColumnArrowMap[z] = m.Arrow( stroke_width=3, color=self.fontColor ) + + +def add( + files: List[str] = typer.Argument( + default=None, + help="The names of one or more files to add to Git's staging area", + ) +): + scene = Add(files=files) + handle_animations(scene=scene) diff --git a/git_sim/animations.py b/git_sim/animations.py new file mode 100644 index 0000000..251ff91 --- /dev/null +++ b/git_sim/animations.py @@ -0,0 +1,66 @@ +import datetime +import inspect +import os +import subprocess +import sys +import time + +import cv2 +import git.repo +from manim import WHITE, Scene +from manim.utils.file_ops import open_file + +from git_sim.settings import settings + + +def handle_animations(scene: Scene) -> None: + scene.render() + + if settings.video_format == "webm": + webm_file_path = str(scene.renderer.file_writer.movie_file_path)[:-3] + "webm" + cmd = f"ffmpeg -y -i {scene.renderer.file_writer.movie_file_path} -hide_banner -loglevel error -c:v libvpx-vp9 -crf 50 -b:v 0 -b:a 128k -c:a libopus {webm_file_path}" + print("Converting video output to .webm format...") + # Start ffmpeg conversion + p = subprocess.Popen(cmd, shell=True) + p.wait() + # if the conversion is successful, delete the .mp4 + if os.path.exists(webm_file_path): + os.remove(scene.renderer.file_writer.movie_file_path) + scene.renderer.file_writer.movie_file_path = webm_file_path + + if not settings.animate: + video = cv2.VideoCapture(str(scene.renderer.file_writer.movie_file_path)) + success, image = video.read() + if success: + t = datetime.datetime.fromtimestamp(time.time()).strftime( + "%m-%d-%y_%H-%M-%S" + ) + image_file_name = ( + "git-sim-" + + inspect.stack()[1].function + + "_" + + t + + "." + + settings.img_format + ) + image_file_path = os.path.join( + os.path.join(settings.media_dir, "images"), image_file_name + ) + cv2.imwrite(image_file_path, image) + if not settings.stdout: + print("Output image location:", image_file_path) + if settings.stdout: + sys.stdout.buffer.write(cv2.imencode(".jpg", image)[1].tobytes()) + else: + print("Output video location:", scene.renderer.file_writer.movie_file_path) + + if settings.auto_open and not settings.stdout: + try: + if not settings.animate: + open_file(image_file_path) + else: + open_file(scene.renderer.file_writer.movie_file_path) + except FileNotFoundError: + print( + "Error automatically opening media, please manually open the image or video file to view." + ) diff --git a/git_sim/git_sim_branch.py b/git_sim/branch.py similarity index 56% rename from git_sim/git_sim_branch.py rename to git_sim/branch.py index 15b9033..f47b3c8 100644 --- a/git_sim/git_sim_branch.py +++ b/git_sim/branch.py @@ -1,16 +1,19 @@ -from argparse import Namespace - import manim as m +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimBranch(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class Branch(GitSimBaseCommand): + def __init__(self, name: str): + super().__init__() + self.name = name def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.name) + if not settings.stdout: + print(f"{settings.INFO_STRING} {type(self).__name__.lower()} {self.name}") self.show_intro() self.get_commits() @@ -19,7 +22,7 @@ def construct(self): self.scale_frame() branchText = m.Text( - self.args.name, + self.name, font="Monospace", font_size=20, color=self.fontColor, @@ -37,13 +40,23 @@ def construct(self): fullbranch = m.VGroup(branchRec, branchText) - if self.args.animate: - self.play(m.Create(fullbranch), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(fullbranch), run_time=1 / settings.speed) else: self.add(fullbranch) self.toFadeOut.add(branchRec, branchText) - self.drawnRefs[self.args.name] = fullbranch + self.drawnRefs[self.name] = fullbranch self.fadeout() self.show_outro() + + +def branch( + name: str = typer.Argument( + ..., + help="The name of the new branch", + ) +): + scene = Branch(name=name) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_cherrypick.py b/git_sim/cherrypick.py similarity index 51% rename from git_sim/git_sim_cherrypick.py rename to git_sim/cherrypick.py index dd01cb2..b5280a3 100644 --- a/git_sim/git_sim_cherrypick.py +++ b/git_sim/cherrypick.py @@ -1,28 +1,32 @@ import sys -from argparse import Namespace import git import manim as m +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimCherryPick(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class CherryPick(GitSimBaseCommand): + def __init__(self, commit: str, edit: str): + super().__init__() + self.commit = commit + self.edit = edit try: - git.repo.fun.rev_parse(self.repo, self.args.commit[0]) + git.repo.fun.rev_parse(self.repo, self.commit) except git.exc.BadName: print( "git-sim error: '" - + self.args.commit[0] + + self.commit + "' is not a valid Git ref or identifier." ) sys.exit(1) - if self.args.commit[0] in [branch.name for branch in self.repo.heads]: - self.selected_branches.append(self.args.commit[0]) + if self.commit in [branch.name for branch in self.repo.heads]: + self.selected_branches.append(self.commit) try: self.selected_branches.append(self.repo.active_branch.name) @@ -30,20 +34,18 @@ def __init__(self, args: Namespace): pass def construct(self): - print( - "Simulating: git " - + self.args.subcommand - + " " - + self.args.commit[0] - + ((' -e "' + self.args.edit + '"') if self.args.edit else "") - ) + if not settings.stdout: + print( + f"{settings.INFO_STRING} cherry-pick {self.commit}" + + ((' -e "' + self.edit + '"') if self.edit else "") + ) if self.repo.active_branch.name in self.repo.git.branch( - "--contains", self.args.commit[0] + "--contains", self.commit ): print( "git-sim error: Commit '" - + self.args.commit[0] + + self.commit + "' is already included in the history of active branch '" + self.repo.active_branch.name + "'." @@ -54,12 +56,12 @@ def construct(self): self.get_commits() self.parse_commits(self.commits[0]) self.orig_commits = self.commits - self.get_commits(start=self.args.commit[0]) + self.get_commits(start=self.commit) self.parse_commits(self.commits[0], shift=4 * m.DOWN) self.center_frame_on_commit(self.orig_commits[0]) self.setup_and_draw_parent( self.orig_commits[0], - self.args.edit if self.args.edit else self.commits[0].message, + self.edit if self.edit else self.commits[0].message, ) self.draw_arrow_between_commits(self.commits[0].hexsha, "abcdef") self.recenter_frame() @@ -67,3 +69,19 @@ def construct(self): self.reset_head_branch("abcdef") self.fadeout() self.show_outro() + + +def cherry_pick( + commit: str = typer.Argument( + ..., + help="The ref (branch/tag), or commit ID to simulate cherry-pick onto active branch", + ), + edit: str = typer.Option( + None, + "--edit", + "-e", + help="Specify a new commit message for the cherry-picked commit", + ), +): + scene = CherryPick(commit=commit, edit=edit) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_commit.py b/git_sim/commit.py similarity index 63% rename from git_sim/git_sim_commit.py rename to git_sim/commit.py index aec397a..e259498 100644 --- a/git_sim/git_sim_commit.py +++ b/git_sim/commit.py @@ -1,18 +1,22 @@ import sys -from argparse import Namespace import git import manim as m +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimCommit(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.maxrefs = 2 - self.defaultNumCommits = 4 if not self.args.amend else 5 - self.numCommits = 4 if not self.args.amend else 5 +class Commit(GitSimBaseCommand): + def __init__(self, message: str, amend: bool): + super().__init__() + self.message = message + self.amend = amend + + self.defaultNumCommits = 4 if not self.amend else 5 + self.numCommits = 4 if not self.amend else 5 self.hide_first_tag = True try: @@ -20,39 +24,38 @@ def __init__(self, args: Namespace): except TypeError: pass - if self.args.amend and self.args.message == "New commit": + if self.amend and self.message == "New commit": print( "git-sim error: The --amend flag must be used with the -m flag to specify the amended commit message." ) sys.exit(1) def construct(self): - print( - "Simulating: git " - + self.args.subcommand - + (" --amend" if self.args.amend else "") - + ' -m "' - + self.args.message - + '"' - ) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()} {'--amend ' if self.amend else ''}" + + '-m "' + + self.message + + '"' + ) self.show_intro() self.get_commits() - if self.args.amend: + if self.amend: tree = self.repo.tree() amended = git.Commit.create_from_tree( self.repo, tree, - self.args.message, + self.message, ) self.commits[0] = amended self.parse_commits(self.commits[self.i]) self.center_frame_on_commit(self.commits[0]) - if not self.args.amend: - self.setup_and_draw_parent(self.commits[0], self.args.message) + if not self.amend: + self.setup_and_draw_parent(self.commits[0], self.message) else: self.draw_ref(self.commits[0], self.drawnCommitIds[amended.hexsha]) self.draw_ref( @@ -65,7 +68,7 @@ def construct(self): self.recenter_frame() self.scale_frame() - if not self.args.amend: + if not self.amend: self.reset_head_branch("abcdef") self.vsplit_frame() self.setup_and_draw_zones( @@ -85,7 +88,6 @@ def populate_zones( firstColumnArrowMap, secondColumnArrowMap, ): - for x in self.repo.index.diff(None): if "git-sim_media" not in x.a_path: firstColumnFileNames.add(x.a_path) @@ -97,3 +99,19 @@ def populate_zones( secondColumnArrowMap[y.a_path] = m.Arrow( stroke_width=3, color=self.fontColor ) + + +def commit( + message: str = typer.Option( + "New commit", + "--message", + "-m", + help="The commit message of the new commit", + ), + amend: bool = typer.Option( + default=False, + help="Amend the last commit message, must be used with the --message flag", + ), +): + scene = Commit(message=message, amend=amend) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_base_command.py b/git_sim/git_sim_base_command.py index 22bbda1..5d1163f 100644 --- a/git_sim/git_sim_base_command.py +++ b/git_sim/git_sim_base_command.py @@ -1,20 +1,21 @@ import platform import sys -from argparse import Namespace import git import manim as m import numpy +from git.exc import GitCommandError, InvalidGitRepositoryError +from git.repo import Repo +from git_sim.settings import settings -class GitSimBaseCommand(m.MovingCameraScene): - def __init__(self, args: Namespace): +class GitSimBaseCommand(m.MovingCameraScene): + def __init__(self): super().__init__() self.init_repo() - self.args = args - self.fontColor = m.BLACK if self.args.light_mode else m.WHITE + self.fontColor = m.BLACK if settings.light_mode else m.WHITE self.drawnCommits = {} self.drawnRefs = {} self.drawnCommitIds = {} @@ -24,28 +25,25 @@ def __init__(self, args: Namespace): self.trimmed = False self.prevRef = None self.topref = None - self.maxrefs = None self.i = 0 - self.numCommits = 5 - self.defaultNumCommits = 5 + self.numCommits = settings.commits + self.defaultNumCommits = settings.commits self.selected_branches = [] - self.hide_first_tag = False self.stop = False self.zone_title_offset = 2.6 if platform.system() == "Windows" else 2.6 - self.allow_no_commits = False - self.logo = m.ImageMobject(self.args.logo) + self.logo = m.ImageMobject(settings.logo) self.logo.width = 3 def init_repo(self): try: - self.repo = git.Repo(search_parent_directories=True) - except git.exc.InvalidGitRepositoryError: + self.repo = Repo(search_parent_directories=True) + except InvalidGitRepositoryError: print("git-sim error: No Git repository found at current path.") sys.exit(1) - def execute(self): - print("Simulating: git " + self.args.subcommand) + def construct(self): + print(f"{settings.INFO_STRING} {type(self).__name__.lower()}") self.show_intro() self.get_commits() self.fadeout() @@ -53,7 +51,7 @@ def execute(self): def get_commits(self, start="HEAD"): if not self.numCommits: - if self.allow_no_commits: + if settings.allow_no_commits: self.numCommits = self.defaultNumCommits self.commits = ["dark"] * 5 self.zone_title_offset = 2 @@ -78,7 +76,7 @@ def get_commits(self, start="HEAD"): self.commits.append(self.create_dark_commit()) self.numCommits = self.defaultNumCommits - except git.exc.GitCommandError: + except GitCommandError: self.numCommits -= 1 self.get_commits(start=start) @@ -110,11 +108,11 @@ def parse_commits( self.i = 0 def show_intro(self): - if self.args.animate and self.args.show_intro: + if settings.animate and settings.show_intro: self.add(self.logo) initialCommitText = m.Text( - self.args.title, + settings.title, font="Monospace", font_size=36, color=self.fontColor, @@ -136,14 +134,13 @@ def show_intro(self): self.camera.frame.save_state() def show_outro(self): - if self.args.animate and self.args.show_outro: - + if settings.animate and settings.show_outro: self.play(m.Restore(self.camera.frame)) self.play(self.logo.animate.scale(4).set_x(0).set_y(0)) outroTopText = m.Text( - self.args.outro_top_text, + settings.outro_top_text, font="Monospace", font_size=36, color=self.fontColor, @@ -151,7 +148,7 @@ def show_outro(self): self.play(m.AddTextLetterByLetter(outroTopText)) outroBottomText = m.Text( - self.args.outro_bottom_text, + settings.outro_bottom_text, font="Monospace", font_size=36, color=self.fontColor, @@ -161,9 +158,9 @@ def show_outro(self): self.wait(3) def fadeout(self): - if self.args.animate: + if settings.animate: self.wait(3) - self.play(m.FadeOut(self.toFadeOut), run_time=1 / self.args.speed) + self.play(m.FadeOut(self.toFadeOut), run_time=1 / settings.speed) else: self.wait(0.1) @@ -177,7 +174,7 @@ def draw_commit( self, commit, prevCircle, shift=numpy.array([0.0, 0.0, 0.0]), dots=False ): if commit == "dark": - commitFill = m.WHITE if self.args.light_mode else m.BLACK + commitFill = m.WHITE if settings.light_mode else m.BLACK elif len(commit.parents) <= 1: commitFill = m.RED else: @@ -193,19 +190,19 @@ def draw_commit( if prevCircle: circle.next_to( - prevCircle, m.RIGHT if self.args.reverse else m.LEFT, buff=1.5 + prevCircle, m.RIGHT if settings.reverse else m.LEFT, buff=1.5 ) start = ( prevCircle.get_center() if prevCircle - else (m.LEFT if self.args.reverse else m.RIGHT) + else (m.LEFT if settings.reverse else m.RIGHT) ) end = circle.get_center() if commit == "dark": arrow = m.Arrow( - start, end, color=m.WHITE if self.args.light_mode else m.BLACK + start, end, color=m.WHITE if settings.light_mode else m.BLACK ) elif commit.hexsha in self.drawnCommits: end = self.drawnCommits[commit.hexsha].get_center() @@ -234,13 +231,13 @@ def draw_commit( color=self.fontColor, ).next_to(circle, m.DOWN) - if self.args.animate and commit != "dark" and not self.stop: + if settings.animate and commit != "dark" and not self.stop: self.play( self.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) elif not self.stop: self.add(circle, commitId, message) @@ -291,8 +288,8 @@ def draw_head(self, commit, commitId): head = m.VGroup(headbox, headText) - if self.args.animate: - self.play(m.Create(head), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(head), run_time=1 / settings.speed) else: self.add(head) @@ -343,8 +340,8 @@ def draw_branch(self, commit): self.prevRef = fullbranch - if self.args.animate: - self.play(m.Create(fullbranch), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(fullbranch), run_time=1 / settings.speed) else: self.add(fullbranch) @@ -355,17 +352,16 @@ def draw_branch(self, commit): self.topref = self.prevRef x += 1 - if x >= self.args.max_branches_per_commit: + if x >= settings.max_branches_per_commit: return def draw_tag(self, commit): x = 0 - if self.hide_first_tag and self.i == 0: + if settings.hide_first_tag and self.i == 0: return for tag in self.repo.tags: - try: if commit.hexsha == tag.commit.hexsha: tagText = m.Text( @@ -387,11 +383,11 @@ def draw_tag(self, commit): self.prevRef = tagRec - if self.args.animate: + if settings.animate: self.play( m.Create(tagRec), m.Create(tagText), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) else: self.add(tagRec, tagText) @@ -402,43 +398,43 @@ def draw_tag(self, commit): self.topref = self.prevRef x += 1 - if x >= self.args.max_tags_per_commit: + if x >= settings.max_tags_per_commit: return except ValueError: pass def draw_arrow(self, prevCircle, arrow): if prevCircle: - if self.args.animate: - self.play(m.Create(arrow), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(arrow), run_time=1 / settings.speed) else: self.add(arrow) self.toFadeOut.add(arrow) def recenter_frame(self): - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.move_to(self.toFadeOut.get_center()), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) else: self.camera.frame.move_to(self.toFadeOut.get_center()) def scale_frame(self): - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.scale_to_fit_width( self.toFadeOut.get_width() * 1.1 ), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) if self.toFadeOut.get_height() >= self.camera.frame.get_height(): self.play( self.camera.frame.animate.scale_to_fit_height( self.toFadeOut.get_height() * 1.25 ), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) else: self.camera.frame.scale_to_fit_width(self.toFadeOut.get_width() * 1.1) @@ -448,7 +444,7 @@ def scale_frame(self): ) def vsplit_frame(self): - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.scale_to_fit_height( self.camera.frame.get_height() * 2 @@ -458,7 +454,7 @@ def vsplit_frame(self): self.camera.frame.scale_to_fit_height(self.camera.frame.get_height() * 2) try: - if self.args.animate: + if settings.animate: self.play( self.toFadeOut.animate.align_to(self.camera.frame, m.UP).shift( m.DOWN * 0.75 @@ -472,7 +468,7 @@ def vsplit_frame(self): def setup_and_draw_zones( self, first_column_name="Untracked files", - second_column_name="Working directory modifications", + second_column_name="Working directory mods", third_column_name="Staging area", reverse=False, ): @@ -511,7 +507,7 @@ def setup_and_draw_zones( (self.camera.frame.get_left()[0], horizontal.get_start()[1], 0), dash_length=0.2, color=self.fontColor, - ).shift(m.RIGHT * 6.5) + ).shift(m.RIGHT * 8) vert2 = m.DashedLine( ( self.camera.frame.get_right()[0], @@ -521,7 +517,7 @@ def setup_and_draw_zones( (self.camera.frame.get_right()[0], horizontal.get_start()[1], 0), dash_length=0.2, color=self.fontColor, - ).shift(m.LEFT * 6.5) + ).shift(m.LEFT * 8) if reverse: first_column_name = "Staging area" @@ -534,8 +530,7 @@ def setup_and_draw_zones( font_size=28, color=self.fontColor, ) - .align_to(self.camera.frame, m.LEFT) - .shift(m.RIGHT * 0.65) + .move_to((vert1.get_center()[0] - 4, 0, 0)) .shift(m.UP * self.zone_title_offset) ) secondColumnTitle = ( @@ -555,8 +550,7 @@ def setup_and_draw_zones( font_size=28, color=self.fontColor, ) - .align_to(self.camera.frame, m.RIGHT) - .shift(m.LEFT * 1.65) + .move_to((vert2.get_center()[0] + 4, 0, 0)) .align_to(firstColumnTitle, m.UP) ) @@ -570,7 +564,7 @@ def setup_and_draw_zones( thirdColumnTitle, ) - if self.args.animate: + if settings.animate: self.play( m.Create(horizontal), m.Create(horizontal2), @@ -663,19 +657,19 @@ def setup_and_draw_zones( thirdColumnFilesDict[f] = text if len(firstColumnFiles): - if self.args.animate: + if settings.animate: self.play(*[m.AddTextLetterByLetter(d) for d in firstColumnFiles]) else: self.add(*[d for d in firstColumnFiles]) if len(secondColumnFiles): - if self.args.animate: + if settings.animate: self.play(*[m.AddTextLetterByLetter(w) for w in secondColumnFiles]) else: self.add(*[w for w in secondColumnFiles]) if len(thirdColumnFiles): - if self.args.animate: + if settings.animate: self.play(*[m.AddTextLetterByLetter(s) for s in thirdColumnFiles]) else: self.add(*[s for s in thirdColumnFiles]) @@ -707,7 +701,7 @@ def setup_and_draw_zones( 0, ), ) - if self.args.animate: + if settings.animate: self.play(m.Create(firstColumnArrowMap[filename])) else: self.add(firstColumnArrowMap[filename]) @@ -726,7 +720,7 @@ def setup_and_draw_zones( 0, ), ) - if self.args.animate: + if settings.animate: self.play(m.Create(secondColumnArrowMap[filename])) else: self.add(secondColumnArrowMap[filename]) @@ -742,7 +736,6 @@ def populate_zones( firstColumnArrowMap={}, secondColumnArrowMap={}, ): - for x in self.repo.index.diff(None): if "git-sim_media" not in x.a_path: secondColumnFileNames.add(x.a_path) @@ -761,7 +754,7 @@ def populate_zones( firstColumnFileNames.add(z) def center_frame_on_commit(self, commit): - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.move_to( self.drawnCommits[commit.hexsha].get_center() @@ -771,7 +764,7 @@ def center_frame_on_commit(self, commit): self.camera.frame.move_to(self.drawnCommits[commit.hexsha].get_center()) def reset_head_branch(self, hexsha, shift=numpy.array([0.0, 0.0, 0.0])): - if self.args.animate: + if settings.animate: self.play( self.drawnRefs["HEAD"].animate.move_to( ( @@ -805,7 +798,7 @@ def reset_head_branch(self, hexsha, shift=numpy.array([0.0, 0.0, 0.0])): ) def translate_frame(self, shift): - if self.args.animate: + if settings.animate: self.play(self.camera.frame.animate.shift(shift)) else: self.camera.frame.shift(shift) @@ -822,7 +815,7 @@ def setup_and_draw_parent( circle.height = 1 circle.next_to( self.drawnCommits[child.hexsha], - m.LEFT if self.args.reverse else m.RIGHT, + m.LEFT if settings.reverse else m.RIGHT, buff=1.5, ) circle.shift(shift) @@ -849,13 +842,13 @@ def setup_and_draw_parent( ).next_to(circle, m.DOWN) self.toFadeOut.add(message) - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) else: self.camera.frame.move_to(circle.get_center()) @@ -865,8 +858,8 @@ def setup_and_draw_parent( self.toFadeOut.add(circle) if draw_arrow: - if self.args.animate: - self.play(m.Create(arrow), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(arrow), run_time=1 / settings.speed) else: self.add(arrow) self.toFadeOut.add(arrow) @@ -877,7 +870,9 @@ def draw_arrow_between_commits(self, startsha, endsha): start = self.drawnCommits[startsha].get_center() end = self.drawnCommits[endsha].get_center() - arrow = DottedLine(start, end, color=self.fontColor).add_tip() + arrow = DottedLine( + start, end, color=self.fontColor, dot_kwargs={"color": self.fontColor} + ).add_tip() length = numpy.linalg.norm(start - end) - 1.65 arrow.set_length(length) self.draw_arrow(True, arrow) @@ -906,8 +901,8 @@ def draw_ref(self, commit, top, text="HEAD", color=m.BLUE): ref = m.VGroup(refbox, refText) - if self.args.animate: - self.play(m.Create(ref), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(ref), run_time=1 / settings.speed) else: self.add(ref) @@ -920,8 +915,8 @@ def draw_ref(self, commit, top, text="HEAD", color=m.BLUE): def draw_dark_ref(self): refRec = m.Rectangle( - color=m.WHITE if self.args.light_mode else m.BLACK, - fill_color=m.WHITE if self.args.light_mode else m.BLACK, + color=m.WHITE if settings.light_mode else m.BLACK, + fill_color=m.WHITE if settings.light_mode else m.BLACK, height=0.4, width=1, ) @@ -931,7 +926,7 @@ def draw_dark_ref(self): self.prevRef = refRec def trim_path(self, path): - return (path[:5] + "..." + path[-15:]) if len(path) > 20 else path + return (path[:15] + "..." + path[-15:]) if len(path) > 30 else path def get_remote_tracking_branches(self): remote_refs = [remote.refs for remote in self.repo.remotes] diff --git a/git_sim/git_sim_log.py b/git_sim/git_sim_log.py deleted file mode 100644 index 2c2c587..0000000 --- a/git_sim/git_sim_log.py +++ /dev/null @@ -1,25 +0,0 @@ -from argparse import Namespace - -from git_sim.git_sim_base_command import GitSimBaseCommand - - -class GitSimLog(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.numCommits = self.args.commits + 1 - self.defaultNumCommits = self.args.commits + 1 - try: - self.selected_branches.append(self.repo.active_branch.name) - except TypeError: - pass - - def construct(self): - print("Simulating: git " + self.args.subcommand) - - self.show_intro() - self.get_commits() - self.parse_commits(self.commits[0]) - self.recenter_frame() - self.scale_frame() - self.fadeout() - self.show_outro() diff --git a/git_sim/log.py b/git_sim/log.py new file mode 100644 index 0000000..f9ecf46 --- /dev/null +++ b/git_sim/log.py @@ -0,0 +1,39 @@ +import typer + +from git_sim.animations import handle_animations +from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings + + +class Log(GitSimBaseCommand): + def __init__(self, commits: int): + super().__init__() + self.numCommits = commits + 1 + self.defaultNumCommits = commits + 1 + try: + self.selected_branches.append(self.repo.active_branch.name) + except TypeError: + pass + + def construct(self): + if not settings.stdout: + print(f"{settings.INFO_STRING} {type(self).__name__.lower()}") + self.show_intro() + self.get_commits() + self.parse_commits(self.commits[0]) + self.recenter_frame() + self.scale_frame() + self.fadeout() + self.show_outro() + + +def log( + commits: int = typer.Option( + default=settings.commits, + help="The number of commits to display in the simulated log output", + min=1, + max=12, + ), +): + scene = Log(commits=commits) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_merge.py b/git_sim/merge.py similarity index 64% rename from git_sim/git_sim_merge.py rename to git_sim/merge.py index 914a9fe..95fa898 100644 --- a/git_sim/git_sim_merge.py +++ b/git_sim/merge.py @@ -4,28 +4,32 @@ import git import manim as m import numpy +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimMerge(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class Merge(GitSimBaseCommand): + def __init__(self, branch: str, no_ff: bool): + super().__init__() + self.branch = branch + self.no_ff = no_ff try: - git.repo.fun.rev_parse(self.repo, self.args.branch[0]) + git.repo.fun.rev_parse(self.repo, self.branch) except git.exc.BadName: print( "git-sim error: '" - + self.args.branch[0] + + self.branch + "' is not a valid Git ref or identifier." ) sys.exit(1) self.ff = False - self.maxrefs = 2 - if self.args.branch[0] in [branch.name for branch in self.repo.heads]: - self.selected_branches.append(self.args.branch[0]) + if self.branch in [branch.name for branch in self.repo.heads]: + self.selected_branches.append(self.branch) try: self.selected_branches.append(self.repo.active_branch.name) @@ -33,14 +37,17 @@ def __init__(self, args: Namespace): pass def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.branch[0]) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()} {self.branch} {'--no-ff' if self.no_ff else ''}" + ) if self.repo.active_branch.name in self.repo.git.branch( - "--contains", self.args.branch[0] + "--contains", self.branch ): print( "git-sim error: Branch '" - + self.args.branch[0] + + self.branch + "' is already included in the history of active branch '" + self.repo.active_branch.name + "'." @@ -50,27 +57,27 @@ def construct(self): self.show_intro() self.get_commits() self.orig_commits = self.commits - self.get_commits(start=self.args.branch[0]) + self.get_commits(start=self.branch) # Use forward slash to determine if supplied branch arg is local or remote tracking branch - if not self.is_remote_tracking_branch(self.args.branch[0]): - if self.args.branch[0] in self.repo.git.branch( + if not self.is_remote_tracking_branch(self.branch): + if self.branch in self.repo.git.branch( "--contains", self.orig_commits[0].hexsha ): self.ff = True else: - if self.args.branch[0] in self.repo.git.branch( + if self.branch in self.repo.git.branch( "-r", "--contains", self.orig_commits[0].hexsha ): self.ff = True if self.ff: - self.get_commits(start=self.args.branch[0]) + self.get_commits(start=self.branch) self.parse_commits(self.commits[0]) reset_head_to = self.commits[0].hexsha shift = numpy.array([0.0, 0.6, 0.0]) - if self.args.no_ff: + if self.no_ff: self.center_frame_on_commit(self.commits[0]) commitId = self.setup_and_draw_parent(self.commits[0], "Merge commit") reset_head_to = "abcdef" @@ -81,9 +88,7 @@ def construct(self): if "HEAD" in self.drawnRefs: self.reset_head_branch(reset_head_to, shift=shift) else: - self.draw_ref( - self.commits[0], commitId if self.args.no_ff else self.topref - ) + self.draw_ref(self.commits[0], commitId if self.no_ff else self.topref) self.draw_ref( self.commits[0], self.drawnRefs["HEAD"], @@ -95,7 +100,7 @@ def construct(self): self.get_commits() self.parse_commits(self.commits[0]) self.i = 0 - self.get_commits(start=self.args.branch[0]) + self.get_commits(start=self.branch) self.parse_commits(self.commits[0], shift=4 * m.DOWN) self.center_frame_on_commit(self.orig_commits[0]) self.setup_and_draw_parent( @@ -113,3 +118,18 @@ def construct(self): self.fadeout() self.show_outro() + + +def merge( + branch: str = typer.Argument( + ..., + help="The name of the branch to merge into the active checked-out branch", + ), + no_ff: bool = typer.Option( + False, + "--no-ff", + help="Simulate creation of a merge commit in all cases, even when the merge could instead be resolved as a fast-forward", + ), +): + scene = Merge(branch=branch, no_ff=no_ff) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_rebase.py b/git_sim/rebase.py similarity index 76% rename from git_sim/git_sim_rebase.py rename to git_sim/rebase.py index f6abf61..e1d3f26 100644 --- a/git_sim/git_sim_rebase.py +++ b/git_sim/rebase.py @@ -1,29 +1,32 @@ import sys -from argparse import Namespace import git import manim as m import numpy +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimRebase(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class Rebase(GitSimBaseCommand): + def __init__(self, branch: str): + super().__init__() + self.branch = branch try: - git.repo.fun.rev_parse(self.repo, self.args.branch[0]) + git.repo.fun.rev_parse(self.repo, self.branch) except git.exc.BadName: print( "git-sim error: '" - + self.args.branch[0] + + self.branch + "' is not a valid Git ref or identifier." ) sys.exit(1) - if self.args.branch[0] in [branch.name for branch in self.repo.heads]: - self.selected_branches.append(self.args.branch[0]) + if self.branch in [branch.name for branch in self.repo.heads]: + self.selected_branches.append(self.branch) try: self.selected_branches.append(self.repo.active_branch.name) @@ -31,26 +34,29 @@ def __init__(self, args: Namespace): pass def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.branch[0]) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()} {self.branch}" + ) - if self.args.branch[0] in self.repo.git.branch( + if self.branch in self.repo.git.branch( "--contains", self.repo.active_branch.name ): print( "git-sim error: Branch '" + self.repo.active_branch.name + "' is already included in the history of active branch '" - + self.args.branch[0] + + self.branch + "'." ) sys.exit(1) if self.repo.active_branch.name in self.repo.git.branch( - "--contains", self.args.branch[0] + "--contains", self.branch ): print( "git-sim error: Branch '" - + self.args.branch[0] + + self.branch + "' is already based on active branch '" + self.repo.active_branch.name + "'." @@ -58,7 +64,7 @@ def construct(self): sys.exit(1) self.show_intro() - self.get_commits(start=self.args.branch[0]) + self.get_commits(start=self.branch) self.parse_commits(self.commits[0]) self.orig_commits = self.commits self.i = 0 @@ -66,7 +72,7 @@ def construct(self): reached_base = False for commit in self.commits: - if commit != "dark" and self.args.branch[0] in self.repo.git.branch( + if commit != "dark" and self.branch in self.repo.git.branch( "--contains", commit ): reached_base = True @@ -79,7 +85,7 @@ def construct(self): to_rebase = [] i = 0 current = self.commits[i] - while self.args.branch[0] not in self.repo.git.branch("--contains", current): + while self.branch not in self.repo.git.branch("--contains", current): to_rebase.append(current) i += 1 if i >= len(self.commits): @@ -113,7 +119,7 @@ def setup_and_draw_parent( circle.height = 1 circle.next_to( self.drawnCommits[child], - m.LEFT if self.args.reverse else m.RIGHT, + m.LEFT if settings.reverse else m.RIGHT, buff=1.5, ) circle.shift(shift) @@ -152,13 +158,13 @@ def setup_and_draw_parent( ).next_to(circle, m.DOWN) self.toFadeOut.add(message) - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) else: self.camera.frame.move_to(circle.get_center()) @@ -168,10 +174,20 @@ def setup_and_draw_parent( self.toFadeOut.add(circle) if draw_arrow: - if self.args.animate: - self.play(m.Create(arrow), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(arrow), run_time=1 / settings.speed) else: self.add(arrow) self.toFadeOut.add(arrow) return sha + + +def rebase( + branch: str = typer.Argument( + ..., + help="The branch to simulate rebasing the checked-out commit onto", + ) +): + scene = Rebase(branch=branch) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_reset.py b/git_sim/reset.py similarity index 60% rename from git_sim/git_sim_reset.py rename to git_sim/reset.py index 4762772..aa746f3 100644 --- a/git_sim/git_sim_reset.py +++ b/git_sim/reset.py @@ -1,30 +1,39 @@ import sys -from argparse import Namespace +from enum import Enum import git import manim as m +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimReset(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class ResetMode(Enum): + DEFAULT = "mixed" + SOFT = "soft" + MIXED = "mixed" + HARD = "hard" + + +class Reset(GitSimBaseCommand): + def __init__( + self, commit: str, mode: ResetMode, soft: bool, mixed: bool, hard: bool + ): + super().__init__() + self.commit = commit + self.mode = mode try: - self.resetTo = git.repo.fun.rev_parse(self.repo, self.args.commit) + self.resetTo = git.repo.fun.rev_parse(self.repo, self.commit) except git.exc.BadName: print( - "git-sim error: '" - + self.args.commit - + "' is not a valid Git ref or identifier." + f"git-sim error: '{self.commit}' is not a valid Git ref or identifier." ) sys.exit(1) - self.commitsSinceResetTo = list( - self.repo.iter_commits(self.args.commit + "...HEAD") - ) - self.maxrefs = 2 + self.commitsSinceResetTo = list(self.repo.iter_commits(self.commit + "...HEAD")) self.hide_first_tag = True try: @@ -32,21 +41,18 @@ def __init__(self, args: Namespace): except TypeError: pass - if self.args.hard: - self.args.mode = "hard" - if self.args.mixed: - self.args.mode = "mixed" - if self.args.soft: - self.args.mode = "soft" + if hard: + self.mode = ResetMode.HARD + if mixed: + self.mode = ResetMode.MIXED + if soft: + self.mode = ResetMode.SOFT def construct(self): - print( - "Simulating: git " - + self.args.subcommand - + (" --" + self.args.mode if self.args.mode != "default" else "") - + " " - + self.args.commit - ) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()}{' --' + self.mode.value if self.mode != ResetMode.DEFAULT else ''} {self.commit}", + ) self.show_intro() self.get_commits() @@ -114,27 +120,53 @@ def populate_zones( if commit.hexsha == self.resetTo.hexsha: break for filename in commit.stats.files: - if self.args.mode == "soft": + if self.mode == ResetMode.SOFT: thirdColumnFileNames.add(filename) - elif self.args.mode == "mixed" or self.args.mode == "default": + elif self.mode in (ResetMode.MIXED, ResetMode.DEFAULT): secondColumnFileNames.add(filename) - elif self.args.mode == "hard": + elif self.mode == ResetMode.HARD: firstColumnFileNames.add(filename) for x in self.repo.index.diff(None): if "git-sim_media" not in x.a_path: - if self.args.mode == "soft": + if self.mode == ResetMode.SOFT: secondColumnFileNames.add(x.a_path) - elif self.args.mode == "mixed" or self.args.mode == "default": + elif self.mode in (ResetMode.MIXED, ResetMode.DEFAULT): secondColumnFileNames.add(x.a_path) - elif self.args.mode == "hard": + elif self.mode == ResetMode.HARD: firstColumnFileNames.add(x.a_path) for y in self.repo.index.diff("HEAD"): if "git-sim_media" not in y.a_path: - if self.args.mode == "soft": + if self.mode == ResetMode.SOFT: thirdColumnFileNames.add(y.a_path) - elif self.args.mode == "mixed" or self.args.mode == "default": + elif self.mode in (ResetMode.MIXED, ResetMode.DEFAULT): secondColumnFileNames.add(y.a_path) - elif self.args.mode == "hard": + elif self.mode == ResetMode.HARD: firstColumnFileNames.add(y.a_path) + + +def reset( + commit: str = typer.Argument( + default="HEAD", + help="The ref (branch/tag), or commit ID to simulate reset to", + ), + mode: ResetMode = typer.Option( + default=ResetMode.MIXED.value, + help="Either mixed, soft, or hard", + ), + soft: bool = typer.Option( + default=False, + help="Simulate a soft reset, shortcut for --mode=soft", + ), + mixed: bool = typer.Option( + default=False, + help="Simulate a mixed reset, shortcut for --mode=mixed", + ), + hard: bool = typer.Option( + default=False, + help="Simulate a soft reset, shortcut for --mode=hard", + ), +): + scene = Reset(commit=commit, mode=mode, soft=soft, mixed=mixed, hard=hard) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_restore.py b/git_sim/restore.py similarity index 62% rename from git_sim/git_sim_restore.py rename to git_sim/restore.py index 151d0e0..d6226bd 100644 --- a/git_sim/git_sim_restore.py +++ b/git_sim/restore.py @@ -1,37 +1,37 @@ import sys -from argparse import Namespace - import manim as m +import typer + +from typing import List +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimRestore(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.maxrefs = 2 +class Restore(GitSimBaseCommand): + def __init__(self, files: List[str]): + super().__init__() self.hide_first_tag = True + self.files = files try: self.selected_branches.append(self.repo.active_branch.name) except TypeError: pass - for name in self.args.name: - if name not in [x.a_path for x in self.repo.index.diff(None)] + [ + for file in self.files: + if file not in [x.a_path for x in self.repo.index.diff(None)] + [ y.a_path for y in self.repo.index.diff("HEAD") ]: - print( - "git-sim error: No modified or staged file with name: '" - + name - + "'" - ) + print(f"git-sim error: No modified or staged file with name: '{file}'") sys.exit() def construct(self): - print( - "Simulating: git " + self.args.subcommand + " " + " ".join(self.args.name) - ) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()} {' '.join(self.files)}" + ) self.show_intro() self.get_commits() @@ -51,12 +51,11 @@ def populate_zones( firstColumnArrowMap, secondColumnArrowMap, ): - for x in self.repo.index.diff(None): if "git-sim_media" not in x.a_path: secondColumnFileNames.add(x.a_path) - for name in self.args.name: - if name == x.a_path: + for file in self.files: + if file == x.a_path: thirdColumnFileNames.add(x.a_path) secondColumnArrowMap[x.a_path] = m.Arrow( stroke_width=3, color=self.fontColor @@ -65,9 +64,19 @@ def populate_zones( for y in self.repo.index.diff("HEAD"): if "git-sim_media" not in y.a_path: firstColumnFileNames.add(y.a_path) - for name in self.args.name: - if name == y.a_path: + for file in self.files: + if file == y.a_path: secondColumnFileNames.add(y.a_path) firstColumnArrowMap[y.a_path] = m.Arrow( stroke_width=3, color=self.fontColor ) + + +def restore( + files: List[str] = typer.Argument( + default=None, + help="The names of one or more files to restore", + ) +): + scene = Restore(files=files) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_revert.py b/git_sim/revert.py similarity index 83% rename from git_sim/git_sim_revert.py rename to git_sim/revert.py index 460b88d..0b18c62 100644 --- a/git_sim/git_sim_revert.py +++ b/git_sim/revert.py @@ -1,28 +1,30 @@ import sys -from argparse import Namespace import git import manim as m import numpy +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimRevert(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class Revert(GitSimBaseCommand): + def __init__(self, commit: str): + super().__init__() + self.commit = commit try: - self.revert = git.repo.fun.rev_parse(self.repo, self.args.commit) + self.revert = git.repo.fun.rev_parse(self.repo, self.commit) except git.exc.BadName: print( "git-sim error: '" - + self.args.commit + + self.commit + "' is not a valid Git ref or identifier." ) sys.exit(1) - self.maxrefs = 2 self.defaultNumCommits = 4 self.numCommits = 4 self.hide_first_tag = True @@ -34,7 +36,10 @@ def __init__(self, args: Namespace): pass def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.commit) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()} {self.commit}" + ) self.show_intro() self.get_commits() @@ -92,7 +97,7 @@ def setup_and_draw_revert_commit(self): circle.height = 1 circle.next_to( self.drawnCommits[self.commits[0].hexsha], - m.LEFT if self.args.reverse else m.RIGHT, + m.LEFT if settings.reverse else m.RIGHT, buff=1.5, ) @@ -119,13 +124,13 @@ def setup_and_draw_revert_commit(self): ).next_to(circle, m.DOWN) self.toFadeOut.add(message) - if self.args.animate: + if settings.animate: self.play( self.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), - run_time=1 / self.args.speed, + run_time=1 / settings.speed, ) else: self.camera.frame.move_to(circle.get_center()) @@ -134,8 +139,8 @@ def setup_and_draw_revert_commit(self): self.drawnCommits["abcdef"] = circle self.toFadeOut.add(circle) - if self.args.animate: - self.play(m.Create(arrow), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(arrow), run_time=1 / settings.speed) else: self.add(arrow) @@ -151,3 +156,13 @@ def populate_zones( ): for filename in self.revert.stats.files: secondColumnFileNames.add(filename) + + +def revert( + commit: str = typer.Argument( + default="HEAD", + help="The ref (branch/tag), or commit ID to simulate revert", + ) +): + scene = Revert(commit=commit) + handle_animations(scene=scene) diff --git a/git_sim/settings.py b/git_sim/settings.py new file mode 100644 index 0000000..563b445 --- /dev/null +++ b/git_sim/settings.py @@ -0,0 +1,47 @@ +import pathlib + +from enum import Enum +from typing import List, Union +from pydantic import BaseSettings + + +class VideoFormat(str, Enum): + mp4 = "mp4" + webm = "webm" + + +class ImgFormat(str, Enum): + jpg = "jpg" + png = "png" + + +class Settings(BaseSettings): + allow_no_commits = False + animate = False + auto_open = True + commits = 5 + files: Union[List[pathlib.Path], None] = None + hide_first_tag = False + img_format: ImgFormat = ImgFormat.jpg + INFO_STRING = "Simulating: git" + light_mode = False + logo = pathlib.Path(__file__).parent.resolve() / "logo.png" + low_quality = False + max_branches_per_commit = 1 + max_tags_per_commit = 1 + media_dir = pathlib.Path().cwd() + outro_bottom_text = "Learn more at initialcommit.com" + outro_top_text = "Thanks for using Initial Commit!" + reverse = False + show_intro = False + show_outro = False + speed = 1.5 + title = "Git-Sim, by initialcommit.com" + video_format: VideoFormat = VideoFormat.mp4 + stdout = False + + class Config: + env_prefix = "git_sim_" + + +settings = Settings() diff --git a/git_sim/git_sim_stash.py b/git_sim/stash.py similarity index 59% rename from git_sim/git_sim_stash.py rename to git_sim/stash.py index 445a3d0..147b0b5 100644 --- a/git_sim/git_sim_stash.py +++ b/git_sim/stash.py @@ -1,44 +1,43 @@ import sys -from argparse import Namespace - -import git import manim as m -import numpy +import typer + +from typing import List +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimStash(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.maxrefs = 2 +class Stash(GitSimBaseCommand): + def __init__(self, files: List[str]): + super().__init__() self.hide_first_tag = True + self.files = files + self.no_files = True if not self.files else False try: self.selected_branches.append(self.repo.active_branch.name) except TypeError: pass - for name in self.args.name: - if name not in [x.a_path for x in self.repo.index.diff(None)] + [ + for file in self.files: + if file not in [x.a_path for x in self.repo.index.diff(None)] + [ y.a_path for y in self.repo.index.diff("HEAD") ]: - print( - "git-sim error: No modified or staged file with name: '" - + name - + "'" - ) + print(f"git-sim error: No modified or staged file with name: '{file}'") sys.exit() - if not self.args.name: - self.args.name = [x.a_path for x in self.repo.index.diff(None)] + [ + if not self.files: + self.files = [x.a_path for x in self.repo.index.diff(None)] + [ y.a_path for y in self.repo.index.diff("HEAD") ] def construct(self): - print( - "Simulating: git " + self.args.subcommand + " " + " ".join(self.args.name) - ) + if not settings.stdout: + print( + f"{settings.INFO_STRING } {type(self).__name__.lower()} {' '.join(self.files) if not self.no_files else ''}" + ) self.show_intro() self.get_commits() @@ -62,11 +61,10 @@ def populate_zones( firstColumnArrowMap, secondColumnArrowMap, ): - for x in self.repo.index.diff(None): firstColumnFileNames.add(x.a_path) - for name in self.args.name: - if name == x.a_path: + for file in self.files: + if file == x.a_path: thirdColumnFileNames.add(x.a_path) firstColumnArrowMap[x.a_path] = m.Arrow( stroke_width=3, color=self.fontColor @@ -74,9 +72,19 @@ def populate_zones( for y in self.repo.index.diff("HEAD"): secondColumnFileNames.add(y.a_path) - for name in self.args.name: - if name == y.a_path: + for file in self.files: + if file == y.a_path: thirdColumnFileNames.add(y.a_path) secondColumnArrowMap[y.a_path] = m.Arrow( stroke_width=3, color=self.fontColor ) + + +def stash( + files: List[str] = typer.Argument( + default=None, + help="The name of the file to stash changes for", + ) +): + scene = Stash(files=files) + handle_animations(scene=scene) diff --git a/git_sim/git_sim_status.py b/git_sim/status.py similarity index 53% rename from git_sim/git_sim_status.py rename to git_sim/status.py index 9d857ec..0aa4e8f 100644 --- a/git_sim/git_sim_status.py +++ b/git_sim/status.py @@ -1,23 +1,19 @@ -from argparse import Namespace - +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimStatus(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.maxrefs = 2 - self.hide_first_tag = True - self.allow_no_commits = True - +class Status(GitSimBaseCommand): + def __init__(self): + super().__init__() try: self.selected_branches.append(self.repo.active_branch.name) except TypeError: pass def construct(self): - print("Simulating: git " + self.args.subcommand) - + if not settings.stdout: + print(f"{settings.INFO_STRING } {type(self).__name__.lower()}") self.show_intro() self.get_commits() self.parse_commits(self.commits[0]) @@ -27,3 +23,11 @@ def construct(self): self.setup_and_draw_zones() self.fadeout() self.show_outro() + + +def status(): + settings.hide_first_tag = True + settings.allow_no_commits = True + + scene = Status() + handle_animations(scene=scene) diff --git a/git_sim/git_sim_tag.py b/git_sim/tag.py similarity index 57% rename from git_sim/git_sim_tag.py rename to git_sim/tag.py index af748ac..cbfa058 100644 --- a/git_sim/git_sim_tag.py +++ b/git_sim/tag.py @@ -1,16 +1,19 @@ -from argparse import Namespace - import manim as m +import typer +from git_sim.animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings -class GitSimTag(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) +class Tag(GitSimBaseCommand): + def __init__(self, name: str): + super().__init__() + self.name = name def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.name) + if not settings.stdout: + print(f"{settings.INFO_STRING } {type(self).__name__.lower()} {self.name}") self.show_intro() self.get_commits() @@ -19,7 +22,7 @@ def construct(self): self.scale_frame() tagText = m.Text( - self.args.name, + self.name, font="Monospace", font_size=20, color=self.fontColor, @@ -37,8 +40,8 @@ def construct(self): fulltag = m.VGroup(tagRec, tagText) - if self.args.animate: - self.play(m.Create(fulltag), run_time=1 / self.args.speed) + if settings.animate: + self.play(m.Create(fulltag), run_time=1 / settings.speed) else: self.add(fulltag) @@ -46,3 +49,13 @@ def construct(self): self.fadeout() self.show_outro() + + +def tag( + name: str = typer.Argument( + ..., + help="The name of the new tag", + ) +): + scene = Tag(name=name) + handle_animations(scene=scene) diff --git a/setup.py b/setup.py index 74d6f9d..f7de254 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="git-sim", - version="0.2.2", + version="0.2.3", author="Jacob Stopak", author_email="jacob@initialcommit.io", description="Simulate Git commands on your own repos by generating an image (default) or video visualization depicting the command's behavior.", @@ -23,6 +23,8 @@ "gitpython", "manim", "opencv-python-headless", + "typer", + "pydantic", ], keywords="git sim simulation simulate git-simulate git-simulation git-sim manim animation gitanimation image video dryrun dry-run", project_urls={ @@ -31,7 +33,7 @@ }, entry_points={ "console_scripts": [ - "git-sim=git_sim.__main__:main", + "git-sim=git_sim.__main__:app", ], }, include_package_data=True,