Meta description: Git commands for Django teams, from daily commits to .po conflict recovery, safer merges, and CI-ready i18n workflows.
You pull main before a deploy, and Git stops cold:
git pull
Then you get the line every Django team with localization work eventually sees.
CONFLICT (content): Merge conflict in locale/fr/LC_MESSAGES/django.po
Open the file and it's a knot of conflict markers around msgid, msgstr, and fuzzy entries. One branch added new strings with makemessages. Another updated translations. Now you're hand-editing a gettext file under release pressure, hoping compilemessages won't choke later.
The 3 AM Merge Conflict in Your Django PO File
You pull main before a deploy, and the release stops on locale/fr/LC_MESSAGES/django.po.
That file becomes a hotspot fast in a production Django app. Product changes button copy. Developers add new verbose_name values. Someone runs makemessages. A translator updates msgstr. CI still needs compilemessages to pass. By the time the branch is ready, Git is trying to merge two different edits to the same gettext entries under time pressure.
I see the same root cause on Django teams over and over. The problem is not Git command syntax. The problem is a workflow that treats translation files as an afterthought. Teams commit regenerated .po output with unrelated view or template changes, pull into a dirty working tree, and wait too long to sync branches that all touch the same locale files.
.po conflicts are also more annoying than normal text conflicts. You are not just choosing between two lines of Python. You are trying to preserve msgid, msgstr, plural forms, comments, and fuzzy markers in a format that Django tooling will parse later. A sloppy manual fix can survive code review and still fail at compilemessages or ship broken translations.
What usually caused the conflict
In Django projects, the pattern is predictable:
- Extraction drift: one developer runs
manage.py makemessagesafter changing templates, forms, or model labels. - Translation drift: another branch edits the same
.pofile to updatemsgstrvalues or clear fuzzy entries. - Late sync: the feature branch does not rebase or merge from
mainuntil review or deploy time. - Mixed commits: code changes, extracted strings, and translator edits land in one commit, so the conflict surface gets bigger than it needs to be.
My rule is simple. Treat .po files as generated artifacts that still need human review. They deserve isolated commits, fast feedback in CI, and a branch policy that keeps them close to main.
If your team is still choosing hosting, review, or desktop workflows, it helps to find version control tools that fit the way you ship. Once you are on Git, safety comes from consistency. Small commits. Frequent sync. Automated checks for message compilation before anything reaches production.
Core Snapshot Commands You Use Every Day
Git's mental model gets easier once you stop thinking in diffs and start thinking in snapshots. The Git book describes the project history as a sequence of snapshots. In that model, git add stages content, git commit records the staged snapshot, and git status tells you whether files are modified, staged, or committed (Git book on what Git is).
For Django work, that matters because you often touch code, templates, migrations, tests, and locale files in one pass. If you stage lazily, your history gets muddy fast.
Stage with intent
Most developers know this:
git add .
git commit -m "updates"
That works until your commit includes a model rename, a migration, and half-finished copy changes in django.po.
Use git add to build the commit you want:
git status
git add your_app/views.py
git add your_app/tests/test_checkout.py
git commit -m "Fix checkout locale selection in middleware"
Then handle the locale update separately:
git add locale/fr/LC_MESSAGES/django.po
git commit -m "Update French translations for checkout strings"
Separate commits make review easier and merge conflicts cheaper.
Read status before you do anything else
git status is the command I run most. Not because it's fancy. Because it catches bad assumptions.
git status
Use it to answer three questions:
- What's modified: files changed but not staged yet.
- What's staged: the exact snapshot in your next commit.
- What's untracked: new files you may have forgotten, like a generated fixture or debug script.
For Django repos, status catches accidental junk early. Compiled files, local notes, and temporary exports all love to sneak into commits when you're in a hurry.
If
git statussurprises you, stop there. Don't pull, don't merge, don't rebase.
Make log readable enough to debug from
Your commit history should explain why the app changed, not narrate your typing.
Good:
git commit -m "Add locale-aware validation message for signup form"
Bad:
git commit -m "fix"
Then inspect history in compact form:
git log --oneline
For Django debugging, path-specific history is more useful than a giant global log:
git log, locale/fr/LC_MESSAGES/django.po
git log, your_app/views.py
That narrows the question from "what happened in this repo" to "who changed this file and when."
A clean daily loop is short:
| Command | What I use it for in Django work |
|---|---|
git status |
Check working tree before pull, rebase, or test run |
git add <path> |
Stage only the files that belong together |
git commit -m "..." |
Record one logical change |
git log --oneline, <path> |
Trace a file such as a view, template, or .po file |
That's the foundation for every other Git command that matters.
Branching and Merging for Team Workflows
A bad branch policy shows up during release week, not during a calm demo. Someone changes a template string, another developer regenerates translations, a third adds a migration, and the pull request turns into conflict cleanup instead of review.
Branches protect main from that kind of drift. In a production Django app, they also give you a clean place to keep one unit of work together: code, tests, migrations, templates, and any .po updates caused by copy changes.

