Skip to content

Git Mastery: Branching, Rebasing & GitHub Workflows

From Git's object model to Git Flow and GitHub team workflows — the commands and habits that keep a team's history clean.

GitGitHubGit Flow
RR

Ranjith R

Linux SysAdmin & Cloud Engineer

Published January 20, 2026Updated May 18, 2026
10 min readBeginner#Git#Version Control#GitHub#Workflow
Share
Section 01

Introduction

Git is the distributed version control system that underpins almost every modern software team's workflow. Unlike older centralized systems (CVS, SVN), every clone of a Git repository is a full copy of the project's history — there is no single point of failure, and nearly every operation (branching, committing, diffing, browsing history) happens instantly on your local machine, offline.

This guide covers Git the way it's actually used on a production team: the object model that makes branching cheap, the day-to-day commands for branching, merging, and rebasing, how to resolve a real merge conflict under time pressure, the GitHub workflows (fork-and-PR vs. trunk-based with protected branches) teams build around it, and the habits — signed commits, disciplined .gitignoreuse, never rewriting shared history — that keep a team's history trustworthy instead of a source of dread.

Who this is for

  • Engineers who know git add/commit/push but freeze at a rebase conflict.
  • Anyone about to work on a shared feature branch with a team for the first time.
  • Developers preparing for Git-heavy technical interviews.
Section 02

Prerequisites

