Isn't It Obvious That C Programmers Wrote Git?

(Published: 12 January 2023)

The Problem

Git sucks. Reason? It is entirely imperative. At that, it is a great example of how the language you use influences your approach to problem-solving. There's literally not a iota of Do What I Mean in the whole damned system. All of Git "commands" might as well have been function calls with key arguments. And they probably would be exactly that if C had key arguments.

And, well, just like in C, there's no safety pillow - if you screw up a call, something simply goes pear-shaped.

I think the next Big Thing (TM) (c) in version control ought to… simply not suck. (And if you are thinking about mercurial, then well, no, that one is almost as bad as Git.)

My theory on good UI is simple: a powerful interface requires exposing the building blocks to the user. And by that I don't mean the implementation details. I mean the program, in order for it to be usable, has to be understood by the user at the level of its crucial concepts. And, I believe, it's up to the program to reveal these concepts in the most direct manner possible.

Git doesn't do this. Git just gives you a set of commands. If you want: go read the documentation and try to understand the underlying data structures.

But even when you do understand those, you are still not that much better off than before. The data structures used in Git aren't difficult to understand.

Every time you do a commit, Git builds a tree of your whole project and puts every file into that tree. A node in a tree is called a blob - a binary large object. Basically, it's a compressed version of the original file.

So, a commit is a tree of blobs. A directory-tree of files. A full snapshot of your repository: all of it. Each blob is identified by its SHA1 hash and is stored under this name somewhere in the .git directory. And that's the trick to compression: same files/blobs shared by different commits have the same hash-value, and so they don't have to be duplicated in the file system. However, if a file changes even by a single character, it will have a different hash, and so it will be saved as a separate blob.

This is referred to as content-addressable storage.

My point here is not to make you understand how Git works, though. And there are many better explanations of this online. My point here is: blobs, trees, commits, and the commit graph (acyclic and directed). That's all there's to Git. These are your building blocks. These are your concepts.

"Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." – Fred Brooks, The Mythical Man-Month.

So, we have identified the first problem with the command-line Git interface:

  • It doesn't make the building blocks obvious.

But also:

  • It doesn't provide any way to see the relationships and connections between all the building blocks in your particular repository.

Next, there are so many Git commands (~150) that the user simply feels lost:

  • Commands are neither discoverable nor apparent.

And then, there's also no context-awareness. Suppose you were to select a range of commits (there's no such thing as selecting in Git, but bear with me). But there's only so many commands that work on ranges, such as squashing, and so the user should be able to see all those relevant commands.

  • There's no context-sensitive hinting.

All of this is exacerbated by the fact that you aren't working in an actual IDE, but in a terminal emulator. There's no completion, there's no way to know if your command is sound, or that the arguments you are passing to a command are of the correct type.

  • Command-line editing is very far from an IDE-level experience.

To a degree, you could cope with all these things. You could learn all the commands, install all the Git porcelains that you wanted, read a book on Git internals.

But would you learn to stop making mistakes? Because, still:

  • There's no undo.

You can manipulate the history, but an undo is not there.

The staging area used in Git for preparing commits is a good idea. But it's not a staging area for all commands. It's just an area for the next commit to form, and a commit is just a piece of a (potentially huge) history graph.

  • The staging step is very limited. (I will discuss how this should be done later.)

And, still, there's a degree of complexity that the existing interface has no good way of dealing with.

Let me give you an example. Suppose you have put too many changes into a commit and now want to split it. There's no such thing in Git as split. Instead, what you do is:

Splitting and Existing Git Commit

  • run git rebase -i <commit-hash>~ (note the ~) or git rebase -i <hash-of-previous-commit>
  • find the commit you want to split in the rebase edit screen, change the pick to e (edit)
  • save and exit (ESC followed by :wq to close VIM)
  • git reset HEAD~ to reset the staged changes
  • git add [files-to-add] all the files we want to add to the first commit (here would be Git add A)
  • git commit normally, with a message etc
  • Run as many other rounds of as you want commits:
    • git add [other-files-to-add]
    • git commit
  • git rebase --``continue to indicate that the splitting has been finished and to continue the rebase
  • Finally we can git cherry-pick <new-commit-hash> to get the changes into our branch

