Simulate a Production Git Workflow
Resolve merge conflicts, rebase history, and recover lost commits.
Introduction
30 Second Summary
When developers work in teams, the biggest fear isn't writing bad code. It's accidentally overwriting someone else's work, or losing hours of changes to a bad merge.
In this project, you will simulate a production team workflow on a solo repository. You'll create competing feature branches, resolve merge conflicts, clean up commit history, and recover lost work using the exact techniques that production teams rely on every day.
What You'll Build
A production-style Git workflow simulation where you play multiple developer roles on a single repository, building a Python task tracker across competing feature branches.
By the end of this project, you'll have:
- A public GitHub repository with branch protection rules enforcing a PR-based workflow on main.
- Multiple merged pull requests showing clean, rebased history with resolved merge conflicts.
- A commit history demonstrating interactive rebase and disaster recovery with git reflog.
- Secret Mission: Simulate a production hotfix by cherry-picking a bug fix from a feature branch directly onto main while other feature work is in flight.
Are there any prerequisites?
You should have basic Git knowledge (clone, commit, push, pull) and a GitHub account. No advanced Git experience is needed.
Set Up Your Tools and Repository
A production Git workflow starts with a properly protected repository. On real teams, no one pushes code directly to the main branch. Every change goes through a pull request, gets reviewed, and then merges.
In this step, you will create a public GitHub repository from the terminal, add starter files for a Python task tracker, and lock down main with branch protection rules that enforce a PR-based workflow.
In this step, get ready to:
- Create a public GitHub repository using the GitHub CLI.
- Add initial project files and push them to main.
- Configure branch protection to require pull requests.
Create the GitHub repository
The GitHub CLI lets you create repositories, open pull requests, and merge code without leaving the terminal. You will use it throughout this project to manage your workflow.
- Open Git Bash by pressing the Windows key, typing Git Bash, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the repository and clone it locally by running this command:
gh repo create git-workflow-sim --public --clone
What does this command do?
gh repo create git-workflow-sim creates a new public repository called git-workflow-sim on your GitHub account.
The --public flag makes it visible to anyone on the internet. The --clone flag immediately clones it into a new folder on your Desktop.
- Move into the new project folder by running:
cd git-workflow-sim
- Confirm you are in the right folder by running:
pwd
You should see a path ending in /Desktop/git-workflow-sim.
Seeing an error from gh repo create?
If you see a message about authentication, confirm you are signed in by running gh auth status. If you are not logged in, run gh auth login and follow the prompts.
If the repository name already exists on your account, choose a different name or delete the existing one from GitHub first.
help me troubleshoot gh repo create errors
Create the initial project files
You need two files to get started: a Python script that defines a simple task list, and a README that describes the project. These files will become the base that your feature branches modify later.
- Open VS Code by pressing the Windows key, typing VS Code, and pressing Enter.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the git-workflow-sim folder, and click Select Folder.
- Create a new file by clicking File then New File.
- Save it immediately with Ctrl+S and name it tasks.py.
- Add the task data to tasks.py by copying the following code into the file:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline"
},
{
"id": 2,
"title": "Write unit tests"
},
{
"id": 3,
"title": "Update documentation"
}
]
What does this code do?
tasks is a Python list of dictionaries. Each dictionary represents one task with an id and a title.
This data structure is deliberately simple right now. In later steps, competing feature branches will add different fields to the same structure, creating the merge conflicts you will practice resolving.
- Below the task data in tasks.py, add the display function and main block:
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print()
if __name__ == "__main__":
display_tasks(tasks)
What does this code do?
display_tasks() loops through the task list and prints each task's ID and title in a formatted layout.
The if __name__ == "__main__" block ensures the display function runs only when you execute the script directly with python tasks.py.
- Save tasks.py with Ctrl+S.
- Switch back to Git Bash and verify the script works by running:
python tasks.py
You should see output showing all three tasks with their IDs and titles under a === Task Tracker === heading.
Command not found or no output?
If you see command not found, try running python3 tasks.py instead. Some systems use python3 as the command name.
If you see a syntax error, double check that your quotes and brackets in tasks.py match the code above exactly.
help me fix my tasks.py script
- Create another new file in VS Code (File then New File) and save it as README.md.
- Add the following content to README.md:
# Task Tracker
A simple Python task tracker built to practice production Git workflows.
## Usage
```bash
python tasks.py
```
- Save README.md with Ctrl+S.
- Confirm both files exist by running this in Git Bash:
ls
You should see tasks.py and README.md in the output.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete tasks.py file:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline"
},
{
"id": 2,
"title": "Write unit tests"
},
{
"id": 3,
"title": "Update documentation"
}
]
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print()
if __name__ == "__main__":
display_tasks(tasks)
And here is the complete README.md file:
# Task Tracker
A simple Python task tracker built to practice production Git workflows.
## Usage
```bash
python tasks.py
```
Push to main and configure branch protection
With both files ready, you need to commit them to your repository and push to GitHub. After that, you will enable branch protection so that all future changes must go through a pull request.
- Stage and commit your files by running these commands in Git Bash:
git add .
git commit -m "Initial commit: basic task tracker"
- Push your commit to GitHub by running:
git push origin main
You should see output confirming the push to origin/main with object counts and a success message.
Push rejected or branch not found?
If Git says the branch main does not exist, run git branch -M main to rename your current branch, then try the push again.
If you see a permission error, confirm you are authenticated with gh auth status.
help me push my first commit to GitHub
Now you will configure branch protection on GitHub so that no one (including you) can push directly to main.
- Open your repository on GitHub by running:
gh repo view --web
- Click Settings in the repository navigation bar.
- In the left sidebar under Code and automation, click Branches.
- Next to Branch protection rules, click Add rule.
- In the Branch name pattern field, type main.
- Check the box for Require a pull request before merging.
- Scroll down and check Do not allow bypassing the above settings.
- Click Create at the bottom of the page.
Why check 'Do not allow bypassing'?
By default, repository owners can bypass branch protection rules on their own repos. Checking this box simulates a real team environment where even admins must open a pull request to merge into main.
This forces you to use the PR workflow for every change going forward, which is exactly the muscle memory you want to build.
- After saving, navigate back to the repository's main page by clicking the repository name at the top.
- You should see a lock icon next to main in the branch dropdown, confirming protection is active.
Don't see the Settings tab?
Only repository owners see the Settings tab. Make sure you are viewing the repository on your own GitHub account, not a different user's fork.
help me find branch protection settings on GitHub
Your repository is ready. Next up, you will create competing feature branches that deliberately modify the same code, setting up the merge conflicts you will learn to resolve.
Create Competing Feature Branches
Your repository is set up with branch protection, which means nobody (including you) can push directly to main.
In a real team, multiple developers work on different features at the same time. Sometimes those features touch the same files, which creates merge conflicts. In this step, you will simulate that exact scenario by creating two branches that modify the same lines of tasks.py.
In this step, get ready to:
- Create a feature/add-priority branch with 3 commits adding a priority field.
- Create a feature/add-deadline branch with 2 commits modifying the same lines.
- Push both branches to GitHub to set up an inevitable merge conflict.
Create the priority feature branch
The first feature adds a priority level to each task. You will make three small commits so the branch has realistic history.
- Create and switch to a new branch by running this command:
git switch -c feature/add-priority
What does git switch -c do?
The -c flag creates a new branch from your current position (HEAD) and switches to it in one step. It is the equivalent of running git branch followed by git switch.
You should see a message confirming you switched to a new branch.
Now make the first commit: add a priority field to the first task in tasks.py.
- In VS Code, open tasks.py and find the first task dictionary.
- Find this section:
{
"id": 1,
"title": "Set up CI/CD pipeline"
},
- Add a comma after the title value and add the priority field, so it looks like this:
{
"id": 1,
"title": "Set up CI/CD pipeline",
"priority": "high"
},
- Save tasks.py.
- Stage and commit this change by running:
git add tasks.py
git commit -m "Add priority field to task structure"
Now make the second commit: update the display function to show the priority.
- In tasks.py, find the display_tasks function. Locate this line:
print(f"[{task['id']}] {task['title']}")
- Add the following line directly below it:
print(f" Priority: {task['priority']}")
- Save tasks.py and commit:
git add tasks.py
git commit -m "Update display function to show priority"
Now make the third commit: add priority values to the remaining tasks.
- In tasks.py, find the second task dictionary and add a priority field:
{
"id": 2,
"title": "Write unit tests",
"priority": "medium"
},
- Do the same for the third task:
{
"id": 3,
"title": "Update documentation",
"priority": "low"
}
- Save tasks.py and commit:
git add tasks.py
git commit -m "Add priority data for remaining tasks"
- Verify your 3 commits exist by running:
git log --oneline
You should see three commits above the initial commit, each with the messages you wrote.
✔️ Awesome, I've got everything!
Great. Double check you have saved tasks.py and all three commits show in git log --oneline.
ⓧ I'd like to double check the full code
Here is what tasks.py should look like on the feature/add-priority branch after all 3 commits:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline",
"priority": "high"
},
{
"id": 2,
"title": "Write unit tests",
"priority": "medium"
},
{
"id": 3,
"title": "Update documentation",
"priority": "low"
}
]
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print(f" Priority: {task['priority']}")
print()
if __name__ == "__main__":
display_tasks(tasks)
Seeing a KeyError when you run the script?
If you run python tasks.py and get a KeyError for priority, it means one of the task dictionaries is missing the priority field. Check that all three tasks have the "priority" key.
Also make sure you added a comma after the "title" value on each task before adding the new field.
Help me debug this
Create the deadline feature branch
Now you will create a second branch that modifies the exact same lines of tasks.py. This is the key ingredient for a merge conflict. Both branches start from the same main commit, so Git has no way to automatically combine their changes later.
- Switch back to main by running:
git switch main
Notice that tasks.py in VS Code reverts to its original state. Your priority changes still exist safely on the other branch.
- Create and switch to the deadline branch by running:
git switch -c feature/add-deadline
Now make the first commit: add a deadline field to all three tasks.
- In VS Code, open tasks.py and update all three task dictionaries to include a deadline field. The tasks list should look like this:
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline",
"deadline": "2026-07-14"
},
{
"id": 2,
"title": "Write unit tests",
"deadline": "2026-07-21"
},
{
"id": 3,
"title": "Update documentation",
"deadline": "2026-07-28"
}
]
What does this code do?
You are adding a "deadline" key to each task dictionary, stored as a date string. Notice you are editing the same lines where the priority branch added its field. Both branches add a new key-value pair right after "title".
This is exactly how merge conflicts happen on real teams. Two developers modify the same section of a file without knowing about each other's changes.
- Save tasks.py and commit:
git add tasks.py
git commit -m "Add deadline field to tasks"
Now make the second commit: update the display function to show the deadline.
- In tasks.py, find the display_tasks function. Locate this line:
print(f"[{task['id']}] {task['title']}")
- Add the following line directly below it:
print(f" Deadline: {task['deadline']}")
- Save tasks.py and commit:
git add tasks.py
git commit -m "Update display function to show deadline"
- Verify your 2 commits exist by running:
git log --oneline
You should see two commits above the initial commit on this branch.
✔️ Awesome, I've got everything!
Great. Confirm you have saved tasks.py and both commits appear in your log.
ⓧ I'd like to double check the full code
Here is what tasks.py should look like on the feature/add-deadline branch after both commits:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline",
"deadline": "2026-07-14"
},
{
"id": 2,
"title": "Write unit tests",
"deadline": "2026-07-21"
},
{
"id": 3,
"title": "Update documentation",
"deadline": "2026-07-28"
}
]
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print(f" Deadline: {task['deadline']}")
print()
if __name__ == "__main__":
display_tasks(tasks)
Seeing the priority field in your file?
If you see the priority field instead of the deadline field, you forgot to switch back to main before creating this branch. The deadline branch must start from the same base as the priority branch (the original main commit), not from the priority branch itself.
Delete this branch and start over: run git switch main, then git branch -D feature/add-deadline, then create it fresh.
Help me fix my branch
Push both branches to GitHub
Both branches exist only on your machine right now. Pushing them to GitHub makes them visible to the remote repository, which is required for creating pull requests later.
- Push the priority branch by running:
git push origin feature/add-priority
- Push the deadline branch by running:
git push origin feature/add-deadline
- Verify both branches exist locally and remotely by running:
git branch -a
You should see both feature/add-priority and feature/add-deadline listed under local branches, plus their remotes/origin/ counterparts.
Why do both branches modify the same lines?
On a real team, Developer A might add priority tracking while Developer B adds deadlines. Neither knows the other is editing the same section of the same file. When the first PR merges, the second developer has to reconcile both changes.
You have just set up this exact scenario. The next step will merge one branch, then force the second branch to deal with the resulting conflict.
Push rejected or permission denied?
Make sure you are still authenticated with gh. Run gh auth status to check. If your session expired, re-authenticate with gh auth login.
Help me fix my push
Your two competing feature branches are live on GitHub. Next up, you will merge one branch first and then watch the conflict unfold when the second branch tries to rebase.
Open a PR, Merge, Then Resolve the Conflict
In the previous step, you set up two branches that deliberately modify the same lines in tasks.py.
Now you will see what happens when one branch merges first and the other has to catch up. This is the most common source of merge conflicts on real teams. You will open a pull request from the command line, merge it, then rebase the second branch and resolve the resulting conflict by hand.
In this step, get ready to:
- Create and merge a pull request for the priority feature using gh.
- Rebase the deadline branch onto updated main and resolve a merge conflict.
- Force-push safely with --force-with-lease after rewriting history.
Create and merge the priority PR
Branch protection on main means you cannot push directly. Every change must go through a pull request. The GitHub CLI lets you create and merge PRs without leaving your terminal.
- Create a pull request for the priority branch by running this command:
gh pr create --base main --head feature/add-priority --title "Add priority field to tasks" --body "Adds priority levels (high/medium/low) to task tracking"
What does this command do?
- --base main tells GitHub which branch to merge into.
- --head feature/add-priority tells GitHub which branch contains your changes.
- --title and --body fill in the PR description so reviewers know what they are looking at.
- You should see a URL printed to the terminal confirming the PR was created on GitHub.
- Merge the PR with a squash strategy by running:
gh pr merge feature/add-priority --squash --delete-branch
What does this command do?
- --squash combines all 3 priority commits into a single clean commit on main.
- --delete-branch removes the feature/add-priority branch from GitHub after merging, keeping the repository tidy.
- You should see a confirmation message that the PR was merged and the branch was deleted on the remote.
PR merge rejected?
If you see an error about branch protection or required reviews, make sure you did not check "Require approvals" when setting up branch protection in Step 1. Only "Require a pull request before merging" should be enabled.
Help me fix a rejected PR merge
Rebase the deadline branch onto updated main
Main now contains the priority feature. Your deadline branch was created before that merge happened, so it has no idea those changes exist. Rebasing replays your deadline commits on top of the new main, which forces Git to reconcile the two sets of changes.
- Confirm you are on the deadline branch by running:
git switch feature/add-deadline
- Fetch the latest changes from GitHub so your local Git knows main has been updated:
git fetch origin
- Start the rebase onto the updated main by running:
git rebase origin/main
Git will stop and report a conflict in tasks.py. This is expected. Both branches changed the same lines, so Git cannot automatically decide which version to keep.
Why rebase instead of merge?
Merging main into your feature branch creates an extra "merge commit" that clutters history. Rebasing replays your commits on top of the latest main, producing a clean linear history that is easier to read in git log.
This is the standard approach on production teams using GitHub Flow.
- Open tasks.py in VS Code by running:
code tasks.py
You will see conflict markers like this inside the file:
<<<<<<< HEAD
"priority": "high"
=======
"deadline": "2026-07-14"
>>>>>>> add deadline field
What do these markers mean?
- <<<<<<< HEAD marks the start of the conflict. During a rebase, HEAD is the branch you are rebasing onto (main, which now has the priority field).
- ======= separates the two competing versions.
- >>>>>>> marks the end of the conflict. The text after it identifies your deadline branch commit.
Your job is to delete the markers and keep the code you actually want. In this case, you want BOTH fields.
- Resolve each conflict in tasks.py by deleting all conflict markers (<<<<<<<, =======, >>>>>>> lines) and keeping BOTH the priority and deadline fields for each task.
- Do the same for the display_tasks function. Keep both the priority and deadline print lines.
- After resolving, the first task dictionary should look like this:
{
"id": 1,
"title": "Set up CI/CD pipeline",
"priority": "high",
"deadline": "2026-07-14"
},
- The display_tasks function should include both print lines:
print(f" Priority: {task['priority']}")
print(f" Deadline: {task['deadline']}")
- Save tasks.py in VS Code.
Not sure if you removed all markers?
Search for <<<< in VS Code using Ctrl+F. If nothing is found, all markers are gone.
Help me check my conflict resolution
Complete the rebase and force-push safely
With the conflict resolved in your editor, you need to tell Git the conflict is handled, finish the rebase, then push the rewritten history to GitHub.
- Stage the resolved file by running:
git add tasks.py
- Continue the rebase by running:
git rebase --continue
What if an editor opens?
Git may open an editor asking you to confirm or edit the commit message. If this happens, save and close the editor (in the default editor, type :wq and press Enter) to accept the existing message and continue.
- Push the rebased branch to GitHub by running:
git push --force-with-lease origin feature/add-deadline
Why --force-with-lease instead of --force?
Rebasing rewrites commit history, so a normal git push will be rejected. You need to force-push. But a plain --force will overwrite anything on the remote, even work pushed by a teammate.
--force-with-lease checks that nobody else pushed to the branch since your last fetch. If someone did, the push fails safely instead of destroying their work. Always use this on a team.
- Verify your branch has a clean linear history by running:
git log --oneline
You should see your deadline commits sitting neatly on top of the squashed priority commit from main. No merge commits in sight.
Push rejected even with --force-with-lease?
Run git fetch origin first to update your local tracking refs, then retry the push. This error means your local ref was stale.
Help me fix a rejected force-push
✔️ Awesome, I've got everything!
Great. Double check you have saved tasks.py and that git log --oneline shows a clean linear history.
ⓧ I'd like to double check the full code
Here is what tasks.py should look like after resolving the conflict:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline",
"priority": "high",
"deadline": "2026-07-14"
},
{
"id": 2,
"title": "Write unit tests",
"priority": "medium",
"deadline": "2026-07-21"
},
{
"id": 3,
"title": "Update documentation",
"priority": "low",
"deadline": "2026-07-28"
}
]
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print(f" Priority: {task['priority']}")
print(f" Deadline: {task['deadline']}")
print()
if __name__ == "__main__":
display_tasks(tasks)
You just resolved your first real merge conflict during a rebase. Next up, you will clean up the deadline branch history with interactive rebase before opening its PR.
Interactive Rebase to Clean Up History
Your deadline branch has two separate commits from the development process. On a real team, PR reviewers prefer reading one clean commit that tells a complete story over multiple incremental commits like "add deadline field" and "update display function".
Interactive rebase lets you rewrite commit history before it gets merged. In this step, you will squash those two commits into one, push the cleaned branch, and merge your second PR into main.
In this step, get ready to:
- Squash 2 commits into 1 clean commit using interactive rebase.
- Force-push the cleaned branch and merge via pull request.
- Verify a clean linear history on main with no merge commits.
Squash your commits with interactive rebase
Interactive rebase opens an editor showing your recent commits. You can reorder them, remove them, or squash multiple commits into one.
- Start the interactive rebase for your last 2 commits by running this command:
git rebase -i HEAD~2
What happens when you run this?
An editor opens showing your two most recent commits. Each line starts with the word pick followed by a commit hash and message. The oldest commit is at the top, the newest at the bottom.
By default, Git Bash uses the Vim editor. If you see a screen with lines starting with pick and cannot type, you are in Vim's normal mode.
You should see something like this in the editor:
pick abc1234 Add deadline field
pick def5678 Update display function
- Press i to enter Vim's insert mode (you will see -- INSERT -- at the bottom of the screen).
- On the second line, change pick to squash (or just s). Leave the first line as pick.
Your editor should now look like this:
pick abc1234 Add deadline field
squash def5678 Update display function
- Press Esc to exit insert mode.
- Type :wq and press Enter to save and close the file.
What happens next?
A second editor window opens showing both commit messages combined. This is where you write your new, clean commit message that replaces both originals.
- Delete all the existing text in the editor. Press i for insert mode, then clear the lines.
- Type a clean commit message: Add deadline field to tasks.
- Press Esc, type :wq, and press Enter to save.
Git confirms the rebase completed successfully. Your 2 commits are now 1 clean commit.
- Verify the squash worked by running:
git log --oneline
You should see only one commit for your deadline work (plus the earlier commits from main). The two separate commits have been folded into a single entry.
Stuck in the editor?
If you accidentally closed the editor without saving, or made a typo, Git may show an error. Run git rebase --abort to cancel and start over with git rebase -i HEAD~2.
If Vim feels unfamiliar, you can set VS Code as your Git editor for future use. For now, remember: i enters insert mode, Esc exits insert mode, and :wq saves and quits.
Help me troubleshoot my interactive rebase
Force-push and merge your pull request
Your local branch now has different history from the remote branch you pushed earlier. You need to force-push to update the remote with your squashed commit.
- Force-push the cleaned branch safely by running this command:
git push --force-with-lease origin feature/add-deadline
Why --force-with-lease instead of --force?
The --force-with-lease flag refuses to push if the remote branch has been updated by someone else since your last fetch. Regular --force overwrites everything blindly.
On a real team, this prevents you from accidentally erasing a teammate's commits. Always use --force-with-lease instead of --force.
- Create the pull request for your deadline feature by running this command:
gh pr create --base main --head feature/add-deadline --title "Add deadline field to tasks" --body "Adds due dates to task tracking. Resolves conflict with priority feature."
What do these flags do?
- --base main sets the branch you want to merge into.
- --head feature/add-deadline sets the branch containing your changes.
- --title and --body set the PR title and description that reviewers will see.
You should see a URL printed confirming the PR was created on GitHub.
- Merge the PR using the rebase strategy by running this command:
gh pr merge --rebase --delete-branch
Why --rebase this time instead of --squash?
You already squashed locally with interactive rebase, so your branch has exactly one clean commit. Using --rebase places that single commit directly on top of main without creating an extra merge commit. If you used --squash again, it would re-squash an already-squashed commit, which works but changes the commit hash unnecessarily.
You should see confirmation that the PR was merged and the remote branch was deleted.
PR merge failed?
If you see a message about branch protection or merge conflicts, make sure you completed the force-push step before creating the PR. The remote branch must have your squashed commit.
If your admin bypass is disabled and the merge is blocked, you can add the --admin flag to override branch protection for this merge.
Help me fix my PR merge issue
Verify the clean linear history on main
The whole point of rebasing and squashing is to produce a commit history that reads like a changelog. Each commit tells a complete story.
- Switch to main and pull the latest changes by running these commands:
git switch main
git pull origin main
- View the commit history with a graph by running this command:
git log --oneline --graph
You should see a straight line of commits with no branching or merge commits. One commit for the priority feature, one for the deadline feature, and your initial commit at the bottom.
What does a clean linear history look like?
Every commit sits on a single straight line (marked with * in the graph). There are no diagonal lines or merge commit messages. This means every feature was rebased before merging, keeping history simple to read and easy to bisect when debugging.
- Confirm both features are working by running:
python tasks.py
You should see all three tasks displayed with both their priority and deadline fields.
Not seeing deadline fields in the output?
Make sure you pulled the latest from main. If the merge went through on GitHub but your local main is behind, the file will not have both features yet.
Run git pull origin main again to confirm you are up to date.
Help me figure out why my task output is missing fields
Your main branch now has a commit history that looks like it was written by a disciplined team. Next up, you will simulate a disaster scenario and recover from it using one of Git's most powerful safety nets.
Recover a Lost Commit with Reflog
Your repository has a clean, linear history with both features merged. But what happens when something goes wrong?
On real teams, developers accidentally delete branches, reset too far, or lose commits they thought were safe. The reflog is Git's safety net. It records every time HEAD moves, even when the branch pointer is gone. In this step, you will simulate a disaster and recover from it.
In this step, get ready to:
- Create a feature branch, commit work, then delete it to simulate a disaster.
- Use the reflog to find the lost commit's SHA hash.
- Recover the branch and open a pull request for it.
Simulate a disaster by creating and deleting a branch
You will create a new feature branch, add a status field to the task tracker, then switch back to main and force-delete the branch. This simulates the moment a developer accidentally loses work.
- Create and switch to a new branch by running this command:
git switch -c feature/add-status
Now you need to update tasks.py with the status field, filter functions, and an updated display function.
- Open tasks.py in VS Code and replace the entire contents with this code:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline",
"priority": "high",
"deadline": "2026-07-14",
"status": "in-progress"
},
{
"id": 2,
"title": "Write unit tests",
"priority": "medium",
"deadline": "2026-07-21",
"status": "todo"
},
{
"id": 3,
"title": "Update documentation",
"priority": "low",
"deadline": "2026-07-28",
"status": "done"
}
]
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print(f" Priority: {task['priority']}")
print(f" Deadline: {task['deadline']}")
print(f" Status: {task['status']}")
print()
def filter_by_priority(task_list, priority):
"""Filter tasks by priority level."""
return [t for t in task_list if t["priority"] == priority]
def filter_by_status(task_list, status):
"""Filter tasks by status."""
return [t for t in task_list if t["status"] == status]
if __name__ == "__main__":
display_tasks(tasks)
print("--- High Priority Tasks ---")
display_tasks(filter_by_priority(tasks, "high"))
What does this code do?
This version adds a "status" field to every task with values like "in-progress", "todo", and "done".
It also adds filter_by_priority() and filter_by_status() functions so you can query tasks by criteria. The display function now prints the status alongside priority and deadline.
- Save tasks.py.
Next, update the README to document the new features.
- Open README.md and replace the entire contents with this:
# Task Tracker
A simple Python task tracker built to practice production Git workflows.
## Features
- Task priority levels (high, medium, low)
- Deadline tracking
- Status tracking (todo, in-progress, done)
- Filter by priority or status
## Usage
```bash
python tasks.py
```
## Development Workflow
This project uses GitHub Flow:
1. Create a feature branch from `main`
2. Make changes and commit
3. Open a pull request
4. Resolve any conflicts via rebase
5. Squash/rebase merge into `main`
What does this README add?
The updated README documents all three features (priority, deadline, status) and describes the GitHub Flow workflow your team follows. This is realistic. Feature branches include documentation updates alongside code changes.
- Save README.md.
- Stage and commit both files by running these commands:
git add tasks.py README.md
git commit -m "Add status field and filter functions"
Now simulate the disaster. Switch back to main and force-delete the branch.
- Switch to main and delete the feature branch by running these commands:
git switch main
git branch -D feature/add-status
Why use -D instead of -d?
The -D flag is a shortcut for --delete --force. It deletes the branch regardless of whether its changes have been merged. This simulates accidentally losing unmerged work.
The safer -d flag would refuse to delete because the status commits have not been merged into main.
- Verify the branch is gone by running:
git branch
You should only see * main in the output. The feature/add-status branch is gone.
- Confirm the commit is invisible to normal log by running:
git log --oneline
You should see only the existing commits from main. The status commit does not appear here because no branch points to it anymore.
Branch wasn't deleted?
If git branch -D says the branch does not exist, make sure you spelled it exactly as feature/add-status. Run git branch to see which branches currently exist.
help me figure out why my branch deletion isn't working
Use git reflog to find the lost commit
The branch is gone and git log shows no trace of the status commit. On a real team, this is the moment of panic. But Git keeps a local record of every HEAD movement called the reflog.
- View your reflog by running:
git reflog
What does git reflog show?
The reflog records every time HEAD changed position. Each line shows a short SHA hash, a reference like HEAD@{0}, and a description of the action (commit, checkout, rebase, reset).
Even though the branch pointer was deleted, the commit object still exists in Git's database. The reflog entry proves it.
You should see output like this (your hashes will be different):
a1b2c3d HEAD@{0}: checkout: moving from feature/add-status to main
f4e5d6c HEAD@{1}: commit: Add status field and filter functions
...
- Find the line that says commit: Add status field and filter functions. Copy the short SHA hash at the beginning of that line.
- Recover the commit by creating a new branch from that SHA. Replace the placeholder below with the SHA you copied:
git branch feature/add-status-recovered [[SHA="the short SHA hash from your reflog"]]
This creates a new branch pointing directly at the recovered commit. The work is no longer lost.
Can't find the commit in reflog?
Look for the entry with the message commit: Add status field and filter functions. It is typically a few entries down from the top. The SHA is the 7-character string at the very beginning of that line.
help me read my git reflog output to find a lost commit
Verify the recovery and create a pull request
- Switch to the recovered branch by running:
git switch feature/add-status-recovered
- Confirm the status field code is intact by running:
cat tasks.py
You should see the full tasks.py with the "status" field in every task, plus the filter_by_priority and filter_by_status functions. The code survived.
- Push the recovered branch to GitHub by running:
git push origin feature/add-status-recovered
- Create a pull request for the recovered branch by running:
gh pr create --base main --head feature/add-status-recovered --title "Add status field to tasks" --fill
What does --fill do?
The --fill flag automatically populates the PR body using your commit messages. This saves you from typing a description manually for straightforward PRs.
You should see output confirming the PR was created with a URL to view it on GitHub.
PR creation failed?
Make sure you pushed the branch first. The gh pr create command needs the branch to exist on GitHub (origin) before it can open a PR.
If you see an authentication error, run gh auth status to check your login is still active.
help me troubleshoot my gh pr create command
✔️ Awesome, I've got everything!
Great. Double check you have saved both files and the PR is created on GitHub.
ⓧ I'd like to double check the full code
Here is the full tasks.py on your recovered branch:
# Task Tracker - Production Git Workflow Simulation
tasks = [
{
"id": 1,
"title": "Set up CI/CD pipeline",
"priority": "high",
"deadline": "2026-07-14",
"status": "in-progress"
},
{
"id": 2,
"title": "Write unit tests",
"priority": "medium",
"deadline": "2026-07-21",
"status": "todo"
},
{
"id": 3,
"title": "Update documentation",
"priority": "low",
"deadline": "2026-07-28",
"status": "done"
}
]
def display_tasks(task_list):
"""Display all tasks with their details."""
print("\n=== Task Tracker ===\n")
for task in task_list:
print(f"[{task['id']}] {task['title']}")
print(f" Priority: {task['priority']}")
print(f" Deadline: {task['deadline']}")
print(f" Status: {task['status']}")
print()
def filter_by_priority(task_list, priority):
"""Filter tasks by priority level."""
return [t for t in task_list if t["priority"] == priority]
def filter_by_status(task_list, status):
"""Filter tasks by status."""
return [t for t in task_list if t["status"] == status]
if __name__ == "__main__":
display_tasks(tasks)
print("--- High Priority Tasks ---")
display_tasks(filter_by_priority(tasks, "high"))
And here is the full README.md:
# Task Tracker
A simple Python task tracker built to practice production Git workflows.
## Features
- Task priority levels (high, medium, low)
- Deadline tracking
- Status tracking (todo, in-progress, done)
- Filter by priority or status
## Usage
```bash
python tasks.py
```
## Development Workflow
This project uses GitHub Flow:
1. Create a feature branch from `main`
2. Make changes and commit
3. Open a pull request
4. Resolve any conflicts via rebase
5. Squash/rebase merge into `main`
You just recovered from a scenario that makes junior developers panic and experienced developers reach calmly for git reflog. This skill alone has saved countless hours of rework on production teams.
Secret mission
Simulate a Production Hotfix with Cherry-Pick
Your team just discovered a critical bug in the display function. The fix exists on a feature branch, but the full feature is not ready to ship. Can you surgically extract just the bugfix and deploy it to main without disrupting other work in flight?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on GitHub (free public repository) and locally on your machine. There are no ongoing costs or paid resources to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Public GitHub repository (git-workflow-sim).
- Local project folder (git-workflow-sim on your Desktop).
- GitHub CLI (gh) installed via winget.
Keep everything running
No action needed. Choose this if you want to keep the repository as a portfolio piece.
- Your git-workflow-sim repository stays on GitHub as a public showcase of your Git workflow skills. Interviewers can see your PR history, conflict resolutions, and clean commit messages.
- The local git-workflow-sim folder stays on your Desktop for future reference.
- GitHub CLI (gh) remains installed for future projects.
Pause - I'll come back to this later
There are no running processes or paid services to pause. Everything is already at rest.
- Your GitHub repository is static. It costs nothing and runs nothing while idle.
- Your local folder takes minimal disk space. Leave it on your Desktop until you are ready to revisit.
- GitHub CLI sits dormant and uses no resources when you are not running commands.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow the steps below to clean up completely.
Delete the GitHub repository:
- Open your git-workflow-sim repository on GitHub.
- Click Settings in the repository navigation bar.
- Scroll down to the Danger Zone section.
- Click Delete this repository.
- Click I want to delete this repository.
- Read the warnings and click I have read and understand these effects.
- Type git-workflow-sim to confirm, then click Delete this repository.
Delete the local project folder:
- In Git Bash, remove the folder by running:
cd ~/Desktop
rm -rf git-workflow-sim
- If you prefer PowerShell or Command Prompt, run:
cd ~\Desktop
Remove-Item -Recurse -Force git-workflow-sim
Uninstall GitHub CLI (optional):
- If you do not plan to use GitHub CLI in other projects, uninstall it by running this in Command Prompt or PowerShell:
winget uninstall --id GitHub.cli
- If you plan to use gh for other repositories in the future, skip this step.
Nice Work!
Nice Work!
You just simulated a full production Git workflow from start to finish. Your repository now tells the story of a developer who can handle the exact scenarios that cause panic on real teams.
Here's what you accomplished:
- Resolved real merge conflicts during a rebase operation and force-pushed safely with --force-with-lease.
- Used interactive rebase to squash messy commits into clean, PR-ready history before merging.
- Recovered a "lost" commit using git reflog after simulating a disaster scenario.
- Secret Mission: Cherry-picked a hotfix commit to main while feature work continued on a separate branch.
Ready to quiz yourself?