The application is split into a NodeJS server and a web user interface.
All dependencies are vendored. Neither the server nor the web interface uses npm or a build step.
Why NodeJS and web?
Web has the lowest friction for me. The same goes for NodeJS. I like that it is a single binary. Low friction is important so that I don't drop the project.Why not
npm?
This is non-conventional but again the reason is friction. I have been burned too many times so I do not want to be dependent onnpm. I want no gate keepers between me and the project. The way to do that is to vendor the dependencies and accept the trade-offs. I acknowledge that this may increase the friction for others familiar with the ecosystem.
Note that due to vendoring: Dependencies are updated manually. Dependencies must contain version information. Dependencies must not be modified during development. Search and other tools may need to ignore dependencies.
Visual Git uses the git binary itself to interact with the Git repositories.
Why use
gitinstead of an API?
The common alternative is to uselibgit2. Both were considered but I chosegitoverlibgit2for a few reasons.libgit2is a full rewrite of Git as a library and an API, which veers a bit too far fromgitfor me. I started writing Visual Git to help me use Git more efficiently and also as a teaching tool. I want it to be an extension of the command line. Replicating familiargitcommands inlibgit2would require significant engineering effort. As of writinglibgit2is working on their own CLI implementation of Git, which could help, but again I do not want to be dependent onlibgit2and its progress, I'd rather depend ongit. Developing directly againstgitalso helps me learn more about its capabilities, which I can hopefully translate into making Visual Git better.
The trade-off from using git is that the interface is mostly meant for humans not applications. Another trade-off could be performance but the performance of git itself has improved a lot over time and may keep improving.
The server should be as thin a layer as possible between the web user interface and Git. The main reason for this is to leave open the possibility of using Visual Git as a static web app for viewing web-hosted Git repositories.
The server should have low trust in the web user interface. Some trust will be needed for write operations, but this should be configurable. As of writing this is enforced by allow-lists for commands and flags. The reason here is mostly an irrational "it feels bad to just pipe everything through". And also RCE via argument injection. But mostly an irrational bad feeling.
backend/ |
Contains the source code for the server. |
bin/ |
Contains the vg command for Windows and Linux. |
runtime/ |
Currently the location for the node binary. |
Why a dedicated
nodebinary?
The same way I do not like messing withnpm, I would rather just download the binary because I know that it will always work. There may be alternative methods in the future.
The web user interface is written in modern vanilla HTML, CSS and JavaScript. It targets the most recent Firefox and Chromium browsers.
Parts of the UI are split into Web Components. Shadow DOM is not used. Communication is event-based as opposed to reactive. CSS is all in a single file for now, sorry. CSS variables are used to parametrize the UI. SVG is used for the graph and icons. These are subject to change as the growth of the project requires it.
The frontend code starts from the frontend/ directory. It is further split as follows.
git-interface/ |
Code for interfacing with Git. |
models/ |
Classes such as Commit and Reference. |
ui/ |
UI rendering, interactions, animations. May be split further in the future. |
vendor/ |
Vendored dependencies. |
As of writing the web user interface roughly consists of:
main.js |
Entrypoint. |
graph.js |
For drawing a graph of Git commits and references. |
commands.js |
For interfacing with Git. |
settings.js |
For providing configurability. |
The graph rendering terminology is differentiated from Git terminology on purpose. In the graph, a Node represents a commit and a distinct series of Nodes form a Path. Paths often represent branches in Git.
Why different terminology?
The terminology is borrowed from graphs in general. You may think of Git history as flowing in chronological order from the earliest commits to the newest. You "branch out" into the future. But the Git DAG is read in reverse chronological order with each commit pointing to its own parent(s). We also render the graph in this reverse chronological order. From the point of view of the DAG and graph rendering, a commit with two or more parents "branches" or diverges towards the past. ANodeis rendered together with anyPaths diverging from that node. A Git merge becomes a divergence and branching in Git becomes a convergence ofPaths. The other reason is to avoid having to use the same naming for different purposes, rendering and version control. Time will tell if this was the correct decision.
The graph is rendered in two passes.
The first pass creates Nodes and places them on Paths. It prioritizes Paths with an associated name priority such as main and otherwise the first encountered Path.
The Paths are then sorted based on name priority, merge counts and length. Each Path is given a column number that avoids overlap with Nodes in other Paths.
In the second pass everything is rendered according to the collected information.