MarioFlach

MarioFlach

GitGud, GitHub clone entirely written in Elixir

Hello,
I want to share a project I’ve been working on for a while:

https://github.com/almightycouch/gitgud

Background

Some time ago I came across a talk: How we scaled git lab for a 30k employee company.


(basic overview of the system architecture)

The presentation was about how the team at git-lab solved scaling issues on their platform. After a few slides I wondered how this could be approached with languages like Erlang and Elixir.

After some moments of reflection, I had it! A basic concept on how things could fit together:


(joke aside, that’s pretty much what I came with :wink:)

Building blocks

Erlang/Elixir and OTP provide a lot of building blocks to power a scalable Git platform. The idea was to use nothing but Elixir and libgit2.

So here’s a more detailed overview of the architecture:

Authentication

A really nice thing about :ssh is that it provides support for authentication via password and public/private keys out of the box:

NIFs / libgit2

If you are not familiar with libgit2, it’s a C written implementation of the Git core methods and functions. One very unique feature of the library is that you can provide your own storage backend. Which means you can plugin you own distributed K/V database instead of writing everything to the filesystem.

I heavily used code from the Erlang :geef library, refactored a good part and added a bunch of missing functions. Check the Elixir module and the C bindings.

Git transfer protocol & Packfile format

This is the fun part of the project :smirk:.

libgit2 does not support server side commands, it only focuses on the client implementation. In the first iteration I cheated and used Ports to execute git-upload-pack and git-receive-pack. It worked well, both for SSH and HTTP.

But I wanted to have more control over the process (hooks, etc.) and having to depend on git only for the transfer protocol was a shame…

So I started digging in the protocol internals, docs. I worked with a lot of different network protocols in my career (medical field, DICOM, HL7, etc) but I must admit, the Git transfer protocol and the Git Packfile format was a quiet heavy sh**t to grasp.

  • It has lots of different binary optimisations.
  • It uses zlib to inflate chunks but only gives you the resulting size of the deflated data so I had to come with my own zlib C implementation.
  • The transfer protocol’s differs depending on the transport protocol.
  • Documentation is, hard to find, scarce, well hmm.

Its currently quiet messy, but have a look here for implementation details.

Project state

Still a proof of concept, it’s working but still. Almost no tests so unexpected things my happen.
If you are interested, download the code and give it a try. PR are very welcome.

Most Liked

MarioFlach

MarioFlach

I’ve continued my journey and committed to the project every now and then.

So here’s a little update :grin:.

Distributed Setup

The current version is running on Fly :rocket:. I’ve got a small cluster (two nodes: FRA, LAX) setup with :libcluster so adding new nodes should work automatically.

Each Fly instance has it own storage attached for storing Git repositories. When a user creates a new repository it is assigned to the local’s node storage and all the Git objects will be stored there:

I live in Austria, so my Git repositories are stored on the closest instance running in Frankfurt.

When accessing one of my repos from the US, the instance in Los Angeles will route all Git commands to the right node.

In order to get things working without too much latency, I had to refactor a big chunk of code to batch Git commands together and keep the number of roundtrips between instances low. In the end I’m quite happy with the results :partying_face:

Try for yourself:

Repository Pools

I’ve implemented some kind of distributed routing pool on top of Erlang’s :global.

Here’s a screenshot of the supervision tree:

When a node start’s up, GitGud.RepoSupervisor does a few things:

  • tags the local storage (if not already tagged) and registers the resulting id across the cluster.
  • starts a GitGud.RepoStorage worker for handling filesystem operations.
  • starts a GitGud.RepoPool supervisor for handling Git commands.

The GitGud.Repo schema has a :volume field which points to the storage VOLUME where it data is stored. When creating a repositories it is assigned to the local storage:

field :volume, :string, autogenerate: {GitGud.RepoStorage, :volume, []}

With this in mind, let see how we can run Git commands on a specific repository:

repo = GitGud.RepoQuery.user_repo("redrabbit", "git-limo")
{:ok, agent} = GitRekt.GitRepo.get_agent(repo)
{:ok, head} = GitRekt.GitAgent.head(agent, head)
{:ok, commit} = GitRekt.GitAgent.peel(agent, head)
{:ok, commit_msg} = GitRekt.GitAgent.commit_message(agent, commit)
IO.puts commit_msg

The above example prints the HEAD commit message for redrabbit/git-limo.

The interesting part here is GitRekt.GitRepo.get_agent/1 which is implemented by GitGud.Repo:

  defimpl GitRekt.GitRepo, for: GitGud.Repo do
    def get_agent(repo), do: GitGud.RepoPool.checkout(repo)
  end

As you can see, it rely on GitGud.RepoPool for retrieving a Git agent from the pool on the right node. Let’s dive into it :diving_mask:.

Internally, the pool can be seen as a DynamicSupervisor of DynamicSupervisors. GitGud.RepoPool.checkout/1 being the entry-point for fetching agents. It also provides a few nice things:

  • auto scale – grows/shrinks the number of agent processes based on demand.
  • global cache – agents in a pool share a global ETS table.
  • round robin – agents are distributed using round-robin.
  • node aware – a pool will always start on the right node based on the repo’s VOLUME.

