beginner~3h

Git Fundamentals for DevOps

What Git actually tracks under the hood, the three-tree model that every Git command manipulates, and the core commands you'll run dozens of times a day.

Learning objectives

  • Beginner: Explain why Git stores snapshots of the whole project rather than file-by-file diffs, and why that makes branching cheap.
  • Beginner: Describe the working directory, staging area, and HEAD, and how 'git add' and 'git commit' move content between them.
  • Intermediate: Use init, add, commit, status, and log to build and inspect a real commit history.

A lot of engineers carry over a mental model from older version control systems: that Git stores a list of per-file changes (diffs) over time, and reconstructs any version by replaying those changes. That model is wrong, and it causes real confusion later when branching and merging stop making sense. Git actually stores a series of complete snapshots of your entire project. Every time you commit, Git doesn't ask "what changed in this file compared to last time" — it takes a full picture of every tracked file as it exists right now, and saves that picture as a single object.

Under the hood, Git is a content-addressable filesystem built from four object types, all identified by the SHA-1 (or SHA-256, in newer repos) hash of their contents: blobs (the raw contents of a file, with no filename attached), trees (a directory listing that maps names to blob or tree hashes — this is how a snapshot represents nested folders), commits (a pointer to one tree, plus metadata: author, timestamp, commit message, and a pointer to the parent commit(s)), and tags (a human-friendly name pinned to a specific commit). When you run 'git commit', Git writes new blobs for any file whose content changed, writes new tree objects for any directory whose listing changed, and writes one new commit object pointing at the resulting top-level tree. If a file didn't change between two commits, Git doesn't duplicate it — the new commit's tree simply points at the same existing blob, which is why repeated commits of a large, mostly-unchanged codebase don't balloon your '.git' folder the way you might expect.

This snapshot model has a direct, practical consequence that explains why Git branching is so fast and so cheap compared to older systems: a branch is nothing more than a movable pointer to a commit. Creating a branch does not copy any files — it just writes a 41-byte reference. Switching branches just moves the pointer and updates your working directory to match the snapshot that commit points to. There's no per-file diff machinery required to make branching work, which is why you can create and destroy branches constantly without any performance cost, and why Git encourages a workflow built around cheap, disposable branches rather than one long-lived trunk that everyone edits directly.

It's also worth being precise about what "tracked" means here, because it trips up beginners. Git only snapshots files it knows about. A brand-new file sitting in your project folder is "untracked" — Git sees it (git status will mention it) but won't include it in any commit until you explicitly tell Git to start tracking it. This is a deliberate design choice: Git never silently decides what belongs in your project history. You decide, every single time, which exact changes go into the next snapshot.

💻 Code example

# A full commit, start to finish, showing how content becomes a snapshot git init my-service cd my-service echo "console.log('hello')" > app.js git add app.js git commit -m "Add initial app entrypoint" # Inspect the objects Git actually created git cat-file -p HEAD # the commit object: tree hash, author, message git cat-file -p HEAD^{tree} # the tree object: filename -> blob hash mapping git cat-file -p HEAD:app.js # the blob object: the raw file content

Git organizes your project into three distinct areas, often called the "three trees," and almost every command you'll ever run is simply moving content between them. The first is the working directory: the actual files on disk that you edit in your editor. This is the only one of the three areas that looks like a normal filesystem, and it's the only place where your changes are "live" and uncommitted.

The second is the staging area, also called the index. This is a single file (.git/index) that lists exactly which version of which files will go into the next commit. The staging area exists as a deliberate buffer between "I changed something" and "this change is now permanent history." When you run 'git add ', Git copies the current content of that file from your working directory into the staging area. Crucially, this is a snapshot at that exact moment — if you edit the file again after staging it, the staging area still holds the older version until you run 'git add' again. This is what lets you build a commit out of only part of your changes: stage the three lines you're confident about, leave the experimental ones unstaged, and commit just the staged subset.

