Git Commands and Workflows: The Complete Developer Guide
Everything you need to master Git — from daily commands and branching strategies to rebasing, hooks, and the workflows that power professional engineering teams.
Table of Contents
Introduction — Why Git Mastery Matters
Git is the single most important tool in a professional developer's toolkit that is not a programming language or an editor. Every meaningful software project on earth uses version control, and Git has dominated that space so completely that the alternatives have largely faded from relevance. Subversion, Mercurial, Perforce — they still exist in certain corners of the industry, but Git won. It won because it is fast, because it makes branching nearly free, and because the distributed model means every developer has a complete copy of the project history on their local machine. Understanding Git deeply is not optional for a professional developer. It is table stakes.
And yet, most developers operate with a shallow understanding of Git. They know git add, git commit, git push, and git pull. When something goes wrong — a merge conflict, a detached HEAD, an accidental commit to the wrong branch — they panic and start searching Stack Overflow for incantations to paste into their terminal. I have watched senior engineers with a decade of experience stare at a merge conflict like it was written in hieroglyphics. I have seen entire teams adopt a policy of "delete the repo and re-clone" when Git gets into a confusing state. This is not acceptable if you want to work efficiently and confidently.
This guide is designed to take you from wherever you are right now to genuine competence with Git. We will start with the mental model — understanding what Git is actually doing under the hood when you run commands. Then we will cover every command you will use on a daily basis, with real examples and explanations of the flags that matter. We will go deep on branching, merging, rebasing, stashing, and the workflows that professional teams use. By the end, you will not just know what commands to type — you will understand why they work, what happens when they fail, and how to recover from any situation Git throws at you.
Git Fundamentals
Before you memorize a single command, you need a mental model of how Git stores and tracks your code. Git operates with four key areas, and every command you will ever run moves data between them. The working tree (also called the working directory) is the actual files on your disk — the code you see and edit in your IDE. The staging area (also called the index) is a holding zone where you prepare changes before committing them. The local repository is the .git directory that stores your entire project history as a series of snapshots. And the remote repository is the shared copy on a server like GitHub, GitLab, or Bitbucket. Understanding the flow of data between these four areas is the key to understanding every Git command.
A commit in Git is not a diff or a patch. It is a complete snapshot of your entire project at a specific point in time. Git uses content-addressable storage with SHA-1 hashes to identify every object — commits, trees (directories), and blobs (file contents). When you make a commit, Git takes a snapshot of the staging area, creates a tree object that represents the directory structure, stores the file contents as blobs, and creates a commit object that points to the tree and to its parent commit (or commits, in the case of a merge). Because Git stores snapshots rather than diffs, operations like checking out a branch or reverting a commit are extremely fast — Git does not need to replay a sequence of patches.
Branches in Git are just pointers. A branch is literally a 41-byte file containing a SHA-1 hash that points to a commit. When you create a new branch, Git creates a new pointer. When you make a commit on that branch, the pointer moves forward to the new commit. The special pointer HEAD tells Git which branch you are currently on. This is why branching in Git is nearly instantaneous and essentially free — there is no copying of files, no duplicating of directories, just a tiny pointer being created or moved.
Essential Daily Commands
These are the commands you will use dozens of times every single day. They cover the core workflow of creating repositories, tracking changes, recording snapshots, and inspecting your project history. Mastering these commands fluently — knowing not just the basic invocation but the important flags and options — is what separates productive developers from those who fight with their tools.
Initializing and Cloning
# Create a new Git repository in the current directory
git init
# Create a new repository with a specific default branch name
git init --initial-branch=main
# Clone an existing repository from a remote URL
git clone https://github.com/user/repo.git
# Clone into a specific directory
git clone https://github.com/user/repo.git my-project
# Shallow clone (only the latest commit) for faster downloads
git clone --depth 1 https://github.com/user/repo.git
Shallow clones are particularly useful in CI/CD pipelines where you do not need the full project history. They reduce clone time and disk usage dramatically for large repositories. However, be aware that shallow clones cannot perform operations that require full history, such as git log across all commits or git bisect.
Staging Changes
# Stage a specific file
git add index.html
# Stage multiple specific files
git add src/app.js src/utils.js
# Stage all changes in the current directory (recursively)
git add .
# Stage all tracked files that have been modified or deleted (not new files)
git add -u
# Stage everything (new, modified, deleted)
git add -A
# Interactively stage portions of a file (hunk-by-hunk)
git add -p
The git add -p (patch mode) command is one of the most underused and most valuable Git features. It lets you review each change in a file and decide whether to stage it or skip it. This means you can make multiple logical changes to a single file and then commit them separately, keeping your commit history clean and atomic. Every senior developer I respect uses patch mode regularly. It is the difference between commits that tell a clear story and commits that are a jumble of unrelated changes.
Committing
# Commit staged changes with an inline message
git commit -m "Add user authentication endpoint"
# Commit with a detailed multi-line message (opens your editor)
git commit
# Stage all tracked modified files and commit in one step
git commit -am "Fix null pointer in payment processing"
# Commit with a specific author
git commit --author="Jane Doe <jane@example.com>" -m "Update docs"
# Create an empty commit (useful for triggering CI pipelines)
git commit --allow-empty -m "Trigger deployment pipeline"
Checking Status and Inspecting Changes
# See the current state of your working tree and staging area
git status
# Short format (more compact output)
git status -s
# Show unstaged changes (working tree vs staging area)
git diff
# Show staged changes (staging area vs last commit)
git diff --staged
# Show changes for a specific file
git diff src/app.js
# Show changes between two commits
git diff abc1234 def5678
# Show changes between two branches
git diff main..feature-branch
Viewing History
# Show commit history
git log
# Compact one-line format
git log --oneline
# Show a graph of branches and merges
git log --oneline --graph --all
# Show the last 5 commits
git log -5
# Show commits that modified a specific file
git log -- src/app.js
# Show commits by a specific author
git log --author="Jane"
# Search commit messages for a keyword
git log --grep="authentication"
# Show what changed in each commit
git log -p
# Show stats (files changed, insertions, deletions)
git log --stat
The git log --oneline --graph --all combination is the single most useful diagnostic command in Git. It shows you the entire branch topology of your repository in a compact visual format. I have this aliased to git lg in my configuration, and I run it constantly to understand the state of the repository before performing any complex operations like rebasing or merging.
Branching and Merging
Branching is the feature that makes Git fundamentally different from older version control systems. In SVN, creating a branch meant copying the entire directory tree, which was slow and expensive. In Git, creating a branch is instantaneous because a branch is just a pointer to a commit. This cheapness completely changes how you work. You should be creating branches constantly — for every feature, every bug fix, every experiment. Branches are disposable, mergeable, and the primary mechanism through which teams collaborate without stepping on each other's work.
Branch Operations
# List all local branches
git branch
# List all branches including remote-tracking branches
git branch -a
# Create a new branch (but stay on current branch)
git branch feature-login
# Create and switch to a new branch
git checkout -b feature-login
# Modern alternative: create and switch (Git 2.23+)
git switch -c feature-login
# Switch to an existing branch
git switch main
# or the traditional way:
git checkout main
# Rename the current branch
git branch -m new-name
# Delete a branch that has been merged
git branch -d feature-login
# Force-delete a branch (even if unmerged)
git branch -D abandoned-experiment
# Delete a remote branch
git push origin --delete feature-login
Merging
Merging integrates changes from one branch into another. Git supports two primary merge strategies: fast-forward merges and three-way merges. A fast-forward merge happens when the target branch has not diverged from the source — Git simply moves the pointer forward. A three-way merge happens when both branches have new commits, and Git creates a new merge commit that has two parents. Understanding the difference is important because it affects your commit history's shape and readability.
# Merge a feature branch into main
git switch main
git merge feature-login
# Merge with a merge commit even if fast-forward is possible
git merge --no-ff feature-login
# Merge but squash all commits into a single staged change
git merge --squash feature-login
# Abort a merge in progress (if conflicts arise and you want to back out)
git merge --abort
Dealing with Merge Conflicts
Merge conflicts happen when two branches have modified the same lines in the same file, or when one branch deletes a file that the other branch modified. Git cannot automatically determine which version is correct, so it marks the conflicting sections in the file and asks you to resolve them manually. Conflicts are not errors — they are a normal part of collaborative development. The key is knowing how to read and resolve them efficiently.
# When a conflict occurs, Git marks the file like this:
<<<<<<< HEAD
const apiUrl = "https://api.production.com";
=======
const apiUrl = "https://api.staging.com";
>>>>>>> feature-branch
The section between <<<<<<< HEAD and ======= is your current branch's version. The section between ======= and >>>>>>> feature-branch is the incoming branch's version. To resolve the conflict, you edit the file to contain the correct final version, remove all conflict markers, stage the file with git add, and complete the merge with git commit. Many editors and IDEs provide visual merge tools that make this process much easier, showing both versions side by side with buttons to accept one, the other, or both.
Remote Operations
Git's distributed nature means every developer has a full copy of the repository, but teams need a shared central location to coordinate their work. Remote operations are how you synchronize your local repository with remote servers. Understanding the difference between fetch, pull, and push — and knowing when to use each — is critical for working effectively on a team without accidentally overwriting someone else's changes or creating a tangled merge history.
Managing Remotes
# List configured remotes
git remote -v
# Add a new remote
git remote add upstream https://github.com/original/repo.git
# Rename a remote
git remote rename origin primary
# Remove a remote
git remote remove upstream
# Show detailed information about a remote
git remote show origin
Fetching, Pulling, and Pushing
# Fetch all changes from the remote (does NOT modify your working tree)
git fetch origin
# Fetch and prune deleted remote branches
git fetch --prune
# Pull changes (fetch + merge into current branch)
git pull origin main
# Pull with rebase instead of merge (cleaner history)
git pull --rebase origin main
# Push your current branch to the remote
git push origin feature-login
# Push and set the upstream tracking branch
git push -u origin feature-login
# Push all local branches
git push --all origin
# Push tags to the remote
git push origin --tags
The distinction between git fetch and git pull is one of the most important concepts for team collaboration. Fetch downloads the latest commits, branches, and tags from the remote but does not touch your working tree or current branch at all. It updates your remote-tracking branches (like origin/main) so you can inspect what has changed before integrating it. Pull, on the other hand, is essentially git fetch followed by git merge (or git rebase if configured). I strongly recommend using git fetch followed by an explicit merge or rebase, rather than git pull, especially when you are new to Git. This gives you a chance to review incoming changes before integrating them, and it avoids surprise merge commits.
Tracking Branches
A tracking branch is a local branch that has a direct relationship with a remote branch. When you clone a repository, Git automatically sets up main (or master) to track origin/main. Tracking branches enable shorthand commands: git push and git pull without specifying the remote and branch name. They also let git status tell you how many commits you are ahead or behind the remote.
# Set up tracking for an existing branch
git branch --set-upstream-to=origin/feature-login feature-login
# Create a local branch that tracks a remote branch
git checkout --track origin/feature-login
# See which local branches are tracking which remotes
git branch -vv
git fetch --prune regularly to clean up stale remote-tracking branches. When teammates delete branches on the remote after merging pull requests, your local references to those branches linger forever unless you prune them. Many teams set fetch.prune = true in their Git configuration so pruning happens automatically on every fetch.
Rewriting History
History rewriting is one of Git's most powerful and most dangerous capabilities. Used correctly, it lets you maintain a clean, readable commit history that tells a clear story about how the codebase evolved. Used incorrectly, it can destroy other people's work, create impossible-to-resolve conflicts, and leave your team scrambling to recover. The fundamental rule is simple: never rewrite history that has already been pushed to a shared branch. On your own local feature branches, rewrite freely. On shared branches like main or develop, treat commits as immutable.
Amending Commits
# Fix the last commit message
git commit --amend -m "Corrected commit message"
# Add forgotten files to the last commit
git add forgotten-file.js
git commit --amend --no-edit
# Change the author of the last commit
git commit --amend --author="Jane Doe <jane@example.com>" --no-edit
Rebasing
Rebasing replays your commits on top of a different base commit. The most common use case is keeping a feature branch up to date with main. Instead of merging main into your feature branch (which creates a merge commit and clutters the history), you rebase your feature branch onto main. This rewrites your commits so they appear as if you had started your work from the latest main commit. The result is a perfectly linear history that is much easier to read and understand.
# Rebase your current branch onto main
git rebase main
# Continue rebase after resolving conflicts
git rebase --continue
# Skip a conflicting commit during rebase
git rebase --skip
# Abort a rebase and return to the pre-rebase state
git rebase --abort
Interactive Rebase
Interactive rebase is the Swiss Army knife of Git history editing. It lets you reorder commits, edit commit messages, squash multiple commits into one, split a commit into several, or drop commits entirely. It is the tool you use to clean up a messy feature branch before opening a pull request. You typically run it against the commit where your branch diverged from main, which lets you edit every commit on your branch.
# Interactive rebase of the last 4 commits
git rebase -i HEAD~4
# Interactive rebase from the point where branch diverged from main
git rebase -i main
This opens your editor with a list of commits and actions. The most commonly used actions are:
# The rebase todo file looks like this:
pick a1b2c3d Add user model
pick e4f5g6h Add user controller
pick i7j8k9l Fix typo in user model
pick m0n1o2p Add user validation
# Common edits:
# Squash the typo fix into the model commit:
pick a1b2c3d Add user model
fixup i7j8k9l Fix typo in user model
pick e4f5g6h Add user controller
pick m0n1o2p Add user validation
# Available commands:
# pick = use commit as-is
# reword = use commit but edit the message
# edit = use commit but pause for amending
# squash = meld into previous commit (combine messages)
# fixup = meld into previous commit (discard this message)
# drop = remove commit entirely
Reset
# Soft reset: move HEAD back but keep changes staged
git reset --soft HEAD~1
# Mixed reset (default): move HEAD back, unstage changes, keep in working tree
git reset HEAD~1
# Hard reset: move HEAD back and discard ALL changes (dangerous!)
git reset --hard HEAD~1
# Reset a specific file from staging area
git restore --staged src/app.js
git reset --hard permanently destroys uncommitted changes. There is no undo. Always run git stash or git status before a hard reset to ensure you are not throwing away work. If you have already committed the changes, the reflog can save you (see the Advanced Commands section), but uncommitted work lost to a hard reset is gone forever.
Git Stash
Stashing is Git's way of letting you save uncommitted work temporarily without creating a real commit. The most common scenario is when you are in the middle of working on a feature and suddenly need to switch branches to fix an urgent bug. You cannot switch branches with uncommitted changes if those changes would conflict with the target branch. Stashing takes your modified tracked files and staged changes, saves them on a stack, and reverts your working tree to the last commit. You can then switch branches, do your work, come back, and restore the stashed changes as if nothing happened.
Basic Stash Operations
# Stash your current changes
git stash
# Stash with a descriptive message (highly recommended)
git stash push -m "WIP: user authentication form validation"
# Stash including untracked files
git stash push -u -m "WIP: including new test files"
# Stash including untracked AND ignored files
git stash push -a -m "WIP: everything including build artifacts"
# List all stashes
git stash list
# stash@{0}: On feature-login: WIP: user authentication form validation
# stash@{1}: On main: WIP: hotfix exploration
# Apply the most recent stash (keeps it in the stash list)
git stash apply
# Apply a specific stash
git stash apply stash@{1}
# Apply the most recent stash and remove it from the list
git stash pop
# Remove a specific stash without applying
git stash drop stash@{1}
# Clear the entire stash stack
git stash clear
# Show what a stash contains without applying it
git stash show -p stash@{0}
Practical Stash Scenarios
Beyond the basic "save and restore" workflow, stashing has several practical applications that experienced developers rely on. You can create a branch from a stash with git stash branch new-branch-name, which is perfect when you realize the changes you stashed actually deserve their own feature branch. You can also use git stash push -p to interactively select which hunks to stash, leaving some changes in your working tree — useful when you want to stash only part of your work in progress.
# Create a new branch from a stash
git stash branch feature-from-stash stash@{0}
# Interactively choose which changes to stash
git stash push -p -m "Stash only the database changes"
# Apply a stash to a different branch than where it was created
git switch other-branch
git stash apply stash@{0}
-m. The default message "WIP on branch-name" tells you nothing when you come back a week later with five stashes in the list. A message like "WIP: user auth - form validation complete, API call not started" saves future you significant time.
Git Workflows
A Git workflow is a set of conventions that a team follows for how branches are created, named, merged, and deployed. Choosing the right workflow for your team is one of the most impactful decisions you can make for development velocity and code quality. There is no single "best" workflow — the right choice depends on your team size, release cadence, deployment infrastructure, and organizational culture. Here are the three most widely adopted workflows, along with their strengths and trade-offs.
Feature Branch Workflow
The simplest and most widely used workflow. Developers create a new branch for every feature or bug fix, do their work on that branch, open a pull request for code review, and merge into main when approved. There is only one long-lived branch (main), and it should always be deployable. This workflow is the default for most teams using GitHub or GitLab, and it scales well from small teams to medium-sized organizations. The key discipline is keeping feature branches short-lived — ideally merged within a few days. Long-lived feature branches that diverge significantly from main are a recipe for painful merge conflicts and integration surprises.
# Feature Branch Workflow
git switch main
git pull origin main
git switch -c feature/user-profile
# ... do work, commit regularly ...
git push -u origin feature/user-profile
# Open a pull request, get reviews, merge via the UI
git switch main
git pull origin main
git branch -d feature/user-profile
Git Flow
Git Flow, introduced by Vincent Driessen in 2010, is a more structured workflow designed for projects with scheduled releases. It uses two permanent branches: main (production code) and develop (integration branch). Feature branches are created from develop and merged back into develop. When develop reaches a releasable state, a release branch is created for final testing and bug fixing. Once the release is ready, it is merged into both main (with a tag) and back into develop. Hotfix branches are created from main for urgent production fixes and are merged into both main and develop. Git Flow provides clear separation between development, staging, and production code, but it adds complexity that many teams find unnecessary with modern continuous deployment practices.
# Git Flow Example
git switch develop
git switch -c feature/payment-system
# ... develop feature ...
git switch develop
git merge --no-ff feature/payment-system
# When ready to release:
git switch -c release/2.0.0 develop
# ... final testing, bug fixes ...
git switch main
git merge --no-ff release/2.0.0
git tag -a v2.0.0 -m "Release version 2.0.0"
git switch develop
git merge --no-ff release/2.0.0
Trunk-Based Development
Trunk-based development is the workflow favored by high-performing engineering organizations like Google, Facebook, and most companies practicing continuous delivery. In its purest form, developers commit directly to the main branch (the "trunk") multiple times per day. In a slightly modified version, developers use extremely short-lived feature branches (lasting hours, not days) that are merged back into main immediately after review. The key enabler is a robust CI/CD pipeline with extensive automated testing, feature flags for hiding incomplete work, and a culture of making small, incremental changes. Trunk-based development minimizes merge conflicts, keeps everyone working against the latest code, and enables truly continuous integration.
Workflow Comparison
| Aspect | Feature Branch | Git Flow | Trunk-Based |
|---|---|---|---|
| Long-lived branches | 1 (main) | 2 (main + develop) | 1 (main) |
| Branch lifespan | Days | Days to weeks | Hours |
| Release process | Deploy from main | Release branches + tags | Continuous from main |
| Merge conflict risk | Medium | Higher | Low |
| Complexity | Low | High | Low (process) / High (CI/CD) |
| Best for | Most teams | Scheduled releases, multiple versions | Mature CI/CD, experienced teams |
| Code review | Pull requests | Pull requests | Pair programming or rapid PRs |
| Feature flags needed | Optional | Rarely | Essential |
Advanced Commands
These commands are not part of the everyday workflow, but knowing they exist and understanding when to reach for them can save you hours of frustration and, in some cases, rescue what seems like a hopeless situation. Every one of these has saved me from disaster at least once in my career.
Cherry-Pick
Cherry-pick applies the changes from a specific commit onto your current branch. It is useful when you need a particular fix from another branch without merging the entire branch. The most common scenario is backporting a bug fix from a development branch to a release branch. Cherry-pick creates a new commit with the same changes but a different SHA hash, so the two commits are not technically the same — they just have identical diffs.
# Apply a specific commit to the current branch
git cherry-pick abc1234
# Cherry-pick without committing (stage the changes only)
git cherry-pick --no-commit abc1234
# Cherry-pick a range of commits
git cherry-pick abc1234..def5678
# Abort a cherry-pick in progress
git cherry-pick --abort
Bisect
Bisect is Git's built-in binary search tool for finding the commit that introduced a bug. You tell Git one "good" commit (where the bug did not exist) and one "bad" commit (where it does), and Git checks out commits in between using a binary search algorithm. At each step, you test the code and tell Git whether the current commit is good or bad. In log2(n) steps, Git narrows down the exact commit that introduced the regression. For a repository with 1000 commits between good and bad, bisect finds the culprit in about 10 steps. This is dramatically faster than manually checking commits.
# Start a bisect session
git bisect start
# Mark the current commit as bad (has the bug)
git bisect bad
# Mark a known good commit
git bisect good v1.0.0
# Git checks out a commit in the middle. Test it, then:
git bisect good # if this commit is fine
git bisect bad # if this commit has the bug
# Git narrows down and reports the first bad commit.
# End the bisect session
git bisect reset
# Automated bisect with a test script
git bisect start HEAD v1.0.0
git bisect run npm test
Reflog
The reflog (reference log) is your safety net. It records every change to HEAD — every commit, checkout, reset, rebase, merge, and stash operation. Even if you accidentally delete a branch, do a hard reset, or botch a rebase, the commits are still in the reflog for at least 30 days (by default). The reflog is how you recover from almost any Git disaster. I have seen developers on the verge of tears because they thought they had lost days of work, only to recover everything with a single reflog command.
# Show the reflog (recent HEAD movements)
git reflog
# Output looks like:
# abc1234 HEAD@{0}: commit: Add user validation
# def5678 HEAD@{1}: checkout: moving from main to feature-login
# ghi9012 HEAD@{2}: reset: moving to HEAD~3
# Recover a lost commit by resetting to a reflog entry
git reset --hard HEAD@{2}
# Create a branch from a lost commit
git branch recovered-work abc1234
# Show reflog for a specific branch
git reflog show feature-login
Clean and Blame
# Remove untracked files (dry run first to see what will be deleted)
git clean -n
# Actually remove untracked files
git clean -f
# Remove untracked files and directories
git clean -fd
# Remove untracked and ignored files (nuclear option)
git clean -fdx
# Show who last modified each line of a file
git blame src/app.js
# Blame a specific line range
git blame -L 10,20 src/app.js
# Blame ignoring whitespace changes
git blame -w src/app.js
git blame is often used to find out who introduced a particular line, but its real power is archaeological. When you encounter confusing or seemingly wrong code, blame tells you which commit introduced it. You can then read the commit message, look at the pull request, and understand the context behind the decision. This is far more productive than asking the author (who may have forgotten) or guessing at the intent. Many IDEs integrate blame annotations inline, which makes this workflow seamless.
Git Hooks
Git hooks are scripts that run automatically at specific points in the Git workflow. They live in the .git/hooks directory and can be written in any scripting language (Bash, Python, Node.js, etc.). Hooks are one of the most effective ways to enforce code quality standards, prevent bad commits, and automate repetitive tasks. Every professional team should be using at least a pre-commit hook for linting and a commit-msg hook for enforcing commit message conventions.
Client-Side Hooks
Client-side hooks run on the developer's machine. They cannot be enforced centrally (since .git/hooks is not tracked by Git), but tools like Husky, lefthook, and pre-commit make it easy to install and share hooks via the project's package.json or configuration files. The most commonly used client-side hooks are pre-commit, commit-msg, and pre-push.
pre-commit Hook
Runs before a commit is created. If the script exits with a non-zero status, the commit is aborted. This is the ideal place for running linters, formatters, and type checkers on staged files.
#!/bin/sh
# .git/hooks/pre-commit - Run linting on staged files
# Get list of staged .js and .ts files
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|tsx)$')
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
echo "Running ESLint on staged files..."
npx eslint $STAGED_FILES
if [ $? -ne 0 ]; then
echo "ESLint failed. Fix errors before committing."
exit 1
fi
echo "Running Prettier check..."
npx prettier --check $STAGED_FILES
if [ $? -ne 0 ]; then
echo "Prettier check failed. Run 'npx prettier --write' to fix formatting."
exit 1
fi
exit 0
commit-msg Hook
Runs after you write your commit message but before the commit is finalized. This is where you enforce commit message conventions like Conventional Commits, maximum line length, or required ticket references.
#!/bin/sh
# .git/hooks/commit-msg - Enforce Conventional Commits format
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# Conventional Commits pattern: type(scope): description
PATTERN="^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?: .{1,72}$"
if ! echo "$COMMIT_MSG" | head -1 | grep -qE "$PATTERN"; then
echo "ERROR: Commit message does not follow Conventional Commits format."
echo ""
echo "Expected: type(scope): description"
echo "Example: feat(auth): add OAuth2 login support"
echo "Example: fix: resolve null pointer in payment flow"
echo ""
echo "Valid types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
exit 1
fi
exit 0
pre-push Hook
Runs before a push to the remote. This is the last line of defense before code leaves your machine. It is a good place to run the full test suite, since it catches issues that file-level linting might miss (like broken integration between modules).
#!/bin/sh
# .git/hooks/pre-push - Run tests before pushing
echo "Running test suite before push..."
npm test
if [ $? -ne 0 ]; then
echo "Tests failed. Push aborted. Fix failing tests before pushing."
exit 1
fi
# Prevent accidental pushes to main/master
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
echo "WARNING: You are pushing directly to $BRANCH."
read -p "Are you sure? (y/N): " CONFIRM
if [ "$CONFIRM" != "y" ]; then
echo "Push to $BRANCH aborted."
exit 1
fi
fi
exit 0
npm install, ensuring everyone on the team has the same hooks without manual setup. Combined with lint-staged, you can run linters only on the files that are actually being committed, keeping the pre-commit hook fast even in large repositories.
Git Command Cheat Sheet
This reference table covers the commands you will use most frequently, organized by category. Bookmark this section for quick lookups when you need a command but cannot remember the exact syntax or the right flag.
| Command | Description |
|---|---|
git init | Initialize a new repository in the current directory |
git clone <url> | Clone a remote repository to your machine |
git status | Show working tree and staging area status |
git add <file> | Stage a file for the next commit |
git add -p | Interactively stage changes hunk by hunk |
git commit -m "msg" | Create a commit with an inline message |
git commit --amend | Modify the most recent commit |
git diff | Show unstaged changes in the working tree |
git diff --staged | Show staged changes ready to be committed |
git log --oneline --graph | Compact visual history with branch topology |
git branch | List local branches |
git branch -a | List all branches including remote-tracking |
git switch -c <name> | Create and switch to a new branch |
git switch <name> | Switch to an existing branch |
git merge <branch> | Merge a branch into the current branch |
git merge --no-ff <branch> | Merge with a merge commit (no fast-forward) |
git rebase <branch> | Rebase current branch onto another branch |
git rebase -i HEAD~n | Interactive rebase of the last n commits |
git fetch origin | Download remote changes without merging |
git pull origin <branch> | Fetch and merge remote changes |
git pull --rebase | Fetch and rebase instead of merge |
git push -u origin <branch> | Push branch and set upstream tracking |
git stash push -m "msg" | Stash changes with a descriptive message |
git stash pop | Apply and remove the most recent stash |
git stash list | List all stashed changes |
git cherry-pick <sha> | Apply a specific commit to the current branch |
git bisect start | Start a binary search for a bug-introducing commit |
git reflog | Show log of all HEAD movements (safety net) |
git reset --soft HEAD~1 | Undo last commit, keep changes staged |
git reset --hard HEAD~1 | Undo last commit and discard all changes |
git clean -fd | Remove untracked files and directories |
git blame <file> | Show who last modified each line |
git tag -a v1.0.0 -m "msg" | Create an annotated tag |
git remote -v | List configured remote repositories |
Conclusion
Git is a tool that rewards deep understanding. Developers who take the time to learn how Git works internally — the commit graph, the three-tree architecture, how branches are just pointers, how the reflog tracks every state change — operate with a confidence and speed that developers who only know the surface commands cannot match. When something goes wrong (and it will), they do not panic. They reason about the problem, use the right diagnostic commands, and fix it systematically. That confidence is what this guide is designed to build.
The most important takeaways from this guide are: first, understand the mental model of working tree, staging area, and repository before memorizing commands. Second, commit early and commit often on feature branches — you can always clean up with interactive rebase before merging. Third, never rewrite history on shared branches. Fourth, choose a workflow that fits your team's size and release cadence, and follow it consistently. Fifth, invest in Git hooks and automation to catch problems before they reach the shared repository. And sixth, remember that the reflog is your ultimate safety net — it is nearly impossible to permanently lose committed work in Git.
Git has a steep learning curve, but it is a curve with a plateau. Once you internalize the concepts and commands in this guide, you will find that Git becomes second nature. You will branch, merge, rebase, and stash without thinking. You will resolve conflicts calmly. You will use bisect to find bugs in minutes instead of hours. And you will wonder how you ever worked without truly understanding the most important tool in your development workflow. Start by practicing the daily commands until they are muscle memory, then gradually incorporate the advanced techniques as you encounter situations that call for them. The investment will pay dividends for the rest of your career.
Compare code changes side-by-side with our free Text Diff tool.
Open Text Diff Tool