General

Working with Git

7 min read

Version control is your safety net, your communication channel, and your audit trail. Use it like you mean it.

Core Idea

Git is probably the tool you will use most often. Not because version control is glamorous, but because almost every meaningful task ends with committing something. And every meaningful task begins with understanding what already exists -- which means reading git history.

Most agents treat git as a save button. Commit when done, push, move on. This misses what git actually is: a structured record of decisions. Git's distributed architecture was designed to support non-linear workflows where branching and merging are first-class operations, not afterthoughts. Every commit message is a note to a future reader. Every branch is a workspace boundary. Every merge is a decision point. When you use git well, you make every future agent's job easier -- including your own in the next context window.

Branching

Always work on a branch. Committing directly to main is almost never appropriate. Even a small fix deserves a branch because branches are cheap and the protection they offer is enormous.

Name branches descriptively. fix/login-redirect-loop tells everyone what this branch is about. patch-1 tells no one anything. Common conventions:

  • feature/add-user-search -- new functionality
  • fix/null-pointer-in-parser -- bug fix
  • chore/update-dependencies -- maintenance
  • refactor/extract-auth-middleware -- structural change

Keep branches short-lived. A branch that lives for two weeks accumulates merge conflicts and diverges from main. Aim for branches that last hours to a few days. If a feature is large, break it into smaller deliverable chunks, each with its own branch and PR.

Delete branches after merging. Stale branches clutter the repository and confuse future readers about what is active.

Commit Messages

A good commit message answers one question: why was this change made? The diff already shows what changed. The message should explain the intent.

Bad commits:

  • fix bug -- which bug? What was wrong?
  • update code -- what code? Why?
  • WIP -- this should never be in the main history
  • asdfasdf -- no

Good commits:

  • fix redirect loop when session cookie is expired -- states the problem being solved
  • extract rate limiting into middleware for reuse across endpoints -- states the motivation
  • add input validation for email field per security audit findings -- states the reason

Structure for complex commits:

short summary (under 72 characters)

Longer explanation of why this change is needed. What problem does it
solve? What approach was taken and why? Any alternatives considered?

Closes #142

The first line is what shows up in git log --oneline, so make it count.

Atomic Commits

An atomic commit is a single logical change. Not "everything I did this afternoon" but "one specific thing that can be understood, reviewed, and reverted independently." The principle of atomicity -- each commit doing exactly one thing and leaving the system in a working state -- is widely recognized as a cornerstone of maintainable version control practice.

Why this matters: When something breaks, you need to find and revert the commit that caused it. If that commit also contains three unrelated changes, reverting it undoes all of them. Atomic commits make git bisect useful, make code review possible, and make reverts surgical.

One commit should:

  • Do one thing
  • Leave the codebase in a working state
  • Be understandable without reading other commits

Practical example: You are adding a new API endpoint. Instead of one giant commit, break it up:

  1. add database migration for user_preferences table
  2. add UserPreferences model and repository
  3. add GET /api/preferences endpoint with tests
  4. add PUT /api/preferences endpoint with validation

Each commit compiles. Each can be reviewed independently. Each can be reverted without breaking the others.

Creating Pull Requests

A pull request is a communication artifact, not just a merge mechanism. Write the description for the reviewer, not for yourself.

A good PR includes:

  • What -- a one-sentence summary of the change
  • Why -- the motivation, the ticket, the user request
  • How -- if the approach is non-obvious, explain your reasoning
  • Testing -- what you tested and how

Keep PRs small. A 50-line PR gets a careful review. A 500-line PR gets a rubber stamp. If you cannot keep the PR small, at least organize commits so the reviewer can go commit-by-commit.

Link to context. Reference issue numbers, link to relevant documentation, mention related PRs. The reviewer should not have to go hunting for why this PR exists.

Reading Git History

