Chapter 3: Version Control (Git) - Ultimate Guide

The "Why": What is Version Control?

Imagine you are working on your project. You create project_v1.zip. Then you add a new feature and save it as project_v2.zip. Then you fix a bug and save it as project_v2_final.zip. Then your friend helps and you have project_v2_final_amans_changes.zip. Then you find another bug and save project_v2_final_REALLY_final.zip.

This is **hell**. It's unmanageable, you can't see *what* changed, and you can't easily merge your friend's changes with your new bug fix. You have no "history."

A **Version Control System (VCS)** solves this. It's a system that tracks and manages changes to files over time. It's like a "save" button on steroids. It allows you to take "snapshots" (called **commits**) of your project, see who changed what, and go back to any previous snapshot at any time.

Centralized (CVCS) vs. Distributed (DVCS)

There are two main types of version control:

[Image of Centralized (SVN) vs. Distributed (Git) architecture diagram]
  • Centralized (CVCS):** (e.g., Subversion/SVN, Perforce) There is **one central server** that holds all the project history. You "check out" the files, make changes, and "check in" the changes. **Problem:** If the central server goes down, nobody can work. You can't commit, see history, or create branches.
  • Distributed (DVCS):** (e.g., **Git**, Mercurial) This is what we use. When you "clone" a project, you don't just get the files—you get the **entire history of the project**. Every developer has a full-fledged local repository. You can commit, create branches, and view history *without an internet connection*.

What is Git? What is GitHub?

This is the most common point of confusion for beginners.

[Image of Git logo vs GitHub/GitLab logos]
  • Git:** Is the **tool**. It's the command-line program (git commit, git push) that you install on your computer. It manages your project's history locally in a hidden folder called .git.
  • GitHub / GitLab / Bitbucket:** These are **websites** (remote services). They are a place to *host* (store) your Git repositories in the cloud. Think of Git as the software, and GitHub as the "Google Drive" or "social network" for your Git projects.

As a DevOps engineer, you will live and breathe Git. It's the starting point for all CI/CD pipelines.

Part 1: First-Time Git Setup

Before you do anything, you must install Git and configure it.

Installation

  • Linux (Ubuntu/Debian): $ sudo apt install git
  • Linux (Fedora/RHEL): $ sudo dnf install git
  • macOS: Easiest way is to install Homebrew (/bin/bash -c "$(curl -fsSL ...)") and then run $ brew install git. Or just by installing Xcode Command Line Tools.
  • Windows: Go to git-scm.com/downloads and download the installer. This will give you "Git Bash," a terminal that lets you use Git and common Linux commands on Windows.

The Most Important First Step: `git config`

You *must* do this once per computer. Git records the "author" of every single commit. You need to tell Git who you are.

# Set your name
$ git config --global user.name "Your Name"

# Set your email (use the same one you use for GitHub)
$ git config --global user.email "youremail@example.com"

# Recommended: Set the default branch name to "main"
$ git config --global init.defaultBranch main

# Recommended: Set your default text editor (e.g., nano or vscode)
$ git config --global core.editor "nano"
  • --global: This flag means "apply this setting to *every* Git project on this computer." You only need to run this once.
  • --local: (Default) You can run git config user.name ... *without* --global to set a different name/email for just *one* specific project.

You can check your settings at any time:

$ git config --list --show-origin

Part 2: The Core Concepts (The 3 Trees)

To master Git, you must understand that your project exists in three "trees" (or areas) at all times. This is the single most important concept in Git.

[Image of the Git 3-Tree Architecture (Working Directory, Staging Area, .git Repository)]
  1. The Working Directory: This is your project folder. It's the files you can see and edit in your code editor (e.g., style.css, index.html). These are your "live" files.
  2. The Staging Area (or "Index"): This is the "on-deck" circle. It's a file inside the .git folder that stores a *list* of all the changes you are *about to commit*. This is the most unique part of Git. It lets you build your "snapshot" (commit) piece-by-piece, instead of committing all your changes at once.
  3. The Repository (`.git` folder): This is the "history book." It's a hidden folder (.git) inside your project that contains all your "snapshots" (commits) from the beginning of time. When you "commit," you are saving the snapshot from the Staging Area into this permanent history.