You should have the following before working through the commands in this guide:

  • Git installed locally (this guide shows install steps if you don't have it yet).
  • A terminal you're comfortable navigating (cd, ls, a text editor).
  • A free GitHub account, plus SSH or HTTPS access configured to it.
  • A throwaway local repository or GitHub repo to practice branching and rebasing on — don't practice history rewrites on a repo you can't afford to break.

Git vs. GitHub

Git is the version control tool that runs entirely on your machine. GitHub is a hosted service that stores Git repositories remotely and adds collaboration features — pull requests, code review, issues, Actions. You can use Git without ever touching GitHub; this guide covers both because that's how most teams actually work.
Section 03

Theory

Git Architecture: the object model

Everything Git tracks is stored as one of four object types in the .git/objects database, each addressed by the SHA-1 (or SHA-256, on newer repos) hash of its content:

ObjectWhat it holds
blobThe raw contents of a single file (no filename, just bytes)
treeA directory listing — maps names/modes to blob or tree hashes
commitA pointer to one tree, plus parent commit(s), author, and message
tagAn annotated, signable pointer to a specific commit

Because objects are addressed by content hash, identical file content anywhere in the repository — across commits, across branches — is stored exactly once. A commit doesn't store a diff; it stores a full snapshot (a tree), and Git computes diffs on demand by comparing trees. This is why git log -p can be slow on huge repos but git checkout between commits is fast.

The commit graph is a DAG

Commits form a directed acyclic graph (DAG): each commit points backward to its parent(s). A normal commit has one parent; a merge commit has two (or more). This graph structure is what makes operations like git log --graph, git merge-base, and three-way merges possible — Git can always find the common ancestor of any two commits by walking the graph.

A branch is just a pointer

This is the single most important mental model in Git: a branch is not a copy of the code. It is a 41-byte text file in .git/refs/heads/ containing the SHA of one commit. git branch feature-x creates a new pointer at the current commit — an O(1) operation regardless of repo size. Committing on a branch simply moves that pointer forward to the new commit. HEAD is itself a pointer — almost always a symbolic pointer to the branch you have checked out (refs/heads/main, for example), which is how Git knows which branch to advance when you commit.

Why this makes branching cheap

Because a branch is only a pointer, creating one, switching between them, and deleting them are all near-instant, regardless of how large the codebase is — Git never copies files to create a branch.
Section 04

Architecture

Git data moves through four distinct areas as you work. Understanding these boundaries is what makes commands like git add, git commit, and git restore --staged click:

Working directory → staging area → local repository → remote repository, and back

  • Working directory — the actual files on disk you edit. Git compares these against the index to compute git status and git diff.
  • Staging area (the index) — a snapshot-in-progress. git add copies working-directory changes here; this is what the next commit will contain.
  • Local repository — the committed history in .git, private to your machine until pushed.
  • Remote repository — a copy hosted elsewhere (GitHub, GitLab) thatgit push/git fetch/git pull synchronize with.
Section 05

Installation & Initial Setup

Install Git for your platform, then set your identity before making any commits:

terminal
sudo apt update
sudo apt install git -y
git --version
terminal
# Identity used on every commit you make
git config --global user.name "Ranjith R"
git config --global user.email "you@example.com"

# Sane defaults
git config --global init.defaultBranch main
git config --global core.editor "vim"
git config --global pull.rebase false

# Confirm everything
git config --list

Global vs. local config

--global writes to ~/.gitconfig and applies to every repo for your user. Drop --global inside a specific repository to override just that repo — useful for a work identity that differs from your personal one.
Section 06

Configuration

Repository: init vs. clone

git init creates a brand-new, empty repository (a fresh .git directory) in the current folder — used when starting a project from scratch. git clonecopies an existing remote repository's full history to your machine and automatically wires up origin as the remote name.

terminal
# Start a brand-new project
mkdir my-app && cd my-app
git init

# Copy an existing project, including all history
git clone git@github.com:acme/my-app.git
git clone https://github.com/acme/my-app.git

# Clone only recent history (faster for huge repos)
git clone --depth 1 git@github.com:acme/my-app.git

.git internals, briefly

Everything Git needs lives inside the .git directory — delete it and the folder is just plain files with no history. Key pieces:

PathPurpose
.git/objects/The content-addressed object database (blobs, trees, commits, tags)
.git/refs/heads/One file per local branch, containing a commit SHA
.git/refs/remotes/Read-only tracking refs mirroring remote branches
.git/HEADA symbolic reference to the currently checked-out branch
.git/configRepo-local configuration (remotes, aliases, overrides)
.git/indexThe binary staging area file

.gitignore and remotes

.gitignore
node_modules/
.env
.env.local
dist/
*.log
.DS_Store
terminal
# View, add, and change remotes
git remote -v
git remote add origin git@github.com:acme/my-app.git
git remote set-url origin git@github.com:acme/my-app-renamed.git

# Useful per-repo aliases
git config alias.st status
git config alias.lg "log --oneline --graph --decorate --all"
Section 07

Commands

Branch

git branch creates or lists pointers; git switch (or the older checkout) moves HEAD to point at one.

terminal
git branch                       # list local branches
git branch feature/checkout-flow # create a branch, stay where you are
git switch feature/checkout-flow # move HEAD to that branch
git switch -c feature/new-thing  # create + switch in one step
git checkout -b feature/new-thing # older equivalent of switch -c
git branch -d feature/checkout-flow   # delete (safe, blocks if unmerged)
git branch -D feature/checkout-flow   # delete (force)
git branch -m old-name new-name       # rename

Merge: fast-forward vs. three-way

If the target branch hasn't diverged (no new commits since the feature branch was created), Git performs a fast-forward — it just moves the pointer forward, no new commit created. If both branches have diverged, Git creates a three-way merge commit with two parents, using the common ancestor to compute what changed on each side.

terminal
git switch main
git merge feature/checkout-flow          # fast-forward if possible
git merge --no-ff feature/checkout-flow  # always create a merge commit
git merge --abort                        # bail out of a conflicted merge

Rebase

Rebase replays your branch's commits one by one onto a new base commit, producing linear history instead of a merge commit. Interactive rebase additionally lets you reorder, squash, reword, or drop commits before they land.

terminal
git switch feature/checkout-flow
git rebase main                     # replay this branch's commits onto main
git rebase -i HEAD~5                # interactively edit the last 5 commits
git rebase --onto main old-base feature/x  # replay a range onto a new base
git rebase --continue               # after resolving a conflict mid-rebase
git rebase --abort                  # bail out, restore original branch state

The golden rule of rebasing

Never rebase a branch that other people have already pulled or based work on. Rebasing rewrites commit SHAs; anyone with the old history will get duplicate, diverged commits the next time they pull. Rebase freely on your own unpublished feature branches — merge (or a team-agreed force-push protocol) once a branch is shared.

Cherry-pick

Apply one specific commit from another branch onto your current branch:

terminal
git cherry-pick a1b2c3d          # apply a single commit here
git cherry-pick a1b2c3d..f9e8d7c # apply a range (exclusive of first)
git cherry-pick --continue       # after resolving a conflict
git cherry-pick --abort

Stash

Shelve uncommitted work temporarily without committing it, e.g. to switch branches:

terminal
git stash push -m "wip: payment retry logic"
git stash list
git stash show -p stash@{0}
git stash pop            # re-apply and remove from the stash list
git stash apply          # re-apply but keep it in the stash list
git stash drop stash@{0}
git stash -u              # include untracked files

Reset: --soft, --mixed, --hard

ModeHEADStaging areaWorking directory
--softmovedunchanged (kept staged)unchanged
--mixed (default)movedreset to match new HEADunchanged
--hardmovedresetreset — uncommitted work is lost
terminal
git reset --soft HEAD~1   # undo last commit, keep changes staged
git reset HEAD~1          # (--mixed) undo commit + staging, keep edits
git reset --hard HEAD~1   # discard the commit and all its changes entirely
git reset HEAD~1 -- file.txt   # unstage a single file

Revert

Unlike reset, git revertdoesn't rewrite history — it creates a new committhat undoes a previous one. This makes it the safe choice for undoing something that's already been pushed and shared.

terminal
git revert a1b2c3d          # create a commit undoing a1b2c3d
git revert --no-commit a1b2c3d..HEAD  # revert a range, stage without committing yet

Tag

A lightweight tag is just a named pointer to a commit. An annotated tag is a full Git object with its own message, tagger, date, and optional GPG signature — use annotated tags for releases.

terminal
git tag v1.2.0                              # lightweight
git tag -a v1.2.0 -m "Release 1.2.0"        # annotated
git tag -s v1.2.0 -m "Release 1.2.0"        # annotated + GPG-signed
git push origin v1.2.0                       # push a single tag
git push origin --tags                       # push all tags
git tag -d v1.2.0 && git push origin :refs/tags/v1.2.0  # delete local + remote
Section 08

Examples

A full feature branch lifecycle, from creation to a merged pull request:

  1. Branch off main

    git switch main && git pull, then git switch -c feature/checkout-flow to start isolated work.
  2. Commit in small, logical chunks

    git add -p to stage hunks selectively, then git commit -m "feat: add coupon field to checkout" — small commits make review and future git bisect runs far easier.
  3. Stay current with main

    Periodically git fetch origin then git rebase origin/main to replay your commits on top of the latest main, resolving small conflicts early instead of one large one at the end.
  4. Push and open a pull request

    git push -u origin feature/checkout-flow, then open a PR on GitHub against main and request review.
  5. Address review feedback

    Push additional commits, or git commit --amend / git rebase -i to tidy history before merge if the team prefers a clean log.
  6. Merge and clean up

    Merge via GitHub (squash, rebase, or merge commit per team convention), then locally git switch main && git pull && git branch -d feature/checkout-flow.
Section 10

Real World Example

Scenario: two engineers both edited src/lib/pricing.ts — one on feature/discount-codes, one already merged to main. Rebasing the feature branch produces a conflict.

  1. Rebase surfaces the conflict

    git rebase origin/main stops mid-replay: CONFLICT (content): Merge conflict in src/lib/pricing.ts.
  2. Inspect what's conflicted

    git statuslists the file as "both modified". Opening it shows Git's conflict markers:
  3. Read the conflict markers

    <<<<<<< HEAD through ======= is the incoming commit being replayed; ======= through >>>>>>> a1b2c3d is the commit already on main. Both edits are kept side by side until a human decides.
  4. Resolve, verifying with a diff

    Edit the file to keep the correct combined logic, remove all three marker lines, then git diff to confirm the result actually makes sense (not just that markers are gone) before git add src/lib/pricing.ts.
  5. Continue the rebase

    git rebase --continue — repeat for any further conflicting commits in the replay, then run the test suite before force-pushing.
  6. Force-push the rewritten branch safely

    git push --force-with-lease origin feature/discount-codes — unlike a plain --force, this refuses to push if someone else has pushed to the branch since your last fetch, preventing you from silently discarding a teammate's work.

A merge tool can help on messier conflicts

For conflicts spanning many hunks, git mergetool (wired to VS Code, meld, or similar) shows base/local/remote side by side — often faster than reading raw markers on a large diff.
Troubleshooting

Common Issues

Detached HEAD state

Checking out a specific commit or tag (git checkout a1b2c3d) puts you in detached HEAD — commits made here belong to no branch and can be lost once you switch away. Fix it immediately with git switch -c recovery-branch to give those commits a real branch to live on, or use git log --all --oneline plus git reflog to find and rescue them if you already switched away.

Accidentally committed directly to main

Don't push. Create a branch at the current position (git branch feature/my-fix), then move main back with git reset --hard origin/main, and switch to feature/my-fix to continue and open a PR properly.

Force-push overwrote a teammate's commits

A plain git push --force replaces the remote branch unconditionally, even if someone else pushed in between. Prevent it going forward with git push --force-with-lease, which fails safely in that situation. To recover lost commits, the teammate's local git reflog still has the SHA — cherry-pick or reset back to it and re-push.

A large file was committed by mistake

Removing it in a new commit doesn't shrink the repo — it's still in history. For a file not yet pushed, git reset --soft HEAD~1, unstage it, and recommit. For history already pushed, use git filter-repo (preferred over the legacy filter-branch) to strip it from all history, then have every collaborator re-clone, since this rewrites every downstream SHA.

Merge conflict markers accidentally committed

If <<<<<<</=======/>>>>>>> lines slip into a commit, it usually means git addran before the conflict was fully resolved. Fix the file, re-add, and amend if it hasn't been pushed yet; otherwise commit a follow-up fix — add a pre-commit hook that greps for ^<<<<<<< to catch this before it happens again.
Best Practices

Best Practices

  • Write commit messages in the imperative mood ("add", not "added") with a short summary line and a blank line before any body detail.
  • Commit small, logically complete units of work — it makes review, git bisect, and reverts precise instead of all-or-nothing.
  • Pull with rebase (git pull --rebase) on your own feature branches to avoid noisy merge-of-merge commits in history.
  • Never rewrite history on a branch others have already pulled — see the golden rule of rebasing.
  • Keep main always deployable; do all work on short-lived feature branches merged via reviewed pull requests.
  • Tag every release with an annotated tag so git describe and changelogs have a stable reference point.
Security

Security Hardening

  • Sign your commitsgit config --global commit.gpgsign true with a GPG or SSH signing key gives GitHub a verified badge and proves authorship.
  • Protect the main branch — require pull requests, passing status checks, and at least one approving review before merge; disallow direct pushes and force-pushes on GitHub's branch protection rules.
  • Never commit secrets — API keys, .env files, and credentials belong in a secrets manager, not source control; scan history with tools like gitleaks or trufflehog before it ships.
  • Maintain .gitignore discipline — commit a sensible .gitignore from day one; a secret committed once still exists in history even after a later commit deletes it.
  • Use a credential helper, not plaintext tokensgit config --global credential.helper (or an OS keychain) stores credentials securely; never paste a personal access token into a remote URL that gets committed anywhere.
Interview Questions

Interview Questions

Merge combines two branches' histories with a new merge commit that has two parents, preserving exactly what happened and when. Rebase replays your branch's commits one by one onto a new base, producing linear history with no merge commit — but it rewrites commit SHAs, so it should never be used on commits others have already pulled.
Cheat Sheet

Cheat Sheet

git-cheatsheet.sh
# --- Setup ---
git init ; git clone <url>
git config --global user.name "..." ; git config --global user.email "..."

# --- Status / History ---
git status
git log --oneline --graph --decorate --all
git diff ; git diff --staged

# --- Stage / Commit ---
git add <file> ; git add -p ; git add -A
git commit -m "feat: ..." ; git commit --amend

# --- Branching ---
git branch ; git switch -c <name> ; git branch -d <name>

# --- Sync ---
git fetch ; git pull --rebase ; git push -u origin <branch>
git push --force-with-lease   # safe force-push after a rebase

# --- Merge / Rebase ---
git merge <branch> ; git merge --no-ff <branch>
git rebase main ; git rebase -i HEAD~5 ; git rebase --continue|--abort

# --- Undo ---
git restore <file> ; git restore --staged <file>
git reset --soft|--mixed|--hard HEAD~1
git revert <commit>

# --- Stash / Cherry-pick / Tag ---
git stash push -m "wip" ; git stash pop
git cherry-pick <commit>
git tag -a v1.0.0 -m "Release" ; git push origin --tags

# --- Recovery ---
git reflog   # find "lost" commits after a reset/rebase gone wrong
Summary

Summary

Git's entire feature set follows from one idea: content-addressed objects (blobs, trees, commits) linked into a DAG, with branches as cheap, movable pointers into that graph. Once that clicks, merge, rebase, cherry-pick, reset, and revert all read as different ways of moving pointers and replaying or combining snapshots rather than separate tricks to memorize. Layer a GitHub workflow — protected main, required reviews, feature branches — on top, and the same primitives scale from a solo project to a large team without the history turning into chaos.

Resources

Resources

  • Official Git documentation and the free Pro Git book — git-scm.com
  • GitHub Docs — pull requests, Actions, branch protection — docs.github.com
  • Interactive branching visualizer — learngitbranching.js.org
  • Atlassian's Git tutorials (merge vs. rebase, workflows) — atlassian.com/git
  • Oh Shit, Git!?! — plain-English fixes for common mistakes — ohshitgit.com