diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00aa4f0..319d682 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,13 +40,14 @@ steps: 2) [Fork the Git-Sim codebase](https://github.com/initialcommit-com/git-sim/fork) so that you have a copy on GitHub that you can clone and work with 3) Clone the codebase down to your local machine -4) If you previously installed Git-Sim normally using pip, uninstall it first using: +4) Checkout and commit new work to the `dev` branch +5) If you previously installed Git-Sim normally using pip, uninstall it first using: ```console $ pip uninstall git-sim ``` -5) To run the code locally from source, install the development package by running: +6) To run the code locally from source, install the development package by running: ```console $ cd path/to/git-sim @@ -61,7 +62,7 @@ If you already have the dependencies, you can ignore those using the `--no-deps` $ python -m pip install --no-deps -e . ``` -6) You can run your local Git-Sim commands from within other local repos like this: +7) You can run your local Git-Sim commands from within other local repos like this: ```console $ git-sim [global options] [subcommand options] @@ -74,8 +75,8 @@ $ cd path/to/any/local/git/repo $ git-sim --animate add newfile.txt ``` -6) After pushing your code changes up to your fork, [submit a pull request](https://github.com/initialcommit-com/git-sim/compare) for me -to review your code, provide feedback, and integrate it into the codebase! +8) After pushing your code changes up to your fork, [submit a pull request to the `dev` branch](https://github.com/initialcommit-com/git-sim/compare) for me +to review your code, provide feedback, and merge it into the codebase! ## Code style guide diff --git a/README.md b/README.md index 85c3851..60691c2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # git-sim +![git-sim-logo-with-tagline-1440x376p45](https://user-images.githubusercontent.com/49353917/232990611-58d0693f-69c0-45c8-b51d-cd540793d18c.gif) + [![GitHub license](https://img.shields.io/github/license/initialcommit-com/git-sim)](https://github.com/initialcommit-com/git-sim/blob/main/LICENSE) [![GitHub tag](https://img.shields.io/github/v/release/initialcommit-com/git-sim)](https://img.shields.io/github/v/release/initialcommit-com/git-sim) [![Downloads](https://static.pepy.tech/badge/git-sim)](https://pepy.tech/project/git-sim) @@ -18,15 +20,15 @@ Example: `$ git-sim merge ` ## Use cases - Visualize Git commands to understand their effects on your repo before actually running them - Prevent unexpected working directory and repository states by simulating before running -- Share visualizations (jpg image or mp4 video) of your Git commands with your team, or the world +- Share visualizations (jpg/png image or mp4/webm video) of your Git commands with your team, or the world - Save visualizations as a part of your team documentation to document workflow and prevent recurring issues -- Create static Git diagrams (jpg) or dynamic animated videos (mp4) to speed up content creation +- Create static Git diagrams (jpg/png) or dynamic animated videos (mp4/webm) to speed up content creation - Help visual learners understand how Git commands work - Combine with bundled command [git-dummy](https://github.com/initialcommit-com/git-dummy) to generate a dummy Git repo and then simulate operations on it ## Features - Run a one-liner git-sim command in the terminal to generate a custom Git command visualization (.jpg) from your repo -- Supported commands: `log`, `status`, `add`, `restore`, `commit`, `stash`, `branch`, `tag`, `reset`, `revert`, `merge`, `rebase`, `cherry-pick`, `switch`, `checkout`, `fetch`, `pull`, `push`, `clone` +- Supported commands: `log`, `status`, `add`, `restore`, `commit`, `stash`, `branch`, `tag`, `reset`, `revert`, `merge`, `rebase`, `cherry-pick`, `switch`, `checkout`, `fetch`, `pull`, `push`, `clone`, `rm`, `mv`, `clean` - 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) - Color commits by parameter, such as author the `--color-by=author` option - Choose between dark mode (default) and light mode @@ -126,7 +128,7 @@ $ git-sim -h * [Manim (Community version)](https://www.manim.community/) ## Commands -Basic usage is similar to Git itself - `git-sim` takes a familiar set of subcommands including "log", "status", "add", "restore", "commit", "stash", "branch", "tag", "reset", "revert", "merge", "rebase", "cherry-pick", "switch", "checkout", "fetch", "pull", "push", "clone", along with corresponding options. +Basic usage is similar to Git itself - `git-sim` takes a familiar set of subcommands including "log", "status", "add", "restore", "commit", "stash", "branch", "tag", "reset", "revert", "merge", "rebase", "cherry-pick", "switch", "checkout", "fetch", "pull", "push", "clone", "rm", "mv", "clean" along with corresponding options. ```console $ git-sim [global options] [subcommand options] @@ -148,7 +150,8 @@ The `[global options]` apply to the overarching `git-sim` simulation itself, inc `--stdout`: Write raw image data to stdout while suppressing all other program output. `--output-only-path`: Only output the path to the generated media file to stdout. Useful for other programs to ingest. `--quiet, -q`: Suppress all output except errors. -`--highlight-commit-messages`: Make commit message text bigger and bold, and hide commit ids. +`--highlight-commit-messages`: Make commit message text bigger and bold, and hide commit ids. +`--style`: Graphical style of the output image or animated video, i.e. `clean` (default) or `thick`. Animation-only global options (to be used in conjunction with `--animate`): @@ -289,17 +292,23 @@ Usage: `git-sim switch [-c] ` - Switches the checked-out branch to ``, i.e. moves `HEAD` to the specified `` - The `-c` flag creates a new branch with the specified name `` and switches to it, assuming it doesn't already exist +![git-sim-switch_04-09-23_21-42-43](https://user-images.githubusercontent.com/49353917/230827783-a8740ace-b66f-4cac-b94e-5d101d27e0b5.jpg) + ### git checkout Usage: `git-sim checkout [-b] ` - Checks out `` into the working directory, i.e. moves `HEAD` to the specified `` - The `-b` flag creates a new branch with the specified name `` and checks it out, assuming it doesn't already exist +![git-sim-checkout_04-09-23_21-46-04](https://user-images.githubusercontent.com/49353917/230827836-e9f23a0e-2576-4716-b2fb-6327d3cf9b22.jpg) + ### git fetch Usage: `git-sim fetch ` - Fetches the specified `` from the specified `` to the local repo +![git-sim-fetch_04-09-23_21-47-59](https://user-images.githubusercontent.com/49353917/230828090-acae8979-4097-43a8-96ea-525890e0e0a8.jpg) + ### git pull Usage: `git-sim pull [ ]` @@ -307,6 +316,8 @@ Usage: `git-sim pull [ ]` - If `` and `` are not specified, the active branch is pulled from the default remote - If merge conflicts occur, they are displayed in a table +![git-sim-pull_04-09-23_21-50-15](https://user-images.githubusercontent.com/49353917/230828298-455c0a9d-cf94-499e-9e35-623e7b218772.jpg) + ### git push Usage: `git-sim push [ ]` @@ -314,12 +325,44 @@ Usage: `git-sim push [ ]` - If `` and `` are not specified, the active branch is pushed to the default remote - If the push fails due to remote changes that don't exist in the local repo, a message is included telling the user to pull first, along with color coding which commits need to be pulled +![git-sim-push_04-21-23_13-41-57](https://user-images.githubusercontent.com/49353917/233731005-51fd7887-ae14-4ceb-a5d5-e5aed79e9fd8.jpg) + ### git clone Usage: `git-sim clone ` - Clone the remote repo from `` (web URL or filesystem path) to a new folder in the current directory - Output will report if clone operation is successful and show log of local clone +![git-sim-clone_04-09-23_21-51-53](https://user-images.githubusercontent.com/49353917/230828521-80c8d2d1-2a31-46bb-aeed-746f0441c86e.jpg) + +### git rm +Usage: `git-sim rm ... ` + +- Specify one or more `` as a *tracked* file +- Simulated output will show files being removed from Git tracking +- Note that simulated output will also show the most recent 5 commits on the active branch + +![git-sim-rm_04-09-23_22-01-29](https://user-images.githubusercontent.com/49353917/230829899-f5d688ea-bc8e-46f9-a54a-55d251c8915d.jpg) + +### git mv +Usage: `git-sim mv ` + +- Specify `` as file to update name/path +- Specify `` as new name/path of file +- Simulated output will show the name/path of the file being updated +- Note that simulated output will also show the most recent 5 commits on the active branch + +![git-sim-mv_04-09-23_22-05-13](https://user-images.githubusercontent.com/49353917/230829978-0a64dbe2-d974-4cef-9c6e-ed26e987342f.jpg) + +### git clean +Usage: `git-sim clean` + +- Simulated output will show untracked files being deleted +- Since this is just a simulation, no need to specify `-i`, `-n`, `-f` as in regular Git +- Note that simulated output will also show the most recent 5 commits on the active branch + +![git-sim-clean_04-09-23_22-05-54](https://user-images.githubusercontent.com/49353917/230830043-779e7230-f439-461a-a408-b19b263e86e4.jpg) + ## Video animation examples ```console $ git-sim --animate reset HEAD^ diff --git a/git_sim/__init__.py b/git_sim/__init__.py index e69de29..493f741 100644 --- a/git_sim/__init__.py +++ b/git_sim/__init__.py @@ -0,0 +1 @@ +__version__ = "0.3.0" diff --git a/git_sim/__main__.py b/git_sim/__main__.py index fdcb27f..3b523d4 100644 --- a/git_sim/__main__.py +++ b/git_sim/__main__.py @@ -1,17 +1,29 @@ -import pathlib -import typer +import datetime import os +import pathlib import sys -import datetime import time -import git_sim.commands +import typer -from git_sim.settings import ImgFormat, VideoFormat, settings +import git_sim.commands +from git_sim.settings import ( + ColorByOptions, + StyleOptions, + ImgFormat, + VideoFormat, + settings, +) app = typer.Typer(context_settings={"help_option_names": ["-h", "--help"]}) +def version_callback(value: bool) -> None: + if value: + print(f"git-sim version {git_sim.__version__}") + raise typer.Exit() + + @app.callback(no_args_is_help=True) def main( ctx: typer.Context, @@ -126,17 +138,28 @@ def main( settings.all, help="Display all local branches in the log output", ), - color_by: str = typer.Option( + color_by: ColorByOptions = typer.Option( settings.color_by, - help="Color commits by parameter, such as author", + help="Color commits by parameter", ), highlight_commit_messages: bool = typer.Option( settings.highlight_commit_messages, help="Make the displayed commit messages more prominent", ), + version: bool = typer.Option( + False, + "--version", + "-v", + help="Show the version of git-sim and exit", + callback=version_callback, + ), + style: StyleOptions = typer.Option( + settings.style.value, + help="Graphical style of the output image or animated video", + ), ): import git - from manim import config, WHITE + from manim import WHITE, config settings.animate = animate settings.n = n @@ -165,6 +188,7 @@ def main( settings.all = all settings.color_by = color_by settings.highlight_commit_messages = highlight_commit_messages + settings.style = style try: if sys.platform == "linux" or sys.platform == "darwin": @@ -190,7 +214,7 @@ def main( config.background_color = WHITE if settings.transparent_bg: - settings.img_format = ImgFormat.png + settings.img_format = ImgFormat.PNG t = datetime.datetime.fromtimestamp(time.time()).strftime("%m-%d-%y_%H-%M-%S") config.output_file = "git-sim-" + ctx.invoked_subcommand + "_" + t + ".mp4" @@ -200,17 +224,20 @@ def main( app.command()(git_sim.commands.branch) app.command()(git_sim.commands.checkout) app.command()(git_sim.commands.cherry_pick) +app.command()(git_sim.commands.clean) app.command()(git_sim.commands.clone) app.command()(git_sim.commands.commit) app.command()(git_sim.commands.fetch) app.command()(git_sim.commands.log) app.command()(git_sim.commands.merge) +app.command()(git_sim.commands.mv) app.command()(git_sim.commands.pull) app.command()(git_sim.commands.push) app.command()(git_sim.commands.rebase) app.command()(git_sim.commands.reset) app.command()(git_sim.commands.restore) app.command()(git_sim.commands.revert) +app.command()(git_sim.commands.rm) app.command()(git_sim.commands.stash) app.command()(git_sim.commands.status) app.command()(git_sim.commands.switch) diff --git a/git_sim/animations.py b/git_sim/animations.py index 93c91a3..027fa6a 100644 --- a/git_sim/animations.py +++ b/git_sim/animations.py @@ -11,12 +11,13 @@ from manim.utils.file_ops import open_file from git_sim.settings import settings +from git_sim.enums import VideoFormat def handle_animations(scene: Scene) -> None: scene.render() - if settings.video_format == "webm": + if settings.video_format == VideoFormat.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...") diff --git a/git_sim/clean.py b/git_sim/clean.py new file mode 100644 index 0000000..e95afd7 --- /dev/null +++ b/git_sim/clean.py @@ -0,0 +1,122 @@ +import sys +import git +import manim as m + +from typing import List + +from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings + + +class Clean(GitSimBaseCommand): + def __init__(self): + super().__init__() + self.hide_first_tag = True + self.allow_no_commits = True + settings.hide_merged_branches = True + self.n = self.n_default + + try: + self.selected_branches.append(self.repo.active_branch.name) + except TypeError: + pass + + def construct(self): + if not settings.stdout and not settings.output_only_path and not settings.quiet: + print(f"{settings.INFO_STRING} {type(self).__name__.lower()}") + + self.show_intro() + self.parse_commits() + self.recenter_frame() + self.scale_frame() + self.vsplit_frame() + self.setup_and_draw_zones( + first_column_name="Untracked files", + second_column_name="----", + third_column_name="Deleted files", + ) + self.fadeout() + self.show_outro() + + def create_zone_text( + self, + firstColumnFileNames, + secondColumnFileNames, + thirdColumnFileNames, + firstColumnFiles, + secondColumnFiles, + thirdColumnFiles, + firstColumnFilesDict, + secondColumnFilesDict, + thirdColumnFilesDict, + firstColumnTitle, + secondColumnTitle, + thirdColumnTitle, + horizontal2, + ): + for i, f in enumerate(firstColumnFileNames): + text = ( + m.Text( + self.trim_path(f), + font="Monospace", + font_size=24, + color=self.fontColor, + ) + .move_to( + (firstColumnTitle.get_center()[0], horizontal2.get_center()[1], 0) + ) + .shift(m.DOWN * 0.5 * (i + 1)) + ) + firstColumnFiles.add(text) + firstColumnFilesDict[f] = text + + for j, f in enumerate(secondColumnFileNames): + text = ( + m.Text( + self.trim_path(f), + font="Monospace", + font_size=24, + color=self.fontColor, + ) + .move_to( + (secondColumnTitle.get_center()[0], horizontal2.get_center()[1], 0) + ) + .shift(m.DOWN * 0.5 * (j + 1)) + ) + secondColumnFiles.add(text) + secondColumnFilesDict[f] = text + + for h, f in enumerate(thirdColumnFileNames): + text = ( + m.MarkupText( + "" + + self.trim_path(f) + + "", + font="Monospace", + font_size=24, + color=self.fontColor, + ) + .move_to( + (thirdColumnTitle.get_center()[0], horizontal2.get_center()[1], 0) + ) + .shift(m.DOWN * 0.5 * (h + 1)) + ) + thirdColumnFiles.add(text) + thirdColumnFilesDict[f] = text + + def populate_zones( + self, + firstColumnFileNames, + secondColumnFileNames, + thirdColumnFileNames, + firstColumnArrowMap={}, + secondColumnArrowMap={}, + thirdColumnArrowMap={}, + ): + for z in self.repo.untracked_files: + if "git-sim_media" not in z: + firstColumnFileNames.add(z) + thirdColumnFileNames.add(z) + firstColumnArrowMap[z] = m.Arrow(stroke_width=3, color=self.fontColor) diff --git a/git_sim/commands.py b/git_sim/commands.py index 52fc2a3..54ae298 100644 --- a/git_sim/commands.py +++ b/git_sim/commands.py @@ -77,6 +77,14 @@ def cherry_pick( handle_animations(scene=scene) +def clean(): + from git_sim.clean import Clean + + settings.hide_first_tag = True + scene = Clean() + handle_animations(scene=scene) + + def clone( url: str = typer.Argument( ..., @@ -166,6 +174,23 @@ def merge( handle_animations(scene=scene) +def mv( + file: str = typer.Argument( + default=None, + help="The name of the file to change the name/path of", + ), + new_file: str = typer.Argument( + default=None, + help="The new name/path of the file", + ), +): + from git_sim.mv import Mv + + settings.hide_first_tag = True + scene = Mv(file=file, new_file=new_file) + handle_animations(scene=scene) + + def pull( remote: str = typer.Argument( default=None, @@ -265,6 +290,19 @@ def revert( handle_animations(scene=scene) +def rm( + files: List[str] = typer.Argument( + default=None, + help="The names of one or more files to remove from Git's index", + ) +): + from git_sim.rm import Rm + + settings.hide_first_tag = True + scene = Rm(files=files) + handle_animations(scene=scene) + + def stash( command: StashSubCommand = typer.Argument( default=None, diff --git a/git_sim/enums.py b/git_sim/enums.py index fe5c9c2..7a22e17 100644 --- a/git_sim/enums.py +++ b/git_sim/enums.py @@ -12,3 +12,25 @@ class StashSubCommand(Enum): POP = "pop" APPLY = "apply" PUSH = "push" + + +class ColorByOptions(Enum): + AUTHOR = "author" + BRANCH = "branch" + NOTLOCAL1 = "notlocal1" + NOTLOCAL2 = "notlocal2" + + +class StyleOptions(Enum): + CLEAN = "clean" + THICK = "thick" + + +class VideoFormat(str, Enum): + MP4 = "mp4" + WEBM = "webm" + + +class ImgFormat(str, Enum): + JPG = "jpg" + PNG = "png" diff --git a/git_sim/git_sim_base_command.py b/git_sim/git_sim_base_command.py index 7c8498d..a8de08b 100644 --- a/git_sim/git_sim_base_command.py +++ b/git_sim/git_sim_base_command.py @@ -1,9 +1,9 @@ -import platform -import sys import os -import tempfile +import platform import shutil import stat +import sys +import tempfile import git import manim as m @@ -11,6 +11,7 @@ from git.exc import GitCommandError, InvalidGitRepositoryError from git.repo import Repo +from git_sim.enums import ColorByOptions, StyleOptions from git_sim.settings import settings @@ -61,6 +62,17 @@ def __init__(self): self.fill_opacity = 0.5 self.ref_fill_opacity = 1.0 + if settings.style == StyleOptions.CLEAN: + self.commit_stroke_width = 5 + self.arrow_stroke_width = 5 + self.arrow_tip_shape = m.ArrowTriangleFilledTip + self.font_weight = m.NORMAL + elif settings.style == StyleOptions.THICK: + self.commit_stroke_width = 30 + self.arrow_stroke_width = 10 + self.arrow_tip_shape = m.StealthTip + self.font_weight = m.BOLD + def init_repo(self): try: self.repo = Repo(search_parent_directories=True) @@ -242,6 +254,7 @@ def draw_commit(self, commit, i, prevCircle, shift=numpy.array([0.0, 0.0, 0.0])) circle = m.Circle( stroke_color=commit_fill, + stroke_width=self.commit_stroke_width, fill_color=commit_fill, fill_opacity=self.fill_opacity, ) @@ -279,7 +292,14 @@ def draw_commit(self, commit, i, prevCircle, shift=numpy.array([0.0, 0.0, 0.0])) ) end = self.drawnCommits[commit.hexsha].get_center() - arrow = m.Arrow(start, end, color=self.fontColor) + arrow = m.Arrow( + start, + end, + color=self.fontColor, + stroke_width=self.arrow_stroke_width, + tip_shape=self.arrow_tip_shape, + max_stroke_width_to_length_ratio=1000, + ) if commit == "dark": arrow = m.Arrow( @@ -298,7 +318,13 @@ def draw_commit(self, commit, i, prevCircle, shift=numpy.array([0.0, 0.0, 0.0])) for commitCircle in self.drawnCommits.values(): inter = m.Intersection(lineRect, commitCircle) if inter.has_points(): - arrow = m.CurvedArrow(start, end, color=self.fontColor) + arrow = m.CurvedArrow( + start, + end, + color=self.fontColor, + stroke_width=self.arrow_stroke_width, + tip_shape=self.arrow_tip_shape, + ) if start[1] == end[1]: arrow.shift(m.UP * 1.25) if start[0] < end[0] and start[1] == end[1]: @@ -319,7 +345,10 @@ def draw_commit(self, commit, i, prevCircle, shift=numpy.array([0.0, 0.0, 0.0])) font="Monospace", font_size=20 if settings.highlight_commit_messages else 14, color=self.fontColor, - weight=m.BOLD if settings.highlight_commit_messages else m.NORMAL, + weight=m.BOLD + if settings.highlight_commit_messages + or settings.style == StyleOptions.THICK + else m.NORMAL, ).next_to(circle, m.DOWN) if settings.animate and commit != "dark" and isNewCommit: @@ -372,7 +401,13 @@ def get_nonparent_branch_names(self): def build_commit_id_and_message(self, commit, i): hide_refs = False if commit == "dark": - commitId = m.Text("", font="Monospace", font_size=20, color=self.fontColor) + commitId = m.Text( + "", + font="Monospace", + font_size=20, + color=self.fontColor, + weight=self.font_weight, + ) commitMessage = "" else: commitId = m.Text( @@ -380,6 +415,7 @@ def build_commit_id_and_message(self, commit, i): font="Monospace", font_size=20, color=self.fontColor, + weight=self.font_weight, ) commitMessage = commit.message.split("\n")[0][:40].replace("\n", " ") return commitId, commitMessage, commit, hide_refs @@ -396,7 +432,11 @@ def draw_head(self, commit, i, commitId): else: headbox.next_to(commitId, m.UP) headText = m.Text( - "HEAD", font="Monospace", font_size=20, color=self.fontColor + "HEAD", + font="Monospace", + font_size=20, + color=self.fontColor, + weight=self.font_weight, ).move_to(headbox.get_center()) head = m.VGroup(headbox, headText) @@ -443,7 +483,11 @@ def draw_branch(self, commit, i, make_branches_remote=False): ) branchText = m.Text( - text, font="Monospace", font_size=20, color=self.fontColor + text, + font="Monospace", + font_size=20, + color=self.fontColor, + weight=self.font_weight, ) branchRec = m.Rectangle( color=m.GREEN, @@ -489,6 +533,7 @@ def draw_tag(self, commit, i): font="Monospace", font_size=20, color=self.fontColor, + weight=self.font_weight, ) tagRec = m.Rectangle( color=m.YELLOW, @@ -652,6 +697,7 @@ def setup_and_draw_zones( font="Monospace", font_size=28, color=self.fontColor, + weight=m.BOLD, ) .move_to((vert1.get_center()[0] - 4, 0, 0)) .shift(m.UP * self.zone_title_offset) @@ -662,6 +708,7 @@ def setup_and_draw_zones( font="Monospace", font_size=28, color=self.fontColor, + weight=m.BOLD, ) .move_to(self.camera.frame.get_center()) .align_to(firstColumnTitle, m.UP) @@ -672,6 +719,7 @@ def setup_and_draw_zones( font="Monospace", font_size=28, color=self.fontColor, + weight=m.BOLD, ) .move_to((vert2.get_center()[0] + 4, 0, 0)) .align_to(firstColumnTitle, m.UP) @@ -841,6 +889,10 @@ def setup_and_draw_zones( self.toFadeOut.add(firstColumnFiles, secondColumnFiles, thirdColumnFiles) + self.firstColumnFiles = firstColumnFiles + self.secondColumnFiles = secondColumnFiles + self.thirdColumnFiles = thirdColumnFiles + def populate_zones( self, firstColumnFileNames, @@ -980,7 +1032,10 @@ def setup_and_draw_parent( color=m.RED, ): circle = m.Circle( - stroke_color=color, fill_color=color, fill_opacity=self.ref_fill_opacity + stroke_color=color, + stroke_width=self.commit_stroke_width, + fill_color=color, + fill_opacity=self.ref_fill_opacity, ) circle.height = 1 circle.next_to( @@ -992,12 +1047,23 @@ def setup_and_draw_parent( start = circle.get_center() end = self.drawnCommits[child.hexsha].get_center() - arrow = m.Arrow(start, end, color=self.fontColor) + arrow = m.Arrow( + start, + end, + color=self.fontColor, + stroke_width=self.arrow_stroke_width, + tip_shape=self.arrow_tip_shape, + max_stroke_width_to_length_ratio=1000, + ) length = numpy.linalg.norm(start - end) - (1.5 if start[1] == end[1] else 3) arrow.set_length(length) commitId = m.Text( - "abcdef", font="Monospace", font_size=20, color=self.fontColor + "abcdef", + font="Monospace", + font_size=20, + color=self.fontColor, + weight=self.font_weight, ).next_to(circle, m.UP) self.toFadeOut.add(commitId) @@ -1009,6 +1075,7 @@ def setup_and_draw_parent( font="Monospace", font_size=14, color=self.fontColor, + weight=self.font_weight, ).next_to(circle, m.DOWN) self.toFadeOut.add(message) @@ -1056,7 +1123,13 @@ def get_nondark_commits(self): return nondark_commits def draw_ref(self, commit, top, i=0, text="HEAD", color=m.BLUE): - refText = m.Text(text, font="Monospace", font_size=20, color=self.fontColor) + refText = m.Text( + text, + font="Monospace", + font_size=20, + color=self.fontColor, + weight=self.font_weight, + ) refbox = m.Rectangle( color=color, fill_color=color, @@ -1179,7 +1252,7 @@ def create_zone_text( thirdColumnFilesDict[f] = text def color_by(self, offset=0): - if settings.color_by == "author": + if settings.color_by == ColorByOptions.AUTHOR: sorted_authors = sorted( self.author_groups.keys(), key=lambda k: len(self.author_groups[k]), @@ -1191,6 +1264,7 @@ def color_by(self, offset=0): font="Monospace", font_size=36, color=self.colors[int(i % 11)], + weight=self.font_weight, ) authorText.move_to( [(-5 - offset) if settings.reverse else (5 + offset), -i, 0] @@ -1208,17 +1282,17 @@ def color_by(self, offset=0): self.recenter_frame() self.scale_frame() - elif settings.color_by == "branch": + elif settings.color_by == ColorByOptions.BRANCH: pass - elif settings.color_by == "notlocal1": + elif settings.color_by == ColorByOptions.NOTLOCAL1: for commit_id in self.drawnCommits: try: self.orig_repo.commit(commit_id) except ValueError: self.drawnCommits[commit_id].set_color(m.GOLD) - elif settings.color_by == "notlocal2": + elif settings.color_by == ColorByOptions.NOTLOCAL2: for commit_id in self.drawnCommits: if not self.orig_repo.is_ancestor(commit_id, "HEAD"): self.drawnCommits[commit_id].set_color(m.GOLD) diff --git a/git_sim/merge.py b/git_sim/merge.py index 6f5b480..d00a90d 100644 --- a/git_sim/merge.py +++ b/git_sim/merge.py @@ -83,7 +83,13 @@ def construct(self): if head_commit.hexsha in self.drawnCommits: start = self.drawnCommits["abcdef"].get_center() end = self.drawnCommits[head_commit.hexsha].get_center() - arrow = m.CurvedArrow(start, end, color=self.fontColor) + arrow = m.CurvedArrow( + start, + end, + color=self.fontColor, + stroke_width=self.arrow_stroke_width, + tip_shape=self.arrow_tip_shape, + ) self.draw_arrow(True, arrow) reset_head_to = "abcdef" @@ -152,7 +158,10 @@ def construct(self): self.repo.git.clear_cache() # Delete the local clone - shutil.rmtree(new_dir, onerror=self.del_rw) + try: + shutil.rmtree(new_dir, onerror=self.del_rw) + except (FileNotFoundError, UnboundLocalError): + pass def check_merge_conflict(self, branch1, branch2): git_root = self.repo.git.rev_parse("--show-toplevel") diff --git a/git_sim/mv.py b/git_sim/mv.py new file mode 100644 index 0000000..a882cd7 --- /dev/null +++ b/git_sim/mv.py @@ -0,0 +1,90 @@ +import sys +import git +import manim as m + +from typing import List + +from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings + + +class Mv(GitSimBaseCommand): + def __init__(self, file: str, new_file: str): + super().__init__() + self.hide_first_tag = True + self.allow_no_commits = True + self.file = file + self.new_file = new_file + settings.hide_merged_branches = True + self.n = self.n_default + + try: + self.selected_branches.append(self.repo.active_branch.name) + except TypeError: + pass + + try: + self.repo.git.ls_files("--error-unmatch", self.file) + except: + print(f"git-sim error: No tracked file with name: '{file}'") + sys.exit() + + def construct(self): + if not settings.stdout and not settings.output_only_path and not settings.quiet: + print( + f"{settings.INFO_STRING} {type(self).__name__.lower()} {self.file} {self.new_file}" + ) + + self.show_intro() + self.parse_commits() + self.recenter_frame() + self.scale_frame() + self.vsplit_frame() + self.setup_and_draw_zones( + first_column_name="Working directory", + second_column_name="Staging area", + third_column_name="Renamed files", + ) + self.rename_moved_file() + self.fadeout() + self.show_outro() + + def populate_zones( + self, + firstColumnFileNames, + secondColumnFileNames, + thirdColumnFileNames, + firstColumnArrowMap={}, + secondColumnArrowMap={}, + thirdColumnArrowMap={}, + ): + if self.file in [x.a_path for x in self.repo.index.diff("HEAD")]: + secondColumnFileNames.add(self.file) + secondColumnArrowMap[self.file] = m.Arrow( + stroke_width=3, color=self.fontColor + ) + else: + firstColumnFileNames.add(self.file) + firstColumnArrowMap[self.file] = m.Arrow( + stroke_width=3, color=self.fontColor + ) + + thirdColumnFileNames.add(self.file) + + def rename_moved_file(self): + for file in self.thirdColumnFiles: + new_file = m.Text( + self.trim_path(self.new_file), + font="Monospace", + font_size=24, + color=self.fontColor, + ) + new_file.move_to(file.get_center()) + if settings.animate: + self.play(m.FadeOut(file), run_time=1 / settings.speed) + self.toFadeOut.remove(file) + self.play(m.AddTextLetterByLetter(new_file)) + self.toFadeOut.add(new_file) + else: + self.remove(file) + self.add(new_file) diff --git a/git_sim/push.py b/git_sim/push.py index 24ad905..a8781bf 100644 --- a/git_sim/push.py +++ b/git_sim/push.py @@ -12,6 +12,7 @@ from git_sim.git_sim_base_command import GitSimBaseCommand from git_sim.settings import settings +from git_sim.enums import ColorByOptions class Push(GitSimBaseCommand): @@ -76,12 +77,12 @@ def construct(self): push_result = 1 self.orig_repo = self.repo self.repo = self.remote_repo - settings.color_by = "notlocal1" + settings.color_by = ColorByOptions.NOTLOCAL1 elif "rejected" in e.stderr and ("non-fast-forward" in e.stderr): push_result = 2 self.orig_repo = self.repo self.repo = self.remote_repo - settings.color_by = "notlocal2" + settings.color_by = ColorByOptions.NOTLOCAL2 else: print(f"git-sim error: git push failed: {e.stderr}") return @@ -173,7 +174,7 @@ def failed_push(self, push_result): text2.move_to(text1.get_center()).shift(m.DOWN / 2) text3 = m.Text( - f"Gold commits exist are ahead of your current branch tip (need to be pulled).", + f"Gold commits are ahead of your current branch tip (need to be pulled).", font="Monospace", font_size=20, color=m.GOLD, diff --git a/git_sim/rebase.py b/git_sim/rebase.py index b283127..8950717 100644 --- a/git_sim/rebase.py +++ b/git_sim/rebase.py @@ -111,7 +111,12 @@ def setup_and_draw_parent( shift=numpy.array([0.0, 0.0, 0.0]), draw_arrow=True, ): - circle = m.Circle(stroke_color=m.RED, fill_color=m.RED, fill_opacity=0.25) + circle = m.Circle( + stroke_color=m.RED, + stroke_width=self.commit_stroke_width, + fill_color=m.RED, + fill_opacity=0.25, + ) circle.height = 1 circle.next_to( self.drawnCommits[child], @@ -122,7 +127,14 @@ def setup_and_draw_parent( start = circle.get_center() end = self.drawnCommits[child].get_center() - arrow = m.Arrow(start, end, color=self.fontColor) + arrow = m.Arrow( + start, + end, + color=self.fontColor, + stroke_width=self.arrow_stroke_width, + tip_shape=self.arrow_tip_shape, + max_stroke_width_to_length_ratio=1000, + ) length = numpy.linalg.norm(start - end) - (1.5 if start[1] == end[1] else 3) arrow.set_length(length) diff --git a/git_sim/revert.py b/git_sim/revert.py index b487fe8..9552501 100644 --- a/git_sim/revert.py +++ b/git_sim/revert.py @@ -91,7 +91,12 @@ def build_commit_id_and_message(self, commit, i): return commitId, commitMessage, commit, hide_refs def setup_and_draw_revert_commit(self): - circle = m.Circle(stroke_color=m.RED, fill_color=m.RED, fill_opacity=0.25) + circle = m.Circle( + stroke_color=m.RED, + stroke_width=self.commit_stroke_width, + fill_color=m.RED, + fill_opacity=0.25, + ) circle.height = 1 circle.next_to( self.drawnCommits[self.get_commit().hexsha], @@ -101,7 +106,14 @@ def setup_and_draw_revert_commit(self): start = circle.get_center() end = self.drawnCommits[self.get_commit().hexsha].get_center() - arrow = m.Arrow(start, end, color=self.fontColor) + arrow = m.Arrow( + start, + end, + color=self.fontColor, + stroke_width=self.arrow_stroke_width, + tip_shape=self.arrow_tip_shape, + max_stroke_width_to_length_ratio=1000, + ) length = numpy.linalg.norm(start - end) - (1.5 if start[1] == end[1] else 3) arrow.set_length(length) diff --git a/git_sim/rm.py b/git_sim/rm.py new file mode 100644 index 0000000..fcf7183 --- /dev/null +++ b/git_sim/rm.py @@ -0,0 +1,140 @@ +import sys +import git +import manim as m + +from typing import List + +from git_sim.git_sim_base_command import GitSimBaseCommand +from git_sim.settings import settings + + +class Rm(GitSimBaseCommand): + def __init__(self, files: List[str]): + super().__init__() + self.hide_first_tag = True + self.allow_no_commits = True + self.files = files + settings.hide_merged_branches = True + self.n = self.n_default + + try: + self.selected_branches.append(self.repo.active_branch.name) + except TypeError: + pass + + for file in self.files: + try: + self.repo.git.ls_files("--error-unmatch", file) + except: + print(f"git-sim error: No tracked file with name: '{file}'") + sys.exit() + + def construct(self): + if not settings.stdout and not settings.output_only_path and not settings.quiet: + print( + f"{settings.INFO_STRING} {type(self).__name__.lower()} {' '.join(self.files)}" + ) + + self.show_intro() + self.parse_commits() + self.recenter_frame() + self.scale_frame() + self.vsplit_frame() + self.setup_and_draw_zones( + first_column_name="Working directory", + second_column_name="Staging area", + third_column_name="Removed files", + ) + self.fadeout() + self.show_outro() + + def create_zone_text( + self, + firstColumnFileNames, + secondColumnFileNames, + thirdColumnFileNames, + firstColumnFiles, + secondColumnFiles, + thirdColumnFiles, + firstColumnFilesDict, + secondColumnFilesDict, + thirdColumnFilesDict, + firstColumnTitle, + secondColumnTitle, + thirdColumnTitle, + horizontal2, + ): + for i, f in enumerate(firstColumnFileNames): + text = ( + m.Text( + self.trim_path(f), + font="Monospace", + font_size=24, + color=self.fontColor, + ) + .move_to( + (firstColumnTitle.get_center()[0], horizontal2.get_center()[1], 0) + ) + .shift(m.DOWN * 0.5 * (i + 1)) + ) + firstColumnFiles.add(text) + firstColumnFilesDict[f] = text + + for j, f in enumerate(secondColumnFileNames): + text = ( + m.Text( + self.trim_path(f), + font="Monospace", + font_size=24, + color=self.fontColor, + ) + .move_to( + (secondColumnTitle.get_center()[0], horizontal2.get_center()[1], 0) + ) + .shift(m.DOWN * 0.5 * (j + 1)) + ) + secondColumnFiles.add(text) + secondColumnFilesDict[f] = text + + for h, f in enumerate(thirdColumnFileNames): + text = ( + m.MarkupText( + "" + + self.trim_path(f) + + "", + font="Monospace", + font_size=24, + color=self.fontColor, + ) + .move_to( + (thirdColumnTitle.get_center()[0], horizontal2.get_center()[1], 0) + ) + .shift(m.DOWN * 0.5 * (h + 1)) + ) + thirdColumnFiles.add(text) + thirdColumnFilesDict[f] = text + + def populate_zones( + self, + firstColumnFileNames, + secondColumnFileNames, + thirdColumnFileNames, + firstColumnArrowMap={}, + secondColumnArrowMap={}, + thirdColumnArrowMap={}, + ): + for file in self.files: + if file in [x.a_path for x in self.repo.index.diff("HEAD")]: + secondColumnFileNames.add(file) + secondColumnArrowMap[file] = m.Arrow( + stroke_width=3, color=self.fontColor + ) + else: + firstColumnFileNames.add(file) + firstColumnArrowMap[file] = m.Arrow( + stroke_width=3, color=self.fontColor + ) + + thirdColumnFileNames.add(file) diff --git a/git_sim/settings.py b/git_sim/settings.py index 03ab880..014610b 100644 --- a/git_sim/settings.py +++ b/git_sim/settings.py @@ -1,18 +1,9 @@ import pathlib - -from enum import Enum from typing import List, Union -from pydantic import BaseSettings - - -class VideoFormat(str, Enum): - mp4 = "mp4" - webm = "webm" +from pydantic import BaseSettings -class ImgFormat(str, Enum): - jpg = "jpg" - png = "png" +from git_sim.enums import StyleOptions, ColorByOptions, ImgFormat, VideoFormat class Settings(BaseSettings): @@ -23,7 +14,7 @@ class Settings(BaseSettings): n = 5 files: Union[List[pathlib.Path], None] = None hide_first_tag = False - img_format: ImgFormat = ImgFormat.jpg + img_format: ImgFormat = ImgFormat.JPG INFO_STRING = "Simulating: git" light_mode = False transparent_bg = False @@ -39,15 +30,16 @@ class Settings(BaseSettings): show_outro = False speed = 1.5 title = "Git-Sim, by initialcommit.com" - video_format: VideoFormat = VideoFormat.mp4 + video_format: VideoFormat = VideoFormat.MP4 stdout = False output_only_path = False quiet = False invert_branches = False hide_merged_branches = False all = False - color_by: Union[str, None] = None + color_by: Union[ColorByOptions, None] = None highlight_commit_messages = False + style: Union[StyleOptions, None] = StyleOptions.CLEAN class Config: env_prefix = "git_sim_" diff --git a/setup.py b/setup.py index a4c8295..65b84f1 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,13 @@ import setuptools +from git_sim import __version__ + with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="git-sim", - version="0.2.9", + version=__version__, 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.",