Intro

 Git is one of those tools that sits quietly at the center of nearly everything we build — from solo side projects to giant open-source collaborations. It keeps track of changes, lets us experiment without fear, and makes it possible to sync work across machines and teams. Without it, keeping track of changes in complex projects would be chaos.

But Git can sometimes feel cryptic with numerous commands, strange terminology, and the occasional “oh no, what did I just do?” moment. The truth is, you don’t need to know every dark corner of Git to use it well. With a few key concepts, a solid cheatsheet, and some helper scripts, Git becomes a trusted and powerful tool.

In this guide I’ll share:

  • A conceptual crash course to demystify Git
  • A task-based cheatsheet with collapsible sections for quick reference
  • My gcheck function — a Bash helper that gives a friendly status report on any repo
  • Handy aliases I rely on daily
  • Links and resources for when you want to go deeper

What is Git?

At its heart, Git is a time machine for text files.
It lets you move backward and forward through the history of a project, explore alternate timelines, and share those timelines with others.

  • Commits are snapshots.
    Each commit is like a photograph of your project at a moment in time.
    It doesn’t just save what changed, but how the whole directory looked — so you can always step back to that state.
    Specifically, Git stores full snapshots (not just diffs) and shows diffs only when you compare snapshots.

  • Branches are pointers.
    A branch isn’t a copy of your work — it’s just a movable label pointing to a commit.
    This makes branching lightweight and cheap, so you can spin off experiments freely without bloating your repo.
    The HEAD is a pointer too—it tells Git where you’re currently standing. Switching branches just moves HEAD—your current position—to a different branch reference.

  • Merging is weaving timelines.
    When two branches diverge, merging pulls their histories together into a single story.
    Sometimes this is automatic; other times you have to resolve conflicts where the timelines disagree.
    Sometimes Git can simply fast-forward a branch if there’s no divergence—no new merge commit needed.

  • Rebasing is replaying commits.
    Instead of weaving two histories together, rebasing lifts your commits and replays them onto a different base.
    The result is a cleaner-looking history — like rewriting your diary so events flow in a straight line.

  • Remotes are copies of the archive.
    A remote (like GitHub, GitLab, or Gitea) is just another place your repo lives.
    You can fetch from it, push to it, or clone it elsewhere. Remotes make collaboration and backup possible.

Together, these metaphors paint Git not as a mess of arcane commands, but as a system for time travel, parallel universes, and shared archives.

Think of it as a directed graph of changes where you always have a way back to safety

Key Concepts

a video game style pixel art ninja and his master sitting in a dojo
Mastering Git just takes time and practice

Now that we’ve framed Git in simple metaphors, here’s the more technical version — the details behind the magic.

  • Repos: local vs remote
    A local repo is your working copy with a .git/ folder inside it.
    A remote is just another full copy of the repo, usually hosted on a server (GitHub, GitLab, Gitea).
    By default, the first remote is called origin. You can rename it, or add others (upstream, backup, etc).

  • Staging area
    Also called the index. This is where you prepare changes before committing. Think of it as a shopping cart or draft tray: you can stage some edits, leave others unstaged, and commit only what you want.

  • Commits
    Immutable snapshots with parent pointers. Each has a SHA-1 ID (SHA-256 is becoming standard), author info, and message. Together they form a graph (specifically a 🔗directed acyclic graph or DAG ) of your project history.

  • Branches
    Lightweight movable labels pointing at commits. Switching branches just moves the HEAD pointer to a different branch reference.

  • Rebase vs Merge

    • Merge combines two histories into one, preserving all bumps.
    • Rebase rewrites your commits so they look like they happened on top of another branch, creating a cleaner line of history.
  • Upstream/Tracking branches
    When a local branch “tracks” a remote one, Git knows where to pull and push by default.

The Local Git Repo

When you git init in a folder, Git creates a hidden directory called .git/.
That directory is the repository — everything else is just your working files.

This is what makes Git powerful and portable: if you copy a project folder with its .git/ intact, you’ve copied the entire history, branches, tags, and settings. You don’t need a server, a database, or any special tooling.

