The recording for this lesson has not been published yet.
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.
main.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.
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 of | Write | Why |
|---|---|---|
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
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.
If db.sqlite3 is already committed, adding it to the file changes nothing.
Run git rm --cached db.sqlite3 and commit that. It stays in the old history,
which is exactly why secrets must never get in.
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.
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.
print() most times.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
| Situation | Command | Effect |
|---|---|---|
| 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 |
--amend, reset --hard and rebase are fine on your
own unpushed branch and destructive on a shared one. On anything public, undo with
git revert instead. If you truly must, --force-with-lease is
the least dangerous force push.
.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.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.main, read your own
diff, then merge and delete the branch.git restore. Commit something silly, then remove the commit with
git reset --soft HEAD~1 and check that your changes are still staged.main and try to push to it directly. The
rejection message is the point of the exercise.git log --oneline --graph --all and check that the story your history
tells matches what you actually did.