The third is HEAD, which points at the last commit on your current branch — effectively, the most recent snapshot that's actually part of your permanent history. 'git commit' takes everything currently in the staging area and writes it as a brand-new commit object, then moves HEAD (and the branch pointer it's attached to) forward to point at that new commit.

Seeing commands through this lens makes the whole system click. 'git status' compares all three trees pairwise and tells you what differs: which working-directory files differ from the staging area (changes not staged for commit), and which staged files differ from HEAD (changes to be committed). 'git diff' with no arguments shows working-directory-vs-staging differences; 'git diff --staged' shows staging-vs-HEAD differences. 'git checkout -- ' or the newer 'git restore ' throws away working-directory changes by copying the staged (or HEAD, if nothing's staged) version back over them. 'git reset ' does the opposite: it unstages a file by copying its HEAD version back into the staging area, without touching your working directory at all. None of these are special cases — they're all just copies between the same three trees, in different directions.

💻 Code example

# Watch all three trees diverge and then reconverge echo "v1" > notes.txt git add notes.txt # working dir == staging area, both differ from HEAD (no commit yet) git commit -m "Add notes v1" # staging area == HEAD now too echo "v2" >> notes.txt # working dir now differs from staging area and HEAD git status # shows "changes not staged for commit" git add notes.txt # staging area now matches working dir, both differ from HEAD git status # shows "changes to be committed" git diff --staged # shows staging-area-vs-HEAD diff: the v1 -> v2 change git commit -m "Update notes to v2"

'git init' creates a new repository by writing a '.git' directory in the current folder — this hidden folder is the entire database: objects, refs, config, everything. There's nothing special about the folder it lives in; a Git repository is just a normal directory with this one extra subfolder. You can delete a repository entirely, with full history intact, by deleting just the '.git' folder.

'git add' stages changes. 'git add ' stages one file; 'git add .' stages every change in the current directory and below; 'git add -p' is the option worth learning early, since it walks through each contiguous block of changes (a "hunk") and lets you stage or skip it individually — this is how experienced engineers build small, focused commits out of a messy working session instead of committing everything at once.

'git commit' writes a new commit from whatever is currently staged. 'git commit -m "message"' is the quick form; running 'git commit' with no '-m' opens your configured editor for a longer message, which matters because good commit messages have a short summary line (ideally under ~50 characters, written in the imperative: "Fix off-by-one in pagination" rather than "Fixed a bug"), a blank line, and then as much explanatory body text as the change deserves. 'git commit -am "message"' is a shortcut that stages all already-tracked, modified files and commits in one step — useful, but it silently skips new untracked files, which is a common source of "wait, where did my new file go" confusion.

'git status' is the command you should run reflexively before and after almost any other Git command. It tells you your current branch, whether it's ahead/behind its remote tracking branch, and which files are staged, unstaged, or untracked. 'git status -s' gives the same information in a compact two-column format (one column for staging-area state, one for working-directory state) that's much faster to scan once you've learned the letter codes (M for modified, A for added, ?? for untracked, D for deleted).

'git log' shows commit history, newest first. The defaults are verbose; in practice you'll reach for 'git log --oneline' (one line per commit: short hash plus subject), 'git log --oneline --graph --all' (adds an ASCII graph of branch/merge topology across every branch, not just the current one — invaluable for understanding how branches diverged and rejoined), and 'git log -p ' (shows the actual diff introduced by each commit that touched a specific file, which is often faster than opening a GUI blame tool when you're trying to understand why a line of code exists).

💻 Code example

# A realistic first session in a new repo git init git config user.name "Dev Name" git config user.email "dev@example.com" echo "node_modules/" > .gitignore git add .gitignore git commit -m "Add gitignore for node_modules" mkdir src && echo "export const add = (a,b) => a+b;" > src/math.js git status -s # ?? src/math.js git add src/math.js git status -s # A src/math.js git commit -m "Add math utility module" git log --oneline --graph --all

Want a visual for this concept?

Generate a diagram tailored to “Git Fundamentals for DevOps” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Git Branching and Merging →← Back to all Git & GitHub for DevOps chapters