On This Page

What Is a Version Control System (VCS)?

TL;DR: A version control system (VCS) records and manages changes to files over time, so you can review versions, compare work, restore an earlier state, and collaborate without overwriting one another. Git is a distributed VCS: a full local clone normally includes project history, while services such as GitHub provide hosting and collaboration features.

▶ See it live: You don’t need to install anything to understand this. Open the interactive playground and watch the .git/ folder build itself as you run your first commands.

What does VCS mean?

VCS stands for version control system: software that records changes to files over time so you can compare versions, restore an earlier state, and collaborate safely. Git is a distributed version control system. GitHub is a hosting and collaboration service built around Git repositories.

TermWhat it meansExample
VCSA system that tracks file versions and historyGit, Subversion, Mercurial
GitA distributed VCS that stores full history locallyThe git command-line tool
GitHubA hosting service for Git repositoriesgithub.com

Why Use a Version Control System?

A VCS helps you:

  • see who changed a file and when;
  • compare versions and review proposed changes;
  • create branches for independent work;
  • combine contributions from multiple people; and
  • inspect or restore an earlier version when something goes wrong.

A VCS can manage source code, documentation, configuration, and other project files—not just application code.


What Does a Version Control System Actually Do?

At its core, software development is an exercise in managing complexity and change. Every line of code added, modified, or deleted represents a transition from one state of a system to another. Without a formal tracking mechanism, developers are left to manage these states manually.

Imagine a team working on a project without a version control system. To collaborate, they might copy project folders to a shared network drive, renaming them with suffix variations like project_final, project_final_v2, or project_final_v2_fixed_do_not_delete. This manual approach fails catastrophically for several reasons:

  • No Conflict Resolution: If two developers edit the same file simultaneously, whoever saves last overwrites the other’s changes.
  • No Audit Trail: There is no record of who made a change, when it was made, or why it was introduced.
  • No Rollback Capability: If a bug is introduced, reverting to a functional state requires manual debugging or copy-pasting from older backup zip files.
  • High Risk: A single accidental deletion or file corruption can derail the entire codebase.

A VCS replaces manual folder copies with a managed history of versions or changes. A repository is a project together with its version history. The way a VCS records changes varies by tool. In Git, a commit records a snapshot of tracked files along with metadata such as the author, timestamp, parent commit or commits, and message.

The Developer Analogy: The Multiverse and Time Travel

To understand a VCS, think of it as a combination of time travel and parallel universes (multiverses):

  1. Time Travel: You can transport your entire project back to any previous snapshot. If a feature written three weeks ago broke the system today, you can jump back to inspect the working state, compare it line-by-line with the current state, and pinpoint the bug.
  2. Parallel Universes (Branching): You can split your codebase into isolated timelines called Branches. This allows you to build an experimental feature in one branch without affecting the stable code in another. Once the experiment is successful, you can merge those timelines back together.

For senior developers, a VCS is not just a safety net; it is an infrastructure tool that powers modern Continuous Integration and Continuous Deployment (CI/CD) pipelines, supports code review, and provides a history of how software evolves.

What Are the Main Types of Version Control?

  • Local VCS: keeps version history on one computer.
  • Centralized VCS: keeps the shared repository on a central server.
  • Distributed VCS: gives each full clone a local repository and history; Git and Mercurial are examples.

The detailed comparison below explains the trade-offs between these models.


What Happens Inside .git/ (Hands-on exploration)

Git is a Distributed Version Control System (DVCS). In a standard, non-bare Git working repository, its administrative database lives in a hidden .git/ directory. A full clone normally includes history locally, so you can inspect commits, compare branches, and make commits without contacting a remote; teams often use a hosted remote to share work.

Let’s explore how Git tracks files under the hood. We will initialize a repository, add a file, commit it, and inspect the internal database structure file by file. The object-ID examples below use Git’s default SHA-1 format; Git can also use SHA-256.

Step 1: Initialize an Empty Repository

First, create a new directory and initialize a Git repository using the git init command:

$ mkdir vcs-under-the-hood
$ cd vcs-under-the-hood
$ git init
Initialized empty Git repository in /home/admin/Code/vcs-under-the-hood/.git/

Now let’s look at the anatomy of the hidden .git/ directory that Git created:

$ ls -F1 .git/
HEAD
config
description
hooks/
info/
objects/
refs/

