What is Git? The Version Control System Every Developer Uses

Git tracks every change to every file in your codebase, lets multiple developers work simultaneously without overwriting each other, and gives you a complete history you can rewind to any point. It is the foundation of modern software development. Here is how it actually works.

Before version control, teams shared code by emailing files. One developer would work on app.php, email it to a colleague, the colleague would make changes, email it back, and hope nobody else had changed the file in the meantime. When two people's changes conflicted, someone manually reconciled them. When a bug was introduced, finding when and what required reading through email threads. When the hard drive failed, everything was gone.

This sounds absurd now. It was the reality for a significant period of software development history.

Version control systems track changes to files over time. They record who changed what, when, and why. They allow multiple people to work on the same codebase simultaneously and merge their changes. They let you roll back to any previous state. They are the foundation that makes collaborative software development possible.

Git is the dominant version control system. It is used by essentially every software team that is not using it by legacy accident. GitHub, GitLab, and Bitbucket are built on top of Git. When developers talk about "pushing code" or "merging a PR," they are talking about Git operations. Understanding Git is not optional for software development. It is baseline knowledge.

The History

Linus Torvalds created Git in 2005. The Linux kernel team had been using a proprietary version control system called BitKeeper. The company that built BitKeeper revoked the free license for open-source projects after a licensing dispute. Torvalds, who needed a replacement immediately, built Git in about ten days.

His design goals were explicit: speed, simple design, strong support for non-linear development (thousands of parallel branches), fully distributed operation, and the ability to handle large projects like the Linux kernel efficiently.

Every one of those goals shaped Git's architecture. The decisions made in those ten days are still why Git works the way it does today.

What Git Actually Is

Git is a distributed version control system. "Distributed" means every developer has a complete copy of the repository, including the full history. There is no single central server that holds the authoritative version. Any developer's copy could serve as the canonical source.

In practice, teams designate a shared remote (GitHub, GitLab, a self-hosted server) as the canonical repository. Developers clone the repository, work locally, and push changes to the shared remote. The architecture is distributed in capability even when the workflow is centralized in practice.

Git stores data as a content-addressable filesystem. Every object (commit, tree, blob, tag) is identified by the SHA-1 hash of its contents. Two objects with the same contents have the same hash. The same hash always refers to the same content. This design makes Git extremely efficient at deduplication and gives you cryptographic verification that history has not been tampered with.

The three states a file can be in:

  • Working tree: your local file system, where you edit files
  • Staging area (index): where you collect changes you intend to commit
  • Repository: the database of committed history

The Core Operations

git init
git clone https://github.com/username/repo.git
git clone https://github.com/username/repo.git my-folder
 
git status
git diff
git diff --staged
 
git add file.py
git add src/
git add .
git add -p
 
git commit -m "feat: add user authentication endpoint"
git commit --amend
 
git log --oneline -20
git log --oneline --graph --decorate --all
git log --author="Traven" --since="1 week ago"
git show abc123f
 
git push origin main
git push origin feature/auth-rework
git push --force-with-lease origin feature/auth-rework
 
git fetch origin
git pull
git pull --rebase origin main

git add -p is one of the most useful commands beginners skip. It opens an interactive patch view and lets you select individual hunks (sections of changes) to stage rather than staging the entire file. If you made two unrelated changes to the same file, you can commit them separately for a cleaner history.

--force-with-lease instead of --force when force-pushing rewrites branch history. --force overwrites the remote branch regardless of what is on it. --force-with-lease fails if the remote branch has commits that you do not have locally, which prevents accidentally overwriting a colleague's work.

git pull --rebase fetches remote changes and rebases your local commits on top of them instead of creating a merge commit. On branches with a linear history, this produces cleaner logs.

Branches

Branches are the mechanism for parallel development. A branch is a lightweight movable pointer to a commit. Creating a branch is nearly instantaneous. Switching between branches is fast. Git encourages branching far more than older version control systems did.

git branch feature/payment-integration
git checkout -b feature/payment-integration
git switch -c feature/payment-integration
 
git branch -a
git branch -v
 
git checkout main
git merge feature/payment-integration
git merge --no-ff feature/payment-integration
 
