Video coming soon

The recording for this lesson has not been published yet.

What we cover

  • Feature branches and pull requests
  • Writing commit messages you will understand later
  • A .gitignore that fits a Django project
  • Pushing to GitHub and protecting the main branch

Lesson notes

1. The git you learned, and the git you need

In the tutorial git had one job: move files to PythonAnywhere. Three commands were enough - add, commit, push - and everything happened on main. That is fine for one person shipping one page.

From now on you will change several files at once, break things halfway through, and want to come back tomorrow. For that, git has to do a second job: keep a history you can read and undo. Three habits give you that.

  1. Every change happens on a branch, never directly on main.
  2. Every commit is one idea, with a message that says why.
  3. Nothing generated, secret or machine-specific ever enters the repository.

2. Feature branches

A branch is a movable label pointing at a commit. Creating one costs nothing and changes no files, which is why you should create one for every piece of work.

git switch main
git pull                          # start from the latest main
git switch -c feature/add-tags    # create and switch in one step

# ... edit files ...
git status                        # read this before every commit
git add blog/models.py blog/migrations/0002_tag.py
git commit
git switch main                   # your work is safe on the branch

git switch and git restore are the modern replacements for the overloaded git checkout: one switches branches, the other throws away changes. Use them and you will never confuse the two again.

Name branches so that a stranger can guess what is inside: feature/comments-form, fix/duplicate-slug, chore/bump-django, lesson/1-2-secrets. Keep them short-lived - a branch that lives for three weeks becomes a merge conflict.

main merge feature/ add-tags add Tag model add migration show tags in list pull request: review, CI, then merge

3. Commit messages you will understand in six months

A commit message has two parts: a subject line of at most 50 characters written in the imperative, then a blank line, then a body that explains why. The diff already says what changed; only you know why it had to.

Add Tag model and many-to-many field on Post

Readers ask for posts about one topic, and categories are too coarse:
a post can belong to several topics at once.

Tag.slug is unique so that /tag/<slug>/ can be a permanent URL.
Existing posts get no tags; a data migration comes in the next commit.
Instead ofWriteWhy
fixed stuff Fix crash when a post has no author Names the bug, so git log --grep can find it
changes to views.py Paginate the post list at 10 per page Describes behaviour, not files
wip Do not commit it yet, or say WIP: tag form, validation missing A history of wip commits cannot be bisected
One commit with 40 files Six commits with one idea each Only small commits can be reverted safely

Use git commit with no -m so that your editor opens and you write a body. To see what you are about to commit, one file at a time:

git add -p            # stage hunk by hunk
git diff --staged     # exactly what the commit will contain
git log --oneline --graph --decorate -10

4. A .gitignore that fits a Django project

The rule is simple: commit what a human wrote, ignore what a machine produced or what only makes sense on your laptop.

# .gitignore

# Python
__pycache__/
*.py[cod]
*.egg-info/

# virtual environments
.venv/
venv/
env/

# Django
db.sqlite3
db.sqlite3-journal
/media/
/staticfiles/
*.log

# secrets and local configuration
.env
.env.*
!.env.example
*.pem
*.key

# editors and operating systems
.idea/
.vscode/
.DS_Store
Thumbs.db

# tooling caches
.pytest_cache/
.ruff_cache/
htmlcov/
.coverage

Two entries are commonly got wrong. Migrations are not ignored - they are source code, and without them nobody can rebuild the database. And staticfiles/, the output of collectstatic, is ignored, while your hand-written static/ is committed.

5. Remotes and pushing to GitHub

A remote is just a nickname for a URL. origin is the one you cloned from.

git remote -v
git remote add origin git@github.com:you/myblog.git   # if there is none yet

git push -u origin feature/add-tags   # -u links the branch to its remote
git push                              # afterwards this is enough

Use SSH keys rather than a password: ssh-keygen -t ed25519, add the public key to your GitHub account, then test with ssh -T git@github.com. HTTPS works too, but needs a personal access token.

6. Pull requests and protecting main

A pull request asks "please merge this branch into main". Even working alone it is worth opening one: it gives you a diff of the whole feature, a place to write down what you did, and somewhere for the test suite to report.

  1. Push the branch and open the pull request. Title it like a commit subject.
  2. Read your own diff first. You will find a leftover print() most times.
  3. Let CI run (module 8). A red pull request does not get merged.
  4. Merge, then delete the branch. git switch main && git pull.

On GitHub, protect main in Settings - Branches: require a pull request, require status checks to pass, and forbid force pushes. Then make the habit permanent by removing the temptation locally:

git config branch.main.pushRemote no_push   # accidental pushes now fail

7. Undoing mistakes

SituationCommandEffect
Edited a file, want the committed version back git restore blog/views.py Discards your edits - unrecoverable, so be sure
Staged too much git restore --staged blog/views.py Unstages, keeps your edits
Bad message in the last commit git commit --amend Rewrites it - only if not pushed
Last commit was too big git reset --soft HEAD~1 Removes the commit, keeps the changes staged
Want the changes back in the working tree git reset --mixed HEAD~1 Removes the commit, unstages the changes
A pushed commit is wrong git revert <sha> A new commit that undoes it - safe for shared branches
Need to switch branch mid-work git stash then git stash pop Parks your changes temporarily
"I have lost a commit" git reflog Every position HEAD has had - almost nothing is really lost

8. Exercise

  1. Add the .gitignore above to the starter repository, run git status and confirm that .venv/ and db.sqlite3 have disappeared from the list. If they have not, use git rm --cached.
  2. Create feature/post-summary, add a summary field to Post, and make two commits: one for the model and migration, one for the template. Write a real body for each.
  3. Push the branch to GitHub, open a pull request against main, read your own diff, then merge and delete the branch.
  4. Break something on purpose: edit a template, then restore it with git restore. Commit something silly, then remove the commit with git reset --soft HEAD~1 and check that your changes are still staged.
  5. Enable branch protection on main and try to push to it directly. The rejection message is the point of the exercise.
  6. Run git log --oneline --graph --all and check that the story your history tells matches what you actually did.

Further reading

After this lesson you will

  • Work on branches instead of committing to main
  • Keep secrets and junk out of the repository