Your workflow will always be:
**Work (Working Directory) -> Stage (git add) -> Commit (git commit)**

Part 3: Basic Local Workflow

Let's create our first repository and make our first commit.

`git init` (Initialize)

This command creates a new, empty Git repository in your current folder. It does this by creating the hidden .git folder.

$ mkdir my-first-project
$ cd my-first-project

# Create the repository
$ git init
Initialized empty Git repository in /home/msmaxpro/my-first-project/.git/

# Let's look inside the .git folder
$ ls -a .git
HEAD  config  description  hooks  info  objects  refs
  • objects: This is where Git stores all your data (commits, files).
  • refs/heads: This is where your branches (like `main`) are stored.
  • HEAD: A special file that points to the branch you are *currently* on.

`git status` (The Most Important Command)

This is your "dashboard." You should run git status *all the time*. It tells you the state of all your files.

# 1. Create two new files
$ touch index.html
$ touch style.css
$ mkdir src
$ touch src/app.js

# 2. Check the status
$ git status

On branch main
No commits yet
Untracked files:
  (use "git add ..." to include in what will be committed)
        index.html
        src/
        style.css

nothing added to commit but untracked files present (use "git add" to track)

  • Untracked files: Git sees these files, but is not "tracking" their history yet.

`git add` (Stage)

This command moves changes from the Working Directory to the Staging Area. This is you saying, "I am happy with this change and I want to include it in my next 'snapshot'."

# Let's stage just one file
$ git add index.html

# Check the status again
$ git status

On branch main
No commits yet
Changes to be committed:
  (use "git rm --cached ..." to unstage)
        new file:   index.html

Untracked files:
  (use "git add ..." to include in what will be committed)
        src/
        style.css

Notice `index.html` is now green and "Changes to be committed" (staged) and `style.css` is still red and "Untracked."

# Stage all remaining changes (the ".")
# "." means "everything in the current directory and subdirectories"
$ git add .

$ git status

On branch main
No commits yet
Changes to be committed:
  (use "git rm --cached ..." to unstage)
        new file:   index.html
        new file:   src/app.js
        new file:   style.css

`git commit` (Commit)

This command takes all the changes in the Staging Area, creates a permanent "snapshot" (commit), and saves it to your Repository (`.git`) history. A commit is a snapshot of your *entire project* at that moment in time.

# The -m flag lets you write your commit message inline
$ git commit -m "Initial commit: Add HTML, CSS, and JS files"

[main (root-commit) abc1234] Initial commit: Add HTML, CSS, and JS files
 3 files changed, 0 insertions(+)
 create mode 100644 index.html
 create mode 100644 src/app.js
 create mode 100644 style.css

Now, your history has one snapshot, identified by the hash `abc1234`.

Deep Dive: Writing a Good Commit Message

A "commit" is useless without a good message. "updated files" is a *bad* message. "fix: correct user login bug (issue #42)" is a *good* message.

The standard is called **Conventional Commits**. A message should have a **type**, a **subject**, and (optionally) a **body** and **footer**.

type(scope): subject

body

footer
  • type: feat (new feature), fix (bug fix), docs (documentation), style, refactor, test, chore (boring stuff).
  • scope (optional):** What part of the app? (e.g., auth, api, ui)
  • subject: Short description, imperative tense (e.g., "add login button," not "added login button").

Examples of good commit messages:

git commit -m "feat(auth): Add user login form to landing page"
git commit -m "fix(api): Correct password validation regex (closes #123)"

This is critical for DevOps. CI/CD pipelines can use these types to *automatically* generate changelogs or trigger new version bumps (e.g., a fix: triggers a patch, a feat: triggers a minor version).

`git log` (View History)

Now that you've made a commit, you can see it in your history.

$ git log

commit abc1234567890defabcdef1234567890abcdef (HEAD -> main)
Author: Your Name 
Date:   Mon Nov 10 13:30:00 2025 +0530

    Initial commit: Add HTML, CSS, and JS files


