Git From Scratch: Concepts, Commands, and the Workflow That Actually Sticks

Published On: March 30th, 2026|Categories: WordPress|10 min read|

How Git Thinks About Your Files

Git does not store diffs between file versions. Every commit is a full snapshot of the entire tracked tree, compressed into blob objects and referenced through a tree hash. Two commits that share an identical file point to the same blob – no duplication, no wasted disk space. This distinction matters because it explains why operations like branching and checking out old commits are nearly instant: Git just swaps pointers instead of reconstructing patches.

The .git directory holds everything. Objects, refs, the index (staging area), hooks, and configuration all live inside this single folder at the root of your repository.

Repositories, Clones, and Remotes

A Git repository is the .git folder plus the working tree around it. Running git init creates a fresh repository in the current directory, while git clone copies an entire remote repository – including full commit history – to your local machine. The remote that was cloned from is automatically named origin, and you can add more remotes with git remote add.

git clone [email protected]:user/project.git
cd project
git remote -v
# origin  [email protected]:user/project.git (fetch)
# origin  [email protected]:user/project.git (push)

Remotes are just named URLs. Fetching from a remote downloads objects and refs without touching your working directory. Pushing sends your local commits to the remote. Anyone comfortable with command-line tools and SSH will recognize the authentication flow: SSH keys or HTTPS tokens handle access control.

Staging, Committing, and the Index

The staging area (also called the index) sits between your working directory and the repository. git add moves changes from the working tree into the index. git commit takes whatever is in the index and writes it as a new commit object.

This two-step process exists for a reason. You might change five files but only want to commit three of them right now. Staging gives you that control. Running git add -p even lets you stage individual hunks within a single file – handy when one file contains both a bugfix and an unrelated formatting change.

git add -p src/checkout.php
# Stage specific hunks interactively
git commit -m "Fix tax calculation on checkout totals"

A common mistake is running git commit -a on every commit, which stages all modified tracked files automatically and defeats the purpose of selective staging.

Branching Without Overhead

A branch in Git is a 41-byte file containing a commit hash. Creating a branch costs almost nothing – no file copies, no directory duplication. The HEAD ref points to whatever branch (or commit) is currently checked out.

git branch feature/cart-ajax
git switch feature/cart-ajax
# or the shortcut:
git switch -c feature/cart-ajax

Developers working on plugin refactoring projects typically create a branch per logical change – one for extracting a class, another for swapping a legacy hook. This keeps pull requests small and reviewable.

git branch -d feature/cart-ajax deletes a branch only if it has been fully merged. Use -D to force-delete an unmerged branch when you are certain the work is disposable. Remote branches are deleted with git push origin --delete feature/cart-ajax.

Merging: Fast-Forward vs. Three-Way

When the target branch has not moved since the feature branch was created, Git performs a fast-forward merge – it simply moves the branch pointer forward. No merge commit is created.

If both branches have diverged, Git performs a three-way merge using the common ancestor commit, the tip of the current branch, and the tip of the incoming branch. This produces a merge commit with two parents.

git switch main
git merge feature/cart-ajax
# Fast-forward if main hasn't moved, three-way merge otherwise

Merge conflicts happen when both branches modify the same lines. Git marks the conflicting sections with <<<<<<>>>>>> markers inside the affected files. Resolve them manually, then run git add on each resolved file and git commit to finalize.

Rebase: Rewriting History on Purpose

git rebase main takes the commits on your current branch and replays them on top of the latest main. The result is a linear history without merge commits.

git switch feature/cart-ajax
git rebase main
# Replays feature commits on top of main's latest commit

Interactive rebase (git rebase -i HEAD~4) opens an editor listing the last four commits. You can reorder them, squash multiple commits into one, edit commit messages, or drop commits entirely. This is the standard cleanup step before publishing code to a shared repository. Rewriting commits that other people have already pulled causes ref divergence and forces everyone to reconcile their local history. The rule is simple: rebase local work, merge shared work.

Inspecting Changes With diff and log

git diff shows unstaged changes. git diff --staged shows what is in the index but not yet committed. git diff main..feature/cart-ajax compares two branches.

git log --oneline --graph --all
# Visual commit graph across all branches
git log --author="alex" --since="2 weeks ago" --stat
# Commits by a specific author with file change stats

git log defaults to a linear list, but --graph reveals the branching and merging structure. Pair it with --oneline to keep output compact. The --stat flag adds a per-file summary of insertions and deletions, which is useful when reviewing what changed in a deployment branch before pushing.