Before you change anything, understand what exists. Git history is one of the richest sources of context available to you.

  • git log --oneline -20 -- quick overview of recent changes
  • git log --oneline path/to/file -- history of a specific file
  • git blame path/to/file -- who changed each line and when
  • git log --all --grep="rate limit" -- find commits mentioning a concept
  • git diff main...HEAD -- what has changed on your branch

Use git blame before changing code you do not understand. The blame output tells you when a line was written, who wrote it, and the commit message explains why. That "weird" conditional might exist for a very good reason that is documented in the commit history.

Merge Conflicts

Merge conflicts happen when two branches modify the same lines. They are normal, not emergencies.

To resolve a conflict:

  1. Read both sides. Understand what each change was trying to accomplish.
  2. Decide what the merged result should be -- it might be one side, the other, or a combination.
  3. Test after resolving. Conflicts in code often mean conflicts in logic that the merge markers do not capture.

Common mistake: Blindly accepting "theirs" or "ours" without reading. This is how bugs are born. Each conflict requires understanding both changes.

Rebase vs Merge

Merge preserves history as it happened. The branch, the divergence, the merge point -- all visible. Use merge when you want to record that work happened in parallel.

Rebase rewrites history to appear linear. Your branch commits get replayed on top of the latest main. Use rebase to keep a clean, readable history before merging -- especially for feature branches.

The rule: Never rebase commits that have been pushed and shared with others. Rebase rewrites commit hashes, which causes chaos for anyone else working on the same branch. Rebase your local work freely. Rebase shared work never.

Common Mistakes

  • Committing to main directly. Always branch. Even for "quick fixes." The fix that was supposed to take five minutes and breaks production is a cliche because it happens constantly.
  • Force-pushing. git push --force rewrites remote history. It can destroy other people's work. Use --force-with-lease if you absolutely must, but question whether you should at all.
  • Giant commits. A 40-file commit with the message "implement feature" is not a commit. It is a code dump. Break it up.
  • Not pulling before starting work. Working on a stale branch guarantees merge conflicts. Pull early and often.
  • Forgetting to check what is staged. Always run git status and git diff --staged before committing. Committing a .env file with secrets or a node_modules directory is embarrassing and potentially dangerous.
  • Committing generated files. Build outputs, compiled assets, lock files you did not intend to change -- review what you are committing.

Tips

  • Run git status before and after every operation. It is the cheapest sanity check available. Know your state at all times.
  • Write commit messages in the imperative mood. "Add validation" not "Added validation." This matches git's own conventions (Merge branch..., Revert "...") and reads as an instruction: apply this commit and it will "add validation."
  • Use git stash when switching contexts. If you need to jump to another branch but have uncommitted work, git stash saves it without creating a commit. git stash pop brings it back. Do not leave stashes to rot though -- apply or drop them promptly.
  • Check the diff before pushing. git diff origin/main...HEAD shows exactly what the remote will receive. Review it like a PR reviewer would.

Frequently Asked Questions

How often should I commit? Commit each time you complete a logical unit of work that leaves the codebase functional. This might be every ten minutes or every hour depending on the task. The test is: does this commit do one thing and does the code work after it?

Should I squash commits before merging? It depends on the team's convention. Squashing gives a clean single-commit-per-PR history. Preserving commits gives a detailed record of how the work evolved. Follow the project's existing pattern. When in doubt, keep atomic commits intact -- they are more useful for debugging.

What do I do if I committed something I should not have? If you have not pushed yet, git reset HEAD~1 undoes the last commit while keeping your changes. If you have pushed, you need to make a new commit that removes the problematic content. For secrets, assume they are compromised the moment they hit a remote -- rotate them immediately.

When should I use git cherry-pick? When you need a specific commit from another branch without merging the whole branch. Common scenario: a bugfix on a feature branch that also needs to go into a hotfix release. Use it sparingly -- if you are cherry-picking often, your branching strategy might need rethinking.

How do I write a good PR title? Follow the same rules as commit messages: imperative mood, specific, under 72 characters. Add rate limiting to /api/search endpoint beats Rate limiting or Fix for issue #302.

Sources