Master the art of version control with Git. From basic workflows to advanced branching strategies and internals used in professional DevOps environments.
February 2026
In modern software engineering, the ability to track changes, collaborate across disparate teams, and maintain a historical record of a project is not merely a convenience—it is a foundational requirement. Version Control Systems (VCS) provide a mechanism for managing changes to source code over time. Without such systems, developers would be forced to manually manage file copies (e.g., project_v1, project_final_v2), which is inherently error-prone and lacks the granularity required for complex systems.
Historically, VCS was divided into two primary architectures: Centralized and Distributed.
Systems like Subversion (SVN) and Perforce rely on a single central server that contains all the versioned files. Clients check out files from that central place. This model offers a single point of authority and fine-grained access control. However, it introduces a single point of failure: if the server goes down, collaboration ceases, and if the disk is corrupted without proper backups, the entire history is lost.
Git belongs to the distributed category. In a DVCS, every client maintains a full clone of the repository, including the entire history. This redundancy ensures that if any server dies, any client repository can be used to restore the system. Furthermore, most operations are local, providing significant performance advantages.
Git was created in 2005 by Linus Torvalds during the development of the Linux kernel, following the loss of access to BitKeeper. Torvalds designed Git with several non-negotiable goals:
Unlike older VCS that store deltas (file changes), Git captures snapshots of the entire filesystem. When you commit, Git records what every file looks like at that moment. If a file has not changed, Git simply stores a link to the previous version, significantly optimizing storage and retrieval.
Git utilizes SHA-1 hashes to identify content. Every file or directory is referred to by its checksum, making it impossible to alter the records without Git detecting the change. A commit is identified by a 40-character hexadecimal string, ensuring a permanent and verifiable state.
Because Git stores the entire history locally, most operations look like they are instantaneous. For example, to browse the history of a project, Git doesn’t need to go to the server to get the log—it simply reads it directly from your local database. This architecture enables a workflow where developers can commit frequently and experiment with branches without overhead.
Waiting for signal...
Git is designed to be portable across all POSIX-compliant systems and Windows. In academic and professional settings, you will likely encounter a heterogeneous environment where developers use different operating systems.
The preferred method for installing Git is through the system’s native package manager to ensure compatibility and easy updates.
On Debian-based systems (Ubuntu, Mint):
sudo apt update && sudo apt install git
On Red Hat-based systems (Fedora, RHEL):
sudo dnf install git
On Arch Linux:
sudo pacman -S git
On FreeBSD:
pkg install git
While macOS comes with a version of Git installed via Xcode Command Line Tools, many developers prefer the more up-to-date version from Homebrew:
brew install git
For Windows, Git for Windows (also known as Git Bash) is the standard. It provides a BASH emulation environment which is critical for maintaining script compatibility across teams. You can install it via Winget:
winget install --id Git.Git -e --source winget
Git records the identity of the author for every commit. This is not for authentication, but for accountability and metadata. These settings are stored in the ~/.gitconfig file (or %USERPROFILE%\.gitconfig on Windows).
git config --global user.name "Leonardo da Vinci"
git config --global user.email "leo@renaissance.org"
The --global flag ensures that these settings are applied to every repository on your machine. For project-specific identities (e.g., using a work email for a specific repo), you can omit the flag while inside that repository.
One of the most common issues in cross-platform development is how different operating systems handle the end of a line in a text file.
\r\n).\n).If not managed, Git will see the change in line endings as a change to the entire file, leading to “merge hell.”
You should configure Git to convert LF to CRLF when checking out code, and convert CRLF back to LF when committing:
git config --global core.autocrlf true
You should ensure that Git only converts CRLF to LF on commit, but doesn’t do anything on checkout:
git config --global core.autocrlf input
By default, Git may fall back to vi or vim. If you are not comfortable with modal editors, you should change it to a simpler one like nano or a code editor like VS Code:
# To use Nano
git config --global core.editor "nano"
# To use VS Code
git config --global core.editor "code --wait"
# Command to list all active configurations
git config While user.name and user.email identify you in the history, they do not verify your identity. In a DevOps pipeline, you will typically use SSH Keys or Personal Access Tokens (PAT) for authentication with remotes like GitHub or GitLab.
If you prefer SSH for secure communication without typing passwords:
ssh-keygen -t ed25519 -C "your_email@example.com"
This generates a public/private key pair. You would then provide the public key (~/.ssh/id_ed25519.pub) to your Git hosting provider.
Waiting for signal...
To understand Git, one must understand its workflow, which is centered around three main sections: the Working Directory, the Staging Area (also known as the Index), and the Git Directory (Repository).
Under the hood, Git is essentially a content-addressable filesystem. It is a simple key-value store. When you insert any piece of content into the Git repository, it gives you back a unique key (the SHA-1 hash) that you can use to retrieve that content.
There are three primary types of objects in the Git database:
A blob stores the file data, but not the file name or any metadata. If two files have the exact same content, they will share the same blob in the Git database, regardless of their names.
A tree solves the problem of storing filenames and also allows you to group files together. One tree object contains a list of entries, each of which is the SHA-1 hash of a blob or another tree, along with its associated mode, type, and filename. This is analogous to a directory in a filesystem.
A commit object points to a single tree, marking what the project looked like at that point in time. It also contains the author, the committer, a timestamp, a message, and pointers to the parent commit(s).
.git Directory StructureIf you look inside the hidden .git folder in any repository, you will see the mechanics of how Git works:
config: Project-specific configuration settings.description: Used by the GitWeb program.HEAD: Points to the branch you currently have checked out.hooks/: Scripts that run on certain events (e.g., pre-commit).info/: A global exclude file for ignored patterns.objects/: The heart of Git—all the blobs, trees, and commits.refs/: Pointers to master/main, tags, and remotes.One of the most powerful aspects of Git’s architecture is that objects are immutable. Once a blob or a commit is written to the database, it cannot be changed. If you modify a file and commit it, Git creates a new blob and a new commit. The old ones remain in the database (until pruned by garbage collection), which is why it is so difficult to truly lose data in Git once it has been committed.
Waiting for signal...
The lifecycle of a Git project begins with the init command. This creates the .git directory and sets up the necessary infrastructure for tracking.
mkdir my-university-project
cd my-university-project
git init
By default, Git will create a branch (usually named master or main). From this point forward, Git will watch for changes in this directory.
The most frequently used command is git status. It provides a summary of which files are in which state (Tracked, Untracked, Modified, Staged).
git status
The workflow in Git is a two-step process: Staging and Committing.
git addStaging allows you to group related changes together. You might have changed ten files, but only five of them are related to a specific bug fix. You can stage just those five:
git add file1.c file2.c
git commitA commit is a permanent snapshot. It is critical to write descriptive commit messages that explain why a change was made, not just what was changed.
git commit -m "Refactor memory allocation in parser to prevent overflow"
To see exactly what has changed in your files since the last commit (but before you stage them), use git diff:
git diff
Once staged, you can use git diff --staged to see what is ready to be committed.
The git log command displays the commit history in reverse chronological order.
git log --oneline --graph --decorate
Waiting for signal...
.gitignoreIn any project, there are files that you never want to track:
*.o, *.exe, bin/).vscode/, .idea/).env, secrets.json)node_modules/, venv/)A .gitignore file is a text file where each line contains a pattern for files/directories to ignore.
# Ignore all object files
*.o
# Ignore the build directory
/build/
# Ignore sensitive files
.env
A “Best Practice” in DevOps is the concept of Atomic Commits. Each commit should represent a single logical change. If you are halfway through a feature and you find a typo in a completely different part of the codebase, don’t include the typo fix in your feature commit. Commit them separately. This makes the history easier to read, revert, and debug (e.g., using git bisect).
# Find which branch you are currently on
git In many older VCS, branching involved creating a full copy of the source code—which was slow and expensive. In Git, a branch is simply a lightweight, movable pointer to one of the commits in the repository. The default branch name is usually main. When you create a new branch, Git creates a new pointer; it does not duplicate any file content.
Each pointer is a tiny file (41 bytes) containing the 40-character SHA-1 checksum of the commit it points to.
To create a new branch named testing:
git branch testing
However, creating a branch does not switch you to it. To start working on that branch, you must “check it out” or “switch” to it:
git switch testing
(Note: In older tutorials, you will see git checkout -b testing. Modern Git recommends git switch as it is more intuitive.)
Merging is the process of bringing changes from one branch into another. There are two primary types of merges you will encounter.
If the branch you are merging into is a direct ancestor of the branch you are merging (i.e., there have been no other commits on the base branch), Git simply moves the pointer forward. No new commit is created.
git switch main
git merge feature-x
If the history has diverged (i.e., both main and feature-x have new, different commits), Git performs a three-way merge. It looks at three snapshots:
Git creates a new “Merge Commit” that has two parents.
A conflict occurs when the same line of the same file was modified in both branches being merged. Git will stop and ask you to resolve the conflict manually.
<<<<<<< HEAD
printf("Hello from Main\n");
=======
printf("Hello from Feature\n");
>>>>>>> feature-x
You must edit the file, choose the correct version (or combine them), remove the markers, and then git add and git commit to complete the merge.
# Create and immediately switch to 'dev' branch git switch dev
Waiting for signal...