git rebase main
git rebase -i HEAD~5
 
git branch -d feature/payment-integration
git push origin --delete feature/payment-integration

git switch -c is the modern equivalent of git checkout -b. Both create and switch to a new branch.

--no-ff in git merge forces a merge commit even when a fast-forward merge is possible. Fast-forward is when your branch's base commit is the current tip of the target branch: Git just moves the pointer forward without a merge commit. --no-ff preserves the merge topology in history, showing when feature branches were merged. Teams that want clear feature boundaries in history use --no-ff. Teams that prefer a linear history use --squash (squash all branch commits into one) or rebase before merging.

git rebase -i HEAD~5 opens an interactive rebase for the last 5 commits. You can reorder commits, squash multiple commits into one, edit commit messages, or drop commits entirely. Rebase rewrites history. Never rebase commits that have been pushed to a shared remote branch that others are working from. Rebase your own feature branches freely before merging.

Merge Conflicts

Merge conflicts happen when two branches modify the same lines in the same file. Git cannot automatically decide which version is correct. It marks the conflict in the file and asks you to resolve it.

<<<<<<< HEAD
const maxRetries = 3;
const retryDelay = 1000;
=======
const MAX_RETRIES = 5;
const RETRY_DELAY_MS = 500;
>>>>>>> feature/faster-retries

The <<<<<<< HEAD section is what your current branch has. The >>>>>>> feature/faster-retries section is what the incoming branch has. You edit the file to the correct resolution (could be one version, the other, or something combining both), remove the conflict markers, and stage the file.

git status
git diff --diff-filter=U
 
code conflicted-file.js
 
git add conflicted-file.js
git merge --continue

A good merge tool (VS Code has built-in merge conflict resolution, JetBrains IDEs have a three-way merge view) makes this significantly less painful than editing the raw markers. The three-way view shows the common ancestor, your version, and their version side by side.

The Staging Area

The staging area is where Git becomes more powerful than most developers realize when they start. You do not have to commit all your current changes at once. You curate exactly what goes into each commit.

echo "console.log('debug');" >> app.js
echo "# New Feature" >> README.md
 
git add README.md
 
git status
 
git stash
git stash pop
git stash list
git stash pop stash@{1}

Stash is a stack of temporary saves. git stash saves your current working tree and staging area changes, cleaning the working tree to the last commit. git stash pop restores them. This is useful when you are in the middle of something, need to switch branches urgently, and are not ready to commit.

git add -p
 
git commit -m "docs: update README for new feature"
 
git add app.js
git commit -m "debug: temporary logging (will remove)"

Staging specific changes lets you split one messy working session into multiple clean commits with accurate messages.

Remote Repositories and Collaboration

git remote -v
git remote add origin https://github.com/username/repo.git
git remote add upstream https://github.com/original/repo.git
 
git fetch origin
git fetch --all
 
git push -u origin main
 
git pull upstream main
git push origin main

For contributing to open-source projects, the standard workflow is: fork the repository on GitHub, clone your fork, add the original repository as upstream, make changes on a feature branch, push to your fork, and open a pull request from your fork's branch to the original repository's main branch.

Pull requests (GitHub) / merge requests (GitLab) are a code review workflow built on top of Git's branching model. They are not a Git concept. They are a GitHub/GitLab feature. A PR shows the diff between your branch and the target branch, allows reviewers to leave comments, runs CI checks, and provides a clear merge button when review is complete.

Commit Messages That Mean Something

feat: add JWT refresh token rotation
 
Previously, refresh tokens did not rotate on use. A compromised refresh
token could be used indefinitely until it expired. Now:
 
- Each /auth/refresh call invalidates the used token
- Issues a new refresh token with the same expiry window
- Stores tokens as SHA-256 hashes, not plaintext
 
Closes #247
Breaking change: clients must update their stored refresh token on each
successful refresh call.

The Conventional Commits specification structures commit messages as type(scope): description. Common types: feat (new feature), fix (bug fix), docs (documentation), refactor (code refactoring without behavior change), test (adding or modifying tests), chore (maintenance, dependency updates).

