X Tutup
import os import git import sys import stat import numpy import random import shutil import platform import tempfile import manim as m from git.repo import Repo from git.exc import GitCommandError, InvalidGitRepositoryError, BadName from collections import deque from git_sim.settings import settings from git_sim.enums import ColorByOptions, StyleOptions class GitSimBaseCommand(m.MovingCameraScene): def __init__(self): super().__init__() self.cmd = "git " self.init_repo() self.font = settings.font self.fontColor = m.BLACK if settings.light_mode else m.WHITE self.drawnCommits = {} self.drawnRefs = {} self.drawnRefsByCommit = {} self.drawnCommitIds = {} self.toFadeOut = m.Group() self.prevRef = None self.topref = None self.topelement = None self.n_default = settings.n_default self.n = settings.n self.n_orig = self.n self.n_dark_commits = 0 self.selected_branches = [] self.zone_title_offset = 2.6 if platform.system() == "Windows" else 2.6 self.arrow_map = [] self.arrows = [] self.all = settings.all self.first_parse = True self.author_groups = {} self.colors = [ m.ORANGE, m.YELLOW, m.GREEN, m.BLUE, m.MAROON, m.PURPLE, m.GOLD, m.TEAL, m.RED, m.PINK, m.DARK_BLUE, ] self.logo = m.ImageMobject(settings.logo) self.logo.width = 3 self.hide_first_tag = settings.hide_first_tag self.fill_opacity = 0.25 self.ref_fill_opacity = 0.25 if settings.transparent_bg: 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) repo_name = os.path.basename(self.repo.working_dir) new_dir = os.path.join(tempfile.gettempdir(), "git_sim", repo_name) new_dir2 = os.path.join(tempfile.gettempdir(), "git_sim", repo_name + "2") try: shutil.rmtree(new_dir, onerror=self.del_rw) except FileNotFoundError: pass try: shutil.rmtree(new_dir2, onerror=self.del_rw) except FileNotFoundError: pass except InvalidGitRepositoryError: print("git-sim error: No Git repository found at current path.") sys.exit(1) def construct(self): print(f"{settings.INFO_STRING} {type(self).__name__.lower()}") self.show_intro() self.parse_commits() self.fadeout() self.show_outro() def get_commit(self, sha_or_ref="HEAD"): if self.head_exists(): try: return self.repo.commit(sha_or_ref) except BadName: print(f"git-sim error: {sha_or_ref} did not resolve to a valid Git object.") sys.exit(1) return "dark" def get_default_commits(self): defaultCommits = [self.get_commit()] for x in range(self.n_default - 1): defaultCommits.append(defaultCommits[-1].parents[0]) return defaultCommits def parse_commits( self, commit=None, i=0, prevCircle=None, shift=numpy.array([0.0, 0.0, 0.0]), make_branches_remote=False, ): if not self.head_exists(): commit = self.create_dark_commit() commit = commit or self.get_commit() if commit != "dark": isNewCommit = commit.hexsha not in self.drawnCommits else: isNewCommit = True if i < self.n: commitId, circle, arrow, hide_refs = self.draw_commit( commit, i, prevCircle, shift ) if commit != "dark": if not hide_refs and isNewCommit: self.draw_head(commit, i, commitId) self.draw_branch( commit, i, make_branches_remote=make_branches_remote ) self.draw_tag(commit, i) if ( not isinstance(arrow, m.CurvedArrow) and [arrow.start.tolist(), arrow.end.tolist()] not in self.arrow_map ): self.draw_arrow(prevCircle, arrow) self.arrow_map.append([arrow.start.tolist(), arrow.end.tolist()]) elif ( isinstance(arrow, m.CurvedArrow) and [arrow.get_start().tolist(), arrow.get_end().tolist()] not in self.arrow_map ): self.draw_arrow(prevCircle, arrow) self.arrow_map.append( [arrow.get_start().tolist(), arrow.get_end().tolist()] ) if i == 0 and len(self.drawnRefs) < 2: self.draw_dark_ref() self.first_parse = False i += 1 try: commitParents = list(commit.parents) except AttributeError: if (len(self.drawnCommits) + self.n_dark_commits) < self.n_default: self.n_dark_commits += 1 self.parse_commits(self.create_dark_commit(), i, circle) return if len(commitParents) > 0: if settings.invert_branches: commitParents.reverse() if settings.hide_merged_branches: self.parse_commits(commitParents[0], i, circle) else: for p in range(len(commitParents)): self.parse_commits(commitParents[p], i, circle) else: if (len(self.drawnCommits) + self.n_dark_commits) < self.n_default: self.n_dark_commits += 1 self.parse_commits(self.create_dark_commit(), i, circle) def parse_all(self): if self.all: for branch in self.get_nonparent_branch_names(): self.parse_commits(self.get_commit(branch.name)) def show_intro(self): if settings.animate and settings.show_intro: self.add(self.logo) initialCommitText = m.Text( settings.title, font=self.font, font_size=36, color=self.fontColor, ).to_edge(m.UP, buff=1) self.add(initialCommitText) self.wait(2) self.play(m.FadeOut(initialCommitText)) self.play( self.logo.animate.scale(0.25) .to_edge(m.UP, buff=0) .to_edge(m.RIGHT, buff=0) ) self.camera.frame.save_state() self.play(m.FadeOut(self.logo)) else: self.logo.scale(0.25).to_edge(m.UP, buff=0).to_edge(m.RIGHT, buff=0) self.camera.frame.save_state() def show_outro(self): 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( settings.outro_top_text, font=self.font, font_size=36, color=self.fontColor, ).to_edge(m.UP, buff=1) self.play(m.AddTextLetterByLetter(outroTopText)) outroBottomText = m.Text( settings.outro_bottom_text, font=self.font, font_size=36, color=self.fontColor, ).to_edge(m.DOWN, buff=1) self.play(m.AddTextLetterByLetter(outroBottomText)) self.wait(3) def fadeout(self): if settings.animate: self.wait(3) self.play(m.FadeOut(self.toFadeOut), run_time=1 / settings.speed) else: self.wait(0.1) def get_centers(self): centers = [] for commit in self.drawnCommits.values(): centers.append(commit.get_center()) return centers def draw_commit(self, commit, i, prevCircle, shift=numpy.array([0.0, 0.0, 0.0])): if commit == "dark": commit_fill = m.WHITE if settings.light_mode else m.BLACK elif len(commit.parents) <= 1: commit_fill = m.RED else: commit_fill = m.GRAY circle = m.Circle( stroke_color=commit_fill, stroke_width=self.commit_stroke_width, fill_color=commit_fill, fill_opacity=self.fill_opacity, ) circle.height = 1 if shift.any(): circle.shift(shift) if prevCircle: circle.next_to( prevCircle, m.RIGHT if settings.reverse else m.LEFT, buff=1.5 ) while any((circle.get_center() == c).all() for c in self.get_centers()): circle.shift(m.DOWN * 4) if commit != "dark": isNewCommit = commit.hexsha not in self.drawnCommits else: isNewCommit = True if isNewCommit: start = ( prevCircle.get_center() if prevCircle else (m.LEFT if settings.reverse else m.RIGHT) ) end = circle.get_center() else: circle.move_to(self.drawnCommits[commit.hexsha].get_center()) start = ( prevCircle.get_center() if prevCircle else (m.LEFT if settings.reverse else m.RIGHT) ) end = self.drawnCommits[commit.hexsha].get_center() 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( start, end, color=m.WHITE if settings.light_mode else m.BLACK ) length = numpy.linalg.norm(start - end) - (1.5 if start[1] == end[1] else 3) arrow.set_length(length) angle = arrow.get_angle() lineRect = ( m.Rectangle(height=0.1, width=length, color="#123456") .move_to(arrow.get_center()) .rotate(angle) ) for commitCircle in self.drawnCommits.values(): inter = m.Intersection(lineRect, commitCircle) if inter.has_points(): 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]: arrow.flip(m.RIGHT).shift(m.UP) commitId, commitMessage, commit, hide_refs = self.build_commit_id_and_message( commit, i ) commitId.next_to(circle, m.UP) if commit != "dark": self.drawnCommitIds[commit.hexsha] = commitId message = m.Text( "\n".join( commitMessage[j : j + 20] for j in range(0, len(commitMessage), 20) )[:100], font=self.font, font_size=20 if settings.highlight_commit_messages else 14, color=self.fontColor, 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: self.play( self.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.Text("") if settings.highlight_commit_messages else m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), run_time=1 / settings.speed, ) elif isNewCommit: self.add( circle, m.Text("") if settings.highlight_commit_messages else commitId, message, ) else: return ( m.Text("") if settings.highlight_commit_messages else commitId, circle, arrow, hide_refs, ) if commit != "dark": self.drawnCommits[commit.hexsha] = circle group = m.Group(circle, commitId, message) self.add_group_to_author_groups(commit.author.name, group) self.toFadeOut.add(circle, commitId, message) if settings.highlight_commit_messages: self.prevRef = circle else: self.prevRef = commitId return commitId, circle, arrow, hide_refs def get_nonparent_branch_names(self): branches = [b for b in self.repo.heads if not b.name.startswith("remotes/")] exclude = [] for b1 in branches: for b2 in branches: if b1.name != b2.name and b1.commit != b2.commit: if self.repo.is_ancestor(b1.commit, b2.commit): exclude.append(b1.name) return [b for b in branches if b.name not in exclude] def build_commit_id_and_message(self, commit, i): hide_refs = False if commit == "dark": commitId = m.Text( "", font=self.font, font_size=20, color=self.fontColor, weight=self.font_weight, ) commitMessage = "" else: commitId = m.Text( commit.hexsha[0:6], font=self.font, 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 def draw_head(self, commit, i, commitId): if commit.hexsha == self.repo.head.commit.hexsha: headbox = m.Rectangle( color=m.BLUE, fill_color=m.BLUE, fill_opacity=self.ref_fill_opacity ) headbox.width = 1 headbox.height = 0.4 if settings.highlight_commit_messages: headbox.next_to(self.drawnCommits[commit.hexsha], m.UP) else: headbox.next_to(commitId, m.UP) headText = m.Text( "HEAD", font=self.font, font_size=20, color=self.fontColor, weight=self.font_weight, ).move_to(headbox.get_center()) head = m.VGroup(headbox, headText) if settings.animate: self.play(m.Create(head), run_time=1 / settings.speed) else: self.add(head) self.toFadeOut.add(head) self.drawnRefs["HEAD"] = head self.add_ref_to_drawn_refs_by_commit(commit.hexsha, head) self.prevRef = head if i == 0 and self.first_parse: self.topref = self.prevRef def draw_branch(self, commit, i, make_branches_remote=False): x = 0 remote_tracking_branches = self.get_remote_tracking_branches() branches = [branch.name for branch in self.repo.heads] + list( remote_tracking_branches.keys() ) for selected_branch in self.selected_branches: branches.insert(0, branches.pop(branches.index(selected_branch))) for branch in branches: if ( branch not in remote_tracking_branches # local branch and commit.hexsha == self.repo.heads[branch].commit.hexsha ) or ( branch in remote_tracking_branches # remote tracking branch and commit.hexsha == remote_tracking_branches[branch] ): text = ( (make_branches_remote + "/" + branch) if (make_branches_remote and branch not in remote_tracking_branches) else branch ) branchText = m.Text( text, font=self.font, font_size=20, color=self.fontColor, weight=self.font_weight, ) branchRec = m.Rectangle( color=m.GREEN, fill_color=m.GREEN, fill_opacity=self.ref_fill_opacity, height=0.4, width=branchText.width + 0.25, ) branchRec.next_to(self.prevRef, m.UP) branchText.move_to(branchRec.get_center()) fullbranch = m.VGroup(branchRec, branchText) self.prevRef = fullbranch if settings.animate: self.play(m.Create(fullbranch), run_time=1 / settings.speed) else: self.add(fullbranch) self.toFadeOut.add(fullbranch) self.drawnRefs[branch] = fullbranch self.add_ref_to_drawn_refs_by_commit(commit.hexsha, fullbranch) if i == 0 and self.first_parse: self.topref = self.prevRef x += 1 if x >= settings.max_branches_per_commit: return def draw_tag(self, commit, i): x = 0 if self.hide_first_tag and i == 0: return for tag in self.repo.tags: try: if commit.hexsha == tag.commit.hexsha: tagText = m.Text( tag.name, font=self.font, font_size=20, color=self.fontColor, weight=self.font_weight, ) tagRec = m.Rectangle( color=m.YELLOW, fill_color=m.YELLOW, fill_opacity=self.ref_fill_opacity, height=0.4, width=tagText.width + 0.25, ) tagRec.next_to(self.prevRef, m.UP) tagText.move_to(tagRec.get_center()) fulltag = m.VGroup(tagRec, tagText) self.prevRef = tagRec if settings.animate: self.play( m.Create(fulltag), run_time=1 / settings.speed, ) else: self.add(fulltag) self.toFadeOut.add(fulltag) self.drawnRefs[tag.name] = fulltag self.add_ref_to_drawn_refs_by_commit(commit.hexsha, fulltag) if i == 0 and self.first_parse: self.topref = self.prevRef x += 1 if x >= settings.max_tags_per_commit: return except ValueError: pass def draw_arrow(self, prevCircle, arrow): if prevCircle: if settings.animate: self.play(m.Create(arrow), run_time=1 / settings.speed) else: self.add(arrow) self.arrows.append(arrow) self.toFadeOut.add(arrow) def recenter_frame(self): if settings.animate: self.play( self.camera.frame.animate.move_to(self.toFadeOut.get_center()), run_time=1 / settings.speed, ) else: self.camera.frame.move_to(self.toFadeOut.get_center()) def scale_frame(self): if settings.animate: if self.toFadeOut.get_width() > self.camera.frame.get_width(): self.play( self.camera.frame.animate.scale_to_fit_width( self.toFadeOut.get_width() * 1.1 ), 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 / settings.speed, ) else: if self.toFadeOut.get_width() > self.camera.frame.get_width(): self.camera.frame.scale_to_fit_width(self.toFadeOut.get_width() * 1.1) if self.toFadeOut.get_height() > self.camera.frame.get_height(): self.camera.frame.scale_to_fit_height( self.toFadeOut.get_height() * 1.25 ) def vsplit_frame(self): if settings.animate: self.play( self.camera.frame.animate.scale_to_fit_height( self.camera.frame.get_height() * 2 ) ) else: self.camera.frame.scale_to_fit_height(self.camera.frame.get_height() * 2) try: if settings.animate: self.play( self.toFadeOut.animate.align_to(self.camera.frame, m.UP).shift( m.DOWN * 2.25 ) ) else: self.toFadeOut.align_to(self.camera.frame, m.UP).shift(m.DOWN * 2.25) except ValueError: pass def setup_and_draw_zones( self, first_column_name="Untracked files", second_column_name="Modified files", third_column_name="Staged files", reverse=False, ): if self.check_all_dark(): self.zone_title_offset = 2.0 if platform.system() == "Windows" else 2.0 horizontal = m.Line( ( self.camera.frame.get_left()[0], self.camera.frame.get_center()[1], 0, ), ( self.camera.frame.get_right()[0], self.camera.frame.get_center()[1], 0, ), color=self.fontColor, ).shift(m.UP * 1.75) horizontal2 = m.Line( ( self.camera.frame.get_left()[0], self.camera.frame.get_center()[1], 0, ), ( self.camera.frame.get_right()[0], self.camera.frame.get_center()[1], 0, ), color=self.fontColor, ).shift(m.UP * 0.75) vert1 = m.DashedLine( ( self.camera.frame.get_left()[0], self.camera.frame.get_bottom()[1], 0, ), (self.camera.frame.get_left()[0], horizontal.get_start()[1], 0), dash_length=0.2, color=self.fontColor, ).shift(m.RIGHT * 8) vert2 = m.DashedLine( ( self.camera.frame.get_right()[0], self.camera.frame.get_bottom()[1], 0, ), (self.camera.frame.get_right()[0], horizontal.get_start()[1], 0), dash_length=0.2, color=self.fontColor, ).shift(m.LEFT * 8) if reverse: first_column_name = "Staging area" third_column_name = "Deleted changes" title_v_shift = abs(horizontal2.get_start()[1] - horizontal.get_start()[1]) / 2 firstColumnTitle = ( m.Text( first_column_name, font=self.font, font_size=28, color=self.fontColor, weight=m.BOLD, ) .move_to((vert1.get_center()[0] - 4, horizontal.get_start()[1], 0)) .shift(m.DOWN * title_v_shift) ) secondColumnTitle = ( m.Text( second_column_name, font=self.font, font_size=28, color=self.fontColor, weight=m.BOLD, ) .move_to(self.camera.frame.get_center()) .align_to(firstColumnTitle, m.UP) ) thirdColumnTitle = ( m.Text( third_column_name, font=self.font, font_size=28, color=self.fontColor, weight=m.BOLD, ) .move_to((vert2.get_center()[0] + 4, 0, 0)) .align_to(firstColumnTitle, m.UP) ) self.toFadeOut.add( horizontal, horizontal2, vert1, vert2, firstColumnTitle, secondColumnTitle, thirdColumnTitle, ) if settings.animate: self.play( m.Create(horizontal), m.Create(horizontal2), m.Create(vert1), m.Create(vert2), m.AddTextLetterByLetter(firstColumnTitle), m.AddTextLetterByLetter(secondColumnTitle), m.AddTextLetterByLetter(thirdColumnTitle), ) else: self.add( horizontal, horizontal2, vert1, vert2, firstColumnTitle, secondColumnTitle, thirdColumnTitle, ) firstColumnFileNames = set() secondColumnFileNames = set() thirdColumnFileNames = set() firstColumnArrowMap = {} secondColumnArrowMap = {} thirdColumnArrowMap = {} self.populate_zones( firstColumnFileNames, secondColumnFileNames, thirdColumnFileNames, firstColumnArrowMap, secondColumnArrowMap, thirdColumnArrowMap, ) firstColumnFiles = m.VGroup() secondColumnFiles = m.VGroup() thirdColumnFiles = m.VGroup() firstColumnFilesDict = {} secondColumnFilesDict = {} thirdColumnFilesDict = {} self.create_zone_text( firstColumnFileNames, secondColumnFileNames, thirdColumnFileNames, firstColumnFiles, secondColumnFiles, thirdColumnFiles, firstColumnFilesDict, secondColumnFilesDict, thirdColumnFilesDict, firstColumnTitle, secondColumnTitle, thirdColumnTitle, horizontal2, ) if len(firstColumnFiles): if settings.animate: self.play(*[m.AddTextLetterByLetter(d) for d in firstColumnFiles]) else: self.add(*[d for d in firstColumnFiles]) if len(secondColumnFiles): if settings.animate: self.play(*[m.AddTextLetterByLetter(w) for w in secondColumnFiles]) else: self.add(*[w for w in secondColumnFiles]) if len(thirdColumnFiles): if settings.animate: self.play(*[m.AddTextLetterByLetter(s) for s in thirdColumnFiles]) else: self.add(*[s for s in thirdColumnFiles]) for filename in firstColumnArrowMap: if reverse: firstColumnArrowMap[filename].put_start_and_end_on( ( firstColumnFilesDict[filename].get_right()[0] + 0.25, firstColumnFilesDict[filename].get_right()[1], 0, ), ( secondColumnFilesDict[filename].get_left()[0] - 0.25, secondColumnFilesDict[filename].get_left()[1], 0, ), ) else: firstColumnArrowMap[filename].put_start_and_end_on( ( firstColumnFilesDict[filename].get_right()[0] + 0.25, firstColumnFilesDict[filename].get_right()[1], 0, ), ( thirdColumnFilesDict[filename].get_left()[0] - 0.25, thirdColumnFilesDict[filename].get_left()[1], 0, ), ) if settings.animate: self.play(m.Create(firstColumnArrowMap[filename])) else: self.add(firstColumnArrowMap[filename]) self.toFadeOut.add(firstColumnArrowMap[filename]) for filename in secondColumnArrowMap: secondColumnArrowMap[filename].put_start_and_end_on( ( secondColumnFilesDict[filename].get_right()[0] + 0.25, secondColumnFilesDict[filename].get_right()[1], 0, ), ( thirdColumnFilesDict[filename].get_left()[0] - 0.25, thirdColumnFilesDict[filename].get_left()[1], 0, ), ) if settings.animate: self.play(m.Create(secondColumnArrowMap[filename])) else: self.add(secondColumnArrowMap[filename]) self.toFadeOut.add(secondColumnArrowMap[filename]) for filename in thirdColumnArrowMap: thirdColumnArrowMap[filename].put_start_and_end_on( ( thirdColumnFilesDict[filename].get_left()[0] - 0.25, thirdColumnFilesDict[filename].get_left()[1], 0, ), ( firstColumnFilesDict[filename].get_right()[0] + 0.25, firstColumnFilesDict[filename].get_right()[1], 0, ), ) if settings.animate: self.play(m.Create(thirdColumnArrowMap[filename])) else: self.add(thirdColumnArrowMap[filename]) self.toFadeOut.add(thirdColumnArrowMap[filename]) self.toFadeOut.add(firstColumnFiles, secondColumnFiles, thirdColumnFiles) self.firstColumnFiles = firstColumnFiles self.secondColumnFiles = secondColumnFiles self.thirdColumnFiles = thirdColumnFiles def populate_zones( self, firstColumnFileNames, secondColumnFileNames, thirdColumnFileNames, firstColumnArrowMap={}, secondColumnArrowMap={}, thirdColumnArrowMap={}, ): for x in self.repo.index.diff(None): if "git-sim_media" not in x.a_path: secondColumnFileNames.add(x.a_path) try: for y in self.repo.index.diff("HEAD"): if "git-sim_media" not in y.a_path: thirdColumnFileNames.add(y.a_path) except git.exc.BadName: for (y, _stage), entry in self.repo.index.entries.items(): if "git-sim_media" not in y: thirdColumnFileNames.add(y) for z in self.repo.untracked_files: if "git-sim_media" not in z: firstColumnFileNames.add(z) def center_frame_on_commit(self, commit): if not commit or commit == "dark": return if settings.animate: self.play( self.camera.frame.animate.move_to( self.drawnCommits[commit.hexsha].get_center() ) ) else: self.camera.frame.move_to(self.drawnCommits[commit.hexsha].get_center()) def reset_head_branch(self, hexsha, branch="HEAD", shift=numpy.array([0.0, 0.0, 0.0])): if not self.head_exists(): return if settings.animate: self.play( self.drawnRefs["HEAD"].animate.move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 1.4 + shift[1], 0, ) ), self.drawnRefs[self.repo.active_branch.name if branch == "HEAD" else branch].animate.move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 2 + shift[1], 0, ) ), ) else: self.drawnRefs["HEAD"].move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 1.4 + shift[1], 0, ) ) self.drawnRefs[self.repo.active_branch.name if branch == "HEAD" else branch].move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 2 + shift[1], 0, ) ) def reset_head(self, hexsha, shift=numpy.array([0.0, 0.0, 0.0])): if settings.animate: self.play( self.drawnRefs["HEAD"].animate.move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 2.0 + shift[1], 0, ) ), ) else: self.drawnRefs["HEAD"].move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 2.0 + shift[1], 0, ) ) def reset_branch(self, hexsha, shift=numpy.array([0.0, 0.0, 0.0])): if settings.animate: self.play( self.drawnRefs[self.repo.active_branch.name].animate.move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 1.4 + shift[1], 0, ) ), ) else: self.drawnRefs[self.repo.active_branch.name].move_to( ( self.drawnCommits[hexsha].get_center()[0] + shift[0], self.drawnCommits[hexsha].get_center()[1] + 1.4 + shift[1], 0, ) ) def reset_head_branch_to_ref(self, ref, shift=numpy.array([0.0, 0.0, 0.0])): if settings.animate: self.play(self.drawnRefs["HEAD"].animate.next_to(ref, m.UP)) self.play( self.drawnRefs[self.repo.active_branch.name].animate.next_to( self.drawnRefs["HEAD"], m.UP ) ) else: self.drawnRefs["HEAD"].next_to(ref, m.UP) self.drawnRefs[self.repo.active_branch.name].next_to( self.drawnRefs["HEAD"], m.UP ) def translate_frame(self, shift): if settings.animate: self.play(self.camera.frame.animate.shift(shift)) else: self.camera.frame.shift(shift) def setup_and_draw_parent( self, child, commitMessage="New commit", shift=numpy.array([0.0, 0.0, 0.0]), draw_arrow=True, color=m.RED, ): circle = m.Circle( stroke_color=color, stroke_width=self.commit_stroke_width, fill_color=color, fill_opacity=self.ref_fill_opacity, ) circle.height = 1 if child != "dark": circle.next_to( self.drawnCommits[child.hexsha], m.LEFT if settings.reverse else m.RIGHT, buff=1.5, ) circle.shift(shift) if child != "dark": start = circle.get_center() end = self.drawnCommits[child.hexsha].get_center() 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=self.font, font_size=20, color=self.fontColor, weight=self.font_weight, ).next_to(circle, m.UP) self.toFadeOut.add(commitId) commitMessage = commitMessage.split("\n")[0][:40].replace("\n", " ") message = m.Text( "\n".join( commitMessage[j : j + 20] for j in range(0, len(commitMessage), 20) )[:100], font=self.font, font_size=14, color=self.fontColor, weight=self.font_weight, ).next_to(circle, m.DOWN) self.toFadeOut.add(message) 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 / settings.speed, ) else: self.camera.frame.move_to(circle.get_center()) self.add(circle, commitId, message) self.drawnCommits["abcdef"] = circle self.toFadeOut.add(circle) if draw_arrow and child != "dark": if settings.animate: self.play(m.Create(arrow), run_time=1 / settings.speed) else: self.add(arrow) self.arrows.append(arrow) self.toFadeOut.add(arrow) return commitId def draw_arrow_between_commits(self, startsha, endsha): start = self.drawnCommits[startsha].get_center() end = self.drawnCommits[endsha].get_center() arrow = DottedLine( start, end, color=self.fontColor, dot_kwargs={"color": self.fontColor} ).add_tip() length = numpy.linalg.norm(start - end) - 1.65 arrow.set_length(length) self.draw_arrow(True, arrow) def create_dark_commit(self): return "dark" def get_nondark_commits(self): nondark_commits = [] return nondark_commits def draw_ref(self, commit, top, i=0, text="HEAD", color=m.BLUE): refText = m.Text( text, font=self.font, font_size=20, color=self.fontColor, weight=self.font_weight, ) refbox = m.Rectangle( color=color, fill_color=color, fill_opacity=self.ref_fill_opacity, height=0.4, width=refText.width + 0.25, ) refbox.next_to(top, m.UP) refText.move_to(refbox.get_center()) ref = m.VGroup(refbox, refText) if settings.animate: self.play(m.Create(ref), run_time=1 / settings.speed) else: self.add(ref) self.toFadeOut.add(ref) self.drawnRefs[text] = ref self.prevRef = ref if i == 0 and self.first_parse: self.topref = self.prevRef def draw_dark_ref(self): refRec = m.Rectangle( 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, ) refRec.next_to(self.prevRef, m.UP) self.add(refRec) self.toFadeOut.add(refRec) self.prevRef = refRec def trim_path(self, path): return f"{path[:15]}...{path[-15:]}" if len(path) > 33 else path def trim_cmd(self, path, length=30): return f"{path[:length]}..." if len(path) > (length + 3) else path def get_remote_tracking_branches(self): remote_refs = [remote.refs for remote in self.repo.remotes] remote_tracking_branches = {} for reflist in remote_refs: for ref in reflist: if "HEAD" not in ref.name and ref.name not in remote_tracking_branches: remote_tracking_branches[ref.name] = ref.commit.hexsha return remote_tracking_branches 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=self.font, 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=self.font, 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.Text( self.trim_path(f), font=self.font, 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 color_by(self, offset=0): if settings.color_by == ColorByOptions.AUTHOR: sorted_authors = sorted( self.author_groups.keys(), key=lambda k: len(self.author_groups[k]), reverse=True, ) for i, author in enumerate(sorted_authors): authorText = m.Text( f"{author[:15]} ({str(len(self.author_groups[author]))})", font=self.font, 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] ) self.toFadeOut.add(authorText) if i == 0: self.recenter_frame() self.scale_frame() if settings.animate: self.play(m.AddTextLetterByLetter(authorText)) else: self.add(authorText) for g in self.author_groups[author]: g[0].set_color(self.colors[int(i % 11)]) self.recenter_frame() self.scale_frame() elif settings.color_by == ColorByOptions.BRANCH: pass 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 == 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) def add_group_to_author_groups(self, author, group): if author not in self.author_groups: self.author_groups[author] = [group] else: self.author_groups[author].append(group) def show_command_as_title(self): if settings.show_command_as_title: title_len = 100 while 1: titleText = m.Text( self.trim_cmd(self.cmd, title_len), font=self.font, font_size=36, color=self.fontColor, ) if titleText.width < self.camera.frame.width: break title_len -= 5 top = 0 for element in self.toFadeOut: if element.get_top()[1] > top: top = element.get_top()[1] titleText.move_to( ( self.camera.frame.get_x(), top + titleText.height * 2, 0, ) ) ul = m.Underline( titleText, color=self.fontColor, ) self.toFadeOut.add(titleText, ul) self.recenter_frame() self.scale_frame() if settings.animate: self.play(m.AddTextLetterByLetter(titleText), m.Create(ul)) else: self.add(titleText, ul) def del_rw(self, action, name, exc): os.chmod(name, stat.S_IWRITE) os.remove(name) def head_exists(self): try: hc = self.repo.head.commit except ValueError: return False return True def check_all_dark(self): if not self.drawnCommits: return True return False def add_ref_to_drawn_refs_by_commit(self, hexsha, ref): try: self.drawnRefsByCommit[hexsha].append(ref) except KeyError: self.drawnRefsByCommit[hexsha] = [ ref, ] def generate_random_sha(self): valid_chars = "0123456789abcdef" return "".join(random.choices(valid_chars, k=6)) def get_shortest_distance(self, sha_or_ref1, sha_or_ref2): # Create a queue for BFS that stores (commit, depth) tuples queue = deque([(self.repo.commit(sha_or_ref2), 0)]) visited = set() # Perform BFS from the start commit while queue: current_commit, depth = queue.popleft() # If we reach the end commit if current_commit.hexsha == self.repo.commit(sha_or_ref1).hexsha: return depth # Mark this commit as visited visited.add(current_commit.hexsha) # Queue all unvisited parents for parent in current_commit.parents: if parent.hexsha not in visited: queue.append((parent, depth + 1)) # If no path found return -1 def is_on_mainline(self, sha_or_ref1, sha_or_ref2): current_commit = self.get_commit(sha_or_ref2) # Traverse the first parent history while current_commit: if current_commit.hexsha == self.get_commit(sha_or_ref1).hexsha: return True if current_commit.parents: current_commit = current_commit.parents[0] else: break return False class DottedLine(m.Line): def __init__(self, *args, dot_spacing=0.4, dot_kwargs={}, **kwargs): m.Line.__init__(self, *args, **kwargs) n_dots = int(self.get_length() / dot_spacing) + 1 dot_spacing = self.get_length() / (n_dots - 1) unit_vector = self.get_unit_vector() start = self.start self.dot_points = [start + unit_vector * dot_spacing * x for x in range(n_dots)] self.dots = [m.Dot(point, **dot_kwargs) for point in self.dot_points] self.clear_points() self.add(*self.dots) self.get_start = lambda: self.dot_points[0] self.get_end = lambda: self.dot_points[-1] def get_first_handle(self): return self.dot_points[-1] def get_last_handle(self): return self.dot_points[-2]
X Tutup