Git#
Git is the operator’s logbook. A distributed version-control system where every clone holds the full history, so the tool the operator writes today, the rule set the defender tunes tomorrow, and the campaign artifacts an analyst reviews next week all live in the same auditable graph. Created by Linus Torvalds in 2005 to host Linux kernel development; now the default everywhere code is written.
For the operator Git is more than source control: it is the discipline that lets one operator hand work to another, lets a single campaign be reproduced from clean room to objective, and lets the blue-team operator track what changed on the estate and when.
Mental Model#
A Git repository is a directed acyclic graph (DAG) of commits. Branches and tags are just named pointers into the graph; most “Git problems” become easier once you remember everything is a pointer or an object in this graph. Each commit.
references its parent(s),
points at a tree (a snapshot of the working directory),
has an author, committer, message, and timestamps,
is identified by the SHA-1 (or SHA-256 in modern Git) of its contents.
Branches and tags are just named pointers into the graph. Most “Git problems” are easier once you remember everything is a pointer or an object in the graph.
Daily Commands#
The Git commands an operator types most. Status / diff /
log on the read side; add / restore / commit on the write side;
git add -p for the under-rated “stage hunks interactively”
workflow that keeps commits clean.
$ git status
$ git diff
$ git diff --staged
$ git add path/
$ git add -p
$ git restore --staged path
$ git restore path
$ git commit -m "fix: ..."
$ git commit --amend
$ git log --oneline --graph --all --decorate
$ git show <sha>
Branches#
Lightweight named pointers into the commit graph. Modern Git
prefers git switch over the older overloaded git
checkout; -c creates and switches in one step; -D
force-deletes when -d refuses.
$ git branch
$ git switch -c feature/x
$ git switch main
$ git branch -d feature/x
$ git branch -D feature/x
Remotes#
Move commits between your repo and someone else’s.
--force-with-lease is the safer force-push (refuses if
someone else’s commits arrived after you fetched);
--prune keeps the local view of remote branches honest.
$ git remote -v
$ git remote add origin git@github.com:org/repo.git
$ git fetch origin
$ git fetch --prune
$ git pull --rebase origin main
$ git push origin feature/x
$ git push --force-with-lease
Merging vs. Rebasing#
The two ways to combine branch histories. The split is between preserving how things actually happened (merge) and presenting a clean linear story (rebase). Most teams settle on a convention (rebase on feature branches, merge into main) and stop bikeshedding it.
Merge preserves history exactly as it happened. Two parents on the merge commit; messy graph but truthful.
Rebase rewrites your branch to look like it started from the latest base. Clean linear history; less truthful (you lose the actual development order).
Conventional split.
On feature branches: rebase to keep them up to date with main.
Into main: merge (often via
--no-ffto keep the merge commit), or squash-merge for a tidy main-branch log.
$ git switch feature/x
$ git fetch origin
$ git rebase origin/main
$ git switch main
$ git merge --squash feature/x
$ git commit -m "feat: do the thing"
Interactive Rebase#
The Swiss Army knife for tidying history before pushing. Reorder, squash, drop, edit, or reword commits in any combination, but never on commits that have already been shared, since rebase rewrites the SHAs and breaks anyone else working from the old history.
$ git rebase -i origin/main
In the editor: pick, reword (edit message), edit (edit
commit), squash (merge with previous), fixup (squash, drop
message), drop.
Never rebase commits that are already shared with others.
Stashing#
A drawer for in-progress work. git stash parks dirty
changes on a stack so you can pull, switch branches, or test
something clean; then git stash pop brings them back.
-u includes untracked files, which the default omits.
$ git stash
$ git stash -u
$ git stash list
$ git stash show -p stash@{0}
$ git stash pop
Undoing Things#
The recovery toolkit. Pick the leftmost option that solves the
problem. revert adds a new commit and is always safe;
reset --hard deletes work and is the operation most people
regret. Order of escalating risk: revert < soft < mixed < hard.
$ git restore path
$ git restore --source=HEAD~3 path
$ git revert <sha>
$ git reset --soft HEAD~1
$ git reset --mixed HEAD~1
$ git reset --hard HEAD~1
The order of escalating risk: revert < reset --soft < reset
--mixed < reset --hard. Reach for the leftmost that solves the
problem.
Inspecting History#
The read-only tools for understanding what happened. git
log for the timeline; git blame for line-by-line
authorship; git log -S for “when did this string
appear/disappear”; git bisect for “this used to work.”
$ git log --oneline --graph --all --decorate
$ git log --since='2 weeks' --author=operator
$ git log -p path
$ git log -S 'searchTerm'
$ git blame path
$ git bisect start good_sha bad_sha
.gitignore and .gitattributes#
The two per-repo metadata files. .gitignore keeps build
artifacts and secrets out of the index; .gitattributes
controls per-file behavior like line-ending normalization,
which becomes important the moment a Windows contributor joins
a Unix-only team.
# .gitignore
node_modules/
*.log
.env
dist/
__pycache__/
.DS_Store
# .gitattributes
*.png binary
*.sh text eol=lf
*.bat text eol=crlf
Use .gitattributes for line-ending normalization in cross-platform
repos.
Hooks#
Git hooks live in .git/hooks/, but those are local-only,
so project-shared hooks live in the repo and get installed by
a hook manager. The frameworks below take care of installation,
update, and language portability.
pre-commit, Python framework, runs many tools.
Husky, Node.
lefthook, Go, polyglot.
Common hooks: format, lint, run typechecks, run small test subset on commit.
Submodules and Subtrees#
Two ways to nest one repo inside another. Submodules keep the nested repo as its own entity (separate history, harder to clone correctly); subtrees vendor the nested code into the parent’s history (easier to clone, awkward to push back upstream). Most modern teams avoid both in favor of monorepo tooling.
git submodule, reference another repo at a specific commit. Workflow-heavy; many teams avoid.git subtree, merge another repo’s history into a subdirectory. Less ceremony, more diff noise.
Modern alternatives: monorepo tooling (Bazel, Nx, Turborepo, pnpm workspaces) or simple package management.
LFS and Big Files#
Git LFS stores large files outside the main repo, replacing them with pointer files. Necessary for repos with media, ML weights, or design assets where the binary content would otherwise blow up the repo size.
For very large monorepos, partial clones and sparse checkouts help; you can fetch only the tree paths you need rather than the whole repo.
$ git clone --filter=blob:none --sparse <url>
$ git sparse-checkout set src/myteam
Configuration#
The git config knobs every operator sets on a fresh
machine. User identity, default branch name, pull.rebase
to avoid accidental merge commits, rerere to remember
conflict resolutions, and a saner diff algorithm than the
1980s default.
$ git config --global user.name "Ada Lovelace"
$ git config --global user.email "operator@example.com"
$ git config --global init.defaultBranch main
$ git config --global pull.rebase true
$ git config --global rebase.autoStash true
$ git config --global rerere.enabled true
$ git config --global core.editor "code --wait"
$ git config --global diff.algorithm histogram
Sign Commits#
Many platforms now require it. Signing proves the commit
actually came from you (or your machine) rather than someone
spoofing your name in user.email. The modern approach uses
SSH keys instead of GPG, since you already have one for
authentication.
$ git config --global commit.gpgsign true
$ git config --global gpg.format ssh
$ git config --global user.signingkey ~/.ssh/id_ed25519.pub
GitHub / GitLab will display a “verified” badge once they have your public key.
Recovering Lost Work#
Git rarely loses things outright. The reflog tracks every
movement of HEAD for the past 90 days by default, so almost
any “I deleted my work” situation is a git reflog away
from recovery. The commands below cover the common
recoveries.
$ git reflog
$ git reset --hard HEAD@{2}
Anything reachable in the reflog can be recovered for ~90 days.
Tools and UIs#
The third-party Git tooling that competes with the CLI. Most operators do everyday work in the terminal or their IDE; the GUIs and TUIs below are useful for visual diffs, history exploration, or staging hunks across many files at once.