Guide
Git fundamentals explained
Git is a distributed version control system: it records every change to your codebase as a snapshot you can rewind, compare, branch from, and merge back. Unlike older centralized tools where one server held the only copy of history, every Git clone is a full repository with the complete commit graph. That design is why open source, remote teams, and continuous deployment all run on Git — and why a solid mental model of commits, branches, and remotes pays off whether you are solo or on a fifty-person team. This guide walks through the core objects, the everyday workflow, branching strategies, merge vs rebase trade-offs, and the habits that prevent the disasters (lost work, leaked secrets, broken main branches) that still happen when Git is treated as magic copy-paste.
What Git tracks (and what it ignores)
A Git repository is a directed acyclic graph of commits. Each commit points to a parent (or parents, for merges), carries an author and timestamp, and references a tree — a snapshot of every tracked file at that moment. Files themselves are stored as blobs addressed by content hash (SHA-1 historically, SHA-256 in newer repos), so identical content is never duplicated.
Git does not automatically track everything in your project folder. Untracked files sit
outside history until you git add them. The .gitignore
file lists patterns Git should never stage — build artifacts (node_modules/,
dist/), environment files (.env), OS junk (.DS_Store),
and IDE caches. Ignoring secrets at the repository root is necessary but not sufficient:
once a key is committed, it lives in history forever unless you rewrite history — a painful
operation on shared branches. Pre-commit hooks and secret scanners in
CI pipelines
catch leaks before they land on main.
The staging area
Between your working directory and a commit sits the index (staging area).
git add moves changes into the index; git commit freezes the
index into a new commit. This two-step model lets you craft atomic commits: fix the typo
in one file, stage only that file, commit with a focused message, then stage the larger
refactor separately. git status shows unstaged, staged, and untracked files;
git diff compares working tree vs index; git diff --staged
shows what the next commit will contain.
Essential daily commands
Most work flows through a small set of commands. You do not need to memorize fifty flags on day one — learn these and reach for the manual when something unusual appears.
git clone <url>— copy a remote repository locally with full history.git pull— fetch new commits from the remote and merge (or rebase) into your current branch.git checkout -b feature/x— create and switch to a new branch (modern equivalent:git switch -c feature/x).git add/git commit -m "..."— stage and record a snapshot.git push— upload local commits to the remote so teammates and CI can see them.git log --oneline --graph— visualize recent history compactly.git blame <file>— see which commit last touched each line (invaluable for debugging regressions).
Write commit messages in the imperative mood ("Add rate limiter", not "Added rate limiter") and explain why when the diff alone is not obvious. Good messages are searchable years later when someone asks why a guard clause exists.
Branches: parallel lines of work
A branch is just a movable pointer to a commit. Creating
feature/checkout costs almost nothing — you are not copying files, only
adding a label. That low cost is why feature branches dominate team workflows: each
developer (or agent) works in isolation, runs tests locally, opens a pull request, and
merges when review passes.
Common branching models
Trunk-based development keeps one long-lived main branch. Developers merge small, short-lived branches daily — sometimes behind feature flags so incomplete work ships dark. This pairs well with continuous integration: main stays deployable, and flags decouple release from exposure.
Git Flow (main + develop + release/hotfix branches) adds ceremony suited to scheduled releases and mobile apps where store review gates deployment. Many web teams have simplified away from Git Flow because deploy cadence is hourly, not quarterly.
Pick a model and enforce it with branch protection: require pull request reviews, passing CI, and no direct pushes to main. The tooling enforces what culture alone cannot.
Merge vs rebase
When your branch diverges from main, you must integrate changes. Two mechanisms dominate:
Merge
git merge main (while on your feature branch) creates a merge commit
with two parents, preserving the exact history of both lines. History is truthful but can
become noisy with many merge bubbles. Merging is safe on shared branches because it
rewrites nothing that others already pulled.
Rebase
git rebase main replays your commits on top of the latest main, producing a
linear history as if you had started from today's tip. Cleaner logs, but rebasing
commits that others have already pulled rewrites public history and causes painful
duplicate-work conflicts for teammates. Rule of thumb: rebase local, unpublished work;
merge (or squash-merge via pull request) when integrating to shared branches.
Squash merge
Platforms like GitHub offer "Squash and merge" — all commits on a feature branch collapse into one commit on main. You lose granular history on main but gain readable one-change-one-commit logs. Many teams squash feature branches and never rebase main.
Remotes, pull requests, and collaboration
A remote (usually named origin) is another copy of the
repository hosted on GitHub, GitLab, Bitbucket, or your own server. git fetch
downloads new commits without changing your working tree; git pull is fetch
plus integrate. Push rejected? Someone else advanced the branch — pull (or rebase onto
remote), resolve conflicts, push again.
A pull request (or merge request) is a social and technical gate: diff review, automated tests, discussion, then merge. CI runs unit and integration tests against the PR branch so broken code never reaches main. Link PRs to tickets, keep them small (under ~400 lines changed when possible), and respond to review comments with new commits rather than force-pushing without warning.
Resolving merge conflicts
A conflict happens when Git cannot automatically combine edits to the same lines. Markers appear in the file:
<<<<<<< HEAD
your version
=======
their version
>>>>>>> main
Edit the file to the correct combined result, remove the markers, git add
the file, and complete the merge or rebase. Conflicts are normal on active codebases —
frequency drops when teams coordinate file ownership and merge main frequently (at least
daily on long branches). Never commit conflict markers; linters and
CI gates
should fail if <<<<<<< slips through.
Tags, releases, and rollback
Annotated tags (git tag -a v1.4.0 -m "...") mark release
points immutably. Tags feed release pipelines: build Docker images from tag SHA, publish
changelogs, deploy to production. Lightweight tags are movable bookmarks; prefer annotated
tags for anything customers depend on.
Rolling back production usually means deploying a previous tag or reverting a merge commit
(git revert creates a new commit that undoes a prior one — safer on shared
main than git reset --hard, which erases history). Revert preserves audit
trails; reset is for local cleanup only.
Git with containers and infrastructure
Application repos often sit beside infrastructure-as-code. Dockerfiles reference Git SHAs in image tags so every running container traces to an exact commit. Docker builds in CI clone at a specific ref, run tests, push images to a registry, and trigger deploys. Git is the spine connecting source, artifact, and runtime — not just a save button for text files.
Monorepos (many packages in one repository) vs polyrepos (one service per repo) is an organizational choice, not a Git feature. Monorepos simplify atomic cross-service changes but require path-based CI so a docs edit does not rebuild every microservice. Git handles both; your pipeline design determines whether either scales.
Common mistakes to avoid
- Force-pushing to shared main.
git push --forceon branches others use destroys their basis for pull. Restrict force-push on protected branches. - Committing generated artifacts. Build outputs bloat history and cause merge hell. Generate in CI, not in Git.
- Giant commits. A 5,000-line Friday dump is unreviewable. Commit in logical slices.
- Long-lived branches. Branches open for weeks diverge painfully. Merge or rebase onto main daily.
- Ignoring
.gitignoreuntil too late. Add ignore rules before the first commit on new projects. - Storing secrets in Git. Rotate immediately if leaked; history rewrite is not enough without key rotation.
Key takeaways
- Git stores a graph of content-addressed commits; branches are cheap pointers, not file copies.
- The staging area lets you craft atomic commits — use it.
- Rebase for private cleanup; merge or squash-merge for shared integration.
- Protect main with reviews, CI, and branch rules — Git enables collaboration; process prevents chaos.
- Tags tie releases to SHAs; revert on shared branches instead of destructive reset.
Related reading
- CI/CD pipelines — automated test and deploy on every push
- Software testing fundamentals — what to run before merge
- Feature flags — ship trunk-based without exposing half-built features
- Docker fundamentals — packaging commits into reproducible images