Use one branch per ticket
Keep the rule simple:
git switch -c feature/signup-locale-copy
One branch, one ticket, one reviewable story.
That matters more in Django than in many smaller apps because changes spread fast. A small feature can touch a form, a template, a serializer, a test, and generated locale files. If those edits share a branch with unrelated cleanup, your CI signal gets weaker and your rollback options get worse.
I use git switch instead of git checkout because the command says exactly what it does. Fewer overloaded commands means fewer mistakes, especially for teams that rotate between feature work, hotfixes, and release branches.
Pick a merge strategy on purpose
Every merge style costs something. Choose the cost you want.
| Merge style | Where it helps | Where it hurts |
|---|---|---|
| Fast-forward | Clean history on small, disciplined branches | Removes the visible branch boundary |
| Merge commit | Keeps the feature branch context in history | Adds extra commits to scan |
| Rebase then merge | Produces a tidy pull request and linear history | Unsafe if people rebase commits already shared with others |
For Django teams shipping through CI/CD, my default is straightforward:
- Feature branches: rebase locally before review if the branch is only yours.
- Main branch: merge through the hosting platform with required checks enabled.
- Release or hotfix branches: do not rewrite history after the branch is shared.
That gives reviewers a readable PR and keeps deployment history trustworthy. If production breaks after a release, you want to answer "what shipped?" in one command, not reconstruct it from rewritten branch history.
Branch hygiene matters more for translations and migrations
Two file types punish lazy branching fast: migrations and .po files.
Migrations conflict because order matters. Translation files conflict because they are generated, noisy, and easy to regenerate differently on two machines. If a branch sits open for several days while product copy keeps changing, expect conflict resolution to become manual and error-prone.
Use a tighter workflow:
- Create the branch before changing templates or
gettext_lazystrings. - Pull in current
mainbefore runningmakemessages. - Commit message extraction separately from the code that introduced it.
- Merge translation-heavy branches quickly.
That separation helps review. It also helps CI. If a pipeline fails on i18n validation, you can isolate whether the problem came from application logic or from regenerated locale artifacts.
Teams using GitHub, GitLab, or Azure DevOps all run into the same branch discipline problems. The hosting vendor matters less than the rules your team follows. If your team works in Microsoft's stack, this write-up on Azure DevOps repository workflows for team branching lines up well with the same branch-first approach.
Syncing Your Local Repository with Remotes
Most Git mistakes on teams happen during sync, not during commits. Someone pulls into local changes, resolves the wrong conflict, then pushes a branch nobody can review cleanly.
The core remote commands are enough if you understand the difference:
git fetchgit pullgit push
GitHub's documentation notes that git pull updates a local branch from its remote counterpart, while git push sends local commits to the remote repository. In practice, pull changes your local branch. fetch does not.
Fetch before you integrate
When I'm not sure what's changed upstream, I start here:
git fetch origin
That updates your remote-tracking refs and leaves your working tree alone. No merge commit. No file changes dropped into your editor mid-task.
Then inspect what moved:
git status
git log --oneline HEAD..origin/main
That pattern is safer than pulling blind, especially when your branch contains migrations or .po churn.
Pulling should be a deliberate integration step, not a reflex.
Pull with a reason
If you want the remote changes integrated right now:
git pull origin main
That fetches and then merges.
On feature branches, I usually prefer rebasing my local work on top of the updated branch:
git pull --rebase origin main
Use that only when your local commits are yours to rewrite. The result is often easier to review because you avoid an extra merge commit in the branch.
Push once the branch is ready for review
New branch:
git push -u origin feature/signup-locale-copy
The -u sets the upstream so later pushes are shorter:
git push
On shared branches, don't force-push casually. If you rebased a branch that's already under review, talk to the team first. CI pipelines, review comments, and branch protections all get noisier when branch history shifts under people.
If you're cleaning up old hosted repos or reducing sync confusion across platforms, this guide on how to remove a repository from Bitbucket is useful operationally. Less repo sprawl means fewer remotes people pull from by mistake.
Rewriting Local History for Cleaner Pull Requests
A pull request with commits named wip, fix test, again, and real fix tells the reviewer you never stopped to curate the story. That's fine while you're working locally. It's bad once other people need to understand what changed in your Django app.
git rebase -i is the canonical command for rewriting local history in a controlled way. It lets you reorder, squash, edit, or drop commits before replaying them onto a new base. Because it rewrites commit IDs, it should only be used on commits that haven't been shared publicly. git cherry-pick is the safer choice when you need to transplant a finished commit onto another branch (advanced Git commands and workflows).

