To succeed with source code management in Git, one must understand how commits, branches, and tags interplay; these are all core components that make up your source code history.
Having read this post, you'll understand how they differ, their technical implementation, and, most importantly, how to make the most out of them!
Before we dive into the details, let's brush up with this overview of a local repository to level the playing field.
As you can see above, each component is technically either an object or a reference, with the tag being the odd one out as it can be created either as an object or reference. Being aware of the differences between an object and a reference makes your life with Git much easier, so here it goes.
References vs Objects
In Git, references are pointers to specific commits in the repository history. A reference can be a branch, a tag (lightweight), or a remote-tracking branch, and it serves as a human-readable name for a specific commit. When you make a new commit, the current branch (i.e., the branch
HEAD is referring to) is automatically updated to point to the newly created commit.
References don't contain any information other than what commit they are currently referring to and are stored as plain text files inside:
Objects, however, are the fundamental storage units in Git. All Git objects are immutable and are technically stored as binary files inside:
There are four types of Git objects: blob, tree, commit, and tag (annotated). Blobs and Trees are used to store the versioned source code but are beyond the scope of this post; the intrigued reader can catch up on file storage through the below post.
HEAD, Commit, Branch, and Tag
With a general understanding of references and objects, let's examine the explicit implementation of the main components HEAD, Commit, Branch, and Tag.
HEAD - the special pointer
HEAD answers the question: Where am I right now in the repository? It's a pointer to the currently checked-out branch or commit, which contains an immutable snapshot of your entire code base at a given time.
HEAD is a plain text reference, but unlike branches and lightweight tags that are stored in
.git/refs, it is instead stored inside:
For a thorough rundown of
HEAD, how it operates, and its two states: attached and detached; see the below post.
Commits in Git are immutable snapshots representing the state of all tracked (versioned) files in the code base at a given time. Every commit, but the root commit, contains a reference to its parent commit (or commits if being a merge commit).
This structure, where child commits reference parent commits, is technically a directed acyclic graph (DAG) and is what makes up the repository history. Generally speaking, the DAG is referred to as a history graph or commit graph.
The commit graph allows you to see the history of all changes made to the repository and to trace changes made to specific files or lines of code over time. In addition, any two commits can be easily compared, and the differences (introduced modifications) can be displayed.
To organize commits and work with your history in a more human-friendly and efficient way, branches and tags are used.
In Git, a branch is a lightweight movable pointer referencing a specific commit in the repository's commit history.
Branches are a fundamental feature of Git and are essential for managing code development and collaboration. They allow multiple developers to work on the same codebase simultaneously without interfering with each other's work. For example, each developer can create their own branch to work on a new feature or bug fix and then merge their changes back into the main branch (often called
main). Likewise, a single developer can (and should) use multiple branches to separate her work.
The commit a branch pointer is referencing is considered the tip of that branch, but all preceding commits that can be accessed following the DAG downwards are part of the branch.
On a general level, when speaking of branches, only the divergent commits are what's interesting, so in the above scenario, you'd say that
cool-feature "contains" one commit (C7) and
bug-fix contains two commits (C5 and C6) when compared to
As branches "move" when committed to, how can we highlight an important event, such as a release, that should never move? Luckily, tags are at our disposal!
Tags: Lightweight or Annotated
Tags are Git's component for highlighting important events, such as releases. Unlike branches, tags do not move and cannot be updated — which is the whole point — but come in two versions: annotated or lightweight.
Annotated tags are Git objects that contain more information than just a simple pointer to a commit. For example, an annotated tag includes a tagger name and email, a tagging date, a tag message, and a GnuPG signature. Because of this extra information, annotated tags are generally considered more "official" or "formal" than lightweight tags. Nevertheless, annotated tags are often recommended, especially when sharing or distributing your tags with others.
Lightweight tags, on the other hand, are simply pointers to a specific commit. They do not contain any extra information, such as a tagger name or message. Lightweight tags are helpful for local tags that you don't need to share with others or for temporary tags that you might use during development.
- Commits, branches, and tags are the main high-level components of a Git repository — together, they make up the source code history.
- Commits are immutable snapshots representing the state of all tracked files in the code base at a given time, accessed by its SHA-ID.
- Branches and Tags are human-readable references for a specific commit — similar to how a web address references a particular server IP.
- Branches are lightweight moveable references that are quick to create and delete, consume limited space, and are technically stored in plain text files inside
- Branches are either local or remote-tracking, where the latter is a read-only replica of its remote counterpart. On the other hand, a local branch is your writable version generally set up to track a remote-tracking branch.
- Tags come in two versions, lightweight (as a reference) or annotated (as an object), with the main difference being the amount of information they contain; annotated tags have not only a reference to a specific commit but also include a tagger name and email, a tagging date, a tag message, and a GnuPG signature.
- Tags are similar to branches, but unlike branches, they do not move, as they are intended to highlight specific events such as a release.
HEADis a special pointer answering the question: Where am I right now in the repository? It references the currently checked-out branch or commit,
- References are stored as plain text files inside
.git/refs, while objects reside in
😎 Thanks for reading, and good luck improving your source code management skills!
If you'd like more pieces like this, subscribe to the news feed; to avoid missing anything! By subscribing, you'll also get access to members-only content!
If you have any questions or suggestions, try reaching me on Twitter – @Stjaertfena