Good commit messages answer: what changed, why it changed, what the impact is. The subject line (first line) is the what. The body explains the why. References to issue numbers connect commits to the work item tracking system.

Bad commit messages: "fix", "updates", "wip", "asdf", "changes". These tell you nothing and require reading the diff to understand what happened.

Working with History

git log --oneline --graph --decorate --all
 
git log --since="2024-01-01" --until="2024-03-01"
git log --author="alice" -10
git log --grep="authentication"
git log -- src/auth/
 
git show HEAD
git show abc123f:src/auth/jwt.ts
 
git diff main..feature/auth
git diff HEAD~3 HEAD
 
git blame src/auth/jwt.ts
git blame -L 45,60 src/auth/jwt.ts

git log --all --graph --oneline --decorate is the command you want when you need to understand the shape of the repository — which branches exist, where they diverged, where they merged. The ASCII graph shows the topology visually.

git blame shows who last modified each line and in which commit. When debugging an issue, git blame on the relevant file combined with git show <commit> on the identified commit tells you the context around every change. Not for assigning fault. For understanding the decision-making history.

git bisect start
git bisect bad HEAD
git bisect good v1.2.0
git bisect good
git bisect bad
git bisect reset

git bisect performs a binary search through commits to find when a bug was introduced. Mark the current state as bad, mark a known good commit as good, and Git checks out commits at the midpoint for you to test. You mark each as good or bad. Git narrows the range until it identifies the commit that introduced the issue. For a bug that entered anywhere in a history of 1,000 commits, bisect finds it in 10 steps.

Rewriting History

Rewriting committed history is sometimes necessary. A commit message has a typo. Sensitive data was accidentally committed. Several small fixup commits should be squashed into the feature commit. History rewriting tools:

git commit --amend
git commit --amend --no-edit
 
git rebase -i HEAD~4
 
git rebase -i --root
 
git filter-repo --path secrets.json --invert-paths
 
git push --force-with-lease origin feature/my-branch

The golden rule of Git: never rewrite history that has been pushed to a shared branch that others are working from. Rewriting shared history breaks everyone else's local copy. Rebase, amend, and squash are safe on your own feature branches before they are merged. They are dangerous on main or any branch others have based work on.

git filter-repo (install separately) rewrites repository history to remove files. If you accidentally committed a secret (API key, password, certificate), you need to: revoke the secret immediately (the history is public, assume the secret is compromised), then use git filter-repo to remove it from history and force-push. This requires every contributor to re-clone the repository afterward.

Git Workflows

Teams adopt explicit branching workflows to coordinate parallel development.

GitHub Flow: simple. One main branch. All work happens on feature branches created from main. Feature branches are short-lived (days, not weeks). Merge via PR to main. Deploy main to production continuously. Works well for products that can deploy frequently.

Git Flow: more structured. Two permanent branches: main (production-stable) and develop (integration). Features branch from develop. Releases branch from develop to release/x.y, then merge to both main and develop. Hotfixes branch from main, merge to both main and develop. More process overhead, appropriate for products with scheduled release cycles (mobile apps, packaged software).

Trunk-Based Development: everyone commits to one branch (trunk/main) directly, or uses extremely short-lived feature branches (less than a day). Feature flags hide incomplete features in production. Continuous integration catches breakage immediately. Requires mature CI and strong test coverage. Used by Google, Facebook, and teams that deploy dozens of times per day.

The right workflow depends on your deployment model, team size, and risk tolerance. GitHub Flow is the right default for most teams. Adopt Git Flow if you have external release commitments and need to maintain multiple versions. Move toward trunk-based development as your CI matures.

.gitignore

# Node.js
node_modules/
npm-debug.log*
yarn-error.log
 
# Build output
dist/
build/
.next/
 
# Environment and secrets
.env
.env.local
.env.*.local
*.pem
*.key
 
# IDE
.vscode/
.idea/
*.swp
*.swo
 
# OS
.DS_Store
Thumbs.db
desktop.ini
 
# Python
__pycache__/
*.pyc
*.pyo
venv/
.venv/
*.egg-info/
 
# Logs
logs/
*.log
 
# Test coverage
coverage/
.nyc_output/

