Git is easy to start using and surprisingly easy to use badly. Many of us learned three commands (add, commit, push) and then spent years working around the history we created: commits named "fix", branches that live for months, and the occasional panic after a force push. A clear, simple workflow removes most of that pain. This guide covers how to write commits, how to branch, how to push and open pull requests, and how to get out of the common situations everyone ends up in.
1. Write commits that tell a story
A commit is a note to your future self and your teammates. Months from now, when something breaks, git log and git blame are how you will figure out why a line exists. A history full of "update", "changes" and "fix stuff" is almost useless.
Keep each commit focused
One commit should do one logical thing. Fixing a bug and reformatting an unrelated file are two commits. Small, focused commits are easier to review, easier to revert and easier to understand.
Git lets you stage part of a file, which is handy when you made two unrelated changes in the same place:
git add -p # review each change and choose what to stage
git status # check what is staged before you commit
Write good commit messages
A reliable format is a short summary line, a blank line, then an optional explanation of why the change was needed. The code already shows what changed; the message should explain the reason.
- Keep the summary around 50 characters.
- Use the imperative mood: "Add search filter", not "Added" or "Adds".
- Skip the full stop on the summary line.
- Use the body to describe the problem, the approach and anything surprising.
git commit -m "Fix duplicate orders on double-click" -m "The submit button stayed enabled while the request was in flight,
so a fast double-click created two orders. Disable it until the response arrives."
Consider Conventional Commits
Many teams add a type prefix so history is easy to scan and release notes can be generated automatically:
feat:a new featurefix:a bug fixdocs:documentation onlyrefactor:restructuring without changing behaviourtest:adding or fixing testschore:tooling, dependencies, housekeeping
feat(blog): add reading progress bar
fix(auth): redirect to login when the session expires
docs: explain how to run the project locally
2. Branch for every piece of work
Do not commit directly to main. Create a short-lived branch for each feature or fix, and merge it back when it is reviewed. Name branches so their purpose is obvious:
git switch -c feature/blog-comments
git switch -c fix/mobile-nav-overlap
Keep branches small and short-lived, ideally a few days. The longer a branch lives, the more it drifts from main and the more painful the merge becomes. If a feature is large, split it into smaller pieces that can each be merged on their own.
Stay up to date with main
Pull the latest changes regularly so you notice conflicts early, while they are small:
git switch main
git pull
git switch feature/blog-comments
git merge main # or: git rebase main
3. Merge or rebase?
Both combine work from two branches; they just leave different histories.
- Merge keeps the true history and adds a merge commit. It is safe and simple, and never rewrites existing commits.
- Rebase replays your commits on top of the latest
main, giving a straight, linear history. It rewrites your commits, so it is best used on your own unpushed or private branches.
The golden rule of rebasing: never rebase commits that other people already have.
A common team setup is: rebase or merge main into your branch while you work, then use GitHub's squash merge when the pull request lands, so each feature becomes one tidy commit on main.
4. Pushing safely
The first time you push a new branch, set its upstream so later pushes and pulls are one short command:
git push -u origin feature/blog-comments
Before you push, look at what you are about to share:
git status
git diff --staged # what is about to be committed
git log --oneline -5 # your recent commits
If you must force push
After rebasing or amending a commit that you already pushed, a normal push will be rejected. Use the safer form of force:
git push --force-with-lease
Unlike plain --force, it refuses to overwrite the remote if someone else pushed in the meantime, so you cannot silently destroy a teammate's work. Never force push to main.
5. Pull requests on GitHub
A pull request (PR) is where code gets reviewed, discussed and tested before it joins main. A good one makes the reviewer's job easy.
- Keep it small. Reviewers can give real feedback on 200 lines; they skim 2,000.
- Write a helpful title and description. Say what changed, why, and how to test it. Add screenshots for visual changes.
- Link the issue it addresses, for example "Closes #42", so it closes automatically when merged.
- Review your own diff first. You will often spot a leftover
console.logor an accidental file before anyone else does. - Respond to feedback kindly and promptly. A review is about the code, not the person.
For the team, turn on branch protection for main: require a pull request, at least one approval and passing status checks (tests, lint, build) before merging. It takes a minute to set up and prevents a lot of bad Fridays.
6. Keep the repository clean
Use a .gitignore
Never commit dependencies, build output or local settings. A typical web project ignores:
node_modules/
.next/
dist/
.env
.env.local
.DS_Store
Never commit secrets
API keys, passwords and tokens must stay out of the repository. Keep them in environment variables and commit only a .env.example with placeholder values. If you commit a secret by accident, deleting it in a later commit is not enough, because it remains in history. Treat it as leaked: revoke and replace the key immediately, then clean the history if needed.
7. Fixing mistakes: the common cases
Everyone makes mistakes with Git. Here is what to reach for.
I committed with the wrong message (and have not pushed)
git commit --amend -m "Better message"
I forgot to include a file in the last commit
git add forgotten-file.ts
git commit --amend --no-edit
I want to undo my last commit but keep the changes
git reset --soft HEAD~1 # keeps changes staged
git reset HEAD~1 # keeps changes, unstaged
I need to undo a commit that is already pushed
Do not rewrite shared history. Create a new commit that reverses it:
git revert <commit-hash>
I want to throw away my uncommitted changes in one file
git restore path/to/file.ts
This permanently discards your edits to that file, so be sure first.
I need to switch branches but I am halfway through something
git stash push -m "wip: comments form"
git switch other-branch
# ...later
git switch -
git stash pop
I committed to the wrong branch
git switch correct-branch
git cherry-pick <commit-hash> # copy the commit over
git switch wrong-branch
git reset --hard HEAD~1 # remove it from the wrong branch (careful!)
I think I lost commits
Take a breath. Git rarely loses anything. The reflog records every place HEAD has been, and you can usually find the commit and restore it:
git reflog
git switch -c rescue <hash-from-reflog>
Merge conflicts
A conflict simply means Git cannot decide between two edits to the same lines. Open the file, find the markers (<<<<<<<, =======, >>>>>>>), keep the right combination, remove the markers, then:
git add path/to/file.ts
git merge --continue # or: git rebase --continue
If it goes badly, git merge --abort or git rebase --abort takes you back to where you started.
8. A few more habits worth building
- Commit early and often on your branch. You can tidy the history before opening the PR.
- Pull before you push to avoid avoidable conflicts.
- Tag releases (
git tag v1.2.0) so you can find exactly what shipped. - Use
git log --oneline --graphto see the shape of your history at a glance. - Set up a commit template or hooks (for example with Husky) to run linting and tests before a commit or push.
- Check
git statusconstantly. It is free and prevents most accidents.
A simple daily workflow
git switch main && git pullgit switch -c feature/short-description- Make a small change, then
git add -pandgit commitwith a clear message. - Repeat; keep the branch current with
main. git push -u origin feature/short-descriptionand open a pull request.- Address review feedback, then squash merge when approved.
- Delete the branch, pull
main, and start the next task.
The takeaway
Good Git habits are not about memorising commands. They are about leaving a clear trail: small focused commits with honest messages, short-lived branches, reviewed pull requests, and a protected main. Do that consistently and your history becomes documentation, your reviews get faster, and the next time something breaks you will find the cause in minutes instead of hours.