Anatomy of .git/ (simplified)

└─$ tree -L 1 .git/
.git/
├── COMMIT_EDITMSG
├── FETCH_HEAD
├── HEAD
├── ORIG_HEAD
├── branches
├── config
├── description
├── hooks
├── index
├── info
├── logs
├── objects
├── packed-refs
└── refs
  • HEAD → a text file that tells Git which branch you’re currently on.
  • config → local repo configuration (remotes, branch tracking, custom settings).
  • objects/ → the actual data store. Commits, trees, and blobs (file contents) live here, addressed by their SHA-1 hash.
  • refs/ → pointers to commits: branches, tags, remotes.
  • logs/ → the reflog, Git’s “black box recorder” of branch movements.
  • index → the staging area, stored as a binary file.
  • hooks/ → scripts you can run automatically on events (commits, pushes, merges).

Most of the time you never touch these directly, but it’s useful to know they exist.
Git isn’t a black box — it’s just a folder full of plain-text files and hashed objects. (Peek at COMMIT_EDITMSG if you ever want to see the last commit message you wrote.)

Why it matters

  • Portability → copy the folder to a USB stick, another machine, or zip it up, and you’ve moved the repo with its entire history.
  • Independence → no central server required; you always have a complete copy locally.
  • Transparency → advanced users can inspect .git/config or .git/refs/heads/ to see what’s really happening under the hood.
a video game style pixel art ninja holding a scroll bearing the word Commit

Cheatsheet

Git has a lot of commands so I like to group them into related tasks to make it easier to find what you need based on what you’re trying to do.

Getting Started: Init & Remotes

Every Git journey starts by either creating a new repo or cloning an existing one.
Then, if you want to sync with a remote (GitHub, GitLab, Gitea), you connect it.

Cheatsheet: Init & Remotes
# === Create a new repo ===

# Initialize a repo in the current folder
git init

# Clone an existing repo
git clone https://github.com/user/project.git
git clone git@github.com:user/project.git   # SSH form


# === Check current remotes ===
git remote -v

# Add a new remote (usually 'origin')
git remote add origin https://github.com/user/project.git

# Change the remote URL (e.g., switch to SSH)
git remote set-url origin git@github.com:user/project.git

# Remove a remote
git remote remove origin


# === First push ===

# Push the current branch to 'origin' and set it as upstream
git push -u origin main

# After upstream is set, future pushes are simpler:
git push
git pull

Notes:

  • git init creates a .git/ folder; that’s the repo.
  • HTTPS is simpler to start with, but SSH keys are better long-term (no passwords every push).
  • The -u flag on first push links your branch to the remote, so you can just git push / git pull afterward.
  • You can have multiple remotes (e.g., origin for GitHub, backup for Gitea).


Daily Workflow: Add, Commit, Push, Pull

Most of the time you’re just making changes, saving them, and syncing with a remote.
These are the everyday Git commands you’ll run dozens of times a week.

Cheatsheet: Daily Workflow
# === Checking status ===
git status          # See which files changed, staged, or untracked

# === Staging changes ===
git add file.txt    # Stage one file
git add -A          # Stage all changes (tracked + untracked)

# === Committing ===
git commit -m "Message here"   # Save staged changes with a message
git commit --amend             # Fix last commit (message or staged content)

# === Pushing to remote ===
git push            # Send local commits to upstream
git push origin main  # Explicit remote + branch

# === Pulling updates ===
git pull            # Fetch + merge changes from upstream
git pull --rebase   # Fetch + replay your commits on top (cleaner history)

Notes:

  • git status is your compass — run it constantly.
  • git add = “move to staging area” → nothing is committed until you do.
  • git commit --amend is safe if you haven’t pushed yet.
  • The first git push usually needs -u origin main to set upstream (covered in Init & Remotes).
  • Prefer git pull --rebase to avoid “merge commits” from trivial updates.


Branching & Merging

Branches let you explore ideas without messing up your main line of work.
Merging and rebasing are how you bring those timelines back together.