.gitignore prevents files from being tracked. The most critical entries: node_modules/ (never commit dependencies, install them from package.json), .env (never commit secrets), and build output (dist/, build/) that can be regenerated.

Use gitignore.io to generate language/framework-specific .gitignore files. The generated files cover edge cases you would not think of.

When a file is already tracked and you add it to .gitignore, Git continues tracking it. Stop tracking a file that should have been ignored:

git rm --cached .env
git rm -r --cached node_modules/
git commit -m "chore: untrack files that should be ignored"

--cached removes the file from the staging area and repository tracking without deleting it from your filesystem.

Git Hooks

Git hooks are scripts that run automatically at specific points in the Git workflow. Pre-commit hooks run before a commit is created. Pre-push hooks run before pushing. Commit-msg hooks validate commit messages.

# .git/hooks/pre-commit
#!/bin/sh
npm run lint -- --max-warnings=0
npm run test -- --passWithNoTests
 
# .git/hooks/commit-msg
#!/bin/sh
if ! grep -qE "^(feat|fix|docs|refactor|test|chore|perf|ci)(\(.+\))?: .{1,72}$" "$1"; then
    echo "Commit message does not match Conventional Commits format"
    echo "Expected: type(scope): description"
    exit 1
fi

husky is the tool that manages Git hooks in Node.js projects. It stores hooks in .husky/ in the repository rather than .git/hooks/ (which is not committed), making hooks reproducible across machines:

npm install --save-dev husky lint-staged
npx husky init
 
# .husky/pre-commit
npx lint-staged
 
# package.json
{
    "lint-staged": {
        "*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
        "*.{css,scss,md}": ["prettier --write"]
    }
}

lint-staged runs linters and formatters on only the staged files, not the entire codebase. Pre-commit with lint-staged takes seconds instead of minutes.

Cherry-Picking and Advanced History Operations

git cherry-pick abc123f
git cherry-pick abc123f def456a
git cherry-pick abc123f..ghi789b
 
git cherry-pick --no-commit abc123f
git cherry-pick --signoff abc123f
 
git revert abc123f
git revert abc123f --no-commit
git revert HEAD~3..HEAD

cherry-pick applies the changes from specific commits onto the current branch without merging the entire source branch. The canonical use case: a bug fix committed to a feature branch needs to go into the current release branch without bringing the unfinished feature along with it.

revert creates a new commit that undoes the changes of a previous commit. Unlike cherry-pick which applies changes forward, revert applies changes backward. It is the safe way to undo committed history on a shared branch because it does not rewrite history — it adds a new commit. git reset and git rebase rewrite history; git revert extends it.

git reset --soft HEAD~1
git reset --mixed HEAD~1
git reset --hard HEAD~1
git reset --hard origin/main

--soft moves HEAD back one commit, keeping all changes staged. --mixed (the default) moves HEAD back and unstages changes but keeps them in the working tree. --hard moves HEAD back and discards all changes completely. --hard is destructive. The changes are gone unless you have the reflog.

The Reflog: Your Safety Net

The reflog records every position HEAD has been at. Every commit, every checkout, every merge, every rebase. If you accidentally run git reset --hard to the wrong commit and lose work, the reflog finds it.

git reflog
git reflog show HEAD
git reflog show my-branch
 
git checkout HEAD@{5}
git reset --hard HEAD@{3}
git cherry-pick HEAD@{7}
 
git stash list
git stash show -p stash@{0}

The reflog keeps entries for 90 days by default. Within that window, almost nothing you do in Git is permanently irreversible. Accidentally deleted a branch? git reflog finds the commit hash of its tip. git checkout -b recovered-branch <hash> recreates it.

Force-pushed over commits on a shared branch? The other developers' local copies still have the old commits. They can cherry-pick them back. Reflog does not protect against deleting a repository entirely, but it does protect against virtually every local mistake.

Git Submodules and Subtrees

When your project depends on another Git repository, you have two built-in options: submodules and subtrees.

Submodules embed a reference to another repository inside your repository:

git submodule add https://github.com/company/shared-library lib/shared
git submodule update --init --recursive
 
git submodule foreach git pull origin main
 