Here is a breakdown of what these files and directories represent:

  • HEAD: A text file pointing to the currently active branch or commit. Right now, it contains:
    ref: refs/heads/main
  • config: The repository-specific configuration file (overriding global settings). It contains paths, remote URLs, and branch tracking info.
  • description: Used by the GitWeb program to describe the repository (mostly ignored today).
  • hooks/: A directory containing client-side scripts that run before or after Git commands (e.g., preventing commits with syntax errors).
  • info/exclude: A local-only ignore file, similar to .gitignore, but not shared with others.
  • objects/: Git stores file contents, directory trees, and commits as objects. SHA-1 is the default object format; SHA-256 is also available. Loose objects appear under .git/objects/ in directories split by object-ID prefix, and Git can pack objects together. A new repository usually has info/ and pack/ subdirectories but no project objects yet.
  • refs/: References pointing to specific commits (like branches, tags, and remotes). Currently empty.

Step 2: Create a File and Stage It

Let’s create a simple text file and see how Git responds:

$ echo "Hello, dotgit!" > hello.txt

At this stage, hello.txt is an untracked file. Git can report it when asked, but it is not part of a commit. To include it in the next snapshot, stage it:

$ git add hello.txt

What changed inside .git/ when we ran git add? Let’s check the .git/ folder structure again:

$ ls -F1 .git/
HEAD
config
description
hooks/
index           # <-- A new index file has been created!
info/
objects/        # <-- A new subdirectory has appeared inside objects/!
refs/

Two major changes occurred:

  1. The index file was created: This is a binary file representing the Staging Area. It serves as a preparation ground for the next commit, listing the paths and object hashes of all files to be included in the snapshot.
  2. A new object was added to objects/: Git read the contents of hello.txt, calculated its object ID using this repository’s default SHA-1 format, and saved the content as an object in the database.

Let’s inspect the objects/ directory to see the hash:

$ find .git/objects -type f
.git/objects/e6/e34f23eaeca0aa681d4f40a5c8df0f36e3f476

In this default SHA-1 example, Git splits the 40-character object ID into a two-character directory name (e6) and a 38-character filename (e34f23eaeca0aa681d4f40a5c8df0f36e3f476). The full object ID of the staged file is e6e34f23eaeca0aa681d4f40a5c8df0f36e3f476.

We can use the Git plumbing command git cat-file to inspect this object:

# Get the type of the object
$ git cat-file -t e6e34f23eaeca0aa681d4f40a5c8df0f36e3f476
blob

# Read the content of the object
$ git cat-file -p e6e34f23eaeca0aa681d4f40a5c8df0f36e3f476
Hello, dotgit!

Git stored the contents of our file as a blob (binary large object). Notice that the blob contains only the file content, not the filename or directory structure!

Step 3: Commit the Staged File

Now let’s save this snapshot permanently using the git commit command:

$ git commit -m "feat: add greeting file"
[main (root-commit) 5cf1b42] feat: add greeting file
 1 file changed, 1 insertion(+)
 create mode 100644 hello.txt

Let’s see what was added to our object database:

$ find .git/objects -type f
.git/objects/5c/f1b423fa2fb079b7b9195b058a96677864f1bc  # <-- New Commit Object
.git/objects/e6/e34f23eaeca0aa681d4f40a5c8df0f36e3f476  # <-- Original Blob Object
.git/objects/97/81729177f1548e65874251761668f44ff53cbb  # <-- New Tree Object

We now have two new objects in .git/objects/:

  1. A Tree Object (9781729177f1548e65874251761668f44ff53cbb): This represents the directory structure of our project. It maps the filename (hello.txt) to its content blob hash and file permissions.
  2. A Commit Object (5cf1b423fa2fb079b7b9195b058a96677864f1bc): This contains metadata about the commit (author, committer, timestamp, message) and points to the tree object representing the repository state.

Let’s verify their contents with git cat-file:

# Inspect the Commit Object
$ git cat-file -p 5cf1b423fa2fb079b7b9195b058a96677864f1bc
tree 9781729177f1548e65874251761668f44ff53cbb
author Developer <dev@dotgit.dev> 1778153000 +0000
committer Developer <dev@dotgit.dev> 1778153000 +0000

feat: add greeting file

# Inspect the Tree Object
$ git cat-file -p 9781729177f1548e65874251761668f44ff53cbb
100644 blob e6e34f23eaeca0aa681d4f40a5c8df0f36e3f476    hello.txt

Finally, let’s look at .git/refs/heads/main to see where our branch points:

$ cat .git/refs/heads/main
5cf1b423fa2fb079b7b9195b058a96677864f1bc