Git Agents

The GitRekt.GitAgent module is the backbone for running Git commands. While the public API is quite easy to grasp, it hides a lot of complexity.

An agent is basically a wrapper around GitRekt.Git. Here’s a very basic usage example:

{:ok, agent} = GitRekt.GitAgent.start_link("path/to/workdir")
{:ok, tags} = GitRekt.GitAgent.tags(agent)
for tag <- tags do
  IO.puts "Tag #{tag.name} -> #{Base.decode16(tag.oid)}"
end

In the above example, agent is a dedicated process for running Git commands.

Note that it is also allowed to run Git commands in the current process as well:

{:ok, agent} = GitRekt.Git.repository_open("path/to/workdir")
{:ok, branches} = GitRekt.GitAgent.branches(agent)
for branch <- branches do
  IO.puts "Branch #{branch.name} -> #{Base.decode16(branch.oid)}"
end

In the above example, agent is a NIF-resource representing a libgit2 repository.

Transactions

GitRekt.GitAgent provides support for transactions aka. batching a bunch of Git operations in one call. This is very important when running Git commands on a separate node:

# agent is a PID running on an other node
{:ok, head} = GitRekt.GitAgent.head(agent, head) #1
{:ok, commit} = GitRekt.GitAgent.peel(agent, head) #2
{:ok, commit_msg} = GitRekt.GitAgent.commit_message(agent, commit) #3
IO.puts commit_msg

Running the above code would make three separate GenServer.call/2 resulting in quite some latency. We can fix this by batching the commands in a transaction:

# agent is a PID running on an other node
{:ok, commit_msg} =
  GitRekt.GitAgent.transaction(agent, fn agent ->
    with {:ok, head} <- GitRekt.GitAgent.head(agent, head),
         {:ok, commit} = GitRekt.GitAgent.peel(agent, head) do
      GitRekt.GitAgent.commit_message(agent, commit)
  end)

In the above example, the three commands are execute in a single call on the dedicated agent process. Reducing the overall latency…

Caching

An additional feature of GitRekt.GitAgent is caching. When running transaction/3 we can pass a cache key as the 2nd argument:

def commit_info(agent, commit) do
  GitAgent.transaction(agent, {:commit_info, commit.oid}, fn agent ->
    with {:ok, author} <- GitAgent.commit_author(agent, commit),
        {:ok, committer} <- GitAgent.commit_committer(agent, commit),
        {:ok, message} <- GitAgent.commit_message(agent, commit),
        {:ok, parents} <- GitAgent.commit_parents(agent, commit),
        {:ok, timestamp} <- GitAgent.commit_timestamp(agent, commit),
        {:ok, gpg_sig} <- GitAgent.commit_gpg_signature(agent, commit) do
      {:ok, %{
        oid: commit.oid,
        author: author,
        committer: committer,
        message: message,
        parents: Enum.to_list(parents),
        timestamp: timestamp,
        gpg_sig: gpg_sig
      }}
    end
  end)
end

You may have noticed the {:commit_info, commit.oid} tuple given to transaction/3. This tells the agent that the transaction should be cached using this key.

Calling commit_info/2 two times in a row would result in the following log output:

[debug] [Git Agent] transaction(:commit_info, "b662d32") executed in 361 µs
[debug] [Git Agent] > commit_author(<GitCommit:b662d32>) executed in 6 µs
[debug] [Git Agent] > commit_committer(<GitCommit:b662d32>) executed in 5 µs
[debug] [Git Agent] > commit_message(<GitCommit:b662d32>) executed in 1 µs
[debug] [Git Agent] > commit_parents(<GitCommit:b662d32>) executed in 4 µs
[debug] [Git Agent] > commit_timestamp(<GitCommit:b662d32>) executed in 11 µs
[debug] [Git Agent] > commit_gpg_signature(<GitCommit:b662d32>) executed in 6 µs
[debug] [Git Agent] transaction(:commit_info, "b662d32") executed in ⚡ 3 µs

We can observe that the first call executes the different commands one by one and cache the result while the second one fetches the result directly from the cache without having to actually run the transaction.

There’s a lot more to tell about GitRekt.GitAgent’s internals (streaming support, mechanism to prevent the garbage collector for deleting NIF-resources, etc.). If you’re interested I can write a small post about it.

LiveView

On the frontend, I’ve managed to introduce Phoenix LiveView and replace all my React/Relay components with a LiveView counterpart. For example, the GitGud.Web.TreeBrowserLive is used to navigate across a Git repository tree. Here’s a list of all views/components:

  • GitGud.Web.BlobHeaderLive
  • GitGud.Web.BranchSelectLive
  • GitGud.Web.CommentFormLive
  • GitGud.Web.CommentLive
  • GitGud.Web.CommitDiffLive
  • GitGud.Web.CommitLineReviewLive
  • GitGud.Web.GlobalSearchLive
  • GitGud.Web.IssueEventLive
  • GitGud.Web.IssueFormLive
  • GitGud.Web.IssueLabelSelectLive
  • GitGud.Web.IssueLive
  • GitGud.Web.MaintainerSearchFormLive
  • GitGud.Web.TreeBrowserLive