git submodule update --remote lib/shared
git add lib/shared
git commit -m "chore: update shared-library to latest"
 
git clone --recurse-submodules https://github.com/company/myproject

The parent repository stores the submodule's URL and the exact commit SHA it should be at. git clone without --recurse-submodules gives you an empty submodule directory. git submodule update --init populates it. Submodules pin to a specific commit, which is both the strength (reproducible) and the friction (explicit update commits required).

Subtrees embed the full history of another repository merged into yours:

git subtree add --prefix=lib/shared https://github.com/company/shared-library main --squash
 
git subtree pull --prefix=lib/shared https://github.com/company/shared-library main --squash
 
git subtree push --prefix=lib/shared https://github.com/company/shared-library main

Subtrees are simpler for contributors — no extra commands needed after cloning. The tradeoff: the embedded repository's history (or a squashed version) becomes part of your repository. Updating pulls new commits on top. Most teams find subtrees easier to work with day-to-day; submodules are more precise for projects with strict dependency versioning requirements.

Tagging Releases

git tag v1.2.0
git tag -a v1.2.0 -m "Release version 1.2.0"
git tag -a v1.2.0 abc123f -m "Release version 1.2.0"
 
git tag -l
git tag -l "v1.*"
git show v1.2.0
 
git push origin v1.2.0
git push origin --tags
 
git tag -d v1.2.0-bad
git push origin --delete v1.2.0-bad
 
git checkout v1.2.0
git checkout -b hotfix/v1.2.1 v1.2.0

Lightweight tags (without -a) are just pointers to commits. Annotated tags (with -a) store the tagger name, email, date, and message alongside the tag, and can be signed with GPG. For release tags that other people will use to identify specific versions, annotated tags are the right choice.

Semantic versioning (semver) is the standard: MAJOR.MINOR.PATCH. Increment MAJOR for breaking changes, MINOR for new backward-compatible features, PATCH for backward-compatible bug fixes. Tag releases in CI after tests pass, not manually.

Git with CI/CD: GitHub Actions

name: Test and Release
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  release:
    types: [published]
 
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20, 22]
 
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
 
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage
 
      - name: Upload coverage
        if: matrix.node-version == 22
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
 
  release:
    needs: test
    runs-on: ubuntu-latest
    if: github.event_name == 'release'
    permissions:
      contents: write
      packages: write
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'
 
      - run: npm ci
      - run: npm run build
 
      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
 
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }}

fetch-depth: 0 fetches the full Git history instead of just the latest commit. Full history is needed for tools that compute semantic version numbers from commit history (like semantic-release or conventional-changelog), for accurate git blame output, and for commands like git describe that calculate version strings from tags.

The matrix strategy runs the same job across multiple Node.js versions in parallel. Both 20 and 22 run simultaneously. A failure in either marks the entire job as failed.

Signing Commits with GPG

gpg --list-secret-keys --keyid-format LONG
gpg --gen-key
 
gpg --list-secret-keys --keyid-format LONG
git config --global user.signingkey YOUR_GPG_KEY_ID
git config --global commit.gpgsign true
 
git commit -S -m "feat: add payment integration"
 
gpg --armor --export YOUR_GPG_KEY_ID

Signed commits let others verify that the commit actually came from you. GitHub displays "Verified" badges on GPG-signed commits. For projects where commit authenticity matters — open-source projects, regulated industries — requiring signed commits is a meaningful security control.

SSH signing is a newer alternative to GPG. Git 2.34+ supports signing with SSH keys, which most developers already have. Simpler to set up than GPG:

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

Common Git Mistakes and Fixes

Committed to the wrong branch:

git log --oneline -5
git checkout correct-branch
git cherry-pick abc123f
git checkout wrong-branch
git reset --hard HEAD~1

Pushed sensitive data to a public repo:

# First: revoke the secret immediately
 
git filter-repo --path secrets.json --invert-paths
git filter-repo --replace-text <(echo 'my-secret-value==>**REDACTED**')
 
git push --force --all
git push --force --tags
 
# Require all collaborators to re-clone

Accidentally merged the wrong branch:

git log --oneline
git revert -m 1 HEAD
git push origin main