Cheatsheet: Branching & Merging
# === Working with branches ===
git branch               # List local branches
git branch -r            # List remote branches
git branch new-feature   # Create a new branch (stays on current)
git checkout new-feature # Switch to a branch (old syntax)
git switch new-feature   # Switch to a branch (new syntax)
git switch -c bugfix     # Create + switch in one step

# === Merging ===
git checkout main
git merge new-feature    # Merge branch into main

# Merge conflicts: Git marks conflicts in files
# Edit manually, then:
git add conflicted-file
git commit               # Finish the merge

# === Rebasing ===
git checkout new-feature
git rebase main          # Replay commits on top of main

# If conflicts happen during rebase:
git status               # See what’s wrong
git add fixed-file
git rebase --continue    # Resume rebase

# Abort if it goes sideways
git merge --abort        # during a merge
git rebase --abort       # during a rebase

Notes:

  • Use git branch -a to see local + remote branches together.
  • git switch is the modern command; git checkout still works.
  • Merges preserve full history (good for collaboration).
  • Rebases rewrite history (good for keeping it clean, but don’t rebase shared branches).
  • If things go wrong, git status and git reflog are your lifelines.


Undo & Rescue

Everyone makes mistakes in Git — and that’s fine.
Git has a time machine and a safety net built in. These commands help you undo changes, roll back commits, or recover “lost” history.

Cheatsheet: Undo & Rescue
# === Unstaging changes ===
git restore --staged file.txt    # Unstage a file (leave working copy alone)
git reset HEAD file.txt          # Older syntax, same effect

# === Discarding local changes ===
git restore file.txt             # Throw away unstaged changes
git checkout -- file.txt         # Old form, still works

# === Undoing commits ===
git commit --amend               # Edit last commit (message or staged content)
git reset --soft HEAD~1          # Undo last commit, keep changes staged
git reset --mixed HEAD~1         # Undo last commit, keep changes unstaged
git reset --hard HEAD~1          # Rewind + throw away changes (⚠️ destructive)

# === Reverting commits safely ===
git revert <commit>              # Create a new commit that undoes the given one

# === Reflog (magic undo history) ===
git reflog                       # Show where HEAD has been (local movements)
git checkout <commit-hash>       # Recover a commit from reflog

# === Aborting operations ===
git merge --abort                # Cancel a merge in progress
git rebase --abort               # Cancel a rebase in progress

Notes:

  • Use git restore for day-to-day “oops” moments (unstaging or discarding edits).
  • git reset is more powerful — soft/mixed/hard decide what happens to your changes.
  • git revert is safer in shared branches because it adds a new commit instead of rewriting history.
  • git reflog is the secret weapon — even after resets or rebases, you can usually recover.
  • When in doubt: stop, run git status, then check the reflog before panicking.


Remotes & Sync

Remotes are just other copies of your repo (GitHub, GitLab, Gitea, etc.).
You fetch changes from them, push your commits to them, and pull to stay in sync.

Cheatsheet: Remotes & Sync
# === Checking remotes ===
git remote -v                   # Show remotes and their URLs

# Add a remote (commonly 'origin')
git remote add origin https://github.com/user/project.git

# Change URL (switch HTTPS ↔ SSH)
git remote set-url origin git@github.com:user/project.git


# === Fetching & pulling ===
git fetch                       # Download commits/branches from remote (no merge)
git pull                        # Fetch + merge remote changes into current branch
git pull --rebase               # Fetch + replay your commits on top (cleaner history)


# === Pushing ===
git push                        # Push current branch to its upstream
git push -u origin main         # First push: set upstream branch
git push origin feature         # Push a different branch by name

# Push all tags
git push --tags

# Delete a remote branch
git push origin --delete old-branch

Notes:

  • git fetch is safe — it just updates your local view of the remote without touching your files.
  • Use git pull --rebase to keep history clean (avoids “merge commit” clutter).
  • The -u flag sets tracking so you can just run git push / git pull afterward.
  • Remotes are just names: you can have multiple (origin, backup, upstream).


Tags & Releases

