Most beginners learn Git as three commands: add, commit, push. Straight to main. Every time.
That works until you're contributing to a real project - a job, an open-source repo, a team codebase. Then suddenly there are branches, pull requests, code reviews, and issue numbers you're supposed to reference. Nobody just pushes to main anymore.
Today I didn't want to explain that workflow. I wanted to live it, in front of the class, on a real repository - task_manager - with real issues already open for contribution. Fourteen of them, in fact. Things like adding JWT auth, dockerizing the app, building a frontend. Real tasks, waiting for real pull requests.
I picked Issue #1 - "Add priority field to Task" - and we worked it end to end, exactly the way it should be done. Here's every step, with the actual screenshots from the session.
Step 1: Look at the Actual Issue First
Before touching the terminal, we read the issue itself. This is the part beginners skip - they jump straight to coding before understanding what's actually being asked.
The issue had a clear description, a checklist of what to do, labels (backend, good-first-issue), and acceptance criteria. I told the class: "This is how real work arrives. Not as a vague request - as a structured ticket. Read the whole thing before you write a single line."
The checklist told us exactly what was needed: add a priority column to the Task model, add it to the TaskCreate and TaskUpdate schemas, add it to TaskResponse, and make sure it's optional on updates.
Step 2: Never Work Directly on main
This is the rule I repeat every single session: you never commit straight to main. You create a branch named after what you're doing, work there, and only bring it into main through a reviewed pull request.
git checkout -b feature/add-priority-field
Switched to a new branch 'feature/add-priority-field'
Notice the terminal prompt itself changes - (main) becomes (feature/add-priority-field). I pointed this out immediately: "Your terminal is telling you exactly where you are. Always look at that before you commit anything."
The naming convention matters too: feature/ prefix, then a short, clear description. Not fix-stuff or update2. Six months from now, someone reading your branch list should understand what each one did without opening a single file.
Step 3: Make the Change, Then Commit With a Real Message
We edited models.py to add the priority column, and schemas.py to add it to the three Pydantic schemas the issue described. Then:
git add models.py schemas.py
git commit -m "feat: add priority field to Tas model (#1)"
git push -u origin feature/add-priority-field
[feature/add-priority-field 487325e] feat: add priority field to Tas model (#1)
2 files changed, 6 insertions(+), 2 deletions(-)
Two things I made the class stop and look at here.
First - the commit message format. feat: at the start tells anyone reading the history what kind of change this is, without opening the diff. This is called Conventional Commits, and it's used widely in real projects. feat: for a new feature, fix: for a bug fix, docs: for documentation, refactor: for restructuring without changing behaviour.
Second - the (#1) at the end. That's the issue number. I told them: "This single detail is what turns a random commit into a traceable piece of work. Anyone looking at this commit message instantly knows which issue it addresses." (Yes - I also typo'd "Task" as "Tas" in the live commit message, which I left in the screenshot on purpose. Even trainers make typos mid-demo. It happens. The lesson doesn't change.)
git push -u origin feature/add-priority-field sends the branch to GitHub. The -u flag sets up tracking so future pushes from this branch just need git push, no extra arguments. GitHub even gives you a direct link in the terminal output to open a pull request - convenient, and worth pointing out so students don't go hunting for the button manually.
Step 4: The Repo, Fully Loaded With Real Issues
Before opening the PR, I showed the class the bigger picture - the full issue board waiting for contributors.
Fourteen open issues. Labels for difficulty (advanced, intermediate, good-first-issue) and area (backend, frontend, devops, security, testing). This is deliberate - a real contribution workflow needs real work waiting to be picked up. I told the class: "Pick any one of these. Today we're doing #1 together. The rest are yours to practice on this week."
Step 5: Compare the Branches
Back to the terminal output's link, or directly on GitHub - comparing what changed between main and our feature branch before opening anything formal:
Able to merge - GitHub confirming there's no conflict. One commit, two files changed, six additions, two deletions. This comparison view is where you do a last sanity check on exactly what you're about to propose merging. I told the class: "Always look at this diff view before opening the PR. If you see a file you didn't mean to touch, fix it now - not after someone reviews it."
Step 6: Open the Pull Request - and Tag the Issue
This is the step that ties the whole workflow together, and it's the one beginners forget most often.
In the PR description, typing Closes #1 and then selecting the actual issue from the dropdown - not just typing the number and hoping - does something genuinely useful: when this PR gets merged, GitHub automatically closes the issue for you. No separate step. No manually clicking "close" afterward and risking forgetting.
I made sure guys understood the vocabulary GitHub recognises: Closes, Fixes, and Resolves all trigger this auto-close behaviour, followed by the # and the issue number. Get the keyword and the number right, and GitHub does the bookkeeping.
"This is the line that connects your code change to the conversation that requested it. Anyone reading the issue later sees exactly which PR resolved it. Anyone reading the PR sees exactly what it was solving for. That traceability is the entire point."
Step 7: Review Before You Merge
Once opened, the PR ran its checks automatically.
All checks have passed - a GitGuardian security scan confirming no secrets were accidentally committed, and No conflicts with base branch. On the right sidebar: Closes #1 listed clearly under Development, which is GitHub surfacing the same link we set up in the description.
I paused here to make a point that matters more as projects grow: "In a team, this is where someone else reviews your code before merge - not you. Today we're working solo, so we'll review and merge it ourselves, but the pattern is the same one you'll use everywhere: PR opens, checks run, reviewer approves, then it merges."
Step 8: Merge - and Write a Clear Merge Commit
Clicking Merge pull request doesn't just merge - it gives you one more chance to write a clear commit message for the merge itself.
The default message GitHub suggests is already clear: Merge pull request #15 from Navashub/feature/add-priority-field, with the extended description carrying over the original commit message. I told students: "You can edit this if you want, but GitHub's default is already good practice - it tells you which PR, which branch, and what feature. Don't just blindly accept defaults everywhere, but this one's worth keeping."
Step 9: The Merge - and the Branch Cleanup
Pull request successfully merged and closed
GitHub immediately offers: "You're all set - the feature/add-priority-field branch can be safely deleted."
This is a habit worth building early: delete the branch after merging. The work is already in main. The branch served its purpose. Leaving dozens of stale merged branches around a repo makes it harder to see what's actually active. Click delete. It's gone from GitHub, but the commit history stays exactly intact in main forever.
Step 10: The Payoff - Watch the Issue Close Itself
This is the moment I wanted the whole class to see, because it's the proof that everything we did was connected correctly.
Issue #1 - closed. Automatically. Nobody opened the issue tab and clicked a button. The Closes #1 text in the pull request description did it the moment the merge happened.
That's the full loop: an issue describes work → a branch isolates that work → commits reference the issue number → a pull request formally proposes merging it and links back to the issue → checks verify it's safe → merging brings it into main → the issue closes itself as proof the work is genuinely done.
Why I did This Way
I could have explained this as a diagram on a whiteboard. Branch, commit, push, PR, merge - five boxes with arrows. Guys would nod and forget it by the following week.
Instead we did it on a real repository with real open issues, using the exact same commands and the exact same GitHub interface they'll use the moment they contribute to any actual project - open source, a job, a personal repo with collaborators. The typo in the commit message, the GitGuardian check actually scanning for secrets, the dropdown autocomplete for linking the issue - none of that is staged. It's what really happens.
What's Next for This Cohort
Thirteen issues remain open on task_manager. Each one is real practice for this exact workflow:
- Create a branch named after the issue
- Reference the issue number in your commit messages
- Open a PR that says
Closes #<number> - Get it merged, watch the issue close itself
Next time I write about this, it'll be their pull requests on screen, not mine.
Try It Yourself
If you want to practice this exact workflow on a real project:
- Fork any repo with open issues labelled
good-first-issue - Create a branch following the
type/short-descriptionpattern - Reference the issue number in your commit message
- Open a PR with
Closes #<number>in the description - Watch the issue close itself when it merges
That sequence is the actual day-to-day rhythm of working on a real codebase with other people. Practice it on a toy repo before you need it on a real one.
I'm a data trainer in Nairobi running a full data programme -
Python foundations → Data Science or Data Engineering specialisations.
I write daily about what I taught, what worked, and what surprised me in the classroom.
Follow along or drop your questions in the comments.










Top comments (0)