Fast Git Backend Server

I also refactored the Git backend aka. GitRekt.WireProtocol which was slow and consumed a lot of resources.

When pushing a repository, the incoming PACK file is now directly streamed to the disk. This increases raw performances about 700% and greatly reduced the amount of RAM and CPU used for the operation.

The performance boost allows to fetch/push across nodes in a cluster setup. When you push via SSH you will send the PACK to the nearest node which is then streamed to the right node in the cluster.

MarioFlach

MarioFlach

I’ve been working a lot on this project lately. Just released version 0.2.7.

Also I decided to publish the project on a dedicated server for testing purposes.

So here it is: https://git.limo

Or here if you want to browse some repositories:

This is pretty limited currently because you are not allowed to create new repositories. I need to provide monitoring tools and other things before letting anybody host their repositories.

Here’s a brief summary of the project’s current state:

Basic CRUD operations

  • User registration/authentication
    • with login/email - password credentials.
    • with OAuth2.0 for GitHub and Gitlam.
  • Email management:
    • Each registered user need at least one verified email address in order to create new repositories and maintain other’s repositories.
    • Each verified email address is used to associate Git commits. This is also the case for GPG signed commits.
    • The primary email address is also used to retrieve a user’s Gravatar.
  • SSH and GPG key management:
    • We support Git over SSH and HTTPS. By adding SSH public keys, a user can authenticate without the need of a password.
    • GPG public keys are used to verify that a Git commit has been signed by a given user. In order to show the verification icon:
      1. The committer’s email must match a verified email address
      2. The committer’s email must also match the GPG key email address
  • Repository management:
    • Users can create repositories if they have at least one verified email address.
    • Each repository can have multiple maintainers with read/write/admin rights.

Git Repositories

We support both HTTPS and SSH transport protocols. While HTTPS support Basic Authentication via credentials. SSH supports authentication via public key and password.

Access to repositories depend on the user’s authorisations:

  • Anybody can view/clone a public repo.
  • Only owner and maintainers can view/clone a private repo.
  • Only owner and maintainers with at least :write access can write/push to a repo.
  • Only owner and maintainers with :admin access can edit settings of a repo.

Browsing repositories is similar to GitHub/GitLab. You can browse trees, view blobs (with syntax-highlighting), walk the commit history, walk the commit history for a given tree/blob, display diffs, etc.


Accessing a Git repository content is done via the GitRekt.GitAgent abstraction. Basically the agent provides an API to manipulate Git objects and refs.

The nice part here is that the access to a repository can be done from multiple processes simultaneously (by wrapping the NIFs resources in a GenServer and serialising function calls).

This will be important when implementing support for clustering aka. distributing repositories on multiple nodes.

The GitRekt.GitAgent provides two different modes:

  • :inproc - calling GitRekt.Git functions (NIFs) directly.
  • :shared - serialise function calls through GenServer.

The GitRekt.GitRepo protocol helps to implement custom logic (clustering) for both modes. Currently, only GitGud.Repo implements this protocol.

GraphQL API

The available GraphQL API is quite fully featured. It also provide subscriptions via Phoenix Websockets.

Issues and reviews

There is no issue system at the moment. I hope to get started with issues soon.

I also want to provide a review mechanism, basically the possibility to review changes across commits. Currently, any registered user can comment on any commit. Comments are stored globally (shown at the end of a diff) or on a per-line basis.

Wiki

I’ve played a little bit with a Git based Wiki implementation. But this will come in a later future.

MarioFlach

MarioFlach

Also, the project is sponsored by AppSignal :heart:. You can check the appsignal branch if you’re interested:

You can also check-out the fly branch if you want to deploy on your own Fly instances.

Where Next?

Popular in Discussions Top

matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
jeramyRR
This is an interesting article to read. Elixir’s performance, like usual, is excellent. However, it seems like the high CPU usage is co...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31265 143
New
mbenatti
Following https://github.com/tbrand/which_is_the_fastest |&gt; https://raw.githubusercontent.com/tbrand/which_is_the_fastest/master/imgs...
New
rms.mrcs
A couple of days ago I was discussing with a friend about different approaches to write microservices. He said that if he was going to w...
New
cblavier
Hey there, It’s been more than a year since we started using LiveView as our main UI library and building a whole library of UI componen...
New
acrolink
How does the two languages compare when it comes to server side application development? Any experiences or ideas? Thank you.
New
wmnnd
The Go vs Elixir thread got me thinking: Would it be too hard to implement a simple mechanism for creating Go-style static app binaries f...
New
Markusxmr
Since Drab has been developed for a while in the open, introducing the Liveview functionality in a way it happend appears to undermine th...
New

Other popular topics Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39467 209
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New

We're in Beta

About us Mission Statement