Tags mark important points in history — often used for releases (v1.0, v2.1.3).
Unlike branches, they don’t move: they’re permanent labels on commits.

Cheatsheet: Tags & Releases
# === Creating tags ===
git tag v1.0.0                 # Lightweight tag on latest commit
git tag -a v1.0.0 -m "Release" # Annotated tag (with message, recommended)
git tag v1.0.0 <commit>        # Tag a specific commit

# === Listing & inspecting ===
git tag                        # List all tags
git show v1.0.0                # Show commit + info behind a tag

# === Pushing tags ===
git push origin v1.0.0         # Push one tag
git push --tags                # Push all tags

# === Deleting tags ===
git tag -d v1.0.0              # Delete local tag
git push origin --delete v1.0.0 # Delete remote tag

Notes:

  • Prefer annotated tags (-a) — they carry messages and metadata.
  • Tags are great for marking release points or milestones.
  • GitHub/GitLab can auto-generate releases when you push a tag.


Cleanup & Maintenance

Over time, branches pile up, and Git’s object database grows.
These commands keep your repo tidy and lean.

Cheatsheet: Cleanup & Maintenance
# === Branch cleanup ===
git branch -d feature          # Delete local branch (safe, only if merged)
git branch -D feature          # Force delete local branch
git push origin --delete old   # Delete remote branch

# === Pruning ===
git fetch --prune              # Remove local refs to deleted remote branches
git remote prune origin        # Same effect, manual trigger

# === Garbage collection ===
git gc                         # Cleanup loose objects & optimize repo
git gc --prune=now             # Aggressive cleanup (⚠️ careful)

# === Checking size ===
git count-objects -vH          # Show object counts + repo size

Notes:

  • Always check git branch -a before deleting — make sure the branch isn’t needed.
  • git fetch --prune is safe to run regularly (it just cleans stale refs).
  • git gc runs automatically sometimes, but you can run it manually if a repo feels sluggish.
  • For giant repos, consider tools like BFG Repo-Cleaner to remove large files from history.


a pixel art ninja dashing through a bamboo forest in a dramatic action pose

Tricky Areas Demystified

Some parts of Git trip people up again and again. Here are a few quick clarifications that save a lot of head-scratching.

Merge vs Rebase

Both are ways of bringing one branch’s changes into another, but they tell different stories:

  • Merge → preserves history. Two timelines come together, and the graph shows the divergence + merge point.

    • Good for shared branches (everyone sees the real history).
    • Downside: history can look “messy” with lots of merge commits.
  • Rebase → rewrites history. Your commits are replayed as if they happened after the other branch.

    • Good for private branches you haven’t shared yet (keeps history linear).
    • Downside: don’t rebase commits you’ve already pushed/shared — it confuses collaborators.

💡 Rule of thumb: Merge when collaborating, rebase when cleaning up your own local work.


How Git Diff Works

Diffs compare snapshots, not files directly. That’s why you can run diffs between:

  • Working directory vs staginggit diff
  • Staging vs last commitgit diff --cached
  • Any two commitsgit diff <commit1> <commit2>
  • Current branch vs remotegit diff origin/main

This flexibility comes from Git’s DAG of commits. A “diff” is just “what would I need to apply to one snapshot to make it look like another?”


Detached HEAD

When you check out a commit by hash (not a branch), HEAD points directly at that commit.
You can poke around safely, but new commits won’t belong to any branch unless you explicitly create one.

  • Checking out a commit: git checkout abc123
  • Getting back to a branch: git switch main
  • Saving your detached work: git switch -c experiment

The Safety Net: Reflog

Every time HEAD moves, Git logs it in .git/logs/HEAD.
That means even after a reset, rebase, or branch delete, you can usually recover.

git reflog              # Show recent HEAD movements
git checkout <hash>     # Jump back to a lost commit

Think of it as Git’s black box recorder. When all else fails, check the reflog.


gcheck: A Friendly Git Status Report

Over time I found myself running the same few Git commands again and again:

  • git status to see what’s staged
  • git fetch to check for updates
  • git log or git diff to compare with upstream

That’s a lot of typing just to answer the question: “What’s the state of this repo?”

