Metro, a Git Replacement

A Git-Based commandline tool designed to simplify the process of managing repositories

Posted by Joseph Keane on September 10, 2019

Me and Michael were recently thinking about what to do for the upcoming Hack the Midlands event. One idea we had was for a Git replacement, but we realized it would likely take more than 48 hours to get working well. And more importantly, it wouldn’t be too impressive to show to people. So we decided to work on it straight away and allows us to make a UI at the event.

But that does leave a question.

Why did we make it?

The main reason was because of bad experiences with Git in the past. Trying to push and finding conflicts, reverting over and over after a failed merge, wrestling with adding and stashing every step of the way.

It’s not too hard to get your head around, but it’s very inconvenient and requires a lot of research for even basic thing. You only need to look at the top Stack Overflow questions to see it. A lot of these difficulties are built in, but they don’t have to be. Our idea was to just get rid of all the hard and complex stuff, and make something that’s easy and nice to use.

So we started by working out exactly what we wanted to make. We started writing down some of the principles behind the project. Some of the ideas we had were:

  • Treat repository as a thing that is constantly worked on and changing
  • Use making branches to fix conflicts
  • Less and simpler commands
  • Admins have control over repositories

Once we had that, we thought more carefully about what features specifically the project should have:

  • Make it so that editing the code is basically directly editing the head
  • Sync command that combines push and pull
  • Locking branches to direct commits
  • Compatible with Git tools
  • Branch user control

After this we decided it needed a name. We considered lots of options, such as Tachyon, Sapling and SourceTrain, but eventually decided on Metro. It was short, and the repository does work like multiple train lines connecting together.

At this point we just had to implement it – we could work out specifics of features as we went and compare to the initial specification. We first looked into diff and patch commands, but then decided it was worth using Git as a back-end as it would reduce a lot of work, Git is already solidly used and would provide implicit compatibility with Git. We found a library called LibGit2 which was an accessible Git implementation in C.

When it came to deciding the language, we needed something clean, fast, multi-platform and command-line. From this, we decided on Go. It compiled to binary and was easier than C and C++, and Rust seemed more suited to systems development.

So, after that we were ready to begin. LibGit2 has a binding for Go called Git2Go. We had some trouble to begin with because the interface was more complex than git_init or git_commit and the documentation was sparse. But after enough time looking at the docs, the bindings for Go and some testing to see what compiled, I understood enough to actually create functions. I’m glad now that we used LibGit2 over the direct commands as it made some things quite a bit simpler. As an example, here is the code of how to create a commit:

// Commit all files in the repo directory (excluding those in .gitignore) to the head of the current branch.
// repo: The repo
// message: The commit message
// parentRevs: The revisions corresponding to the commit's parents
func Commit(repo *git.Repository, message string, parentRevs ...string) error {
    // The commit author.
    // TODO: Use an actual user signature
    author := git.Signature{
        "Test User",
        "test@email.com",
        time.Now(),
    }
 
    // Get the repo's index, which we will use to the stage the files to be committed.
    index, err := repo.Index()
    if err != nil { return err }
 
    // Stage all the files in the repo directory (excluding those in .gitignore) for the commit.
    err = index.AddAll(pathSpecs(repo), git.IndexAddDisablePathspecMatch, nil)
    if err != nil { return err }
 
    // Write the files in the index into a tree that can be attached to the commit.
    oid, err := index.WriteTree()
    if err != nil { return err }
    tree, err := repo.LookupTree(oid)
    if err != nil { return err }
 
    // Save the index to disk so that it stays in sync with the contents of the working directory.
    // If we don't do this removals of every file are left staged.
    err = index.Write()
    if err != nil { return err }
 
    // Retrieve the commit objects associated with the given parent revisions.
    var parentCommits []*git.Commit
    for _, parentRev := range parentRevs {
        parentCommit, err := getCommit(parentRev, repo)
        if err != nil { return err }
 
        parentCommits = append(parentCommits, parentCommit)
    }
 
    // Commit the files to the head of the current branch.
    _, err = repo.CreateCommit("HEAD", &author, &author, message, tree, parentCommits...)
    if err != nil { return err }
 
    return nil
}

While we were recreating a Git interface, we were doing something different to almost all the functions over Git. The ones worked on so far:

  • Automatically adding all files not in ignore to commit
  • Automatically add initial commit
  • Switching lines saves current data in WIP commit
  • Merging current changes with last commit

For now we’re working to try and make it as usable as Git for repositories. You can find the project open source here

Edit

After getting further in the project, we realised that as a result of both the number of error checks in Go as well as integration with other software, C++ is more appropriate to write the software in. This can act as a reminder to be careful when choosing the language to write something in, and if you decide to change, do it sooner rather than later.



Joseph Keane

Black-Photon