Well, that's a mouthful, isn't it?

And from this instruction set alone, I am still not sure how to split a commit with changes contained within a single file. You can't pick that apart, apparently, but just by manually editing the file in question repeatedly, once per each resulting commit.

Now, conceptually, all you would have to do in your head is:

Decide what files and lines belong to a new commit and simply move them there.

And so:

  • The command-line interface is simply inadequate for performing complex operations.

Moreover, you couldn't write your own automated workflow to accomplish this. There's no environment in place for this.

  • There's no environment for doing complex things at all.

And that's the root of the Git interface problem. Manipulating a history/version-control graph is an interactive problem.

Simply put, things don't just boil down to C-like function invocations that you call in isolation from each other. There's always some state across the invocations.

Git people had a theory that you could expose the Git internals, and then, someone else would build a so-called porcelain that would make working with Git easier.

"See, here's a bunch of mess we have. Now, if only we had a porcelain to cover it, it would just magically fix everything!"

Well, guess what, we have Magit, it is a porcelain, and I can screw up just as easily.

Can it be much better? In theory? Sure, maybe. With some cooperation from the Git dev team itself and with some miraculous effort, to a degree, you could, perhaps, make it suck less. Maybe. I wouldn't bet on it. Because if software at large has taught us anything, it's that fixing broken things can at times be a lot tougher than starting from scratch.

So, really: no. We simply need a better core. Stop making excuses.

The Solution

So, with all this in mind, what can we do?

Well, to summarize, here is what we want:

  • The ability to visualize the state of the system. The user should see the relationships and the underlying structures.1 And, by the way, I am in no way speaking against the text-based interfaces. Keyboard-driven workflows can be very efficient. Every programmer or admin knows this. I am in no way promoting mouse-orientedness, but it's to be understood that a pointing device can make some operations easier to accomplish. In other words, visualizing things doesn't in any way prevent keyboard control.
  • Discoverable operations via the current context of the objects that the user wants to work with.
  • A staging step for the whole repository.

The idea of a repo-level staging step is quite simple. What we want to have is two images of the whole history graph. The first one is your original graph. The second one is the graph constructed after any and all the operations that you have successively applied to the first graph.

Once you are satisfied with your changes to the graph, you can say OK. And now, your transformed graph is your new version-control repository.

So, it's not just a commit you can be staging. But your whole repository.

On top of that, we need the undo functionality while you are building the staging graph:

  • All operations should be fully reversible.

But once you have committed to a new graph, that operations should, too, be reversible.

In short, we need:

  • A time-travelling undo.

(To this end, local history for your version control graph could be built.)

To accomplish all these we need:

  • An interactive IDE-level environment dedicated to version control.

In fact, this environment should be a running image of a interactive language with a REPL, a programming environment.

This environment should have good visualization capabilities. For example, if you are merging two graphs, you should see the two graphs and the corresponding changes between them. And you would also see the third graph, the resulting staging one (which you may choose to accept as your new version control graph after a satisfactory merge).

So, I imagine that something like choosing a few commits in a branch could automatically suggest you all the legible operations that apply to that series of consecutive commits: squash, remove, cut, etc.

You should also be able to zoom in on any one node in that graph and directly modify that node. Imagine you screwed up a commit message. All you would need to do to fix the situation is zoom in on the commit and edit it. (And then the change gets highlighted in the staging graph.)

That's how easy modifying history should be, and all without sacrificing safety at any point.

Moreover, the user gets the ability to write his own workflow-related functionalities right within the language of the environment.

Structure and Flexibility