-m 1 tells revert which parent to consider the mainline for a merge commit. A merge commit has two parents: the branch that was merged into (parent 1) and the branch that was merged from (parent 2). -m 1 reverts to the state of parent 1, effectively undoing the merge.

Lost commits after hard reset:

git reflog | head -20
git checkout -b recovery-branch HEAD@{4}
git cherry-pick <commit-hash>

Accidentally deleted a branch:

git reflog | grep "checkout: moving from deleted-branch"
git checkout -b deleted-branch <commit-hash>

The pattern is always the same: git reflog shows the history of HEAD movements. Find the commit you need. Recreate a branch pointing to it.

Git Performance on Large Repositories

Large repositories (millions of files, years of history) require specific optimizations:

git gc
git gc --aggressive
git prune
 
git config --global feature.manyFiles true
git config --global core.fscache true
git config --global core.preloadindex true
 
git sparse-checkout init --cone
git sparse-checkout set src/payments src/auth
 
git clone --filter=blob:none --partial https://github.com/company/monorepo
git clone --depth 1 https://github.com/company/large-repo

git sparse-checkout allows checking out only a subset of files. For a monorepo with 10,000 files but you only work on one service, sparse checkout gives you only that service's files in your working tree. The full history is still available; the working tree is filtered.

--filter=blob:none (partial clone) downloads commit and tree objects but not file blobs. Blobs are fetched on demand when you access the files. Initial clone is dramatically faster. Subsequent operations are slightly slower on first access to new files.

--depth 1 (shallow clone) downloads only the most recent commit with no history. Fastest possible clone. Useful for CI environments where history is not needed. Git operations that require history (git log, git blame, git bisect) do not work fully on shallow clones.

What Git Is Not

Git is not a backup system. git push to a remote repository is a form of backup, but it is not a substitute for proper backups. Git does not store binary assets efficiently — large image, video, or model files bloat repository size and slow clone times. Git Large File Storage (LFS) handles binary assets by storing them outside the repository with references to them in the tree.

Git is not a deployment tool. git pull on a production server to deploy is a pattern that works until it does not. It exposes your .git directory to the web server, requires git credentials on the production server, and provides no rollback mechanism. Use container images and a proper deployment pipeline.

Git is not a code review system. Pull requests are GitHub's invention built on top of Git's branching model. The code review workflow, comment threads, and approval mechanisms are platform features, not Git features. You can use Git without any of them. Most teams should not — code review catches bugs and spreads knowledge.

Git is the tool that makes large-scale collaborative software development tractable. A team of twenty developers working on the same codebase simultaneously, with confidence that their changes will not overwrite each other, with the ability to review every change before it lands, with a complete history they can audit and revert: that is what Git enables. The commands are learnable in a week. The mental model takes longer, but it is worth the investment.

Everything in Git is built on four object types stored in .git/objects/:

Blob: the content of a file at a specific point in time. No filename, no metadata. Just the raw content, identified by its SHA-1 hash.

Tree: a directory. Contains references to blobs (files) and other trees (subdirectories), along with their names and permissions.

Commit: a snapshot of the repository. Contains a reference to the root tree, the parent commit(s), the author, the committer, and the commit message.

Tag: an annotated reference to a commit with metadata.

git cat-file -t abc123f
git cat-file -p abc123f
 
git ls-tree HEAD
git ls-tree -r HEAD
 
find .git/objects -type f | head -20

git cat-file -p <hash> prints the contents of any Git object. A commit object looks like:

tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
author Traven <[email protected]> 1704067200 +0000
committer Traven <[email protected]> 1704067200 +0000
 
feat: add JWT refresh token rotation

Understanding the object model demystifies the commands. git diff A B compares the trees pointed to by commits A and B. git checkout branch updates the working tree to the tree pointed to by the branch's latest commit and updates HEAD. Branches are just files in .git/refs/heads/ containing a commit hash. Tags are files in .git/refs/tags/.

GitHub, GitLab, and Forgejo

Git is the tool. GitHub, GitLab, Gitea, and Forgejo are platforms built on top of Git that add collaboration features: pull requests, code review, issue tracking, CI/CD pipelines, wikis, access control, and project management.