Our branch reference main is simply a text file containing the hash of the latest commit! When you create a commit, Git writes the commit object, updates the branch file in refs/heads/ to point to it, and keeps the project structure clean.


Centralized vs. Distributed VCS: The Architectural Divide

Understanding the differences between VCS designs is key to grasping why Git became the industry standard. Version Control Systems generally fall into three categories.

graph TD
    VCS[Version Control Systems] --> Local[Local VCS e.g., RCS]
    VCS --> Centralized[Centralized VCS e.g., SVN, Perforce]
    VCS --> Distributed[Distributed VCS e.g., Git, Mercurial]

1. Local Version Control Systems (LVCS)

In the early days of computing, version control was local. Programs like RCS (Revision Control System) kept track of file changes using local patch sets. Each patch set represented the difference between one version of a file and the next.

  • How it works: It stores version databases as local files on a single computer.
  • The Pitfall: If the hard drive fails, or if files are accidentally overwritten, the entire history is lost. Furthermore, collaboration is nearly impossible because there is no native way to share changes with other computers.

2. Centralized Version Control Systems (CVCS)

To enable team collaboration, Centralized Version Control Systems were developed. Standard examples include Subversion (SVN), CVS, and Perforce.

  • How it works: A single centralized server hosts the official database containing all versioned files and history. Developers connect to this server to check out a single snapshot of the files, modify them locally, and commit them back to the server.
  • The Pitfall: The central server is an important dependency: operations that need the shared repository usually require a network connection. Some work, such as editing files or comparing local changes, can happen offline, but access to shared history and commits to the central repository may wait until the server is reachable. If its database is lost and there are no usable backups, shared history may also be lost.

3. Distributed Version Control Systems (DVCS)

Distributed Version Control Systems, like Git and Mercurial, reduce dependence on a central server by keeping repository history in each full clone. Teams may still use a central remote to share changes.

  • How it works: Instead of checking out only the latest snapshot of files, developers clone the entire repository, including the full metadata and historical database. Every local repository is a complete mirror of the history.
  • The Advantages:
    • Local Operations: In a full clone, many operations—such as viewing history, comparing branches, and creating commits—can run without a network connection.
    • Redundancy: Copies held by collaborators can help recover shared history if a local copy includes the needed commits. Unpushed work, shallow or incomplete clones, and lost local copies are not recoverable from other clones.
    • Flexible Workflows: Developers can collaborate in peer-to-peer networks or maintain multiple remotes (e.g., a staging remote and a production remote) rather than being constrained by a single hub.

Detailed Command Options & Advanced Usage

While basic Git workflows are simple, understanding its core concepts allows you to leverage its power. Let’s look at the primary concepts that distinguish Git from traditional version control.

Deltas vs. Snapshots

Git presents each commit as a snapshot of the tracked project. A commit points to a tree that maps paths to file objects; unchanged content can reuse the same object ID rather than being written as a new object.

Git commit A: tree → [blob A1] [blob B1]
Git commit B: tree → [blob A2] [blob B1 reused]

This is a useful model for understanding Git commits and branches, not a claim that every VCS stores data the same way. When Git packs objects, it may use delta compression to reduce storage.

Cheap Branching

Branching is implemented differently across version-control systems. Some centralized systems, including SVN, represent a branch as a server-side copy of a repository path; unchanged data may be shared, so the cost and workflow vary by system and configuration.

In Git, a branch is a movable pointer to a commit, so creating one is usually fast. With the default files reference backend and SHA-1 object format, an unpacked branch ref is typically a text file containing a 40-character object ID and a newline. Packed refs and the optional reftable backend store refs differently. When you switch branches, Git updates HEAD and the working tree to match the chosen commit.


Real-World Scenario: Seamless Collaboration