The one problem I haven't discussed yet is that of the kinds of data Git is meant to work with. That's primarily code.

And Git can be pretty smart about it. For instance, if two consecutive commits have the same few lines of code, it can heuristically guess that these lines were moved. Or, if a file was renamed and slightly changed, Git will guess so as well.

But what if you moved and changed a few lines? There's a point at which heuristics become problematic.

Git doesn't really understand code. And it doesn't have to, of course. But the problem here is it doesn't understand any data it's working with. It doesn't understand anything much about the contents of what it stores.

And the worst part: the user can't fix this. There's no way to inform Git about a compression mechanism for a certain kind of blob.

In fact, it should be possible to specialize the blob constituents. Or rather, one should have the ability to specify the structure of any given object. A whole file is too large a grain.

This doesn't mean the system has to be significantly more complex, though. All it should take is the ability to specify a relationship between any two objects and a way to get one from the other.

And then, there's also no way in Git to specify if you want to store snapshots or deltas (or a way to infer one commit from another).

This, coupled with the fact of very coarse granularity, makes Git quite unsuitable for binary file storage. Or even for code, if you are demanding enough (which you might not even know you are unless you start using the features offered by this).

I think a better version control system should be flexible enough to enable storing arbitrary kinds of data and to encode arbitrary relationships between them, to let the user maintain the efficiency of the system.

This may take a bit more coordination for anyone who wants to use a repository, like getting the specific plugins required to work with any given repo. In practice, though, the new possibilities should outweigh the costs. (And of course, if one wishes, it would be possible to have just the basic Git-level awareness, and that way no coordination overhead is added.)

What It's All About

The user isn't supposed to read the docs to accomplish something simple. Simple things should be intuitive. Hard things should be discoverably-solvable.

No, just imagine this for a second: you are manipulating a quite complex graph by issuing calls from a bunch of rather obscure set of functions, not knowing what will happen exactly, simply hoping, each time, that everything will be alright.

If this doesn't make you feel wretched, I don't know what can.

It's just a fucking graph. And it just needs the right fucking tool to let you change it, safely, with no tricky bullshit or second-guessing.

It's not asking for much. It's the only way it should be.

So, when you are doing something in a version control system, you ought to be constantly seeing what the state is.

Git makes it very hard to understand the current state of the system and what any of the commands are going to do exactly. You aren't even sure what commands you are allowed to run safely. And there's too many ways to fuck up, and going back is a pain. And no porcelain can help with this.

Git is fast. But it's very frustrating to use. It doesn't have the right stuff to build a nice workflow or support data specialization.

How did Git even appear? I would like to think it started as a prototype. And it did. Linus Torvalds needed a version control system. So he made one. In a couple of weeks.

But we all know where a quick dirty prototype usually ends up: if not in a trash bin, then in production for the next decade or two. The Law of Quick Dirty Prototypes. (If it's too dirty, too quick, and lasts more than two decades, that would be the Law of JavaScript.)

To me, developing a better version control system is important to enable integration with Alchemy and do stuff like structural slot-level history-travelling in Kraken (travelling between code snippets or its binary results). And, I think, writing a decent integration layer for Git could prove just as nearly as hard as developing a new system from scratch. Moreover, it would still not fix Git one bit. (See the section Hero in The Power of Structure for more on this development.)

Most importantly: version control is just a history graph where each node holds a tree. Anybody can understand what it is. It's intuitive and simple. So, there's no reason why manipulating it should be any harder than what it takes for the user to imagine what the result should look like in the end.

Footnotes:

1

And, by the way, I am in no way speaking against the text-based interfaces. Keyboard-driven workflows can be very efficient. Every programmer or admin knows this. I am in no way promoting mouse-orientedness, but it's to be understood that a pointing device can make some operations easier to accomplish. In other words, visualizing things doesn't in any way prevent keyboard control.

...proudly created, delivered and presented to you by: some-mthfka. Mthfka!