Working alone does not mean skipping Git discipline. A clean Git history is a log of your thinking, a safety net for experiments, and a foundation for eventual collaboration. These practices will make you a better developer even when no one else is reviewing your commits.
Even as a solo developer, committing everything to main is a bad habit. When you need to interrupt one feature to fix a bug, a branch-based workflow lets you switch contexts cleanly.
# Create and switch to a feature branch
git checkout -b feature/add-user-auth
# Work, commit, test...
# Merge back to main when done
git checkout main
git merge --no-ff feature/add-user-auth
git branch -d feature/add-user-auth
The --no-ff flag forces a merge commit even when a fast-forward is possible. This preserves the history of when the feature branch existed, making the log easier to read later.
A good commit message answers "why", not just "what". The diff already shows what changed. The commit message should explain your reasoning.
# Bad commit messages
git commit -m "fix"
git commit -m "changes"
git commit -m "wip"
# Good commit messages (imperative mood, under 72 chars for subject)
git commit -m "Fix null pointer error when user has no profile photo"
git commit -m "Add rate limiting to the /login endpoint"
git commit -m "Refactor auth middleware to support multiple JWT issuers"
For complex changes, add a body after a blank line:
git commit -m "Switch from HS256 to RS256 for JWT signing
HS256 requires sharing the secret with third-party services that
need to verify tokens. RS256 lets them use the public key instead,
keeping the private key secure on the auth server only."
Small, focused commits are easier to review, easier to revert, and easier to understand. Each commit should represent a single logical change — not two hours of work lumped together.
If you have accumulated a mess of small WIP commits, use interactive rebase before merging to main to clean them up:
git rebase -i HEAD~5 # Squash/edit the last 5 commits
When you are mid-feature and need to urgently fix a bug on main, git stash saves your uncommitted changes without creating a commit.
# Save current work
git stash push -m "half-done user settings page"
# Switch to main and fix the bug
git checkout main
git checkout -b hotfix/login-redirect
# fix, commit, merge...
# Return to your feature branch
git checkout feature/user-settings
git stash pop # Restore your saved changes
When your feature branch falls behind main, you have two options: merge main into your branch, or rebase your branch onto main. Rebasing gives a cleaner linear history.
# Instead of: git merge main
git fetch origin
git rebase origin/main
# If conflicts arise, resolve them, then:
git rebase --continue
Commit a .gitignore at the start of every project. Never commit build artifacts, dependencies, secrets, or editor configs.
# Common .gitignore entries
node_modules/
.env
.env.local
dist/
build/
*.pyc
__pycache__/
.DS_Store
*.log
coverage/
If you accidentally committed a file that should be ignored:
# Remove it from tracking without deleting locally
git rm --cached .env
git commit -m "Remove accidentally committed .env file"
Tags create permanent named snapshots of your repository at a specific commit. Use them for every release so you can always check out exactly what was deployed to production.
# Create an annotated tag
git tag -a v1.2.0 -m "Release version 1.2.0: adds OAuth login"
# Push tags to remote (they are not pushed by default)
git push origin --tags
# Check out a specific release tag
git checkout v1.1.0
Git has excellent recovery tools. The most powerful is git reflog, which records every position HEAD has been at in the last 90 days — including commits you thought you lost.
# See the full history of where HEAD has been
git reflog
# Recover a "deleted" commit
git checkout -b recovery-branch abc1234
# Undo the last commit but keep the changes staged
git reset --soft HEAD~1
# Undo the last commit and unstage changes
git reset HEAD~1
Good Git hygiene takes five minutes to learn and saves hours of confusion over the life of a project. Use branches for every feature, write commit messages that explain why, use stash for context switching, rebase to keep history clean, and always have a .gitignore. Your future self reading the log will thank you.