The stuff that shows up in senior job descriptions and nowhere in a beginner tutorial. This is where “I know Git” becomes “I actually know Git.”
Tells Git “never track these files, don’t even ask.”
# .gitignore
node_modules/
.env
*.log
dist/
.DS_Store
You wouldn’t mail your neighbor a photocopy of your trash can along with your important
documents. .gitignore is drawing a line around the stuff that’s either regenerable
(node_modules/, dist/), machine-specific junk (.DS_Store), or genuinely dangerous to share
(.env with real API keys) — and telling Git to never even consider it.
# Already accidentally tracked a file BEFORE adding it to .gitignore?
# Adding it to .gitignore alone won't untrack it — you have to explicitly remove it:
git rm --cached .env
echo ".env" >> .gitignore
git commit -m "chore: stop tracking .env, add to gitignore"
[!WARNING]
.gitignoreonly prevents future tracking. If a secret was ever committed, it’s sitting in your Git history forever —git rm --cachedremoves it going forward, but doesn’t erase the old commits that still contain it. If a real secret leaks, the fix is rotating the credential, not just removing the file. gitignore.io generates solid starter files for any language/framework combination.
Tells Git how to handle specific files, not just whether to track them.
# .gitattributes
* text=auto # normalize line endings automatically
*.jpg binary # never try to diff/merge these as text
*.png binary
*.pdf binary
docs/CHANGELOG.md merge=union # union merge: combine both sides instead of conflicting
Windows and Mac/Linux disagree on how to end a line of text (\r\n vs \n) — invisible in your
editor, but it makes Git think every single line of a file changed when only line-endings
differ, drowning real diffs in noise. .gitattributes’s text=auto line settles that argument
once, for the whole repo, so nobody has to think about it again.
Git is fundamentally bad at large binary files (videos, design files, datasets) — every version of every binary gets stored in full, forever, bloating the repo. Git LFS replaces those large files with small text pointers in the actual Git history, storing the real content separately.
git lfs install
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes # LFS tracking rules live here
git add design-mockup.psd
git commit -m "feat: add homepage design mockup"
git push
Instead of mailing a 2GB video file inside every single letter in your correspondence forever, you mail a note that says “the video is at this specific address” — the actual video lives elsewhere, fetched only when someone actually needs it. Your letters (the Git history) stay small and fast, no matter how many video versions accumulate over time.
Cryptographically proves a commit or tag actually came from you, not someone impersonating your name/email (which, unsigned, is trivially easy to fake).
# One-time setup: generate a GPG key, add it to GitHub (Settings → SSH and GPG keys)
git config --global user.signingkey <your-key-id>
git config --global commit.gpgsign true # sign EVERY commit automatically from now on
git commit -S -m "fix: critical security patch" # sign just this one, manually
git tag -s v1.0.0 -m "Signed release" # signed tag
Signed commits show a verified “Verified” badge on GitHub — a real trust signal, especially significant for security-sensitive open-source projects where “did this commit actually come from a trusted maintainer” genuinely matters.
Cherry-pick — apply one specific commit from anywhere onto your current branch:
git log other-branch --oneline # find the commit hash you want
git cherry-pick a3f291c # apply JUST that one commit here
Someone else’s notebook has one page you desperately need — not their whole notebook, just that one page. Cherry-pick copies exactly one commit’s changes onto your branch, without merging everything else that branch has.
Common real scenario: a critical bug fix landed on main, and you need it on an older
release/v1.2 branch too, without merging all of main’s other unrelated changes into that
release branch.
git checkout release/v1.2
git cherry-pick <hash-of-the-fix-commit-on-main>
Patch — export changes as a portable file, apply them elsewhere (useful when you can’t push directly, e.g. emailing a fix to an offline system):
git format-patch -1 HEAD # exports the last commit as a .patch file
git apply 0001-fix-critical-bug.patch # someone else applies it on their machine
The moment two people (or two branches) changed the exact same lines differently, and Git genuinely cannot guess which version you want.
git merge feature/pricing-update
# Auto-merging pricing.js
# CONFLICT (content): Merge conflict in pricing.js
# Automatic merge failed; fix conflicts and then commit the result.
// pricing.js now looks like this:
<<<<<<< HEAD
const discountRate = 0.10;
=======
const discountRate = 0.15;
>>>>>>> feature/pricing-update
<<<<<<< HEAD through ======= is your current branch’s version;
======= through >>>>>>> branch-name is the incoming branch’s version<<<<<<<, =======, and
>>>>>>> marker lines entirelygit add pricing.js — mark it as resolvedgit commit — completes the merge with a merge commitgit status # shows exactly which files still have unresolved conflicts
git merge --abort # changed your mind entirely? bail out, back to pre-merge state
Two editors changed the exact same sentence in a shared document, differently, while both offline. Word can’t guess which version is “correct” — a human has to look at both versions and decide (or write a third, better version combining both intents). That’s precisely what resolving a merge conflict is: reading both sides, understanding what each was actually trying to do, and writing the version that’s genuinely correct.
[!TIP] Reduce conflicts before they happen: pull/rebase from
mainfrequently instead of letting your branch drift for weeks. The longer two branches diverge, the more surface area for conflicting changes. A branch merged after 2 days has far fewer conflicts than the same branch merged after 3 weeks.
Git isn’t just for humans typing commands — CI/CD pipelines (GitHub Actions, Jenkins) are built entirely around Git events.
# .github/workflows/test.yml — a real, minimal example
name: Run Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # this step IS a git clone, done for you
- run: npm install
- run: npm test
CI/CD is an automatic inspector standing at the shared filing cabinet, who reads every new page
the moment it’s filed (git push) and immediately checks it for problems — without waiting for a
human to ask. on: push / on: pull_request are literally Git events triggering that inspection.
Scripts that run automatically at specific points in Git’s workflow — entirely local, entirely yours to customize.
# .git/hooks/pre-commit (make it executable: chmod +x .git/hooks/pre-commit)
#!/bin/sh
npm run lint
if [ $? -ne 0 ]; then
echo "❌ Lint failed. Commit aborted."
exit 1
fi
A hook is a bouncer standing at the door of git commit, checking IDs before letting anything in.
A pre-commit hook that runs your linter means broken, unlinted code physically cannot get
committed in the first place — the check happens before the door opens, not after.
Common hooks: pre-commit (lint/format before allowing a commit), commit-msg (enforce a
message format, e.g. requiring feat:/fix: prefixes), pre-push (run the full test suite
before allowing a push).
[!NOTE] Raw hooks in
.git/hooks/aren’t committed to the repo (that folder is Git-internal, not tracked) — everyone on a team has to set them up individually. Tools like Husky solve this by making hooks a normal, committed, shareable part of the project instead.
A way to include one Git repository inside another, as a linked reference rather than a copy.
git submodule add https://github.com/someorg/shared-ui-library.git libs/shared-ui
git commit -m "chore: add shared-ui-library as a submodule"
# Someone ELSE cloning your repo needs an extra step to get the submodule's content too:
git clone --recurse-submodules <your-repo-url>
# or, if already cloned without it:
git submodule update --init --recursive
Instead of photocopying an entire reference book into your own notebook (and now owning a stale copy that never updates), a submodule is a sticky note saying “see this exact edition of that book, over there” — your repo tracks which specific commit of the other repo it depends on, without duplicating its content or history.
[!WARNING] Submodules have a well-earned reputation for being finicky — forgetting
--recurse-submoduleson clone, or forgetting to commit an updated submodule reference after the submodule itself changed, are the two most common real-world headaches. Many teams now prefer package managers (npm, a monorepo tool) for code-sharing and reserve submodules for genuinely separate, independently-versioned repos.
git remote add upstream <url> # multiple remotes on one repo (Module 03's fork workflow)
git remote rename origin old-origin # rename a remote
git remote show origin # detailed info: tracked branches, push/pull URLs
git remote prune origin # clean up local references to branches deleted on the remote
git ls-remote origin # list every branch/tag on the remote WITHOUT cloning anything
git remote prune origin is the fix for that annoying situation where git branch -a keeps
showing branches that were already deleted on GitHub weeks ago — your local repo just hasn’t been
told yet.
| Term | Meaning |
|---|---|
.gitignore |
Tells Git which files/patterns to never track |
.gitattributes |
Tells Git how to handle specific file types (line endings, diff/merge behavior) |
| Git LFS | Stores large binary files as pointers, keeping the actual repo small and fast |
| Signed commit | A commit cryptographically verified to actually be from you |
| Cherry-pick | Apply one specific commit from any branch onto your current one |
| Merge conflict | When Git can’t automatically reconcile two changes to the same lines — needs human resolution |
| Git hook | A script that runs automatically at a specific point in Git’s workflow (e.g. before a commit) |
| Submodule | A Git repository nested inside another, tracked as a reference to a specific commit |
.gitignore remove it from history if it was already committed?cherry-pick instead of a full merge?pre-commit hook checks vs. what CI checks — and
why do teams often use both, not just one?(#3: a pre-commit hook is fast, local, and optional (someone COULD skip it with --no-verify)
— CI is slower, remote, and mandatory. Teams use both: hooks for a fast local safety net, CI as
the actual, unskippable gate.)