diff --git a/git_sim/__main__.py b/git_sim/__main__.py index 93291a4..775ade1 100644 --- a/git_sim/__main__.py +++ b/git_sim/__main__.py @@ -5,368 +5,138 @@ import subprocess import sys 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 typer + +import git_sim.git_sim_add +import git_sim.git_sim_branch +import git_sim.git_sim_cherrypick +import git_sim.git_sim_commit +import git_sim.git_sim_log +import git_sim.git_sim_merge +import git_sim.git_sim_rebase +import git_sim.git_sim_reset +import git_sim.git_sim_restore +import git_sim.git_sim_revert +import git_sim.git_sim_stash +import git_sim.git_sim_status +import git_sim.git_sim_tag +from git_sim.settings import ImgFormat, Settings, VideoFormat + +app = typer.Typer() + + +@app.callback(no_args_is_help=True) +def main( + 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() - - if sys.platform == "linux" or sys.platform == "darwin": - repo_name = git.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] - - config.media_dir = os.path.join(os.path.expanduser(args.media_dir), "git-sim_media") - config.verbosity = "ERROR" - - # 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, - ) - - if args.low_quality: - config.quality = "low_quality" - - if args.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 - - 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." - ) + ), + 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, + ), +): + 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 = media_dir + 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 + + +app.command()(git_sim.git_sim_add.add) +app.command()(git_sim.git_sim_branch.branch) +app.command()(git_sim.git_sim_cherrypick.cherrypick) +app.command()(git_sim.git_sim_commit.commit) +app.command()(git_sim.git_sim_log.log) +app.command()(git_sim.git_sim_merge.merge) +app.command()(git_sim.git_sim_rebase.rebase) +app.command()(git_sim.git_sim_reset.reset) +app.command()(git_sim.git_sim_restore.restore) +app.command()(git_sim.git_sim_revert.revert) +app.command()(git_sim.git_sim_stash.stash) +app.command()(git_sim.git_sim_status.status) +app.command()(git_sim.git_sim_tag.tag) if __name__ == "__main__": - main() + app() diff --git a/git_sim/animations.py b/git_sim/animations.py new file mode 100644 index 0000000..ef07ca4 --- /dev/null +++ b/git_sim/animations.py @@ -0,0 +1,92 @@ +import datetime +import inspect +import os +import pathlib +import subprocess +import sys +import time + +import cv2 +import git.repo +from manim import WHITE, Scene, config +from manim.utils.file_ops import open_file + +from git_sim.settings import Settings + + +def handle_animations(scene: Scene) -> None: + if sys.platform == "linux" or sys.platform == "darwin": + repo_name = git.repo.Repo( + search_parent_directories=True + ).working_tree_dir.split("/")[-1] + elif sys.platform == "win32": + repo_name = git.repo.Repo( + search_parent_directories=True + ).working_tree_dir.split("\\")[-1] + + config.media_dir = os.path.join( + os.path.expanduser(Settings.media_dir), "git-sim_media" + ) + config.verbosity = "ERROR" + + # If the env variable is set and no argument provided, use the env variable value + if os.getenv("git_sim_media_dir") and Settings.media_dir == ".": + config.media_dir = os.path.join( + os.path.expanduser(os.getenv("git_sim_media_dir")), + "git-sim_media", + repo_name, + ) + + if Settings.low_quality: + config.quality = "low_quality" + + 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-" + inspect.stack()[1].function + "_" + t + ".mp4" + + 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: + image_file_name = ( + "git-sim-" + + inspect.stack()[1].function + + "_" + + t + + "." + + Settings.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 Settings.auto_open: + 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_add.py b/git_sim/git_sim_add.py index 0dc2e52..5c89bda 100644 --- a/git_sim/git_sim_add.py +++ b/git_sim/git_sim_add.py @@ -1,35 +1,35 @@ 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 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) - ) + print(Settings.INFO_STRING + "add " + " ".join(self.files)) self.show_intro() self.get_commits() @@ -49,12 +49,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 +70,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/git_sim_base_command.py b/git_sim/git_sim_base_command.py index 22bbda1..98be3e2 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) + print("Simulating: git " + Settings.subcommand) 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 @@ -570,7 +566,7 @@ def setup_and_draw_zones( thirdColumnTitle, ) - if self.args.animate: + if Settings.animate: self.play( m.Create(horizontal), m.Create(horizontal2), @@ -663,19 +659,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 +703,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 +722,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 +738,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 +756,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 +766,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 +800,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 +817,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 +844,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 +860,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) @@ -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, ) diff --git a/git_sim/git_sim_branch.py b/git_sim/git_sim_branch.py index 15b9033..19cf467 100644 --- a/git_sim/git_sim_branch.py +++ b/git_sim/git_sim_branch.py @@ -1,16 +1,18 @@ -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) + print(f"{Settings.INFO_STRING} branch {self.name}") self.show_intro() self.get_commits() @@ -19,7 +21,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 +39,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/git_sim_cherrypick.py index dd01cb2..8db3e41 100644 --- a/git_sim/git_sim_cherrypick.py +++ b/git_sim/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,17 @@ 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 "") - ) + substring = "" + if self.edit: + substring = f' -e "{self.edit}"' + print(f"{Settings.INFO_STRING} cherrypick {self.commit}{substring}") 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 +55,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 +68,17 @@ def construct(self): self.reset_head_branch("abcdef") self.fadeout() self.show_outro() + + +def cherrypick( + commit: str = typer.Argument( + ..., + help="The ref (branch/tag), or commit ID to simulate cherry-pick onto active branch", + ), + edit: str = typer.Option( + default=None, + 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/git_sim_commit.py index aec397a..7dd7b00 100644 --- a/git_sim/git_sim_commit.py +++ b/git_sim/git_sim_commit.py @@ -1,18 +1,21 @@ import sys -from argparse import Namespace import git import manim as m +import typer +from animations import handle_animations from git_sim.git_sim_base_command import GitSimBaseCommand -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,7 +23,7 @@ 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." ) @@ -28,31 +31,26 @@ def __init__(self, args: Namespace): def construct(self): print( - "Simulating: git " - + self.args.subcommand - + (" --amend" if self.args.amend else "") - + ' -m "' - + self.args.message - + '"' + f"Simulating: git commit {' --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 +63,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 +83,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 +94,17 @@ def populate_zones( secondColumnArrowMap[y.a_path] = m.Arrow( stroke_width=3, color=self.fontColor ) + + +def commit( + message: str = typer.Option( + default="New commit", + 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_log.py b/git_sim/git_sim_log.py index 2c2c587..61904a7 100644 --- a/git_sim/git_sim_log.py +++ b/git_sim/git_sim_log.py @@ -1,21 +1,22 @@ -from argparse import Namespace +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 GitSimLog(GitSimBaseCommand): - def __init__(self, args: Namespace): - super().__init__(args=args) - self.numCommits = self.args.commits + 1 - self.defaultNumCommits = self.args.commits + 1 +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): - print("Simulating: git " + self.args.subcommand) - + print(Settings.INFO_STRING + type(self).__name__) self.show_intro() self.get_commits() self.parse_commits(self.commits[0]) @@ -23,3 +24,15 @@ def construct(self): 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/git_sim_merge.py index 914a9fe..12470f5 100644 --- a/git_sim/git_sim_merge.py +++ b/git_sim/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,14 @@ def __init__(self, args: Namespace): pass def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.branch[0]) + print(Settings.INFO_STRING + "merge " + self.branch) 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 +54,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 +85,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 +97,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 +115,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/git_sim_rebase.py index f6abf61..af5eb61 100644 --- a/git_sim/git_sim_rebase.py +++ b/git_sim/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,26 @@ def __init__(self, args: Namespace): pass def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.branch[0]) + print(Settings.INFO_STRING + "rebase " + 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 +61,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 +69,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 +82,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 +116,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 +155,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 +171,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/git_sim_reset.py index 4762772..79f0e58 100644 --- a/git_sim/git_sim_reset.py +++ b/git_sim/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,20 +41,16 @@ 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 + f"{Settings.INFO_STRING} reset {' --' + self.mode.value if self.mode != ResetMode.DEFAULT else ''} {self.commit}", ) self.show_intro() @@ -114,27 +119,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/git_sim_restore.py index 151d0e0..8efb0eb 100644 --- a/git_sim/git_sim_restore.py +++ b/git_sim/git_sim_restore.py @@ -1,37 +1,33 @@ import sys -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 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) - ) + print(Settings.INFO_STRING + "restore " + " ".join(self.files)) self.show_intro() self.get_commits() @@ -51,12 +47,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 +60,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/git_sim_revert.py index 460b88d..8ccc465 100644 --- a/git_sim/git_sim_revert.py +++ b/git_sim/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,7 @@ def __init__(self, args: Namespace): pass def construct(self): - print("Simulating: git " + self.args.subcommand + " " + self.args.commit) + print(Settings.INFO_STRING + "revert " + self.commit) self.show_intro() self.get_commits() @@ -92,7 +94,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 +121,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 +136,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 +153,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/git_sim_stash.py b/git_sim/git_sim_stash.py index 445a3d0..065b50e 100644 --- a/git_sim/git_sim_stash.py +++ b/git_sim/git_sim_stash.py @@ -1,44 +1,37 @@ 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 -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 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) - ) + print("Simulating: git stash " + " ".join(self.files)) self.show_intro() self.get_commits() @@ -62,11 +55,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 +66,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/git_sim_status.py index 9d857ec..6d379a4 100644 --- a/git_sim/git_sim_status.py +++ b/git_sim/git_sim_status.py @@ -1,23 +1,17 @@ -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) - self.show_intro() self.get_commits() self.parse_commits(self.commits[0]) @@ -27,3 +21,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/git_sim_tag.py index af748ac..fdd1c38 100644 --- a/git_sim/git_sim_tag.py +++ b/git_sim/git_sim_tag.py @@ -1,16 +1,18 @@ -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) + print(f"{Settings.INFO_STRING} tag {self.name}") self.show_intro() self.get_commits() @@ -19,7 +21,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 +39,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 +48,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/git_sim/settings.py b/git_sim/settings.py new file mode 100644 index 0000000..02566a7 --- /dev/null +++ b/git_sim/settings.py @@ -0,0 +1,41 @@ +import pathlib +from dataclasses import dataclass +from enum import Enum + + +class VideoFormat(str, Enum): + mp4 = "mp4" + webm = "webm" + + +class ImgFormat(str, Enum): + jpg = "jpg" + png = "png" + + +@dataclass +class Settings: + commits = 5 + subcommand: str + show_intro = False + show_outro = False + animate = False + title = "Git Sim, by initialcommit.com" + outro_top_text = "Thanks for using Initial Commit!" + outro_bottom_text = "Learn more at initialcommit.com" + speed = 1.5 + light_mode = False + reverse = False + max_branches_per_commit = 1 + max_tags_per_commit = 1 + hide_first_tag = False + allow_no_commits = False + low_quality = False + auto_open = True + INFO_STRING = "Simulating: git " + # os.path.join(str(pathlib.Path(__file__).parent.resolve()), "logo.png") + logo = pathlib.Path(__file__).parent.resolve() / "logo.png" + media_dir = pathlib.Path().cwd() + files: list[pathlib.Path] | None = None + video_format: VideoFormat = VideoFormat.mp4 + img_format: ImgFormat = ImgFormat.jpg diff --git a/setup.py b/setup.py index 74d6f9d..109edfe 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ "gitpython", "manim", "opencv-python-headless", + "typer", ], keywords="git sim simulation simulate git-simulate git-simulation git-sim manim animation gitanimation image video dryrun dry-run", project_urls={ @@ -31,7 +32,7 @@ }, entry_points={ "console_scripts": [ - "git-sim=git_sim.__main__:main", + "git-sim=git_sim.__main__:app", ], }, include_package_data=True,