import platform
import sys
import git
import manim as m
import numpy
class GitSimBaseCommand():
def __init__(self, scene):
self.scene = scene
self.init_repo()
self.drawnCommits = {}
self.drawnRefs = {}
self.commits = []
self.zoomOuts = 0
self.toFadeOut = m.Group()
self.trimmed = False
self.prevRef = None
self.topref = None
self.maxrefs = None
self.i = 0
self.numCommits = 5
self.defaultNumCommits = 5
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.scene.args.logo)
self.logo.width = 3
def init_repo(self):
try:
self.repo = git.Repo(search_parent_directories=True)
except git.exc.InvalidGitRepositoryError:
print("git-sim error: No Git repository found at current path.")
sys.exit(1)
def execute(self):
print("Simulating: git " + self.scene.args.subcommand)
self.show_intro()
self.get_commits()
self.fadeout()
self.show_outro()
def get_commits(self, start="HEAD"):
if not self.numCommits:
if self.allow_no_commits:
self.numCommits = self.defaultNumCommits
self.commits = ["dark"]*5
self.zone_title_offset = 2
return
else:
print("git-sim error: No commits in current Git repository.")
sys.exit(1)
try:
self.commits = list(self.repo.iter_commits(start)) if self.numCommits == 1 else list(self.repo.iter_commits(start + "~" + str(self.numCommits) + "..." + start))
if len(self.commits) < self.defaultNumCommits:
self.commits = list(self.repo.iter_commits(start))
while len(self.commits) < self.defaultNumCommits:
self.commits.append(self.create_dark_commit())
self.numCommits = self.defaultNumCommits;
except git.exc.GitCommandError:
self.numCommits -= 1
self.get_commits(start=start)
def parse_commits(self, commit, prevCircle=None, shift=numpy.array([0., 0., 0.])):
if self.stop:
return
if ( self.i < self.numCommits and commit in self.commits ):
commitId, circle, arrow, hide_refs = self.draw_commit(commit, prevCircle, shift)
if commit != "dark":
if not hide_refs and not self.stop:
self.draw_head(commit, commitId)
self.draw_branch(commit)
self.draw_tag(commit)
self.draw_arrow(prevCircle, arrow)
if self.stop:
return
if self.i == 0 and len(self.drawnRefs) < 2:
self.draw_dark_ref()
if self.i < len(self.commits)-1:
self.i += 1
self.parse_commits(self.commits[self.i], circle)
else:
self.i = 0
def show_intro(self):
if ( self.scene.args.animate and self.scene.args.show_intro ):
self.scene.add(self.logo)
initialCommitText = m.Text(self.scene.args.title, font="Monospace", font_size=36, color=self.scene.fontColor).to_edge(m.UP, buff=1)
self.scene.add(initialCommitText)
self.scene.wait(2)
self.scene.play(m.FadeOut(initialCommitText))
self.scene.play(self.logo.animate.scale(0.25).to_edge(m.UP, buff=0).to_edge(m.RIGHT, buff=0))
self.scene.camera.frame.save_state()
self.scene.play(m.FadeOut(self.logo))
else:
self.logo.scale(0.25).to_edge(m.UP, buff=0).to_edge(m.RIGHT, buff=0)
self.scene.camera.frame.save_state()
def show_outro(self):
if ( self.scene.args.animate and self.scene.args.show_outro ):
self.scene.play(m.Restore(self.scene.camera.frame))
self.scene.play(self.logo.animate.scale(4).set_x(0).set_y(0))
outroTopText = m.Text(self.scene.args.outro_top_text, font="Monospace", font_size=36, color=self.scene.fontColor).to_edge(m.UP, buff=1)
self.scene.play(m.AddTextLetterByLetter(outroTopText))
outroBottomText = m.Text(self.scene.args.outro_bottom_text, font="Monospace", font_size=36, color=self.scene.fontColor).to_edge(m.DOWN, buff=1)
self.scene.play(m.AddTextLetterByLetter(outroBottomText))
self.scene.wait(3)
def fadeout(self):
if self.scene.args.animate:
self.scene.wait(3)
self.scene.play(m.FadeOut(self.toFadeOut), run_time=1/self.scene.args.speed)
else:
self.scene.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, prevCircle, shift=numpy.array([0., 0., 0.])):
if commit == "dark":
commitFill = m.WHITE if self.scene.args.light_mode else m.BLACK
elif ( len(commit.parents) <= 1 ):
commitFill = m.RED
else:
commitFill = m.GRAY
circle = m.Circle(stroke_color=commitFill, fill_color=commitFill, fill_opacity=0.25)
circle.height = 1
if shift.any():
circle.shift(shift)
if prevCircle:
circle.next_to(prevCircle,m.RIGHT if self.scene.args.reverse else m.LEFT, buff=1.5)
start = prevCircle.get_center() if prevCircle else ( m.LEFT if self.scene.args.reverse else m.RIGHT )
end = circle.get_center()
if commit == "dark":
arrow = m.Arrow(start, end, color=m.WHITE if self.scene.args.light_mode else m.BLACK)
elif commit.hexsha in self.drawnCommits:
end = self.drawnCommits[commit.hexsha].get_center()
arrow = m.Arrow(start, end, color=self.scene.fontColor)
self.stop = True
else:
arrow = m.Arrow(start, end, color=self.scene.fontColor)
length = numpy.linalg.norm(start-end) - ( 1.5 if start[1] == end[1] else 3 )
arrow.set_length(length)
commitId, commitMessage, commit, hide_refs = self.build_commit_id_and_message(commit)
commitId.next_to(circle, m.UP)
message = m.Text('\n'.join(commitMessage[j:j+20] for j in range(0, len(commitMessage), 20))[:100], font="Monospace", font_size=14, color=self.scene.fontColor).next_to(circle, m.DOWN)
if self.scene.args.animate and commit != "dark" and not self.stop:
self.scene.play(self.scene.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), run_time=1/self.scene.args.speed)
elif not self.stop:
self.scene.add(circle, commitId, message)
else:
return commitId, circle, arrow, hide_refs
if commit != "dark":
self.drawnCommits[commit.hexsha] = circle
self.toFadeOut.add(circle, commitId, message)
self.prevRef = commitId
return commitId, circle, arrow, hide_refs
def build_commit_id_and_message(self, commit):
hide_refs = False
if commit == "dark":
commitId = m.Text("", font="Monospace", font_size=20, color=self.scene.fontColor)
commitMessage = ""
else:
commitId = m.Text(commit.hexsha[0:6], font="Monospace", font_size=20, color=self.scene.fontColor)
commitMessage = commit.message.split("\n")[0][:40].replace("\n", " ")
return commitId, commitMessage, commit, hide_refs
def draw_head(self, commit, commitId):
if ( commit.hexsha == self.repo.head.commit.hexsha ):
headbox = m.Rectangle(color=m.BLUE, fill_color=m.BLUE, fill_opacity=0.25)
headbox.width = 1
headbox.height = 0.4
headbox.next_to(commitId, m.UP)
headText = m.Text("HEAD", font="Monospace", font_size=20, color=self.scene.fontColor).move_to(headbox.get_center())
head = m.VGroup(headbox, headText)
if self.scene.args.animate:
self.scene.play(m.Create(head), run_time=1/self.scene.args.speed)
else:
self.scene.add(head)
self.toFadeOut.add(head)
self.drawnRefs["HEAD"] = head
self.prevRef = head
if self.i == 0:
self.topref = self.prevRef
def draw_branch(self, commit):
x = 0
branches = [branch.name for branch in self.repo.heads]
for selected_branch in self.selected_branches:
branches.insert(0, branches.pop(branches.index(selected_branch)))
for branch in branches:
if ( commit.hexsha == self.repo.heads[branch].commit.hexsha ):
branchText = m.Text(branch, font="Monospace", font_size=20, color=self.scene.fontColor)
branchRec = m.Rectangle(color=m.GREEN, fill_color=m.GREEN, fill_opacity=0.25, 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 self.scene.args.animate:
self.scene.play(m.Create(fullbranch), run_time=1/self.scene.args.speed)
else:
self.scene.add(fullbranch)
self.toFadeOut.add(branchRec, branchText)
self.drawnRefs[branch] = fullbranch
if self.i == 0:
self.topref = self.prevRef
x += 1
if x >= self.scene.args.max_branches_per_commit:
return
def draw_tag(self, commit):
x = 0
if self.hide_first_tag and self.i == 0:
return
for tag in self.repo.tags:
try:
if ( commit.hexsha == tag.commit.hexsha ):
tagText = m.Text(tag.name, font="Monospace", font_size=20, color=self.scene.fontColor)
tagRec = m.Rectangle(color=m.YELLOW, fill_color=m.YELLOW, fill_opacity=0.25, height=0.4, width=tagText.width+0.25)
tagRec.next_to(self.prevRef, m.UP)
tagText.move_to(tagRec.get_center())
self.prevRef = tagRec
if self.scene.args.animate:
self.scene.play(m.Create(tagRec), m.Create(tagText), run_time=1/self.scene.args.speed)
else:
self.scene.add(tagRec, tagText)
self.toFadeOut.add(tagRec, tagText)
if self.i == 0:
self.topref = self.prevRef
x += 1
if x >= self.scene.args.max_tags_per_commit:
return
except ValueError:
pass
def draw_arrow(self, prevCircle, arrow):
if prevCircle:
if self.scene.args.animate:
self.scene.play(m.Create(arrow), run_time=1/self.scene.args.speed)
else:
self.scene.add(arrow)
self.toFadeOut.add(arrow)
def recenter_frame(self):
if self.scene.args.animate:
self.scene.play(self.scene.camera.frame.animate.move_to(self.toFadeOut.get_center()), run_time=1/self.scene.args.speed)
else:
self.scene.camera.frame.move_to(self.toFadeOut.get_center())
def scale_frame(self):
if self.scene.args.animate:
self.scene.play(self.scene.camera.frame.animate.scale_to_fit_width(self.toFadeOut.get_width()*1.1), run_time=1/self.scene.args.speed)
if ( self.toFadeOut.get_height() >= self.scene.camera.frame.get_height() ):
self.scene.play(self.scene.camera.frame.animate.scale_to_fit_height(self.toFadeOut.get_height()*1.25), run_time=1/self.scene.args.speed)
else:
self.scene.camera.frame.scale_to_fit_width(self.toFadeOut.get_width()*1.1)
if ( self.toFadeOut.get_height() >= self.scene.camera.frame.get_height() ):
self.scene.camera.frame.scale_to_fit_height(self.toFadeOut.get_height()*1.25)
def vsplit_frame(self):
if self.scene.args.animate:
self.scene.play(self.scene.camera.frame.animate.scale_to_fit_height(self.scene.camera.frame.get_height()*2))
else:
self.scene.camera.frame.scale_to_fit_height(self.scene.camera.frame.get_height()*2)
try:
if self.scene.args.animate:
self.scene.play(self.toFadeOut.animate.align_to(self.scene.camera.frame, m.UP).shift(m.DOWN*0.75))
else:
self.toFadeOut.align_to(self.scene.camera.frame, m.UP).shift(m.DOWN*0.75)
except ValueError:
pass
def setup_and_draw_zones(self, first_column_name="Untracked files", second_column_name="Working directory modifications", third_column_name="Staging area", reverse=False):
horizontal = m.Line((self.scene.camera.frame.get_left()[0], self.scene.camera.frame.get_center()[1], 0), (self.scene.camera.frame.get_right()[0], self.scene.camera.frame.get_center()[1], 0), color=self.scene.fontColor).shift(m.UP*2.5)
horizontal2 = m.Line((self.scene.camera.frame.get_left()[0], self.scene.camera.frame.get_center()[1], 0), (self.scene.camera.frame.get_right()[0], self.scene.camera.frame.get_center()[1], 0), color=self.scene.fontColor).shift(m.UP*1.5)
vert1 = m.DashedLine((self.scene.camera.frame.get_left()[0], self.scene.camera.frame.get_bottom()[1], 0), (self.scene.camera.frame.get_left()[0], horizontal.get_start()[1], 0), dash_length=0.2, color=self.scene.fontColor).shift(m.RIGHT*6.5)
vert2 = m.DashedLine((self.scene.camera.frame.get_right()[0], self.scene.camera.frame.get_bottom()[1], 0), (self.scene.camera.frame.get_right()[0], horizontal.get_start()[1], 0), dash_length=0.2, color=self.scene.fontColor).shift(m.LEFT*6.5)
if reverse:
first_column_name = "Staging area"
third_column_name = "Deleted changes"
firstColumnTitle = m.Text(first_column_name, font="Monospace", font_size=28, color=self.scene.fontColor).align_to(self.scene.camera.frame, m.LEFT).shift(m.RIGHT*0.65).shift(m.UP*self.zone_title_offset)
secondColumnTitle = m.Text(second_column_name, font="Monospace", font_size=28, color=self.scene.fontColor).move_to(self.scene.camera.frame.get_center()).align_to(firstColumnTitle, m.UP)
thirdColumnTitle = m.Text(third_column_name, font="Monospace", font_size=28, color=self.scene.fontColor).align_to(self.scene.camera.frame,m.RIGHT).shift(m.LEFT*1.65).align_to(firstColumnTitle, m.UP)
self.toFadeOut.add(horizontal, horizontal2, vert1, vert2, firstColumnTitle, secondColumnTitle, thirdColumnTitle)
if self.scene.args.animate:
self.scene.play(m.Create(horizontal), m.Create(horizontal2), m.Create(vert1), m.Create(vert2), m.AddTextLetterByLetter(firstColumnTitle), m.AddTextLetterByLetter(secondColumnTitle), m.AddTextLetterByLetter(thirdColumnTitle))
else:
self.scene.add(horizontal, horizontal2, vert1, vert2, firstColumnTitle, secondColumnTitle, thirdColumnTitle)
firstColumnFileNames = set()
secondColumnFileNames = set()
thirdColumnFileNames = set()
firstColumnArrowMap = {}
secondColumnArrowMap = {}
self.populate_zones(firstColumnFileNames, secondColumnFileNames, thirdColumnFileNames, firstColumnArrowMap, secondColumnArrowMap)
firstColumnFiles = m.VGroup()
secondColumnFiles = m.VGroup()
thirdColumnFiles = m.VGroup()
firstColumnFilesDict = {}
secondColumnFilesDict = {}
thirdColumnFilesDict = {}
for i, f in enumerate(firstColumnFileNames):
text = m.Text(f, font="Monospace", font_size=24, color=self.scene.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(f, font="Monospace", font_size=24, color=self.scene.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(f, font="Monospace", font_size=24, color=self.scene.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
if len(firstColumnFiles):
if self.scene.args.animate:
self.scene.play(*[m.AddTextLetterByLetter(d) for d in firstColumnFiles])
else:
self.scene.add(*[d for d in firstColumnFiles])
if len(secondColumnFiles):
if self.scene.args.animate:
self.scene.play(*[m.AddTextLetterByLetter(w) for w in secondColumnFiles])
else:
self.scene.add(*[w for w in secondColumnFiles])
if len(thirdColumnFiles):
if self.scene.args.animate:
self.scene.play(*[m.AddTextLetterByLetter(s) for s in thirdColumnFiles])
else:
self.scene.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 self.scene.args.animate:
self.scene.play(m.Create(firstColumnArrowMap[filename]))
else:
self.scene.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 self.scene.args.animate:
self.scene.play(m.Create(secondColumnArrowMap[filename]))
else:
self.scene.add(secondColumnArrowMap[filename])
self.toFadeOut.add(secondColumnArrowMap[filename])
self.toFadeOut.add(firstColumnFiles, secondColumnFiles, thirdColumnFiles)
def populate_zones(self, firstColumnFileNames, secondColumnFileNames, thirdColumnFileNames, firstColumnArrowMap={}, secondColumnArrowMap={}):
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 self.scene.args.animate:
self.scene.play(self.scene.camera.frame.animate.move_to(self.drawnCommits[commit.hexsha].get_center()))
else:
self.scene.camera.frame.move_to(self.drawnCommits[commit.hexsha].get_center())
def reset_head_branch(self, hexsha, shift=numpy.array([0., 0., 0.])):
if self.scene.args.animate:
self.scene.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].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].move_to((self.drawnCommits[hexsha].get_center()[0]+shift[0], self.drawnCommits[hexsha].get_center()[1]+2+shift[1], 0))
def translate_frame(self, shift):
if self.scene.args.animate:
self.scene.play(self.scene.camera.frame.animate.shift(shift))
else:
self.scene.camera.frame.shift(shift)
def setup_and_draw_parent(self, child, commitMessage="New commit", shift=numpy.array([0., 0., 0.]), draw_arrow=True, color=m.RED):
circle = m.Circle(stroke_color=color, fill_color=color, fill_opacity=0.25)
circle.height = 1
circle.next_to(self.drawnCommits[child.hexsha], m.LEFT if self.scene.args.reverse else m.RIGHT, buff=1.5)
circle.shift(shift)
start = circle.get_center()
end = self.drawnCommits[child.hexsha].get_center()
arrow = m.Arrow(start, end, color=self.scene.fontColor)
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.scene.fontColor).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="Monospace", font_size=14, color=self.scene.fontColor).next_to(circle, m.DOWN)
self.toFadeOut.add(message)
if self.scene.args.animate:
self.scene.play(self.scene.camera.frame.animate.move_to(circle.get_center()), m.Create(circle), m.AddTextLetterByLetter(commitId), m.AddTextLetterByLetter(message), run_time=1/self.scene.args.speed)
else:
self.scene.camera.frame.move_to(circle.get_center())
self.scene.add(circle, commitId, message)
self.drawnCommits["abcdef"] = circle
self.toFadeOut.add(circle)
if draw_arrow:
if self.scene.args.animate:
self.scene.play(m.Create(arrow), run_time=1/self.scene.args.speed)
else:
self.scene.add(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.scene.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 = []
for commit in self.commits:
if commit != "dark":
nondark_commits.append(commit)
return nondark_commits
def draw_ref(self, commit, top, text="HEAD", color=m.BLUE):
refText = m.Text(text, font="Monospace", font_size=20, color=self.scene.fontColor)
refbox = m.Rectangle(color=color, fill_color=color, fill_opacity=0.25, 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 self.scene.args.animate:
self.scene.play(m.Create(ref), run_time=1/self.scene.args.speed)
else:
self.scene.add(ref)
self.toFadeOut.add(ref)
self.drawnRefs[text] = ref
self.prevRef = ref
if self.i == 0:
self.topref = self.prevRef
def draw_dark_ref(self):
refRec = m.Rectangle(color=m.WHITE if self.scene.args.light_mode else m.BLACK, fill_color=m.WHITE if self.scene.args.light_mode else m.BLACK, height=0.4, width=1)
refRec.next_to(self.prevRef, m.UP)
self.scene.add(refRec)
self.toFadeOut.add(refRec)
self.prevRef = refRec
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]