GitHub dominates for open-source. Most public repositories, most open-source tools, and the largest developer community are on GitHub. GitHub was acquired by Microsoft in 2018. The acquisition has not meaningfully changed the platform's open-source value despite initial concerns.

GitLab is the self-hosting alternative. GitLab Community Edition is free and open-source. Teams that cannot use GitHub for compliance or security reasons (healthcare, government, financial services) typically run GitLab. GitLab also has a more complete built-in CI/CD system than GitHub Actions for complex pipelines.

Gitea and Forgejo (a Gitea fork) are minimal self-hosted Git platforms for teams that want the Git hosting workflow without the weight of a full GitLab installation.

Understanding Git the tool is independent of which platform you use. The branch model, commit model, and merge workflow work identically across all of them.

Git is the single most widely used developer tool on the planet. More than any language, more than any framework, more than any database. The ability to use it competently — not just the basic add/commit/push cycle, but branching strategy, history rewriting, conflict resolution, and the underlying object model — is the difference between a developer who can work on any team and one who struggles every time the Git situation gets complicated.

Git Internals: The Object Store

Everything in Git is built on four object types stored in .git/objects/:

Blob: the raw content of a file at a specific point in time. No filename. No metadata. Just bytes, identified by the SHA-1 hash of their contents. Two files with identical content share one blob.

Tree: a directory. Contains references to blobs and other trees with their names and file permissions. A commit's root tree represents the entire repository state at that commit.

Commit: a snapshot. Contains a reference to the root tree, one or more parent commit hashes, author and committer identity with timestamps, and the commit message.

Tag: an annotated reference. Points to a commit with additional metadata: tagger identity, date, and message.

git cat-file -t HEAD
git cat-file -p HEAD
 
git cat-file -p HEAD^{tree}
git cat-file -p HEAD:src/auth/jwt.ts
 
git ls-tree -r HEAD --name-only | head -20
 
find .git/objects -type f | wc -l
git count-objects -v

git cat-file -p HEAD prints the raw commit object — parent hashes, tree hash, author, committer, message. git cat-file -p HEAD^{tree} prints the root tree, showing every file and subdirectory with their blob/tree hashes and file modes. This is the data model Git actually operates on.

Branches and tags are nothing more than files containing a commit hash:

cat .git/refs/heads/main
cat .git/refs/tags/v1.2.0
cat .git/HEAD

HEAD is a symbolic ref pointing to the current branch, or a direct commit hash in detached HEAD state. git checkout main writes ref: refs/heads/main to .git/HEAD. git commit writes the new commit hash to .git/refs/heads/main. That is the entire branch tracking mechanism — a text file with a commit hash in it.

Understanding this model makes Git less mysterious. Merges create commit objects with two parents. Rebasing creates new commit objects with adjusted parents. Fast-forward merges just move the branch pointer to the new commit. Tags point to specific commit objects. Everything is content-addressed and immutable.

Git Configuration That Matters

git config --global user.name "Traven"
git config --global user.email "[email protected]"
git config --global core.editor "code --wait"
git config --global init.defaultBranch main
git config --global pull.rebase true
git config --global fetch.prune true
git config --global diff.colorMoved zebra
git config --global merge.conflictStyle zdiff3
 
git config --global alias.lg "log --oneline --graph --decorate --all"
git config --global alias.st "status -sb"
git config --global alias.undo "reset --soft HEAD~1"
git config --global alias.aliases "config --get-regexp '^alias\.'"
 
git config --global credential.helper osxkeychain   # macOS
git config --global credential.helper manager       # Windows
git config --global credential.helper store         # Linux (plain text, not recommended)

pull.rebase true makes git pull rebase instead of merge by default. On feature branches, this keeps history linear. fetch.prune true automatically removes remote tracking branches when the remote branch is deleted, keeping git branch -r output clean.

merge.conflictStyle zdiff3 is a Git 2.35+ option that shows the common ancestor in conflict markers, giving you three-way context instead of just the two conflicting versions. Far more useful for resolving non-trivial conflicts.

Aliases reduce friction for frequently used commands. git lg for the graph log, git st for compact status, git undo for soft reset are commands most developers type dozens of times per day.