Back to blog

Commands in Git: Essential Guide for Django Devs 2026

2026-06-05 17 min read
Commands in Git: Essential Guide for Django Devs 2026

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:

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:

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 status surprises 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.

A diagram illustrating the git branching and merging workflow process for collaborative software development projects.

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:

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:

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:

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).

A comparison chart showing the advantages and disadvantages of using the git rebase interactive command.

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:

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:

Do not use rebase when:

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).

A young programmer using an eraser to remove messy code and merge conflicts on a screen.

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:

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 five-step diagram illustrating the Django internationalization (i18n) translation workflow utilizing Git version control.

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:

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:

These habits create avoidable pain:

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:

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:

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.

Stop editing .po files manually

TranslateBot automates Django translations with AI. One command, all your languages, pennies per translation.