Stashing Unfinished Work

git stash saves uncommitted changes (both staged and unstaged) into a stack and reverts the working tree to the last commit. git stash pop restores the most recent stash and removes it from the stack. git stash list shows all saved stashes.

This is the fastest way to context-switch. A bug report arrives while you are mid-feature – stash, switch to main, fix the bug, commit, switch back, pop the stash.

git stash push -m "WIP: ajax cart loader"
git switch main
# fix the bug, commit it
git switch feature/cart-ajax
git stash pop

Running stash with -m adds a description, which prevents the “what was stash@{3} again?” problem when multiple stashes pile up.

Tagging Releases

Tags mark specific commits as release points. Lightweight tags are just named pointers (git tag v1.0.0). Annotated tags store extra metadata – tagger name, date, and a message – and are the recommended format for releases.

git tag -a v2.1.0 -m "Stable release with HPOS migration support"
git push origin v2.1.0

Automated deployment scripts often trigger on tag pushes. When a cron job or CI pipeline detects a new tag matching a semver pattern, it pulls and deploys that exact tagged commit.

Recovering Lost Commits With reflog

git reflog records every time HEAD moves – commits, checkouts, rebases, resets. If you accidentally reset too far back or deleted a branch with unmerged work, the reflog still holds a reference to those commits for at least 30 days.

git reflog
# find the SHA of the lost commit
git branch recovered-work abc1234

Reflog entries are local-only. They are never pushed to remotes. The garbage collector (git gc) eventually prunes unreachable objects, but the default retention is 90 days for reachable reflog entries and 30 days for unreachable ones.

.gitignore and Tracked File Hygiene

The .gitignore file tells Git which paths to exclude from tracking. Patterns support wildcards (*.log), directory matching (node_modules/), and negation (!important.log). Each directory can have its own .gitignore, and rules cascade downward.

A frequent problem on WordPress projects: someone commits the wp-content/uploads/ directory or the wp-config.php file with database credentials. Once a file is tracked, adding it to .gitignore does nothing – you must first run git rm --cached wp-config.php to untrack it without deleting it from disk. Teams that handle security-sensitive files on the server should enforce this from the very first commit.

Aliases That Remove Friction

Git aliases live in ~/.gitconfig and shorten repetitive commands. A few that save real time across hundreds of daily invocations:

git config --global alias.co checkout
git config --global alias.br "branch -vv"
git config --global alias.lg "log --oneline --graph --all --decorate"
git config --global alias.unstage "reset HEAD --"

The br -vv alias shows each branch alongside its upstream tracking ref and ahead/behind counts. This replaces the need to run git status on each branch individually when managing multiple automation branches in a deployment workflow.

Config Levels and Scope

Git reads configuration from three levels: system (/etc/gitconfig), global (~/.gitconfig), and local (.git/config inside the repository). Local settings override global, and global overrides system.

Setting a per-repository user email is critical when working across personal and client projects from the same machine. Without it, commits end up attributed to the wrong identity.

cd /var/www/client-project
git config user.email "[email protected]"
git config user.name "Alex Developer"

The --global flag writes to ~/.gitconfig and applies everywhere by default. The local config applies only to the current repository. Understanding this layering avoids the common “wrong email in commits” problem that plagues developers juggling multiple codebases.

Често задавани въпроси

  1. What is the difference between git fetch and git pull?

    git fetch downloads new commits from the remote repository but does not modify your working directory or current branch. git pull runs git fetch followed by git merge, so it both downloads and integrates remote changes into your current branch.

  2. How do you undo the last commit without losing changes?

    Run git reset –soft HEAD~1. This moves the branch pointer back one commit but keeps all changes staged in the index, ready for a new commit.

  3. What is a detached HEAD state in Git?

    A detached HEAD means you checked out a specific commit instead of a branch. Any new commits made in this state are not attached to any branch and can be lost if you switch away without creating a branch first.

  4. When should you use git rebase instead of git merge?

    Use rebase to keep a linear commit history on feature branches before merging into main. Avoid rebasing commits that have already been pushed to a shared remote, because it rewrites history and forces collaborators to reconcile divergent refs.

  5. How does Git store file changes internally?

    Git stores full snapshots of every tracked file as compressed blob objects, not incremental diffs. Each commit points to a tree object that references these blobs, and identical files across commits share the same blob hash to save space.




Related Articles

If you enjoyed reading this, then please explore our other articles below:

More Articles

If you enjoyed reading this, then please explore our other articles below: