Git Fundamentals
Git is a distributed version control system. Unlike centralized systems (SVN, Perforce), every developer has a complete copy of the repository history on their local machine. This means you can commit, branch, merge, and view history without a network connection. Understanding this distributed nature is key to understanding why Git works the way it does.
The Three Areas
Git manages your code across three areas. Understanding these is the single most important concept for Git proficiency:
- Working Directory -- the actual files on your filesystem. This is what you see and edit.
- Staging Area (Index) -- a buffer between your working directory and the repository. You choose which changes to include in the next commit by staging them.
- Repository (.git) -- the committed history. Once changes are committed, they are safely stored and can be recovered.
# Initialize a new repository git init # Check the status of all three areas git status # Move changes from Working Directory → Staging Area git add file.js # stage a specific file git add src/ # stage an entire directory git add -p # interactively stage hunks # Move changes from Staging Area → Repository git commit -m "Add user authentication" # View commit history git log --oneline --graph # View changes in working directory (unstaged) git diff # View changes in staging area (staged, ready to commit) git diff --staged
Configuring Git
Before your first commit, configure your identity. These settings are stored in ~/.gitconfig (global) or .git/config (per-repo).
# Required: your name and email (used in commit metadata) git config --global user.name "Your Name" git config --global user.email "you@example.com" # Recommended: default branch name git config --global init.defaultBranch main # Recommended: auto-setup rebase on pull git config --global pull.rebase true # Recommended: colorized diff output git config --global core.pager "delta" # Set default editor git config --global core.editor "code --wait" # Useful aliases git config --global alias.co checkout git config --global alias.br branch git config --global alias.st status git config --global alias.lg "log --oneline --graph --all"
The .gitignore File
A .gitignore file tells Git which files and directories to ignore. This is essential for keeping build artifacts, dependencies, secrets, and OS-specific files out of your repository.
# Dependencies node_modules/ vendor/ .venv/ # Build output dist/ build/ .next/ # Environment & secrets .env .env.local *.pem # OS files .DS_Store Thumbs.db # IDE .idea/ .vscode/ *.swp # Logs *.log npm-debug.log*
Branching Strategies
How your team manages branches determines your development velocity, release stability, and merge conflict frequency. There is no universally best strategy. The right choice depends on your team size, release cadence, and infrastructure.
1. Git Flow
Git Flow uses long-lived main and develop branches with short-lived feature, release, and hotfix branches. It was designed for projects with scheduled releases and is well-suited for mobile apps, enterprise software, and anything with a formal release process.
# Start a feature git checkout develop git checkout -b feature/user-auth # Work on the feature, commit regularly git add . && git commit -m "Add login form component" git add . && git commit -m "Add JWT token validation" # Finish feature: merge back to develop git checkout develop git merge --no-ff feature/user-auth git branch -d feature/user-auth # Create a release git checkout -b release/1.2.0 develop # ... fix bugs, bump version ... git checkout main && git merge --no-ff release/1.2.0 git tag -a v1.2.0 -m "Release 1.2.0" git checkout develop && git merge --no-ff release/1.2.0 # Emergency hotfix git checkout -b hotfix/critical-fix main # ... fix the issue ... git checkout main && git merge --no-ff hotfix/critical-fix git checkout develop && git merge --no-ff hotfix/critical-fix
2. GitHub Flow
GitHub Flow is simpler: one main branch that is always deployable, plus short-lived feature branches. Every change goes through a pull request and code review. After merging, you deploy. This works well for web applications with continuous deployment, small teams, and projects that ship multiple times per day.
# The entire GitHub Flow workflow: # 1. Create a branch from main git checkout main git pull origin main git checkout -b feature/add-search # 2. Make commits git add . && git commit -m "Add search component" # 3. Push and open a Pull Request git push -u origin feature/add-search gh pr create --title "Add search functionality" # 4. Review, discuss, iterate # 5. Merge via GitHub UI (squash merge recommended) # 6. Deploy automatically from main
3. Trunk-Based Development
Trunk-Based Development (TBD) has developers commit directly to main (or use very short-lived branches that last at most one to two days). It requires robust CI/CD, feature flags for incomplete work, and high test coverage. Used by Google, Meta, and most large-scale tech companies. It eliminates merge conflicts and long-lived branch divergence.
# Trunk-Based: short-lived branches (max 1-2 days) # Pull latest main git checkout main && git pull # Create a short-lived branch git checkout -b short/add-button # Make a small, focused change git add . && git commit -m "Add submit button to form" # Push and merge same day git push -u origin short/add-button # PR → Review → Merge → Delete branch # Feature flags for incomplete features: # if (featureFlags.newSearch) { showNewSearch() }
Strategy Comparison
| Criteria | Git Flow | GitHub Flow | Trunk-Based |
|---|---|---|---|
| Complexity | High | Low | Low (process), High (infra) |
| Release cadence | Scheduled | Continuous | Continuous |
| Merge conflicts | Frequent | Moderate | Rare |
| Team size | Large teams | Small-medium | Any size |
| Requires feature flags | No | Optional | Yes |
| Best for | Mobile, enterprise | Web apps, startups | High-velocity teams |
Merge vs Rebase
This is the most debated topic in Git. Both merge and rebase integrate changes from one branch into another, but they do it differently, and the choice has real implications for your team's workflow.
Merge: Preserves History
git merge creates a new "merge commit" that ties two branch histories together. The original commit history of both branches is preserved exactly as it happened. This is the safest option because it never rewrites history.
# Merge feature branch into main git checkout main git merge feature/add-search # --no-ff forces a merge commit even if fast-forward is possible git merge --no-ff feature/add-search
Rebase: Linear History
git rebase replays your branch's commits on top of another branch, creating new commits with the same changes but different hashes. The result is a clean, linear history. The trade-off: it rewrites history, which can cause problems if the branch has already been shared.
# Rebase feature branch onto latest main git checkout feature/add-search git rebase main # Interactive rebase to clean up commits before merging git rebase -i main # In the editor, you can: # pick - keep the commit # squash - combine with previous commit # reword - change the commit message # drop - remove the commit entirely # edit - pause to amend the commit
Recommended Approach
The most common professional workflow combines both: rebase locally, merge publicly. Before opening a PR, rebase your feature branch onto the latest main to get a clean, up-to-date set of commits. Then merge (or squash-merge) the PR via the GitHub/GitLab UI.
# Before opening a PR: rebase onto latest main git checkout feature/my-feature git fetch origin git rebase origin/main # If conflicts, resolve them, then: git add . git rebase --continue # Force push (safe because only you work on this branch) git push --force-with-lease
Conflict Resolution
Merge conflicts happen when two branches modify the same lines of the same file. Git cannot automatically determine which change to keep, so it marks the conflict and asks you to resolve it manually. The key is to not panic. Conflicts are normal and resolvable.
// This is what a conflict looks like in your file: <<<<<<< HEAD const theme = 'dark'; ======= const theme = 'light'; >>>>>>> feature/light-mode // HEAD = your current branch's version // feature/light-mode = the incoming branch's version // You choose: keep one, keep both, or write something new
Resolution Steps
# 1. See which files have conflicts git status # 2. Open the conflicted file, resolve manually # Remove the <<<<<<, =======, >>>>>>> markers # Keep the correct code # 3. Mark as resolved by staging git add conflicted-file.js # 4. Continue the merge or rebase git merge --continue # if merging git rebase --continue # if rebasing # If it gets messy, abort and start over git merge --abort git rebase --abort # Use a visual merge tool git mergetool
Git Hooks
Git hooks are scripts that run automatically at specific points in the Git workflow: before a commit, before a push, after a merge, and more. They are the enforcement mechanism for code quality standards.
# Hooks live in .git/hooks/ (local, not committed) # Use Husky to manage hooks via package.json (committed) # Install Husky npx husky init # Create a pre-commit hook that runs linting echo "npx lint-staged" > .husky/pre-commit # Create a commit-msg hook for conventional commits echo "npx commitlint --edit \$1" > .husky/commit-msg
Common Hook Types
- pre-commit -- runs before a commit is created. Use it for linting, formatting, and type checking. This is the most commonly used hook.
- commit-msg -- validates the commit message format. Enforce conventional commits (
feat:,fix:,docs:) automatically. - pre-push -- runs before pushing. Use it to run tests and prevent pushing broken code.
- post-merge -- runs after a merge. Use it to auto-install dependencies if
package.jsonchanged.
#!/bin/sh # Run lint-staged (only checks staged files) npx lint-staged # Run type checking npx tsc --noEmit
{
"lint-staged": {
"*.{js,ts,jsx,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.css": [
"stylelint --fix",
"prettier --write"
],
"*.md": [
"prettier --write"
]
}
}
Advanced Git Commands
These are the commands that separate Git beginners from Git power users. Each one solves a specific problem that you will inevitably encounter.
git stash -- Save Work Without Committing
# Stash current changes (both staged and unstaged) git stash # Stash with a descriptive name git stash push -m "WIP: auth refactor" # Include untracked files git stash -u # List all stashes git stash list # Apply most recent stash (keeps it in stash list) git stash apply # Apply and remove from stash list git stash pop # Apply a specific stash git stash apply stash@{2} # Create a branch from a stash git stash branch new-branch-from-stash
git cherry-pick -- Copy Specific Commits
# Apply a specific commit to current branch git cherry-pick abc123 # Cherry-pick a range of commits git cherry-pick abc123..def456 # Cherry-pick without committing (stage only) git cherry-pick --no-commit abc123 # Use case: backport a fix to a release branch git checkout release/1.0 git cherry-pick abc123 # the bugfix commit from main
git bisect -- Find the Bug-Introducing Commit
# Start bisect git bisect start # Mark current commit as bad (has the bug) git bisect bad # Mark a known good commit (before the bug existed) git bisect good v1.0.0 # Git checks out a midpoint commit. Test it, then: git bisect good # if this commit is fine git bisect bad # if this commit has the bug # Repeat until Git identifies the exact bad commit # Output: "abc123 is the first bad commit" # Automated bisect with a test script git bisect run npm test # Reset when done git bisect reset
git reflog -- Your Safety Net
git reflog is the most important recovery tool in Git. It records every time HEAD changes, even across resets, rebases, and checkouts. If you accidentally lose commits, reflog has them.
# View reflog (recent HEAD movements) git reflog abc1234 HEAD@{0}: commit: Add new feature def5678 HEAD@{1}: rebase: fast-forward ghi9012 HEAD@{2}: checkout: moving from main to feature jkl3456 HEAD@{3}: reset: moving to HEAD~3 # Recover a "lost" commit after a bad rebase/reset git checkout -b recovery-branch HEAD@{3} # Or reset current branch to a previous state git reset --hard HEAD@{3}
git worktree -- Multiple Working Directories
# Create a worktree for a different branch (no stash needed!) git worktree add ../project-hotfix hotfix/urgent-fix # Work on the hotfix in ../project-hotfix # while your main work stays untouched in the original directory # List worktrees git worktree list # Remove when done git worktree remove ../project-hotfix
Common Mistakes and How to Fix Them
main when you meant to commit on a feature branch.# Create the branch you wanted (it will have your commits) git branch feature/my-feature # Reset main back to before your commits git reset --hard origin/main # Switch to your feature branch git checkout feature/my-feature
.env file or API key.# If not pushed yet: amend the commit git rm --cached .env echo ".env" >> .gitignore git add .gitignore git commit --amend # If already pushed: rewrite history (WARNING: rewrites all history) git filter-branch --force --index-filter \ "git rm --cached --ignore-unmatch .env" \ --prune-empty --tag-name-filter cat -- --all # IMPORTANT: Rotate the exposed secrets immediately!
# Undo commit, keep changes staged git reset --soft HEAD~1 # Undo commit, keep changes unstaged git reset HEAD~1 # Undo commit AND discard changes (DANGEROUS) git reset --hard HEAD~1 # If already pushed: create a revert commit (safe) git revert HEAD
# If you made commits in detached HEAD and want to keep them: git checkout -b save-my-work # If you just want to go back to a branch: git checkout main
# Reflog to the rescue git reflog # Find the commit hash before the reset git reset --hard HEAD@{1} # or the specific hash
Collaboration Best Practices
Write Good Commit Messages
Commit messages are not just for you right now. They are for your teammates trying to understand a change six months from now, and for your future self running git blame on a confusing line.
# Format: type(scope): description feat(auth): add OAuth2 login with Google provider fix(api): handle null response from payment gateway docs(readme): update installation instructions for v2 refactor(database): replace raw SQL with query builder test(cart): add integration tests for checkout flow chore(deps): update React from 19.0 to 19.1 perf(images): add lazy loading to product gallery # Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
Pull Request Etiquette
- Keep PRs small. 200-400 lines of code change is the sweet spot. Larger PRs get worse reviews and sit longer.
- Write a description. Explain what the change does, why it is needed, and how to test it. Link to the relevant issue.
- Self-review first. Read the diff yourself before requesting reviews. You will catch obvious issues.
- Respond to feedback promptly. A PR that sits for days accumulates merge conflicts and blocks downstream work.
- Use draft PRs for work-in-progress that you want early feedback on without triggering a full review.
Protecting Main
# Branch protection rules (configure in GitHub/GitLab settings): # - Require pull request reviews (1-2 approvals) # - Require status checks to pass (CI/CD must be green) # - Require branches to be up to date before merging # - Require signed commits (optional, for high-security projects) # - Restrict force push to main # Using GitHub CLI to set up branch protection: gh api repos/{owner}/{repo}/branches/main/protection \ --method PUT \ -f "required_pull_request_reviews[required_approving_review_count]=1" \ -F "enforce_admins=true"
Keeping a Clean History
# Squash merge: combine all feature commits into one clean commit git merge --squash feature/my-feature git commit -m "feat(auth): add OAuth2 login with Google" # Interactive rebase before PR to clean up messy commits git rebase -i HEAD~5 # squash "wip" and "fix typo" commits into meaningful ones # Amend the last commit message git commit --amend -m "Better commit message" # Add forgotten file to last commit git add forgotten-file.js git commit --amend --no-edit
Tools and Resources
Git CLI Enhancements
- delta -- a beautiful diff viewer with syntax highlighting and side-by-side mode. Set it as your default pager.
- lazygit -- a terminal UI for Git that makes staging, branching, rebasing, and conflict resolution visual and fast.
- gh (GitHub CLI) -- create PRs, manage issues, and trigger actions from the command line.
- git-absorb -- automatically creates fixup commits that amend the right parent commit during interactive rebase.
# Install delta (beautiful diffs) brew install git-delta git config --global core.pager delta # Install lazygit (terminal UI) brew install lazygit # Install GitHub CLI brew install gh gh auth login
Quick Reference: Essential Commands
| Task | Command |
|---|---|
| Undo last commit (keep changes) | git reset --soft HEAD~1 |
| Discard all local changes | git checkout -- . |
| See who changed a line | git blame file.js |
| Find when a bug was introduced | git bisect start |
| Save work without committing | git stash |
| Copy a specific commit | git cherry-pick <hash> |
| Recover lost commits | git reflog |
| Clean up local branches | git branch --merged | xargs git branch -d |
| Search commit messages | git log --grep="keyword" |
| Search code changes | git log -S "function_name" |
| Show changes in a commit | git show <hash> |
| Compare two branches | git diff main..feature |
Final Thoughts
Git is a tool that rewards depth of knowledge. You can use it productively with just add, commit, push, and pull, but understanding the full toolkit transforms how you work. You stop fearing experiments because you know you can always recover. You stop dreading merge conflicts because you have a systematic process. And you stop wasting time because your tools work with you, not against you.
The commands and workflows in this guide represent real-world practices used by professional engineering teams. Start with the basics, adopt a branching strategy that fits your team, and gradually integrate the advanced techniques as you encounter the problems they solve. Git mastery is a career-long investment that pays dividends every day.