To see VCS in action, let’s look at a typical workflow where two developers, Alice and Bob, use Git to collaborate on a new web application without disrupting each other.

  1. Setting up the Repository: Alice initializes the project on her local machine, adds a basic layout, and pushes it to a shared remote repository on GitHub: This example assumes Alice has Git installed and a commit identity configured; see the Git configuration guide. It explicitly chooses main so the branch name matches the push command.

    $ git init --initial-branch=main
    $ git add index.html style.css
    $ git commit -m "Initial skeleton"
    $ git remote add origin https://github.com/example/web-app.git
    $ git push -u origin main
  2. Parallel Feature Development: Bob clones the repository to his machine:

    $ git clone https://github.com/example/web-app.git

    He immediately creates a feature branch to work on user authentication:

    $ git switch -c feature/auth

    At the same time, Alice creates a branch to work on a new navigation bar:

    $ git switch -c feature/navigation
  3. Isolated Committing: Bob edits code locally and creates commits. These commits exist only on his machine, keeping the stable code in main safe. Alice does the same. Neither developer affects the other’s progress.

  4. Integration via Pull Requests: Once Alice finishes, she publishes her branch with git push and opens a Pull Request (PR) for review:

    $ git push origin feature/navigation

    After a code review, her branch is merged into main.

  5. Resolving Conflicts Safely: Bob completes his feature and fetches the latest main before merging it into his local branch; see git merge for the integration step:

    $ git checkout feature/auth
    $ git fetch origin
    $ git merge origin/main

    If Alice and Bob edited the same lines of code, Git pauses the merge and highlights the conflict areas. Bob reviews the conflicting code, resolves the differences, commits, and pushes his branch to be merged. The stable build remains clean, and every step is documented.


Common Pitfalls and Mistakes

1. Confusing Git and GitHub

The Mistake: Many beginners use the terms “Git” and “GitHub” interchangeably. The Fix: Remember that Git is the local tool that runs on your computer to track version history. GitHub is a cloud-based platform that hosts your remote Git repositories and adds collaboration features like pull requests, issue tracking, and user permissions. You can use Git entirely offline without ever touching GitHub.

2. Failing to Configure Identity Settings

The Mistake: Committing files before setting up global user configuration leads to anonymous or incorrect author details in the log history. The Fix: Run the config setup before creating your first commit; follow the Git config guide:

$ git config --global user.name "Your Name"
$ git config --global user.email "your.email@example.com"

3. Staging Generated Files and Dependencies

The Mistake: Tracking build artifacts, logs, or dependency folders like node_modules/ or .DS_Store. This slows down repository operations and clutters the history. The Fix: Create a .gitignore file in your repository root before running your first git add command. Add patterns of files to exclude:

node_modules/
dist/
*.log
.DS_Store

4. Modifying Published History

The Mistake: Re-writing commits (e.g., using git commit --amend or git rebase) after they have been pushed to a public or shared repository branch. The Fix: Only rewrite commits that exist solely on your local branch. Once code is pushed to a shared repository, treat the published history as immutable.


Command Quick Reference

CommandPurposeCommon Flags
git initInitializes a new local Git repository(none)
git add <file>Adds file contents to the staging areagit add . (stages all changes)
git commitRecords staged files as a snapshot-m "msg" (sets message), -am (stages and commits)
git statusDisplays file states (untracked, modified, staged)-s (short format output)
git logShows commit history list--oneline (one line summary), -n 5 (limit to 5)
git clone <url>Clones an existing remote repository locally(none)
git branchLists, creates, or deletes branches-d <name> (delete branch), -a (list all)
git checkout / git switchSwitches active branchswitch -c <name> (create and switch)
git merge <branch>Merges branch history into the active branch--no-ff (forces merge commit creation)

FAQ (People Also Ask)

What is a Version Control System and why is it used?

A version control system is software that records and manages changes to files over time. It helps people review history, compare versions, work in parallel with branches, collaborate through merges or reviews, and inspect or restore an earlier state when something goes wrong.

Is Git the same as GitHub?

No. Git is software that manages version control on your computer. GitHub is a hosted service where you can store remote Git repositories and collaborate through pull requests, issues, access controls, and automated workflows. Git can be used without GitHub.

What are the main differences between centralized and distributed version control?

A centralized VCS (such as many SVN configurations) stores the shared repository on a central server; commands that read or write shared history generally need a connection. A distributed VCS (such as Git) typically gives a full clone its own copy of repository history, so many local operations can run offline. Teams may still rely on remote hosting, and local copies are not a guarantee against every failure.

Can a VCS track non-code files like images or Word documents?

Git can track any file type, but text diffs are useful only for formats Git can interpret as text. For binary files such as .png, .pdf, or .zip, Git stores file versions as objects and may use packing and delta compression; large or frequently changed binary files can still make a repository grow quickly.

Continue learning

Follow the data Git creates after git init and your first commit:

  • Git objects — blobs, trees, and commits explained.
  • Git refs — how branches, tags, and HEAD point to commits.
  • Packfiles — how Git compresses objects into packfiles.
  • Git branches — create and switch independent lines of work.
  • git merge — combine branch histories and resolve conflicts.

Test Your Knowledge

Loading quiz…