# The "power-user" log command (my favorite)
$ git log --oneline --graph --decorate --all

* abc1234 (HEAD -> main) Initial commit: Add HTML, CSS, and JS files

  • --oneline: Shows each commit as one line.
  • --graph: Shows the branch structure with ASCII art.
  • --decorate: Shows where branches (like `main`) are pointing.
  • --all: Shows all branches, not just the one you're on.

`.gitignore` (The "Ignore" File)

This is a critical file in every project. It's a plain text file named .gitignore that tells Git *which files to ignore*. You *never* want to commit large files, log files, secret keys, or "build" artifacts.

Create a file named .gitignore in your project's root.

.gitignore
# Ignore all log files
*.log

# Ignore Node.js dependencies (a massive folder)
node_modules/

# Ignore environment files (with API keys)
.env
*.env

# Ignore Android Studio build files
build/
*.apk

# Ignore macOS metadata files
.DS_Store

Now, git status will no longer show these files as "Untracked."

Part 4: Branching & Merging (Git's Killer Feature)

This is the single most powerful feature of Git. A **branch** is a lightweight, movable pointer to one of your commits. It's a "line of development."

The "main" branch is your stable, production-ready code. You **never** work directly on the `main` branch.
Instead, for *every new feature* (e.g., "add login page"), you create a **new feature branch** (e.g., `feat/login-page`). You do all your work on this branch. When you're 100% done, you **merge** it back into `main`.

[Image of a Git branching diagram (main branch with a feature branch splitting off and merging back)]

The Full Branching Workflow

`git branch` and `git switch`

# 1. See what branch you are on
$ git branch

* main


# 2. Create a new branch for our new feature
$ git branch feat/add-login-button

# See all branches (we are still on 'main')
$ git branch

  feat/add-login-button
* main


# 3. Switch to the new branch to start working
$ git switch feat/add-login-button

Switched to branch 'feat/add-login-button'


# (This is the new command. The old command was 'git checkout')
# You can create and switch in one command (shortcut):
$ git switch -c feat/new-feature

Do Your Work

Now you are "on" the `feat/add-login-button` branch. Any commits you make here will *not* affect the `main` branch. It's completely safe.

# (You edit index.html to add the button)
$git add index.html$ git commit -m "feat: Add login button to header"

# Check the log
$ git log --oneline --graph --decorate --all

* def4567 (HEAD -> feat/add-login-button) feat: Add login button to header
* abc1234 (main) Initial commit

See? The new commit (`def4567`) is on your feature branch, and `main` is still safely back at the first commit.

`git merge` (Bringing it all together)

Your feature is done and tested. Time to merge it into `main`.

# 1. ALWAYS switch back to the 'main' (target) branch first
$ git switch main
Switched to branch 'main'

# 2. Run the 'merge' command
$ git merge feat/add-login-button

Updating abc1234..def4567
Fast-forward
 index.html | 5 +++++
 1 file changed, 5 insertions(+)


# 3. (Optional) Delete the feature branch
$ git branch -d feat/add-login-button

This was a **"fast-forward"** merge. Since `main` had no new commits, Git just moved the `main` pointer up to match your feature branch. It was a simple, clean merge.

Deep Dive: Merge Conflicts (The Scary Part)

A "merge conflict" is not a bug. It's a normal part of working in a team. It happens when **Git doesn't know how to merge automatically**.

This happens when two people (or two branches) **edit the exact same line of the same file**.

[Image of a Git merge conflict diagram]

Git will stop, tell you there's a conflict, and ask you (the human) to fix it.

Example of a Conflict:

# You are on 'main' and try to merge 'feat/login'
$ git merge feat/login

Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Now, if you open index.html, Git will have added "conflict markers" to the file:

<div class="navbar">
  <a href="/">Home</a>
<<<<<<< HEAD
  <a href="/contact">Contact</a>
=======
  <a href="/login">Login</a>
>>>>>>> feat/login
</div>
  • <<<<<<< HEAD: This is *your* change (from `main`).
  • =======: This divides the two conflicting changes.
  • >>>>>>> feat/login: This is *their* change (from the other branch).

How to Fix It (The 3-Step Process)

  1. Edit the File: Open index.html in your editor. Delete *all* the conflict markers (<<<<<<<, =======, >>>>>>>) and manually edit the file to be the *correct* final version. (e.g., keep *both* links).
    <div class="navbar">
      <a href="/">Home</a>
      <a href="/contact">Contact</a>
      <a href="/login">Login</a>
    </div>
    
  2. Stage the Fix: Tell Git you have fixed the file by running git add.
    $ git add index.html
    
  3. Commit the Merge: Run git commit. Git will automatically create a "merge commit" message. You just need to save it.
    $ git commit
    
    (A text editor will open)
    Merge branch 'feat/login'
    
    Conflicts:
        index.html
    #
    # It looks like you may be committing a merge.
    # If this is not correct, please remove the file
    #	.git/MERGE_HEAD
    # and try again.
    ...
    (Just save and close this file)
    
    

That's it! The conflict is resolved and the merge is complete.

Part 5: Working with Remotes (GitHub/GitLab)

This is how you collaborate. A "remote" is just a nickname for a URL that points to your repository on a server (like GitHub).

`git clone`

This is how you get a repository from a server for the *first time*. It downloads the .git history and checks out the `main` branch into your working directory.

# Go to GitHub, find a project, click "Code", copy the URL
$ git clone https://github.com/MSMAXPRO/my-awesome-project.git

$ cd my-awesome-project

This automatically sets up your remote connection. It names the server origin by default.

`git remote -v`

Shows you your remotes.

$ git remote -v

origin  https://github.com/MSMAXPRO/my-awesome-project.git (fetch)
origin  https://github.com/MSMAXPRO/my-awesome-project.git (push)

`git push`

This **sends** your local commits *up to* the remote server (`origin`).

# 1. You work locally and make a commit
$git add .$ git commit -m "fix: update button color"

# 2. You "push" your commit to the 'origin' server,
#    on the 'main' branch
$ git push origin main

Enumerating objects: 5, done.
...
To https://github.com/MSMAXPRO/my-awesome-project.git
   abc1234..def5678  main -> main

`git fetch` vs. `git pull`

This is how you get changes *from* the remote server. This is critical.

`git fetch` (The Safe Way)

git fetch downloads all the new changes from the remote, but **it does not touch your local working files**. It just updates your "remote-tracking" branch (origin/main).

# 1. Download all new changes from 'origin'
$ git fetch origin

# 2. See what the differences are
$ git log --oneline main..origin/main

* 987zyx (origin/main) feat: add contact page (Aman's commit)


# 3. You see the changes are safe, so you merge them manually
$ git merge origin/main

`git pull` (The Easy Way)

git pull is just a shortcut. It does **two commands** at once: git fetch + git merge. It's more convenient, but you don't get a chance to review the changes before they are merged into your local branch.

$ git pull origin main

From https://github.com/MSMAXPRO/my-awesome-project
 * branch            main       -> FETCH_HEAD
Updating abc1234..987zyx
Fast-forward
...

Rule:** If you are working on a branch by yourself, git pull is fine. If you are on a team and working on a shared branch (like main), git fetch is safer.

Part 6: Rewriting History (Advanced & Dangerous)

This section is for "cleaning up" your local history *before* you push it to the remote. **WARNING: Never use these commands on branches that other people are using (like main).**

`git stash` (Your Safety Net)

You are in the middle of a new feature, but you have a hotfix (an emergency bug). Your files are messy and you can't commit. What do you do?

git stash takes all your *uncommitted* changes (both staged and unstaged) and saves them in a temporary "stash," leaving your working directory clean.

# You have messy files...
$ git status

Changes not staged for commit:
        modified:   style.css


# 1. Save your changes to the stash
$ git stash

Saved working directory and index state WIP on main: ...


# 2. Your directory is now 100% clean. Go fix the bug.
$ git switch -c fix/hotfix
# ... (fix bug, commit, merge) ...
$ git switch main

# 3. Bring your changes back from the stash
$ git stash pop # (pop = apply and delete from stash)

On branch main
Changes not staged for commit:
        modified:   style.css

`git reset` (The "Undo" for Local)

git reset is used to undo *local* commits that you have **not pushed** yet. It moves the HEAD pointer to a previous commit.

[Image of git reset --soft vs --mixed vs --hard diagram]
  • --soft: **Moves HEAD only.** Undoes the last commit, but keeps your changes in the Staging Area. (Good for re-committing).
  • --mixed (Default): **Moves HEAD + Staging.** Undoes the last commit, keeps your changes in the Working Directory (unstaged). (Good for re-working files).
  • --hard (Dangerous): **Moves HEAD + Staging + Working Directory.** Undoes the last commit, and **deletes all your changes** forever.
# You just made a bad commit
$ git log --oneline

* 777bbbb (HEAD -> main) oops, bad commit
* 666aaaa good commit


# Go back to '666aaaa' and keep files in Working Dir (mixed)
$ git reset 666aaaa
# OR go back 1 commit from HEAD
$ git reset HEAD~1

$ git log --oneline

* 666aaaa (HEAD -> main) good commit

(The 'oops' commit is gone, but your files are still in your working directory)

`git revert` (The "Safe Undo" for Public)

If you *have* pushed the bad commit to GitHub, you cannot use git reset. You must use git revert.

git revert doesn't delete the bad commit. Instead, it creates a **new commit** that does the *exact opposite* of the bad one.

$ git log --oneline

* 777bbbb (HEAD -> main, origin/main) oops, bad commit
* 666aaaa good commit


# Create a new commit that undoes 777bbbb
$ git revert 777bbbb

(Editor opens, asking you to confirm the message "Revert 'oops, bad commit'")
[main 888cccc] Revert "oops, bad commit"


$ git log --oneline

* 888cccc (HEAD -> main) Revert "oops, bad commit"
* 777bbbb (origin/main) oops, bad commit
* 666aaaa good commit

(The history is safe, and you can now 'git push' this new revert commit)

`git rebase` (The History Cleaner)

Rebase is the biggest rival to `merge`. `git rebase` rewrites history. It takes your branch's commits and *moves* them on top of another branch, creating a **clean, linear history** (no merge commits).

The most powerful version is **Interactive Rebase (-i)**. This lets you clean up your *local* feature branch *before* you merge it.

Imagine your local history is messy:


* ff3344 (HEAD -> my-feature) fix typo
* ee2211 oops, forgot a file
* dd9988 add login button
* abc1234 (main) Initial commit

You don't want to merge 3 messy commits into `main`. Let's clean them up.

# Rebase 'interactively' the last 3 commits from HEAD
$ git rebase -i HEAD~3

This opens your text editor with a "to-do" list:

pick dd9988 add login button
pick ee2211 oops, forgot a file
pick ff3344 fix typo

# Rebase ...
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# d, drop = remove commit

You edit this file to **squash** all three commits into one perfect commit:

pick dd9988 add login button
s ee2211 oops, forgot a file (s = squash)
s ff3344 fix typo (s = squash)

Save and close. Git will then open a *new* editor asking you to write the commit message for the *new, combined* commit. You write:

feat(auth): Add login button and form

Now, your history looks beautiful and clean:

$ git log --oneline

* 9a9a9a (HEAD -> my-feature) feat(auth): Add login button and form
* abc1234 (main) Initial commit

`git cherry-pick`

This command lets you "pick" a single commit from one branch and "copy" it onto another branch. This is useful for hotfixes.

# On main, you find a bug. You fix it on a hotfix branch.
$ git log --oneline origin/hotfix-branch

* ccc4444 fix: critical login bug


# You are on your 'dev' branch, but you need this fix *now*.
$git switch dev$ git cherry-pick ccc4444

[dev ddd5555] fix: critical login bug

Git has copied that one commit onto your `dev` branch.

Read the Official Pro Git Book (Free) →

© 2025 CodeWithMSMAXPRO. All rights reserved.