The Git project has just turned 20 years old. A lot has happened during these years, and while the conceptual design of Git hasn't changed significantly since its inception, the way users interact with the tool has changed quite significantly. We at GitLab are proud to build on top of this critical piece of software and to be part of its history.
Join us on a journey through Git's history to explore how it has evolved over the years.
The first commit
The first commit was made on April 7, 2005, by Linus Torvalds, the creator of the Linux kernel: e83c5163316 (Initial revision of "git", the information manager from hell, 2005-04-07)
.
As we can see, this commit does not contain a lot of files:
$ git ls-tree e83c5163316
100644 blob a6bba79ba1f46a1bbf7773449c3bd2bb9bf48e8b Makefile
100644 blob 27577f76849c09d3405397244eb3d8ae1d11b0f3 README
100644 blob 98a32a9ad39883c6d05a000a68511d4b1ee2b3c7 cache.h
100644 blob 74a0a234dd346fff51c773aa57d82fc4b83a8557 cat-file.c
100644 blob 840307af0cfaab31555795ce7175d5e9c9f981a0 commit-tree.c
100644 blob 25dc13fe101b219f74007f3194b787dd99e863da init-db.c
100644 blob c924a6e0fc4c36bad6f23cb87ee59518c771f936 read-cache.c
100644 blob 1b47742d8cbc0d98903777758b7b519980e7499e read-tree.c
100644 blob b8522886a15db861508fb6d03d4d88d6de912a4b show-diff.c
100644 blob 5085a5cb53ee52e1886ff6d46c609bdb2fc6d6cd update-cache.c
100644 blob 921f981353229db0c56103a52609d35aff16f41b write-tree.c
In addition to build infrastructure, the first commit provides seven top-level commands:
init-db
to initialize a new Git repositoryupdate-cache
to add files to the indexwrite-tree
to take what is in the index and create a new tree from itread-tree
to read a tree objectcommit-tree
to create a commit from a treecat-file
to read a specific object into a temporary file
Note that the git
command itself did not yet exist at this point in time.
Instead, these commands had to be executed directly.
As example, let's create a new repository:
$ mkdir repo
$ cd repo
$ init-db
defaulting to private storage area
$ ls -a
. .. .dircache
That looks quite unfamiliar: There is no .git
directory, but there is a
.dircache
directory. And where was the private storage area?
The early design of Git distinguished between a "shared" and "private" object storage area. This object storage area was where all of your Git objects went. For example, your commits and blobs.
By default, init-db
created a private object storage area that was only used for
the managed directory that it was created in. A "shared" object storage area, on
the other hand, shared object content across multiple managed directories so
that the same object did not need to be stored twice.
Create a commit
So, now that we have a repository, how did we create a commit? Well, it isn't as
easy as today's git add . && git commit
. Instead, you had to:
- Update the index by calling
update-cache
for every file that you want to add. - Write a new tree by calling
write-tree
, which takes everything you have added to the index. - Set up environment variables to tell Git who you are.
- Write a commit object by calling
commit-tree
.
Let’s create a commit in the repository:
$ echo content-1 >file-a
$ update-cache file-a
$ echo content-2 >file-b
$ update-cache file-b
$ write-tree
3f143dfb48f2d84936626e2e5402e1f10c2050fb
$ export COMMITTER_NAME="Patrick Steinhardt"
$ export [email protected]
$ echo "commit message" | commit-tree 3f143dfb48f2d84936626e2e5402e1f10c2050fb
Committing initial tree 3f143dfb48f2d84936626e2e5402e1f10c2050fb
5f8e928066c03cebe5fd0a0cc1b93d058155b969
This isn't exactly ergonomic, but it works! Let's have a look at the generated commit:
$ cat-file 5f8e928066c03cebe5fd0a0cc1b93d058155b969
temp_git_file_rlTXtE: commit
$ cat temp_git_file_rlTXtE
tree 3f143dfb48f2d84936626e2e5402e1f10c2050fb
author Patrick Steinhardt <[email protected]> Wed Mar 26 13:10:16 2025
committer Patrick Steinhardt <[email protected]> Wed Mar 26 13:10:16 2025
commit message
Note that cat-file
didn't print the contents directly, but instead wrote
it into a temporary file first. But the contents of the file looked exactly how a
modern commit would look.
Making changes
Now that we have files, how do we get their status? You might have guessed it:
this could be done with show-diff
:
$ show-diff
file-a: ok
file-b: ok
$ echo modified-content >file-a
$ show-diff
--- - 2025-03-26 13:14:53.457611094 +0100
+++ file-a 2025-03-26 13:14:52.230085756 +0100
@@ -1 +1 @@
-content-1
+modified-content
file-a: 46d8be14cdec97aac6a769fdbce4db340e888bf8
file-b: ok
Amazingly, show-diff
even knew to already generate diffs between the old and
new state of modified files! Funny enough though, Git achieved this by simply
executing the diff(1) Unix tool.
In summary, all of this was still rather bare-bones, but it performed all of the necessary duties to track history. There were still many limitations:
- There was no easy way yet to switch between commits.
- There was no way to show logs.
- There were no branches, tags, or even references. Users were expected to manually keep track of object IDs.
- There was no way to synchronize two repositories with one another. Instead,
users were expected to use rsync(1) to synchronize the
.dircache
directories. - There was no way to perform merges.
Git 0.99
The first test release of Git was Version 0.99. This release came only two months after the initial commit, but already contained 1,076 commits. There had been almost 50 different developers involved. The most frequent committer at this point was Linus himself, but he was closely followed by Junio Hamano, the current maintainer.
A lot of things had changed since the initial commit:
- Git started to track different development branches by using references, which in most cases removes the need to manually track object IDs.
- There was a new remote protocol that allows two repositories to exchange objects with one another.
- The
.dircache
directory was renamed to.git
. - It became possible to merge single files with one another.
The most important visible change, though, was the introduction of
the top-level git
command and its subcommands. Interestingly, this release
also created the notion of "plumbing" and "porcelain" commands:
- "Plumbing" tools are the low-level commands that access the underlying Git repository.
- "Porcelain" tools are shell scripts that wrap the plumbing commands to provide a nicer, high-level user interface.
This split still exists nowadays as documented in
git(1)
, but because
most porcelain tools have been rewritten from shell scripts to C, the line between these two
categories has started to blur significantly.
Linus hands over maintainership
Linus never started Git out of love for version control systems, but because there was a need to replace BitKeeper for Linux kernel development. As such, he never planned to keep maintaining Git forever. The intent was to maintain it until someone trustworthy stepped up.
That someone was Junio Hamano. Junio got involved in Git about a week after Linus’s first commit and already had a couple of hundred commits in the history after the Git 0.99 release. So, on July 26, 2005, Linus made Junio the new maintainer of the Git project. While Linus has continued to contribute to Git, his involvement with the project faded over time, which is only natural considering that he is quite busy as head of the Linux project.
Junio is still leading the Git project today.
Git 1.0
The first major release of Git happened on December 21, 2005, by Junio. Interestingly enough, there had been 34 releases between Version 0.99 and Version 1.0: 0.99.1 to 0.99.7, 0.99.7a to 0.99.7d, 0.99.8 to 0.99.8g, and 0.99.9 up to 0.99.9n.
One of the more important milestones since 0.99 was probably the addition of the git-merge(1)
command that allows one to merge two trees with one another. This is in stark
contrast to before, where one had to basically script the merges file by file.
Remotes
Another significant change was the introduction of shorthand notation for remote repositories. While Git already knew how to talk to remote repositories, users always had to specify the URL to fetch from every single time they wanted to fetch changes from it. This was quite unfriendly to the users, because, typically, they wanted to interact with the same remote over and over again.
You may know about how remotes work now, but the mechanism that existed at
this point in time was still significantly different. There was no git-remote(1)
command that you could use to manage your remotes. Remotes weren't even stored
in your .git/config
file. In fact, when remotes were first introduced in
Version 0.99.2, Git didn't even have config files.
Instead, you had to configure remotes by writing a file into the
.git/branches
directory, which nowadays feels somewhat counterintuitive. But
the mechanism still works today:
$ git init repo --
Initialized empty Git repository in /tmp/repo/.git/
$ cd repo
$ mkdir .git/branches
$ echo https://gitlab.com/git-scm/git.git >.git/branches/origin
$ git fetch origin refs/heads/master
But that isn't all! The directory was soon renamed in Git Version 0.99.5 to "remotes", so there are a total of three different ways to configure remotes in a modern Git client.
Most of you have probably never used either .git/branches
nor .git/remotes
,
and both of these mechanisms have been deprecated since 2005 and 2011,
respectively. Furthermore, these directories will finally be removed in Git 3.0.
Git branding
In 2007, the first Git logo was created. It’s arguable if you can call it a logo, because it only consisted of three red minus signs above three green plus signs, reflecting what the output of git diff
looks like:
A bit later, in 2008, the website git-scm.com was launched:
In 2012, the Git website was revamped by Scott Chacon and Jason Long. It looks pretty similar to how it looks today:
This site redesign sports the new red-orange logo designed by Jason Long; the same logo that's currently used:
Git 2.0
Git already started to look a lot like modern Git at the 1.0 release, so we are going to do a big historical jump to Git 2.0. This version was released around 10 years after Git 1.0 and was the first release that intentionally contained backwards-incompatible changes in central workflows.
git-push(1)
default behavior
The change that arguably caused most the confusion in this release was the
updated default behavior of git-push(1)
.
There are a couple of different actions that Git could take when you push into a remote repository and don’t specify exactly what you want to push:
- Git could refuse to do anything, asking you to provide more information of what exactly you want to push.
- Git could push the currently checked out branch.
- Git could push the currently checked out branch, but only if it knows that it has an equivalent on the remote side.
- Git could push all of your branches that have an equivalent on the remote side.
The behavior of modern Git is the so-called "simple" strategy, which is the third option above. But before Git 2.0, the default behavior was the "matching" strategy, which is the last option.
The “matching” strategy was significantly more risky. You always had to make sure that you were fine with pushing all of your local branches that have an equivalent on the remote side before pushing. Otherwise, you might have ended up pushing changes unintentionally. As such, it was decided to change the strategy to "simple" to reduce the risk and help out Git beginners.
git-add(1)
Another big change was the default behavior of git-add(1)
when it comes to
tracked files that have been deleted. Before Git 2.0, git-add(1)
wouldn't
stage deleted files automatically, but you instead had to manually add each
deleted file by using git-rm(1)
to make them part of a commit. With Git 2.0, this behavior was changed so that git-add(1)
also adds deleted files to the index.
Celebrating the Git community
I won’t bore you with the details around how Git works nowadays – you probably use it daily anyway, and, if you don’t, there are many tutorials out there that can help you get started. Instead, let’s celebrate the Git community, which has ensured that Git works as well as it does 20 years later.
Over time, Git has:
- Accumulated 56,721 commits as of the Git 2.49 release.
- Received contributions from more than 2,000 different individuals.
- Published 60 major releases.
The Git project also has a steady influx of new contributors by taking part in Google Summer of Code and Outreachy. New contributors like these are what will ensure that the Git project will remain healthy in the long term.
As such, let me extend a big thank you to all contributors. It is your contributions that have made Git possible.
Going forward
It should be an uncontroversial take to say that Git has essentially won the competition of version control systems. It has significant market share, and it isn't easy to find open source projects that are using a version control system other than Git. So it has clearly done a lot of things right.
That being said, its development hasn't stood still, and there are still many challenges ahead of Git. On the one hand, we have technical challenges:
- modernization of an aging code base
- scaling with the ever-growing size of monorepos
- handling large binary files better
And on the other hand, there are problems of a more social type:
- improving the usability of Git
- fostering the Git community so that the project remains healthy in the long
term
There always remains work to be done and we at GitLab are proud to be part
of these efforts to make sure that Git continues to be a great version control
system for the next 20 years.