Clean the branch before anyone reviews it
Typical local history on a Django feature branch:
fix text
update tests
wip
more fixes
po update
Turn that into two or three logical commits:
git rebase -i HEAD~5
A common cleanup pass:
- squash temporary fixups into one feature commit
- reword weak messages
- keep translation updates separate from code changes
That last part matters a lot for i18n. If the reviewer wants to inspect locale changes separately, they can.
Here's the pattern I aim for:
Add locale-aware signup validation messages
Update French and German PO files for signup flow
Add tests for translated validation output
One branch. Three understandable commits.
Later in the section, if you want a visual walkthrough, this video is solid:
When rebase is right and when it isn't
Use rebase when:
- Local commits are private: nobody else has pulled them.
- The branch is messy: you need to squash or reorder commits.
- The PR should tell one story: code review gets easier.
Do not use rebase when:
- Commits are already shared: teammates may already depend on those commit IDs.
- The branch is public and active: force-pushing creates avoidable confusion.
- You only need one fix elsewhere:
cherry-pickis usually cleaner.
A clean history isn't vanity. It's a maintenance tool for the next person reading the branch during an incident.
Cherry-pick for release branches
Say you fixed a Django admin translation bug on main, but the release branch only needs that one patch.
Use:
git switch release/2026-01
git cherry-pick <commit-sha>
That's safer than merging the whole feature branch and dragging unrelated work into the release.
My rule is blunt. Rebase for your own unpublished cleanup. Merge or cherry-pick for shared history. Teams that keep that distinction clear avoid a lot of pain.
Finding and Fixing Mistakes Before They Ship
Most Git guides spend too much time on add, commit, and branch, then disappear when something breaks. That's the wrong emphasis. Developers usually search for help after they've already made a mistake.
That gap matters because commands like git revert, git bisect, and the reflog are the primary recovery toolkit, yet they're often taught as isolated tricks instead of one debugging framework (video on advanced Git recovery workflows).

Revert on public branches
If a bad commit already landed on main, don't rewrite history. Revert it.
git revert <commit-sha>
Git creates a new commit that undoes the old one. That's what you want on a shared branch with CI, deploy logs, and other developers watching.
Good use cases in Django:
- Broken migration side effect: a commit introduced invalid data handling.
- Bad translation import: a
.poupdate broke placeholders. - Template regression: localized template output started escaping incorrectly.
Revert is audit-friendly. It leaves a visible trail.
Reflog when you broke your local branch
git reflog is your private breadcrumb trail. If you reset too far, botched a rebase, or detached HEAD and lost your place, reflog usually knows where you were.
git reflog
Then recover by switching or resetting back to the entry you need. I won't pretend this feels comfortable the first time, but it's one of those commands you remember forever after one bad afternoon.
If you want a deeper real-world story about recovery from Git history, this write-up on recovering deleted code history is worth your time.
Bisect regressions in Django instead of guessing
git bisect is what you use when the app worked before and now it doesn't, but nobody knows which commit caused it.
Start the search:
git bisect start
git bisect bad
git bisect good <known-good-commit>
Git checks out commits between those points. At each step, run the test or command that proves the bug exists.
For a Django regression:
python manage.py test your_app.tests.test_i18n
Then mark the result:
git bisect bad
or
git bisect good
Git narrows the range until it finds the offending commit.
A practical CI angle matters here too. If your pipeline already runs locale extraction, translation checks, and test commands, wire those into automation so regressions get caught before merge. TranslateBot's docs on using localization steps in CI are a good example of where this fits into a deployment pipeline.
Recovery mindset: public mistake,
revert. Lost local state,reflog. Unknown regression,bisect.
That's the framework. Keep it small and repeatable.
Git Commands for Your Django I18n Workflow