So I wrote a little Bash function called gcheck. It gives you a clear, human-readable summary of your current repository, including whether you’re ahead or behind your remote branch. I’ve been using it daily for months now and it’s become second nature.

gcheck() {
  if ! git rev-parse --show-toplevel &>/dev/null; then
    echo "🚫 Not inside a Git repository."
    return 1
  fi

  echo "🔍 Checking Git repo status for: $(basename "$(git rev-parse --show-toplevel)")"
  echo "------------------------------------------"
  
  git status || return 1
  
  echo -e "\n📡 Fetching updates from remote..."
  git fetch || return 1

  local branch current remote
  current=$(git symbolic-ref --short HEAD)
  remote=$(git for-each-ref --format='%(upstream:short)' "refs/heads/$current")

  if [ -z "$remote" ]; then
    echo "⚠️  No remote tracking branch set for '$current'"
    return 1
  fi

  echo -e "\n🔄 Comparing local '$current' with '$remote'..."
  local ahead behind
  ahead=$(git rev-list --count "$remote"..HEAD)
  behind=$(git rev-list --count HEAD.."$remote")

  if (( ahead == 0 && behind == 0 )); then
    echo "✅ Branch is up to date with '$remote'"
  else
    echo "⚠️  Local is $ahead commit(s) ahead and $behind commit(s) behind '$remote'"
    echo -e "\n📋 Commits on remote not in local:"
    git log HEAD.."$remote" --oneline

    echo -e "\n🔍 Run this to view changes:\n  git diff $remote"
  fi
}

I drop this in my shell config (~/.bashrc / ~/.zshrc) and now gcheck is just part of my daily workflow.

output of the gcheck function showing some files and sync issues
gcheck combines several commands in a nice simple format to show you the status of your repo at a glance
output of the gcheck function showing a clean, synced repo
This display showing a cleanly synced repo is always a welcome sight

Why I like it

  • One glance tells you if you’re up to date with upstream.
  • If you’re behind, it lists the missing commits so you can see what’s coming.
  • If you’re ahead, it reminds you to push.
  • If you’re both — you know it’s merge/rebase time.
  • And if you’re not even in a Git repo, it politely tells you.
a video game style pixel art ninja seated in a meditation pose surrounded by swirling blue energy and symbols

Handy Aliases

I use the following aliases in my environments. glo is especially handy since it’s useful but long and hard to remember. The others are nice when you find yourself running the full command for the thousandth time while working on a project. You can simply add the following to your shell startup files (.bashrc or .zshrc) and reload.

# Git
alias gs='git status'
alias glo='git log --oneline --graph --decorate --all'
alias ga='git add'
alias gc='git commit'
alias gd='git diff'
alias gco='git checkout'
alias gp='git push'
alias gl='git pull'
alias gitcha='gcheck' # an alias i'm used to for the gcheck function
output of glo alias, git log --oneline --graph --decorate --all
glo is a really handy alias that I run all the time

Core Learning Resources

Visual & Interactive Tools

Advanced/Specialized

  • Oh Shit, Git!?! — How to fix common Git mistakes
  • Git Magic — Advanced concepts explained simply
  • nbdime — Better jupyter notebook diffs for data science
  • pre-commit — Git hooks framework for code quality

Quick Reference


Conclusion

a pixel art tsuba or guard from a katana

Git is a deep and sometimes intimidating system, but it doesn’t have to be a mystery. With just a handful of concepts, a few daily commands, and some helpers like gcheck, you can navigate most workflows with confidence.

This guide isn’t exhaustive — whole books (like Pro Git) exist for a reason — but it covers the moves you’ll use constantly: starting repos, committing, branching, merging, pulling, pushing, and recovering from mistakes.

The rest comes with practice. Every time you branch, merge, or rescue a commit from the reflog, you build muscle memory and intuition. Soon Git feels less like fighting with a tool and more like a trusty time machine you always have at your side.

If you’ve got tips, tricks, or favorite aliases of your own, I’d love to hear them: feedback@adminjitsu.com.

Happy committing ✨