A Django app can tolerate a rough commit message. It cannot tolerate a broken .po file in production.
Localization work creates a different kind of Git pressure than normal feature work. django.po files are partly generated, partly edited by humans, and easy to damage during a rushed merge. In a CI/CD pipeline, one bad placeholder or malformed header can turn a routine deploy into a failed build or, worse, a release with broken translations.
Keep extraction and translation in separate commits
For a production Django app, I want one clear sequence:
python manage.py makemessages --locale=fr
git add locale/fr/LC_MESSAGES/django.po
git commit -m "Extract new French message IDs for billing flow"
Then handle translation as a separate change:
python manage.py translate --locale=fr
git add locale/fr/LC_MESSAGES/django.po
git commit -m "Translate new French billing strings"
Then compile if your deploy process expects compiled catalogs:
python manage.py compilemessages
The project layout should stay standard:
locale/fr/LC_MESSAGES/django.po
locale/fr/LC_MESSAGES/django.mo
This split matters in review. One commit answers, "Did the code introduce the right message IDs?" The next answers, "Are the translated strings correct?" Mixing those together makes pull requests noisy and hides real mistakes.
It also helps CI. If extraction, translation, and compilation happen as distinct steps, your pipeline can fail at the right point instead of leaving the team to guess which part of the locale update broke.
Review PO diffs like code, not content paste
Django i18n bugs usually show up in placeholders, plural rules, and context, not in the obvious headline copy.
A normal .po diff might look like this:
msgid "Welcome back, %(name)s"
msgstr "Bon retour, %(name)s"
msgid "You have %s unread message"
msgid_plural "You have %s unread messages"
msgstr[0] "Vous avez %s message non lu"
msgstr[1] "Vous avez %s messages non lus"
Review for:
- Placeholder integrity:
%(name)s,%s, and{0}must stay unchanged. - HTML safety: inline tags need to remain balanced if they appear in the source string.
- Context collisions: short labels often need
pgettextin code so translators can distinguish them. - Plural logic: some locales need more attention than English-style singular/plural pairs.
If your team already uses gettext_lazy, template {% translate %} tags, and LocaleMiddleware, bad translations usually come from weak review habits or sloppy merge handling, not from Django itself.
Commands I keep close during locale work
I keep the command set small and boring. That is a good thing.
| Task | Command |
|---|---|
| Extract new strings | python manage.py makemessages --locale=fr |
| Review file status | git status |
| Inspect PO-only history | git log, locale/fr/LC_MESSAGES/django.po |
| Commit extraction separately | git commit -m "Extract new message IDs" |
| Compile catalogs | python manage.py compilemessages |
For teams using TranslateBot, manage.py translate can write changes back to locale files so they stay in the same pull request as the code that introduced the new strings. That keeps review and rollback straightforward, which matters once translations are part of the release pipeline.
What works in production
These habits reduce breakage:
- Short-lived i18n branches: merge locale work quickly before source strings drift.
- One locale at a time: smaller diffs are easier to review and easier to revert.
- Glossary rules in the repo: terminology decisions belong under version control.
- Compilation in CI: run
compilemessagesbefore merge so broken catalogs fail early.
These habits create avoidable pain:
- Regenerating every locale file on every branch: that creates conflict-heavy pull requests.
- Combining refactors with bulk translation output: reviewers will miss both code issues and translation issues.
- Fixing conflict markers directly in a hurry: that is how malformed
.pofiles get committed. - Committing
.mofiles inconsistently: either version them as part of your deploy strategy or rebuild them consistently in CI.
For Django teams, Git supports i18n best when the workflow is predictable and automatable. Extract. Commit. Translate. Commit. Compile. Test. Merge.
Your Lean Git Command Set for Django in 2026
You don't need a huge Git vocabulary. You need a team-wide default that people can remember under pressure.
Recent Git guidance has pushed clearer commands like git switch and git restore instead of relying on the older, overloaded git checkout, and that shift is useful because older tutorials still blur too many jobs together (GitKraken guide to Git commands). For a Django team, explicit commands reduce mistakes during releases and incident fixes.
Drop the overloaded commands when you can
git checkout used to mean all of this at once:
- switch branches
- restore files
- move to commits
- leave people guessing
That's too much surface area for a command your whole team uses daily.
Prefer:
git switch feature/i18n-cleanup
git restore locale/fr/LC_MESSAGES/django.po
When someone joins the team, that naming teaches intent by itself.
The command set I actually teach
Here's the lean set I want every Django developer to know cold:
| Stage of work | Command |
|---|---|
| Start a branch | git switch -c feature/thing |
| Inspect local state | git status |
| Stage targeted files | git add <path> |
| Save a logical unit | git commit -m "..." |
| Review compact history | git log --oneline |
| Sync remote state safely | git fetch origin |
| Integrate remote work | git pull --rebase origin main |
| Publish branch | git push -u origin feature/thing |
| Merge shared work | git merge branch-name |
| Undo public mistake | git revert <sha> |
| Clean local history | git rebase -i HEAD~N |
| Recover local misstep | git reflog |
| Find regression | git bisect start |
| Restore unstaged file | git restore <path> |
That list covers almost every day on a production Django app.
Team conventions matter more than memorizing edge cases
A lean command set only works if the team agrees on a few rules:
- Never rebase shared history: keep force-pushes rare and explicit.
- Keep commits scoped: code changes and
.poupdates shouldn't be one blob. - Fetch before risky integration: don't pull blind.
- Use revert on public branches: preserve history during incidents.
- Prefer
switchandrestore: reduce ambiguity for newer developers.
The best Git workflow is the one your team can follow when they're tired, on call, and trying not to break production.
If you tighten those rules, most Git problems turn from repo disasters into routine cleanup.
If your Django app already uses makemessages and compilemessages, TranslateBot is worth a look for the translation step. It runs as a management command, writes changes back to your locale files, and fits the Git review flow described above, which is the only part that really matters.