From a0a9a5a0d98de11ff3d8b135b8215365d2df3d38 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Sun, 11 Jan 2026 14:50:10 +0100 Subject: [PATCH 01/61] feat: initial branching+merge+conflicts module merge --- .../03-branching-and-merging/README.md | 839 ++++++++++++++++++ .../03-branching-and-merging/challenge | 1 + .../03-branching-and-merging/reset.ps1 | 216 +++++ .../03-branching-and-merging/setup.ps1 | 279 ++++++ .../03-branching-and-merging/verify.ps1 | 320 +++++++ 01_essentials/03-branching/README.md | 92 -- 01_essentials/03-branching/reset.ps1 | 26 - 01_essentials/03-branching/setup.ps1 | 85 -- 01_essentials/03-branching/verify.ps1 | 105 --- .../README.md | 0 .../reset.ps1 | 0 .../setup.ps1 | 0 .../verify.ps1 | 0 01_essentials/04-merging/README.md | 229 ----- 01_essentials/04-merging/reset.ps1 | 26 - 01_essentials/04-merging/setup.ps1 | 246 ----- 01_essentials/04-merging/verify.ps1 | 122 --- 01_essentials/05-merge-conflicts/README.md | 481 ---------- 01_essentials/05-merge-conflicts/reset.ps1 | 22 - 01_essentials/05-merge-conflicts/setup.ps1 | 97 -- 01_essentials/05-merge-conflicts/verify.ps1 | 151 ---- .../README.md | 0 .../reset.ps1 | 0 .../setup.ps1 | 0 .../verify.ps1 | 0 .../{08-stash => 06-stash}/README.md | 0 .../{08-stash => 06-stash}/reset.ps1 | 0 .../{08-stash => 06-stash}/setup.ps1 | 0 .../{08-stash => 06-stash}/verify.ps1 | 0 .../FACILITATOR-SETUP.md | 0 .../README.md | 0 README.md | 86 +- 32 files changed, 1696 insertions(+), 1727 deletions(-) create mode 100644 01_essentials/03-branching-and-merging/README.md create mode 160000 01_essentials/03-branching-and-merging/challenge create mode 100755 01_essentials/03-branching-and-merging/reset.ps1 create mode 100755 01_essentials/03-branching-and-merging/setup.ps1 create mode 100755 01_essentials/03-branching-and-merging/verify.ps1 delete mode 100644 01_essentials/03-branching/README.md delete mode 100644 01_essentials/03-branching/reset.ps1 delete mode 100644 01_essentials/03-branching/setup.ps1 delete mode 100644 01_essentials/03-branching/verify.ps1 rename 01_essentials/{06-cherry-pick => 04-cherry-pick}/README.md (100%) rename 01_essentials/{06-cherry-pick => 04-cherry-pick}/reset.ps1 (100%) rename 01_essentials/{06-cherry-pick => 04-cherry-pick}/setup.ps1 (100%) rename 01_essentials/{06-cherry-pick => 04-cherry-pick}/verify.ps1 (100%) delete mode 100644 01_essentials/04-merging/README.md delete mode 100644 01_essentials/04-merging/reset.ps1 delete mode 100644 01_essentials/04-merging/setup.ps1 delete mode 100644 01_essentials/04-merging/verify.ps1 delete mode 100644 01_essentials/05-merge-conflicts/README.md delete mode 100644 01_essentials/05-merge-conflicts/reset.ps1 delete mode 100644 01_essentials/05-merge-conflicts/setup.ps1 delete mode 100644 01_essentials/05-merge-conflicts/verify.ps1 rename 01_essentials/{07-reset-vs-revert => 05-reset-vs-revert}/README.md (100%) rename 01_essentials/{07-reset-vs-revert => 05-reset-vs-revert}/reset.ps1 (100%) rename 01_essentials/{07-reset-vs-revert => 05-reset-vs-revert}/setup.ps1 (100%) rename 01_essentials/{07-reset-vs-revert => 05-reset-vs-revert}/verify.ps1 (100%) rename 01_essentials/{08-stash => 06-stash}/README.md (100%) rename 01_essentials/{08-stash => 06-stash}/reset.ps1 (100%) rename 01_essentials/{08-stash => 06-stash}/setup.ps1 (100%) rename 01_essentials/{08-stash => 06-stash}/verify.ps1 (100%) rename 01_essentials/{09-multiplayer => 07-multiplayer}/FACILITATOR-SETUP.md (100%) rename 01_essentials/{09-multiplayer => 07-multiplayer}/README.md (100%) diff --git a/01_essentials/03-branching-and-merging/README.md b/01_essentials/03-branching-and-merging/README.md new file mode 100644 index 0000000..55c028b --- /dev/null +++ b/01_essentials/03-branching-and-merging/README.md @@ -0,0 +1,839 @@ +# Module 03: Branching and Merging + +## About This Module + +Welcome to Module 03! This module is different from the others - it uses a **checkpoint system** that lets you work through three related concepts in one continuous repository: + +1. **Branching Basics** - Create and work with feature branches +2. **Merging Branches** - Combine branches together +3. **Resolving Merge Conflicts** - Fix conflicts when Git can't merge automatically + +Instead of three separate modules, you'll progress through checkpoints in a single Git repository, building on each previous section. You can jump between checkpoints, skip ahead, or restart any section at any time! + +### Why Checkpoints? + +Branching, merging, and conflict resolution are naturally connected - you can't understand merging without branches, and you can't master conflicts without trying to merge. The checkpoint system lets you learn these concepts as a continuous workflow, just like real development. + +## Quick Start + +### Setup + +Create the challenge environment: + +```bash +.\setup.ps1 +``` + +This creates a complete Git repository with all checkpoints ready. + +### Working with Checkpoints + +**View available checkpoints:** +```bash +.\reset.ps1 +``` + +**Jump to a specific checkpoint:** +```bash +.\reset.ps1 start # Checkpoint 1: Branching Basics +.\reset.ps1 merge # Checkpoint 2: Merging Branches +.\reset.ps1 merge-conflict # Checkpoint 3: Resolving Conflicts +``` + +**Verify your progress:** +```bash +.\verify.ps1 # Verify all checkpoints complete +.\verify.ps1 start # Verify Checkpoint 1 only +.\verify.ps1 merge # Verify Checkpoint 2 only +.\verify.ps1 merge-conflict # Verify Checkpoint 3 only +``` + +### Recommended Workflow + +Complete checkpoints in order: +1. Start with Checkpoint 1 (Branching Basics) +2. Progress to Checkpoint 2 (Merging) +3. Finish with Checkpoint 3 (Merge Conflicts) + +Or skip to any checkpoint if you already know the earlier concepts! + +--- + +## Checkpoint 1: Branching Basics + +### Learning Objectives + +- Understand what a branch is in Git +- Create new branches with `git switch -c` +- Switch between branches with `git switch` +- View all branches with `git branch` +- Understand that branches are independent lines of development + +### Your Task + +Create a feature branch called `feature-login`, add a `login.py` file, and make commits to demonstrate that branches allow independent development. + +**Steps:** + +1. Navigate to the challenge directory: `cd challenge` +2. Create a new branch: `git switch -c feature-login` +3. Create a file: `login.py` (with any content you like) +4. Commit your file: `git add login.py && git commit -m "Add login module"` +5. Make another change to `login.py` and commit it +6. Switch back to main: `git switch main` +7. Notice that `login.py` doesn't exist on main! +8. Switch back to your feature: `git switch feature-login` +9. Notice that `login.py` exists again! + +**Verify:** Run `.\verify.ps1 start` to check your solution. + +### What is a Branch? + +A **branch** in Git is an independent line of development. Think of it as a parallel universe for your code - you can make changes without affecting the main timeline. + +**Visual representation:** + +``` +main: A---B---C + \ +feature-login: D---E +``` + +- Both branches share commits A and B +- Branch `main` continues with commit C +- Branch `feature-login` goes in a different direction with commits D and E +- Changes in one branch don't affect the other! + +### Why Use Branches? + +Branches let you: +- **Experiment safely** - Try new ideas without breaking main +- **Work in parallel** - Multiple features can be developed simultaneously +- **Organize work** - Each feature/fix gets its own branch +- **Collaborate better** - Team members work on separate branches + +### Key Concepts + +- **Branch**: A lightweight movable pointer to a commit +- **HEAD**: A pointer showing which branch you're currently on +- **main**: The default branch (formerly called "master") +- **Feature branch**: A branch created for a specific feature or task + +### Useful Commands + +```bash +# View all branches (current branch marked with *) +git branch + +# Create a new branch +git branch feature-login + +# Switch to a branch +git switch feature-login + +# Create AND switch in one command +git switch -c feature-login + +# Switch back to previous branch +git switch - + +# Delete a branch (only if merged) +git branch -d feature-login + +# Force delete a branch +git branch -D feature-login +``` + +### Understanding HEAD + +`HEAD` is Git's way of saying "you are here." It points to your current branch. + +When you run `git switch main`, HEAD moves to point to main. +When you run `git switch feature-login`, HEAD moves to point to feature-login. + +--- + +## Checkpoint 2: Merging Branches + +**Prerequisites:** Complete Checkpoint 1 OR run `.\reset.ps1 merge` + +### Learning Objectives + +- Understand what merging means in Git +- Merge a feature branch back into main +- Use `git merge` to combine branches +- Understand merge commits +- Visualize merged branches with `git log --graph` + +### Your Task + +You've completed work on your `feature-login` branch. Now merge it back into `main` to include the login functionality in your main codebase. + +**Scenario:** +- You created the `feature-login` branch and added login functionality +- Meanwhile, development continued on `main` (README and app.py were added) +- Now you need to merge your login feature into main + +**Steps:** + +1. Make sure you're in the challenge directory: `cd challenge` +2. Check which branch you're on: `git branch` +3. Switch to main if needed: `git switch main` +4. View the branch structure: `git log --oneline --graph --all` +5. Merge feature-login into main: `git merge feature-login` +6. View the result: `git log --oneline --graph --all` + +**Verify:** Run `.\verify.ps1 merge` to check your solution. + +### What is Merging? + +**Merging** is the process of combining changes from one branch into another. + +Think of it like combining two streams into one river - all the water (code) flows together. + +#### Before Merging + +You have two branches with different work: + +``` +main: A---B---C---D + \ +feature-login: E---F +``` + +- Main branch progressed with commits C and D +- Feature-login branch has commits E and F +- They diverged at commit B + +#### After Merging + +You bring the feature branch into main: + +``` +main: A---B---C---D---M + \ / +feature-login: E-----F +``` + +- Commit M is a **merge commit** - it combines both branches +- Main now has all the work from both branches +- Your login feature is now part of main! + +### How to Merge + +Merging is simple - just two steps: + +**1. Switch to the branch you want to merge INTO:** +```bash +git switch main +``` +This is the branch that will receive the changes. + +**2. Merge the other branch:** +```bash +git merge feature-login +``` +This brings changes from `feature-login` into `main`. + +**That's it!** Git automatically combines the changes. + +### Understanding Merge Commits + +When you merge, Git creates a special commit called a **merge commit**. + +**What makes it special?** +- It has TWO parent commits (one from each branch) +- It represents the point where branches come back together +- The message typically says "Merge branch 'feature-login'" + +**See your merge commit:** +```bash +git log --oneline +``` + +Look for the merge commit at the top - it will say something like: +``` +abc1234 Merge branch 'feature-login' +``` + +### Types of Merges + +**Three-way merge** (what you just did): +- Both branches have new commits +- Git creates a merge commit +- History shows both branches clearly + +**Fast-forward merge**: +- Main hasn't changed since the branch was created +- Git just moves the main pointer forward +- No merge commit needed! + +``` +# Before (fast-forward merge) +main: A---B + \ +feature: C---D + +# After (main just moves forward) +main: A---B---C---D +``` + +### Visualizing Branches + +The `--graph` flag is your best friend: + +```bash +git log --oneline --graph --all +``` + +**What the graph shows:** +- `*` = A commit +- `|` = A branch line +- `/` and `\` = Branches splitting/joining +- Branch names in parentheses + +**Example output:** +``` +* a1b2c3d (HEAD -> main) Merge branch 'feature-login' +|\ +| * e4f5g6h (feature-login) Add password validation +| * i7j8k9l Add login module +* | m1n2o3p Add README documentation +* | q4r5s6t Add app.py entry point +|/ +* u7v8w9x Add main functionality +* y1z2a3b Initial commit +``` + +### Useful Commands + +```bash +# Merge a branch into your current branch +git merge + +# Abort a merge if something goes wrong +git merge --abort + +# View merge commits only +git log --merges + +# View branch structure +git log --oneline --graph --all + +# See which branches have been merged into main +git branch --merged main + +# See which branches haven't been merged +git branch --no-merged main +``` + +--- + +## Checkpoint 3: Resolving Merge Conflicts + +**Prerequisites:** Complete Checkpoint 2 OR run `.\reset.ps1 merge-conflict` + +### Learning Objectives + +- Understand what merge conflicts are and why they occur +- Identify merge conflicts in your repository +- Read and interpret conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) +- Resolve merge conflicts manually +- Complete a merge after resolving conflicts + +### Your Task + +You have an `update-config` branch that modified `config.json`, and the main branch also modified `config.json` in a different way. When you try to merge, Git can't automatically combine them - you'll need to resolve the conflict manually. + +**Your mission:** +1. Attempt to merge the `update-config` branch into `main` +2. Git will tell you there's a conflict - don't panic! +3. Resolve the conflict by keeping BOTH settings (timeout AND debug) +4. Complete the merge + +**Steps:** + +1. Make sure you're in challenge directory: `cd challenge` +2. Verify you're on main: `git branch` +3. Try to merge: `git merge update-config` +4. Git will report a conflict! +5. Open `config.json` in your text editor +6. Follow the resolution guide below +7. Save the file +8. Stage the resolved file: `git add config.json` +9. Complete the merge: `git commit` + +**Verify:** Run `.\verify.ps1 merge-conflict` to check your solution. + +### What Are Merge Conflicts? + +A **merge conflict** occurs when Git cannot automatically combine changes because both branches modified the same part of the same file. + +**Example scenario:** +``` +main branch: changes line 5 to: "timeout": 5000 +update-config: changes line 5 to: "debug": true +``` + +Git doesn't know which one you want (or if you want both)! So it asks you to decide. + +**When do conflicts happen?** +- ✅ Two branches modify the same lines in a file +- ✅ One branch deletes a file that another branch modifies +- ✅ Complex changes Git can't merge automatically +- ❌ Different files are changed (no conflict!) +- ❌ Different parts of the same file are changed (no conflict!) + +**Don't fear conflicts!** They're a normal part of collaborative development. Git just needs your help to decide what the final code should look like. + +### Step-by-Step: Resolving Your First Conflict + +#### Step 1: Attempt the Merge + +```bash +cd challenge +git merge update-config +``` + +**You'll see:** +``` +Auto-merging config.json +CONFLICT (content): Merge conflict in config.json +Automatic merge failed; fix conflicts and then commit the result. +``` + +**Don't panic!** This is normal. Git is just asking for your help. + +#### Step 2: Check What Happened + +```bash +git status +``` + +**You'll see:** +``` +On branch main +You have unmerged paths. + (fix conflicts and run "git commit") + (use "git merge --abort" to abort the merge) + +Unmerged paths: + (use "git add ..." to mark resolution) + both modified: config.json +``` + +This tells you that `config.json` needs your attention! + +#### Step 3: Open the Conflicted File + +Open `config.json` in your text editor. You'll see special **conflict markers**: + +```json +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, +<<<<<<< HEAD + "timeout": 5000 +======= + "debug": true +>>>>>>> update-config + } +} +``` + +#### Step 4: Understand the Conflict Markers + +``` +<<<<<<< HEAD + "timeout": 5000 ← Your current branch (main) +======= + "debug": true ← The branch you're merging (update-config) +>>>>>>> update-config +``` + +**What each marker means:** +- `<<<<<<< HEAD` - Start of your changes (current branch) +- `=======` - Separator between the two versions +- `>>>>>>> update-config` - End of their changes (branch being merged) + +#### Step 5: Decide What to Keep + +You have three options: + +**Option 1: Keep ONLY your changes (timeout)** +```json + "timeout": 5000 +``` + +**Option 2: Keep ONLY their changes (debug)** +```json + "debug": true +``` + +**Option 3: Keep BOTH changes** ← This is what we want! +```json + "timeout": 5000, + "debug": true +``` + +For this challenge, choose **Option 3** - keep both settings! + +#### Step 6: Edit the File + +Delete ALL the conflict markers and keep both settings: + +**Before (with conflict markers):** +```json +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, +<<<<<<< HEAD + "timeout": 5000 +======= + "debug": true +>>>>>>> update-config + } +} +``` + +**After (resolved):** +```json +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, + "timeout": 5000, + "debug": true + } +} +``` + +**Important:** +- Remove `<<<<<<< HEAD` +- Remove `=======` +- Remove `>>>>>>> update-config` +- Keep both the timeout and debug settings +- Ensure valid JSON syntax (notice the comma after timeout!) + +#### Step 7: Save the File + +Save `config.json` with your changes. + +#### Step 8: Stage the Resolved File + +Tell Git you've resolved the conflict: + +```bash +git add config.json +``` + +#### Step 9: Check Status + +```bash +git status +``` + +**You'll see:** +``` +On branch main +All conflicts fixed but you are still merging. + (use "git commit" to conclude merge) +``` + +Perfect! Git confirms the conflict is resolved. + +#### Step 10: Complete the Merge + +Commit the merge: + +```bash +git commit +``` + +Git will open an editor with a default merge message. You can accept it or customize it. + +**Done!** Your merge is complete! + +### Common Mistakes to Avoid + +❌ **Forgetting to remove conflict markers** +```json +<<<<<<< HEAD ← Don't leave these in! + "timeout": 5000, + "debug": true +>>>>>>> update-config ← Don't leave these in! +``` +This breaks your code! Always remove ALL markers. + +❌ **Committing without staging** +```bash +git commit # Error! You didn't add the file +``` +Always `git add` the resolved file first! + +❌ **Keeping only one side when both are needed** +If you delete one setting, you lose that work. For this challenge, you need BOTH! + +❌ **Breaking syntax** +```json +"timeout": 5000 ← Missing comma! +"debug": true +``` +Always verify your file is valid after resolving! + +### Aborting a Merge + +Changed your mind? You can abort the merge anytime: + +```bash +git merge --abort +``` + +This returns your repository to the state before you started the merge. No harm done! + +### Useful Commands + +```bash +# Attempt a merge +git merge + +# Check which files have conflicts +git status + +# Abort the merge and start over +git merge --abort + +# After resolving conflicts: +git add +git commit + +# View conflicts in a different style +git diff --ours # Your changes +git diff --theirs # Their changes +git diff --base # Original version +``` + +### Pro Tips + +💡 **Prevent conflicts** +- Pull changes frequently: `git pull` +- Communicate with your team about who's working on what +- Keep branches short-lived and merge often + +💡 **Make conflicts easier** +- Work on different files when possible +- If you must edit the same file, coordinate with teammates +- Make small, focused commits + +💡 **When stuck** +- Read the conflict markers carefully +- Look at `git log` to understand what each side changed +- Ask a teammate to review your resolution +- Use a merge tool: `git mergetool` + +--- + +## Complete Command Reference + +### Branching + +```bash +git branch # List all branches +git branch feature-name # Create a new branch +git switch branch-name # Switch to a branch +git switch -c feature-name # Create and switch +git switch - # Switch to previous branch +git branch -d feature-name # Delete branch (if merged) +git branch -D feature-name # Force delete branch +``` + +### Merging + +```bash +git merge branch-name # Merge a branch into current branch +git merge --no-ff branch-name # Force a merge commit +git merge --abort # Abort a merge in progress +git log --merges # View only merge commits +``` + +### Viewing History + +```bash +git log --oneline --graph --all # Visual branch structure +git log --oneline # Compact commit list +git log --graph --decorate --all # Detailed branch view +git log main..feature-login # Commits in feature not in main +git diff main...feature-login # Changes between branches +``` + +### Conflict Resolution + +```bash +git status # See conflicted files +git diff # View conflicts +git add resolved-file # Mark file as resolved +git commit # Complete the merge +git merge --abort # Give up and start over +``` + +### Checkpoint Commands (This Module) + +```bash +.\reset.ps1 # Show available checkpoints +.\reset.ps1 start # Jump to Checkpoint 1 +.\reset.ps1 merge # Jump to Checkpoint 2 +.\reset.ps1 merge-conflict # Jump to Checkpoint 3 +.\verify.ps1 # Verify all complete +.\verify.ps1 start # Verify Checkpoint 1 +.\verify.ps1 merge # Verify Checkpoint 2 +.\verify.ps1 merge-conflict # Verify Checkpoint 3 +``` + +--- + +## Troubleshooting + +### "I'm on the wrong branch!" + +```bash +git switch main # Switch to main +git branch # Verify current branch +``` + +### "I made commits on the wrong branch!" + +Don't panic! You can move them: + +```bash +# You're on main but should be on feature-login +git switch feature-login # Switch to correct branch +git merge main # Bring the commits over +git switch main +git reset --hard HEAD~1 # Remove from main (careful!) +``` + +Or use cherry-pick (covered in a later module). + +### "The merge created a mess!" + +Abort and try again: + +```bash +git merge --abort +git status # Verify you're back to clean state +``` + +### "I want to start this checkpoint over!" + +Use the reset script: + +```bash +.\reset.ps1 start # Go back to Checkpoint 1 +``` + +This resets your repository to the beginning of that checkpoint. + +### "I can't find my branch!" + +List all branches: + +```bash +git branch --all # Shows all branches including remote +``` + +The branch might have been deleted after merging (this is normal!). + +### "How do I know which checkpoint I'm on?" + +```bash +.\reset.ps1 # Shows current checkpoint +git log --oneline --graph --all --decorate # Shows all tags/branches +``` + +--- + +## Real-World Workflow Example + +Here's how professional developers use these skills: + +**Day 1: Start a new feature** +```bash +git switch main +git pull # Get latest changes +git switch -c feature-dark-mode # New feature branch +# ... make changes ... +git add . +git commit -m "Add dark mode toggle" +``` + +**Day 2: Continue work** +```bash +git switch feature-dark-mode # Resume work +# ... make more changes ... +git add . +git commit -m "Add dark mode styles" +``` + +**Day 3: Ready to merge** +```bash +git switch main +git pull # Get latest main +git switch feature-dark-mode +git merge main # Bring main's changes into feature +# Resolve any conflicts +git switch main +git merge feature-dark-mode # Merge feature into main +git push # Share with team +git branch -d feature-dark-mode # Clean up +``` + +**This is exactly what you just practiced!** + +--- + +## What You've Learned + +By completing all three checkpoints, you now understand: + +### Checkpoint 1: Branching Basics +- ✅ Branches create independent lines of development +- ✅ `git switch -c` creates and switches to a new branch +- ✅ Changes in one branch don't affect others +- ✅ Branches are lightweight and easy to create + +### Checkpoint 2: Merging Branches +- ✅ Merging combines work from two branches +- ✅ Merge commits have two parent commits +- ✅ `git merge` brings changes into your current branch +- ✅ Three-way merges create a merge commit + +### Checkpoint 3: Resolving Merge Conflicts +- ✅ Conflicts happen when the same lines are changed differently +- ✅ Conflict markers show both versions +- ✅ You choose what the final code should look like +- ✅ Conflicts are normal and easy to resolve with practice + +--- + +## Next Steps + +**Completed the module?** Great work! You're ready to move on. + +**Want more practice?** Jump to any checkpoint and try again: +```bash +.\reset.ps1 start # Practice branching +.\reset.ps1 merge # Practice merging +.\reset.ps1 merge-conflict # Practice conflict resolution +``` + +**Ready for the next module?** +Continue to Module 04 to learn about cherry-picking specific commits! + +--- + +**Need help?** Review the relevant checkpoint section above, or run `git status` to see what Git suggests! diff --git a/01_essentials/03-branching-and-merging/challenge b/01_essentials/03-branching-and-merging/challenge new file mode 160000 index 0000000..40d5f6c --- /dev/null +++ b/01_essentials/03-branching-and-merging/challenge @@ -0,0 +1 @@ +Subproject commit 40d5f6c19ca1ed3a264d326118297304caabc2da diff --git a/01_essentials/03-branching-and-merging/reset.ps1 b/01_essentials/03-branching-and-merging/reset.ps1 new file mode 100755 index 0000000..7027dc4 --- /dev/null +++ b/01_essentials/03-branching-and-merging/reset.ps1 @@ -0,0 +1,216 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Resets the challenge environment to a specific checkpoint. + +.DESCRIPTION + This script allows you to jump to any checkpoint in the module, + resetting your repository to that state. Useful for skipping ahead, + starting over, or practicing specific sections. + +.PARAMETER Checkpoint + The checkpoint to reset to: start, merge, or merge-conflict. + If not specified, displays help information. + +.EXAMPLE + .\reset.ps1 + Shows available checkpoints and current status. + +.EXAMPLE + .\reset.ps1 start + Resets to the beginning (branching basics section). + +.EXAMPLE + .\reset.ps1 merge + Jumps to the merging section (feature-login branch already exists). + +.EXAMPLE + .\reset.ps1 merge-conflict + Jumps to the conflict resolution section (merge already complete). +#> + +param( + [ValidateSet('start', 'merge', 'merge-conflict', '')] + [string]$Checkpoint = '' +) + +# Checkpoint to tag mapping +$checkpointTags = @{ + 'start' = 'checkpoint-start' + 'merge' = 'checkpoint-merge' + 'merge-conflict' = 'checkpoint-merge-conflict' +} + +# Checkpoint descriptions +$checkpointDescriptions = @{ + 'start' = 'Branching Basics - Create and work with feature branches' + 'merge' = 'Merging Branches - Merge feature-login into main' + 'merge-conflict' = 'Resolving Conflicts - Fix merge conflicts in config.json' +} + +# ============================================================================ +# Display help if no checkpoint specified +# ============================================================================ +if ($Checkpoint -eq '') { + Write-Host "`n=== Module 03: Branching and Merging - Checkpoints ===" -ForegroundColor Cyan + Write-Host "`nAvailable checkpoints:" -ForegroundColor White + Write-Host "" + + foreach ($key in @('start', 'merge', 'merge-conflict')) { + $desc = $checkpointDescriptions[$key] + Write-Host " $key" -ForegroundColor Green -NoNewline + Write-Host " - $desc" -ForegroundColor White + } + + Write-Host "`nUsage:" -ForegroundColor Cyan + Write-Host " .\reset.ps1 " -ForegroundColor White + Write-Host "" + Write-Host "Examples:" -ForegroundColor Cyan + Write-Host " .\reset.ps1 start # Start from the beginning" -ForegroundColor White + Write-Host " .\reset.ps1 merge # Jump to merging section" -ForegroundColor White + Write-Host " .\reset.ps1 merge-conflict # Jump to conflict resolution" -ForegroundColor White + Write-Host "" + + # Try to detect current checkpoint + if (Test-Path "challenge/.git") { + Push-Location "challenge" + $currentBranch = git branch --show-current 2>$null + $currentCommit = git rev-parse HEAD 2>$null + + # Check which checkpoint we're at + $currentCheckpoint = $null + foreach ($cp in @('start', 'merge', 'merge-conflict')) { + $tagCommit = git rev-parse $checkpointTags[$cp] 2>$null + if ($currentCommit -eq $tagCommit) { + $currentCheckpoint = $cp + break + } + } + + if ($currentCheckpoint) { + Write-Host "Current checkpoint: " -ForegroundColor Yellow -NoNewline + Write-Host "$currentCheckpoint" -ForegroundColor Green -NoNewline + Write-Host " (on branch $currentBranch)" -ForegroundColor Yellow + } else { + Write-Host "Current status: " -ForegroundColor Yellow -NoNewline + Write-Host "In progress (on branch $currentBranch)" -ForegroundColor White + } + + Pop-Location + } + + Write-Host "" + exit 0 +} + +# ============================================================================ +# Validate challenge directory exists +# ============================================================================ +if (-not (Test-Path "challenge")) { + Write-Host "[ERROR] Challenge directory not found." -ForegroundColor Red + Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow + exit 1 +} + +if (-not (Test-Path "challenge/.git")) { + Write-Host "[ERROR] No git repository found in challenge directory." -ForegroundColor Red + Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow + exit 1 +} + +# Navigate to challenge directory +Push-Location "challenge" + +# ============================================================================ +# Verify the checkpoint tag exists +# ============================================================================ +$targetTag = $checkpointTags[$Checkpoint] +$tagExists = git tag -l $targetTag + +if (-not $tagExists) { + Write-Host "[ERROR] Checkpoint tag '$targetTag' not found." -ForegroundColor Red + Write-Host "Run ..\setup.ps1 to recreate the challenge environment." -ForegroundColor Yellow + Pop-Location + exit 1 +} + +# ============================================================================ +# Check for uncommitted changes +# ============================================================================ +$statusOutput = git status --porcelain 2>$null + +if ($statusOutput) { + Write-Host "`n[WARNING] You have uncommitted changes!" -ForegroundColor Yellow + Write-Host "The following changes will be lost:" -ForegroundColor Yellow + Write-Host "" + git status --short + Write-Host "" + + $response = Read-Host "Continue and discard all changes? (y/N)" + if ($response -ne 'y' -and $response -ne 'Y') { + Write-Host "`nReset cancelled." -ForegroundColor Cyan + Pop-Location + exit 0 + } +} + +# ============================================================================ +# Reset to checkpoint +# ============================================================================ +Write-Host "`nResetting to checkpoint: $Checkpoint" -ForegroundColor Cyan +Write-Host "Description: $($checkpointDescriptions[$Checkpoint])" -ForegroundColor White +Write-Host "" + +try { + # Reset to the checkpoint tag + git reset --hard $targetTag 2>&1 | Out-Null + + # Clean untracked files + git clean -fd 2>&1 | Out-Null + + # Ensure we're on main branch + $currentBranch = git branch --show-current + if ($currentBranch -ne 'main') { + git switch main 2>&1 | Out-Null + git reset --hard $targetTag 2>&1 | Out-Null + } + + Write-Host "[SUCCESS] Reset to checkpoint '$Checkpoint' complete!" -ForegroundColor Green + Write-Host "" + + # Show what to do next + switch ($Checkpoint) { + 'start' { + Write-Host "Next steps:" -ForegroundColor Cyan + Write-Host " 1. Create a new branch: git switch -c feature-login" -ForegroundColor White + Write-Host " 2. Create login.py and make 2+ commits" -ForegroundColor White + Write-Host " 3. Verify: ..\verify.ps1 start" -ForegroundColor White + } + 'merge' { + Write-Host "Next steps:" -ForegroundColor Cyan + Write-Host " 1. View branch structure: git log --oneline --graph --all" -ForegroundColor White + Write-Host " 2. Merge feature-login: git merge feature-login" -ForegroundColor White + Write-Host " 3. Verify: ..\verify.ps1 merge" -ForegroundColor White + } + 'merge-conflict' { + Write-Host "Next steps:" -ForegroundColor Cyan + Write-Host " 1. Attempt merge: git merge update-config" -ForegroundColor White + Write-Host " 2. Resolve conflicts in config.json" -ForegroundColor White + Write-Host " 3. Complete merge: git add config.json && git commit" -ForegroundColor White + Write-Host " 4. Verify: ..\verify.ps1 merge-conflict" -ForegroundColor White + } + } + + Write-Host "" + Write-Host "View current state: git log --oneline --graph --all" -ForegroundColor Cyan + Write-Host "" + +} catch { + Write-Host "[ERROR] Failed to reset to checkpoint." -ForegroundColor Red + Write-Host $_.Exception.Message -ForegroundColor Red + Pop-Location + exit 1 +} + +Pop-Location +exit 0 diff --git a/01_essentials/03-branching-and-merging/setup.ps1 b/01_essentials/03-branching-and-merging/setup.ps1 new file mode 100755 index 0000000..8d15534 --- /dev/null +++ b/01_essentials/03-branching-and-merging/setup.ps1 @@ -0,0 +1,279 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Sets up the Module 03 checkpoint-based challenge environment. + +.DESCRIPTION + This script creates a challenge directory with a complete Git repository + containing all commits and checkpoints for learning branching, merging, + and merge conflict resolution in one continuous workflow. + + The script creates three checkpoints: + - checkpoint-start: Beginning of branching basics + - checkpoint-merge: Beginning of merging section + - checkpoint-merge-conflict: Beginning of conflict resolution +#> + +Write-Host "`n=== Setting up Module 03: Branching and Merging ===" -ForegroundColor Cyan + +# Remove existing challenge directory if it exists +if (Test-Path "challenge") { + Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" +} + +# Create fresh challenge directory +Write-Host "Creating challenge directory..." -ForegroundColor Green +New-Item -ItemType Directory -Path "challenge" | Out-Null +Set-Location "challenge" + +# Initialize Git repository +Write-Host "Initializing Git repository..." -ForegroundColor Green +git init | Out-Null + +# Configure git for this repository +git config user.name "Workshop Student" +git config user.email "student@example.com" + +# ============================================================================ +# PHASE 1: Branching Basics - Initial commits on main +# ============================================================================ +Write-Host "`nPhase 1: Creating initial project structure..." -ForegroundColor Cyan + +# Commit 1: Initial commit +$mainContent = @" +# main.py - Main application file + +def main(): + print("Welcome to the Application!") + print("This is the main branch") + +if __name__ == "__main__": + main() +"@ +Set-Content -Path "main.py" -Value $mainContent +git add . +git commit -m "Initial commit" | Out-Null + +# Commit 2: Add main functionality +$mainContent = @" +# main.py - Main application file + +def main(): + print("Welcome to the Application!") + print("This is the main branch") + run_application() + +def run_application(): + print("Application is running...") + print("Ready for new features!") + +if __name__ == "__main__": + main() +"@ +Set-Content -Path "main.py" -Value $mainContent +git add . +git commit -m "Add main functionality" | Out-Null + +# Tag checkpoint-start (students begin here - will create feature-login) +Write-Host "Creating checkpoint: start" -ForegroundColor Green +git tag checkpoint-start + +# ============================================================================ +# PHASE 2: Create feature-login branch (what students will do in checkpoint 1) +# ============================================================================ +Write-Host "Phase 2: Creating feature-login branch..." -ForegroundColor Cyan + +# Create and switch to feature-login branch +git switch -c feature-login | Out-Null + +# Commit 3: Add login module +$loginContent = @" +# login.py - User login module + +def login(username, password): + """Authenticate a user.""" + print(f"Authenticating user: {username}") + # TODO: Add actual authentication logic + return True + +def logout(username): + """Log out a user.""" + print(f"Logging out user: {username}") + return True +"@ +Set-Content -Path "login.py" -Value $loginContent +git add . +git commit -m "Add login module" | Out-Null + +# Commit 4: Add password validation +$loginContent = @" +# login.py - User login module + +def validate_password(password): + """Validate password strength.""" + if len(password) < 8: + return False + return True + +def login(username, password): + """Authenticate a user.""" + if not validate_password(password): + print("Password too weak!") + return False + print(f"Authenticating user: {username}") + # TODO: Add actual authentication logic + return True + +def logout(username): + """Log out a user.""" + print(f"Logging out user: {username}") + return True +"@ +Set-Content -Path "login.py" -Value $loginContent +git add . +git commit -m "Add password validation" | Out-Null + +# Switch back to main +git switch main | Out-Null + +# Now create divergence - add commits to main while feature-login exists +Write-Host "Creating divergent history on main..." -ForegroundColor Cyan + +# Commit 5: Add app.py with basic functionality +$appContent = @" +# app.py - Main application entry point + +from main import main + +def run(): + """Run the application.""" + print("Starting application...") + main() + print("Application finished.") + +if __name__ == "__main__": + run() +"@ +Set-Content -Path "app.py" -Value $appContent +git add . +git commit -m "Add app.py entry point" | Out-Null + +# Commit 6: Add README +$readmeContent = @" +# My Application + +Welcome to my application! + +## Features + +- Main functionality +- More features coming soon + +## Setup + +Run: python app.py +"@ +Set-Content -Path "README.md" -Value $readmeContent +git add . +git commit -m "Add README documentation" | Out-Null + +# Tag checkpoint-merge (students begin merging here - divergent branches ready) +Write-Host "Creating checkpoint: merge" -ForegroundColor Green +git tag checkpoint-merge + +# ============================================================================ +# PHASE 3: Merge feature-login into main (what students will do in checkpoint 2) +# ============================================================================ +Write-Host "Phase 3: Merging feature-login into main..." -ForegroundColor Cyan + +# Merge feature-login into main (will create three-way merge commit) +git merge feature-login --no-edit | Out-Null + +# ============================================================================ +# PHASE 4: Create conflict scenario (what students will do in checkpoint 3) +# ============================================================================ +Write-Host "Phase 4: Creating merge conflict scenario..." -ForegroundColor Cyan + +# Create config.json file on main +$initialConfig = @" +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000 + } +} +"@ +Set-Content -Path "config.json" -Value $initialConfig +git add config.json +git commit -m "Add initial configuration" | Out-Null + +# On main branch: Add timeout setting +$mainConfig = @" +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, + "timeout": 5000 + } +} +"@ +Set-Content -Path "config.json" -Value $mainConfig +git add config.json +git commit -m "Add timeout configuration" | Out-Null + +# Create update-config branch from the commit before timeout was added +git switch -c update-config HEAD~1 | Out-Null + +# On update-config branch: Add debug setting (conflicting change) +$featureConfig = @" +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, + "debug": true + } +} +"@ +Set-Content -Path "config.json" -Value $featureConfig +git add config.json +git commit -m "Add debug mode configuration" | Out-Null + +# Switch back to main +git switch main | Out-Null + +# Tag checkpoint-merge-conflict (students begin conflict resolution here - on main with timeout, update-config has debug) +Write-Host "Creating checkpoint: merge-conflict" -ForegroundColor Green +git tag checkpoint-merge-conflict + +# ============================================================================ +# Reset to checkpoint-start so students begin at the beginning +# ============================================================================ +Write-Host "`nResetting to checkpoint-start..." -ForegroundColor Yellow +git reset --hard checkpoint-start | Out-Null +git clean -fd | Out-Null + +# Return to module directory +Set-Location .. + +Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green +Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan +Write-Host "`nThis module uses a CHECKPOINT SYSTEM:" -ForegroundColor Yellow +Write-Host " You'll work through 3 sections in one continuous repository:" -ForegroundColor White +Write-Host " 1. Branching Basics (checkpoint: start)" -ForegroundColor White +Write-Host " 2. Merging Branches (checkpoint: merge)" -ForegroundColor White +Write-Host " 3. Resolving Merge Conflicts (checkpoint: merge-conflict)" -ForegroundColor White +Write-Host "`nCommands:" -ForegroundColor Cyan +Write-Host " .\reset.ps1 - Show available checkpoints" -ForegroundColor White +Write-Host " .\reset.ps1 start - Jump to branching section" -ForegroundColor White +Write-Host " .\reset.ps1 merge - Jump to merging section" -ForegroundColor White +Write-Host " .\verify.ps1 - Verify all sections complete" -ForegroundColor White +Write-Host " .\verify.ps1 start - Verify only branching section" -ForegroundColor White +Write-Host "`nNext steps:" -ForegroundColor Cyan +Write-Host " 1. Read the README.md for detailed instructions" -ForegroundColor White +Write-Host " 2. cd challenge" -ForegroundColor White +Write-Host " 3. Start with Checkpoint 1: Branching Basics" -ForegroundColor White +Write-Host "" diff --git a/01_essentials/03-branching-and-merging/verify.ps1 b/01_essentials/03-branching-and-merging/verify.ps1 new file mode 100755 index 0000000..8311d87 --- /dev/null +++ b/01_essentials/03-branching-and-merging/verify.ps1 @@ -0,0 +1,320 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Verifies the Module 03 challenge solution (checkpoint-aware). + +.DESCRIPTION + This script can verify completion of individual checkpoints or + the entire module. Without arguments, it verifies all checkpoints. + +.PARAMETER Checkpoint + The checkpoint to verify: start, merge, or merge-conflict. + If not specified, verifies all checkpoints. + +.EXAMPLE + .\verify.ps1 + Verifies all three checkpoints are complete. + +.EXAMPLE + .\verify.ps1 start + Verifies only the branching basics checkpoint. + +.EXAMPLE + .\verify.ps1 merge + Verifies only the merging checkpoint. +#> + +param( + [ValidateSet('start', 'merge', 'merge-conflict', '')] + [string]$Checkpoint = '' +) + +$script:allChecksPassed = $true + +# ============================================================================ +# Helper Functions +# ============================================================================ + +function Write-Pass { + param([string]$Message) + Write-Host "[PASS] $Message" -ForegroundColor Green +} + +function Write-Fail { + param([string]$Message) + Write-Host "[FAIL] $Message" -ForegroundColor Red + $script:allChecksPassed = $false +} + +function Write-Hint { + param([string]$Message) + Write-Host "[HINT] $Message" -ForegroundColor Yellow +} + +# ============================================================================ +# Checkpoint 1: Branching Basics Verification +# ============================================================================ + +function Verify-Branching { + Write-Host "`n=== Checkpoint 1: Branching Basics ===" -ForegroundColor Cyan + + # Save current branch + $originalBranch = git branch --show-current 2>$null + + # Check if feature-login branch exists + $branchExists = git branch --list "feature-login" 2>$null + if ($branchExists) { + Write-Pass "Branch 'feature-login' exists" + } else { + Write-Fail "Branch 'feature-login' not found" + Write-Hint "Create the branch with: git switch -c feature-login" + return + } + + # Check if feature-login has commits beyond main (or if they've been merged) + $commitCount = git rev-list main..feature-login --count 2>$null + $mergeCommitExists = (git log --merges --oneline 2>$null | Select-String "Merge.*feature-login") + + if ($mergeCommitExists -and $commitCount -eq 0) { + # Commits were merged into main - this is correct! + Write-Pass "Branch 'feature-login' commits have been merged into main" + } elseif ($commitCount -ge 2) { + Write-Pass "Branch 'feature-login' has $commitCount new commits" + } else { + Write-Fail "Branch 'feature-login' needs at least 2 new commits (found: $commitCount)" + Write-Hint "Make sure you've committed login.py and made at least one more commit" + } + + # Switch to feature-login and check for login.py + git switch feature-login 2>$null | Out-Null + if (Test-Path "login.py") { + Write-Pass "File 'login.py' exists in feature-login branch" + } else { + Write-Fail "File 'login.py' not found in feature-login branch" + Write-Hint "Create login.py and commit it to the feature-login branch" + } + + # Switch to main and verify login.py doesn't exist there yet (unless merged) + git switch main 2>$null | Out-Null + + # Check if merge happened - if so, login.py can exist on main + $mergeCommitExists = (git log --merges --oneline 2>$null | Select-String "Merge.*feature-login") + + if (-not $mergeCommitExists) { + # No merge yet - login.py should NOT be on main + if (-not (Test-Path "login.py")) { + Write-Pass "File 'login.py' does NOT exist in main branch (branches are independent!)" + } else { + Write-Fail "File 'login.py' should not exist in main branch yet (before merge)" + Write-Hint "Make sure you created login.py only on the feature-login branch" + } + } + + # Switch back to original branch + if ($originalBranch) { + git switch $originalBranch 2>$null | Out-Null + } +} + +# ============================================================================ +# Checkpoint 2: Merging Verification +# ============================================================================ + +function Verify-Merging { + Write-Host "`n=== Checkpoint 2: Merging Branches ===" -ForegroundColor Cyan + + # Check current branch is main + $currentBranch = git branch --show-current 2>$null + if ($currentBranch -eq "main") { + Write-Pass "Currently on main branch" + } else { + Write-Fail "Should be on main branch (currently on: $currentBranch)" + Write-Hint "Switch to main with: git switch main" + } + + # Check if login.py exists on main (indicates merge happened) + if (Test-Path "login.py") { + Write-Pass "File 'login.py' exists on main branch (merged successfully)" + } else { + Write-Fail "File 'login.py' not found on main branch" + Write-Hint "Merge feature-login into main with: git merge feature-login" + } + + # Check for merge commit + $mergeCommitExists = (git log --merges --oneline 2>$null | Select-String "Merge.*feature-login") + + if ($mergeCommitExists) { + Write-Pass "Merge commit exists" + } else { + Write-Fail "No merge commit found" + Write-Hint "Create a merge commit with: git merge feature-login" + } + + # Check commit count (should have both branches' commits) + $commitCount = [int](git rev-list --count HEAD 2>$null) + if ($commitCount -ge 6) { + Write-Pass "Repository has $commitCount commits (merge complete)" + } else { + Write-Fail "Repository should have at least 6 commits after merge (found: $commitCount)" + } +} + +# ============================================================================ +# Checkpoint 3: Merge Conflicts Verification +# ============================================================================ + +function Verify-MergeConflicts { + Write-Host "`n=== Checkpoint 3: Resolving Merge Conflicts ===" -ForegroundColor Cyan + + # Check current branch is main + $currentBranch = git branch --show-current 2>$null + if ($currentBranch -eq "main") { + Write-Pass "Currently on main branch" + } else { + Write-Fail "Should be on main branch (currently on: $currentBranch)" + Write-Hint "Switch to main with: git switch main" + } + + # Check that merge is not in progress + if (Test-Path ".git/MERGE_HEAD") { + Write-Fail "Merge is still in progress (conflicts not resolved)" + Write-Hint "Resolve conflicts in config.json, then: git add config.json && git commit" + return + } else { + Write-Pass "No merge in progress (conflicts resolved)" + } + + # Check if config.json exists + if (Test-Path "config.json") { + Write-Pass "File 'config.json' exists" + } else { + Write-Fail "File 'config.json' not found" + Write-Hint "Merge update-config branch with: git merge update-config" + return + } + + # Verify config.json is valid JSON + try { + $configContent = Get-Content "config.json" -Raw + $config = $configContent | ConvertFrom-Json -ErrorAction Stop + Write-Pass "File 'config.json' is valid JSON" + } catch { + Write-Fail "File 'config.json' is not valid JSON" + Write-Hint "Make sure you removed all conflict markers (<<<<<<<, =======, >>>>>>>)" + return + } + + # Check for conflict markers + if ($configContent -match '<<<<<<<|=======|>>>>>>>') { + Write-Fail "Conflict markers still present in config.json" + Write-Hint "Remove all conflict markers (<<<<<<<, =======, >>>>>>>)" + return + } else { + Write-Pass "No conflict markers in config.json" + } + + # Verify both settings are present (timeout and debug) + if ($config.app.timeout -eq 5000) { + Write-Pass "Timeout setting preserved (5000)" + } else { + Write-Fail "Timeout setting missing or incorrect" + Write-Hint "Keep the timeout: 5000 setting from main branch" + } + + if ($config.app.debug -eq $true) { + Write-Pass "Debug setting preserved (true)" + } else { + Write-Fail "Debug setting missing or incorrect" + Write-Hint "Keep the debug: true setting from update-config branch" + } + + # Verify merge commit exists for update-config + $updateConfigMerge = (git log --merges --oneline 2>$null | Select-String "Merge.*update-config") + if ($updateConfigMerge) { + Write-Pass "Merge commit exists for update-config branch" + } else { + Write-Fail "No merge commit found for update-config" + Write-Hint "Complete the merge with: git commit (after resolving conflicts)" + } +} + +# ============================================================================ +# Main Script Logic +# ============================================================================ + +# Check if challenge directory exists +if (-not (Test-Path "challenge")) { + Write-Host "[ERROR] Challenge directory not found." -ForegroundColor Red + Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow + exit 1 +} + +Push-Location "challenge" + +# Check if git repository exists +if (-not (Test-Path ".git")) { + Write-Host "[ERROR] Not a git repository." -ForegroundColor Red + Write-Host "Run ..\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow + Pop-Location + exit 1 +} + +# Run appropriate verification +if ($Checkpoint -eq '') { + # Verify all checkpoints + Write-Host "`n=== Verifying All Checkpoints ===" -ForegroundColor Cyan + + Verify-Branching + Verify-Merging + Verify-MergeConflicts + +} else { + # Verify specific checkpoint + switch ($Checkpoint) { + 'start' { Verify-Branching } + 'merge' { Verify-Merging } + 'merge-conflict' { Verify-MergeConflicts } + } +} + +Pop-Location + +# Final summary +Write-Host "" +if ($script:allChecksPassed) { + Write-Host "=========================================" -ForegroundColor Green + Write-Host " CONGRATULATIONS! CHALLENGE PASSED!" -ForegroundColor Green + Write-Host "=========================================" -ForegroundColor Green + + if ($Checkpoint -eq '') { + Write-Host "`nYou've completed the entire module!" -ForegroundColor Cyan + Write-Host "You've mastered:" -ForegroundColor Cyan + Write-Host " ✓ Creating and working with branches" -ForegroundColor White + Write-Host " ✓ Merging branches together" -ForegroundColor White + Write-Host " ✓ Resolving merge conflicts" -ForegroundColor White + Write-Host "`nReady for the next module!" -ForegroundColor Green + } else { + Write-Host "`nCheckpoint '$Checkpoint' complete!" -ForegroundColor Cyan + + switch ($Checkpoint) { + 'start' { + Write-Host "Next: Move to the merging checkpoint" -ForegroundColor White + Write-Host " ..\reset.ps1 merge OR continue to merge feature-login" -ForegroundColor Yellow + } + 'merge' { + Write-Host "Next: Move to the conflict resolution checkpoint" -ForegroundColor White + Write-Host " ..\reset.ps1 merge-conflict" -ForegroundColor Yellow + } + 'merge-conflict' { + Write-Host "Module complete! Ready for the next module!" -ForegroundColor Green + } + } + } + Write-Host "" + exit 0 +} else { + Write-Host "[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red + Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow + Write-Host "" + exit 1 +} diff --git a/01_essentials/03-branching/README.md b/01_essentials/03-branching/README.md deleted file mode 100644 index cc361db..0000000 --- a/01_essentials/03-branching/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Module 03: Branching Basics - -## Learning Objectives - -In this module, you will: -- Understand what a branch is in Git -- Create new branches using `git branch` or `git switch -c` -- Switch between branches using `git switch` -- View all branches with `git branch` -- Understand that branches allow parallel development - -## Challenge - -### Setup - -Run the setup script to create your challenge environment: - -```powershell -.\setup.ps1 -``` - -This will create a `challenge/` directory with a Git repository that has some initial commits on the main branch. - -### Your Task - -Your goal is to create a feature branch, make commits on it, and understand how branches work independently. - -**Steps:** - -1. Create a new branch called `feature-login` -2. Switch to the new branch -3. Create a new file `login.py` with some login functionality -4. Commit the new file to your feature branch -5. Make another change to `login.py` and commit it -6. Switch back to the main branch and observe that `login.py` doesn't exist there - -**Suggested Approach:** - -1. Navigate to the challenge directory: `cd challenge` -2. View existing branches: `git branch` -3. Create and switch to new branch: `git switch -c feature-login` -4. Create `login.py` with any content you like -5. Stage and commit: `git add login.py` and `git commit -m "Add login functionality"` -6. Modify `login.py`, then commit again -7. Switch back to main: `git switch main` -8. Run `ls` (or check your file explorer) and notice that `login.py` doesn't exist on main! -9. Switch back to feature-login: `git switch feature-login` -10. Run `ls` (or check your file explorer) again and see that `login.py` is back! - -> **Important Notes:** -> - Use `git switch` to change branches -> - `git switch -c ` creates and switches in one command -> - Branches are independent - files in one branch don't affect another until you merge -> - You can switch between branches as many times as you want - -## Key Concepts - -- **Branch**: A lightweight movable pointer to a commit. Branches allow you to work on different features independently. -- **HEAD**: A pointer to the current branch you're working on. When you switch branches, HEAD moves. -- **main/master**: The default branch name in Git (main is the modern convention, master is older). -- **Feature Branch**: A branch created to develop a specific feature, separate from the main codebase. - -## Useful Commands - -```bash -git branch # List all branches (* shows current) -git branch # Create a new branch -git switch # Switch to an existing branch -git switch -c # Create and switch to new branch -git switch - # Switch back to previous branch -git branch -d # Delete a branch (we won't use this yet) -``` - -## Verification - -Once you've completed the challenge, verify your solution: - -```powershell -.\verify.ps1 -``` - -The verification script will check that you've created the branch, made commits, and that the branches are independent. - -## Need to Start Over? - -If you want to reset the challenge and start fresh: - -```powershell -.\reset.ps1 -``` - -This will remove the challenge directory and run the setup script again, giving you a clean slate. diff --git a/01_essentials/03-branching/reset.ps1 b/01_essentials/03-branching/reset.ps1 deleted file mode 100644 index cd38f66..0000000 --- a/01_essentials/03-branching/reset.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Resets the Module 03 challenge environment. - -.DESCRIPTION - This script removes the existing challenge directory and runs - the setup script again to create a fresh challenge environment. -#> - -Write-Host "`n=== Resetting Module 03 Challenge ===" -ForegroundColor Cyan - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} else { - Write-Host "No existing challenge directory found." -ForegroundColor Cyan -} - -Write-Host "" -Write-Host "----------------------------------------" -ForegroundColor Cyan -Write-Host "" - -# Run setup script -& "$PSScriptRoot\setup.ps1" diff --git a/01_essentials/03-branching/setup.ps1 b/01_essentials/03-branching/setup.ps1 deleted file mode 100644 index 6548f40..0000000 --- a/01_essentials/03-branching/setup.ps1 +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Sets up the Module 03 challenge environment for learning about branches. - -.DESCRIPTION - This script creates a challenge directory with a Git repository that - contains a couple of commits on the main branch. Students will create - a feature branch and make commits on it. -#> - -Write-Host "`n=== Setting up Module 03 Challenge ===" -ForegroundColor Cyan - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} - -# Create fresh challenge directory -Write-Host "Creating challenge directory..." -ForegroundColor Green -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize Git repository -Write-Host "Initializing Git repository..." -ForegroundColor Green -git init | Out-Null - -# Configure git for this repository -git config user.name "Workshop Student" -git config user.email "student@example.com" - -# Commit 1: Initial commit -Write-Host "Creating initial project..." -ForegroundColor Green -$mainContent = @" -# main.py - Main application file - -def main(): - print("Welcome to the Application!") - print("This is the main branch") - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "main.py" -Value $mainContent - -git add . -git commit -m "Initial commit" | Out-Null - -# Commit 2: Add main functionality -Write-Host "Adding main functionality..." -ForegroundColor Green -$mainContent = @" -# main.py - Main application file - -def main(): - print("Welcome to the Application!") - print("This is the main branch") - run_application() - -def run_application(): - print("Application is running...") - print("Ready for new features!") - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "main.py" -Value $mainContent - -git add . -git commit -m "Add main functionality" | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green -Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan -Write-Host "`nNext steps:" -ForegroundColor Cyan -Write-Host " 1. cd challenge" -ForegroundColor White -Write-Host " 2. Create a new branch: git switch -c feature-login" -ForegroundColor White -Write-Host " 3. Create login.py and commit it" -ForegroundColor White -Write-Host " 4. Make another commit on the feature branch" -ForegroundColor White -Write-Host " 5. Switch back to main: git switch main" -ForegroundColor White -Write-Host " 6. Observe that login.py doesn't exist on main!" -ForegroundColor White -Write-Host " 7. Run '..\verify.ps1' to check your solution" -ForegroundColor White -Write-Host "" diff --git a/01_essentials/03-branching/verify.ps1 b/01_essentials/03-branching/verify.ps1 deleted file mode 100644 index b8b3eb0..0000000 --- a/01_essentials/03-branching/verify.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Verifies the Module 03 challenge solution. - -.DESCRIPTION - This script checks that: - - The challenge directory exists - - A Git repository exists - - The feature-login branch exists - - The branch has at least 2 new commits - - login.py exists in the feature branch but not in main -#> - -Write-Host "`n=== Verifying Module 03 Solution ===" -ForegroundColor Cyan - -$allChecksPassed = $true - -# Check if challenge directory exists -if (-not (Test-Path "challenge")) { - Write-Host "[FAIL] Challenge directory not found. Did you run setup.ps1?" -ForegroundColor Red - exit 1 -} - -Set-Location "challenge" - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] Not a git repository. Did you run setup.ps1?" -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Save current branch -$originalBranch = git branch --show-current 2>$null - -# Check if feature-login branch exists -$branchExists = git branch --list "feature-login" 2>$null -if ($branchExists) { - Write-Host "[PASS] Branch 'feature-login' exists" -ForegroundColor Green -} else { - Write-Host "[FAIL] Branch 'feature-login' not found" -ForegroundColor Red - Write-Host "[HINT] Create the branch with: git switch -c feature-login" -ForegroundColor Yellow - $allChecksPassed = $false - Set-Location .. - exit 1 -} - -# Check if feature-login has commits beyond main -$commitCount = git rev-list main..feature-login --count 2>$null -if ($commitCount -ge 2) { - Write-Host "[PASS] Branch 'feature-login' has $commitCount new commits" -ForegroundColor Green -} else { - Write-Host "[FAIL] Branch 'feature-login' needs at least 2 new commits (found: $commitCount)" -ForegroundColor Red - Write-Host "[HINT] Make sure you've committed login.py and made at least one more commit" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Switch to feature-login and check for login.py -git checkout feature-login 2>$null | Out-Null -if (Test-Path "login.py") { - Write-Host "[PASS] File 'login.py' exists in feature-login branch" -ForegroundColor Green -} else { - Write-Host "[FAIL] File 'login.py' not found in feature-login branch" -ForegroundColor Red - Write-Host "[HINT] Create login.py and commit it to the feature-login branch" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Switch to main and verify login.py doesn't exist -git switch main 2>$null | Out-Null -if (-not (Test-Path "login.py")) { - Write-Host "[PASS] File 'login.py' does NOT exist in main branch (branches are independent!)" -ForegroundColor Green -} else { - Write-Host "[FAIL] File 'login.py' should not exist in main branch" -ForegroundColor Red - Write-Host "[HINT] Make sure you created login.py only on the feature-login branch" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Switch back to original branch -if ($originalBranch) { - git switch $originalBranch 2>$null | Out-Null -} - -Set-Location .. - -# Final summary -if ($allChecksPassed) { - Write-Host "`n" -NoNewline - Write-Host "=====================================" -ForegroundColor Green - Write-Host " CONGRATULATIONS! CHALLENGE PASSED!" -ForegroundColor Green - Write-Host "=====================================" -ForegroundColor Green - Write-Host "`nYou've successfully learned about Git branches!" -ForegroundColor Cyan - Write-Host "You now understand:" -ForegroundColor Cyan - Write-Host " - How to create branches with git switch -c" -ForegroundColor White - Write-Host " - How to switch between branches" -ForegroundColor White - Write-Host " - That branches are independent lines of development" -ForegroundColor White - Write-Host " - That files in one branch don't affect another" -ForegroundColor White - Write-Host "`nReady for the next module!" -ForegroundColor Green - Write-Host "" -} else { - Write-Host "`n[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red - Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow - Write-Host "" - exit 1 -} diff --git a/01_essentials/06-cherry-pick/README.md b/01_essentials/04-cherry-pick/README.md similarity index 100% rename from 01_essentials/06-cherry-pick/README.md rename to 01_essentials/04-cherry-pick/README.md diff --git a/01_essentials/06-cherry-pick/reset.ps1 b/01_essentials/04-cherry-pick/reset.ps1 similarity index 100% rename from 01_essentials/06-cherry-pick/reset.ps1 rename to 01_essentials/04-cherry-pick/reset.ps1 diff --git a/01_essentials/06-cherry-pick/setup.ps1 b/01_essentials/04-cherry-pick/setup.ps1 similarity index 100% rename from 01_essentials/06-cherry-pick/setup.ps1 rename to 01_essentials/04-cherry-pick/setup.ps1 diff --git a/01_essentials/06-cherry-pick/verify.ps1 b/01_essentials/04-cherry-pick/verify.ps1 similarity index 100% rename from 01_essentials/06-cherry-pick/verify.ps1 rename to 01_essentials/04-cherry-pick/verify.ps1 diff --git a/01_essentials/04-merging/README.md b/01_essentials/04-merging/README.md deleted file mode 100644 index b3c388c..0000000 --- a/01_essentials/04-merging/README.md +++ /dev/null @@ -1,229 +0,0 @@ -# Module 04: Merging Branches - -## Learning Objectives - -In this module, you will: -- Understand what merging means in Git -- Merge a feature branch back into main -- Use `git merge` to combine branches -- Visualize merged branches with `git log --graph` - -## Challenge - -### Setup - -Run the setup script to create your challenge environment: - -```powershell -.\setup.ps1 -``` - -This will create a `challenge/` directory with a Git repository that has a main branch and a feature branch. - -### Your Task - -You've been working on a new login feature in a separate branch. Now it's time to merge your work back into the main branch! - -**Scenario:** -- You created a `feature-login` branch to add login functionality -- You made some commits on that branch -- Meanwhile, your teammate updated the README on main -- Now you need to bring your login feature back into main - -**Steps:** - -1. Navigate to the challenge directory: `cd challenge` -2. Check which branch you're on: `git branch` -3. View all branches: `git log --oneline --graph --all` -4. Merge the feature-login branch into main: `git merge feature-login` -5. View the result: `git log --oneline --graph --all` - -> **That's it!** Merging is how you bring work from one branch into another. - -## What is Merging? - -**Merging** is the process of taking changes from one branch and bringing them into another branch. - -Think of it like combining two streams into one river - all the water (code) flows together. - -### Before Merging - -You have two branches with different work: - -``` -main: A---B---C - \ -feature-login: D---E -``` - -- Main branch has commits A, B, and C -- Feature-login branch has commits D and E -- They split apart at commit B - -### After Merging - -You bring the feature branch into main: - -``` -main: A---B---C---M - \ / -feature-login: D-E -``` - -- Commit M is a **merge commit** - it combines both branches -- Main now has all the work from both branches -- Your login feature is now part of main! - -## How to Merge - -Merging is simple - just two steps: - -**1. Switch to the branch you want to merge INTO** -```bash -git switch main -``` -This is the branch that will receive the changes. - -**2. Merge the other branch** -```bash -git merge feature-login -``` -This brings changes from feature-login into main. - -**That's it!** Git does the work of combining the changes. - -## Understanding the Merge Commit - -When you merge, Git creates a special commit called a **merge commit**. - -**What makes it special?** -- It has TWO parent commits (one from each branch) -- It represents the point where branches came back together -- Git writes a message like "Merge branch 'feature-login'" - -You can view the merge commit just like any other commit: -```bash -git show HEAD -``` - -## Visualizing Merges - -Use `git log --graph` to see how branches merged: - -```bash -git log --oneline --graph --all -``` - -**Example output:** -``` -* a1b2c3d (HEAD -> main) Merge branch 'feature-login' -|\ -| * e5f6g7h (feature-login) Implement login validation -| * h8i9j0k Add login form -* | k1l2m3n Update README with setup instructions -|/ -* n4o5p6q Initial project structure -``` - -**Reading the graph:** -- `*` = A commit -- `|` = Branch line -- `/` and `\` = Branches splitting or joining -- `(HEAD -> main)` = You are here - -The graph shows how the branches split and came back together! - -## Useful Commands - -```bash -# Merging -git merge # Merge a branch into current branch - -# Viewing -git log --oneline --graph # See branch history visually -git log --oneline --graph --all # Include all branches - -# Branch management -git branch # List branches -git switch # Switch to a branch -git branch -d # Delete a branch (after merging) -``` - -## Common Questions - -### "What if I'm on the wrong branch when I merge?" - -Don't worry! The branch you're currently on is the one that receives the changes. - -**Example:** -```bash -git switch main # Go to main -git merge feature-login # Bring feature-login INTO main -``` - -Always switch to the destination branch first! - -### "Can I undo a merge?" - -Yes! Before you push to a remote: -```bash -git reset --hard HEAD~1 -``` - -This removes the merge commit. (We'll cover this more in later modules) - -### "What happens to the feature branch after merging?" - -The feature branch still exists! The merge just copies its commits into main. - -You can delete it if you're done: -```bash -git branch -d feature-login # Safe delete (only if merged) -``` - -The commits are still in history - you're just removing the branch label. - -### "What if Git can't merge automatically?" - -Sometimes Git needs your help when the same lines were changed in both branches. This is called a **merge conflict**. - -Don't worry - we'll learn how to handle conflicts in the next module! - -If you encounter a conflict now and want to cancel: -```bash -git merge --abort -``` - -## Verification - -Once you've merged the feature-login branch, verify your solution: - -```powershell -.\verify.ps1 -``` - -The verification script will check that you've successfully merged. - -## Need to Start Over? - -If you want to reset the challenge and start fresh: - -```powershell -.\reset.ps1 -``` - -## What's Next? - -**Next module:** Merge Conflicts - Learn what to do when Git can't automatically merge changes. - -**Later:** In the advanced modules, you'll learn about different merging strategies and when to use them. For now, understanding basic merging is all you need! - -## Quick Summary - -✅ **Merging** combines work from one branch into another -✅ Switch to the destination branch, then run `git merge ` -✅ Git creates a **merge commit** to record the merge -✅ Use `git log --graph` to visualize how branches merged -✅ The feature branch still exists after merging - you can delete it if you want - -That's all there is to basic merging! 🎉 diff --git a/01_essentials/04-merging/reset.ps1 b/01_essentials/04-merging/reset.ps1 deleted file mode 100644 index 576a8fa..0000000 --- a/01_essentials/04-merging/reset.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Resets the Module 04 challenge environment. - -.DESCRIPTION - This script removes the existing challenge directory and runs - the setup script again to create a fresh challenge environment. -#> - -Write-Host "`n=== Resetting Module 04 Challenge ===" -ForegroundColor Cyan - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} else { - Write-Host "No existing challenge directory found." -ForegroundColor Cyan -} - -Write-Host "" -Write-Host "----------------------------------------" -ForegroundColor Cyan -Write-Host "" - -# Run setup script -& "$PSScriptRoot\setup.ps1" diff --git a/01_essentials/04-merging/setup.ps1 b/01_essentials/04-merging/setup.ps1 deleted file mode 100644 index b9f31ca..0000000 --- a/01_essentials/04-merging/setup.ps1 +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Sets up the Module 04 challenge environment for learning about merging. - -.DESCRIPTION - This script creates a challenge directory with a Git repository that - contains two divergent branches ready to merge (three-way merge scenario). -#> - -Write-Host "`n=== Setting up Module 04 Challenge ===" -ForegroundColor Cyan - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} - -# Create fresh challenge directory -Write-Host "Creating challenge directory..." -ForegroundColor Green -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize Git repository -Write-Host "Initializing Git repository..." -ForegroundColor Green -git init | Out-Null - -# Configure git for this repository -git config user.name "Workshop Student" -git config user.email "student@example.com" - -# Commit 1: Initial project structure on main -Write-Host "Creating initial project structure..." -ForegroundColor Green -$readmeContent = @" -# My Project - -A simple web application project. - -## Setup - -Coming soon... -"@ -Set-Content -Path "README.md" -Value $readmeContent - -$appContent = @" -# app.py - Main application file - -def main(): - print("Welcome to My App!") - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Initial project structure" | Out-Null - -# Commit 2: Add configuration file on main -Write-Host "Adding configuration..." -ForegroundColor Green -$configContent = @" -# config.py - Configuration settings - -APP_NAME = "My Project" -VERSION = "1.0.0" -DEBUG = False -"@ -Set-Content -Path "config.py" -Value $configContent - -git add . -git commit -m "Add basic configuration" | Out-Null - -# Commit 3: Add database utilities on main -Write-Host "Adding database utilities..." -ForegroundColor Green -$dbContent = @" -# database.py - Database utilities - -def connect(): - """Connect to database.""" - print("Connecting to database...") - return True - -def disconnect(): - """Disconnect from database.""" - print("Disconnecting...") - return True -"@ -Set-Content -Path "database.py" -Value $dbContent - -git add . -git commit -m "Add database utilities" | Out-Null - -# Create feature-login branch -Write-Host "Creating feature-login branch..." -ForegroundColor Green -git switch -c feature-login | Out-Null - -# Commit 4: Add login module on feature-login -Write-Host "Adding login functionality on feature-login..." -ForegroundColor Green -$loginContent = @" -# login.py - User login module - -def login(username, password): - """Authenticate a user.""" - print(f"Authenticating user: {username}") - # TODO: Add actual authentication logic - return True - -def logout(username): - """Log out a user.""" - print(f"Logging out user: {username}") - return True -"@ -Set-Content -Path "login.py" -Value $loginContent - -git add . -git commit -m "Add login module" | Out-Null - -# Commit 5: Add password validation on feature-login -$loginContent = @" -# login.py - User login module - -def validate_password(password): - """Validate password strength.""" - if len(password) < 8: - return False - return True - -def login(username, password): - """Authenticate a user.""" - if not validate_password(password): - print("Password too weak!") - return False - print(f"Authenticating user: {username}") - # TODO: Add actual authentication logic - return True - -def logout(username): - """Log out a user.""" - print(f"Logging out user: {username}") - return True -"@ -Set-Content -Path "login.py" -Value $loginContent - -git add . -git commit -m "Add password validation" | Out-Null - -# Commit 6: Integrate login with app on feature-login -$appContent = @" -# app.py - Main application file -from login import login, logout - -def main(): - print("Welcome to My App!") - # Add login integration - if login("testuser", "password123"): - print("Login successful!") - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Integrate login with main app" | Out-Null - -# Commit 7: Add session management on feature-login -$sessionContent = @" -# session.py - Session management - -class Session: - def __init__(self, username): - self.username = username - self.active = True - - def end(self): - """End the session.""" - self.active = False - print(f"Session ended for {self.username}") -"@ -Set-Content -Path "session.py" -Value $sessionContent - -git add . -git commit -m "Add session management" | Out-Null - -# Switch back to main branch -Write-Host "Switching back to main branch..." -ForegroundColor Green -git switch main | Out-Null - -# Commit 8: Update README on main (creates divergence) -Write-Host "Adding documentation on main (creates divergence)..." -ForegroundColor Green -$readmeContent = @" -# My Project - -A simple web application project. - -## Setup - -1. Install Python 3.8 or higher -2. Run: python app.py - -## Features - -- User authentication (coming soon) -- Data management (coming soon) - -## Contributing - -Please follow our coding standards when contributing. -"@ -Set-Content -Path "README.md" -Value $readmeContent - -git add . -git commit -m "Update README with setup instructions" | Out-Null - -# Commit 9: Update configuration on main -$configContent = @" -# config.py - Configuration settings - -APP_NAME = "My Project" -VERSION = "1.0.0" -DEBUG = False -DATABASE_PATH = "./data/app.db" -LOG_LEVEL = "INFO" -"@ -Set-Content -Path "config.py" -Value $configContent - -git add . -git commit -m "Add database path to config" | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green -Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan -Write-Host "`nScenario: You have two divergent branches!" -ForegroundColor Yellow -Write-Host " - main: Has 5 commits (config, database utils, README updates)" -ForegroundColor White -Write-Host " - feature-login: Has 4 commits (login, validation, session)" -ForegroundColor White -Write-Host "`nNext steps:" -ForegroundColor Cyan -Write-Host " 1. cd challenge" -ForegroundColor White -Write-Host " 2. View the branch structure: git log --oneline --graph --all" -ForegroundColor White -Write-Host " 3. Merge feature-login into main: git merge feature-login" -ForegroundColor White -Write-Host " 4. View the merge result: git log --oneline --graph --all" -ForegroundColor White -Write-Host " 5. Run '..\verify.ps1' to check your solution" -ForegroundColor White -Write-Host "" diff --git a/01_essentials/04-merging/verify.ps1 b/01_essentials/04-merging/verify.ps1 deleted file mode 100644 index 6c3f9cd..0000000 --- a/01_essentials/04-merging/verify.ps1 +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Verifies the Module 04 challenge solution. - -.DESCRIPTION - This script checks that: - - The challenge directory exists - - A Git repository exists - - Currently on main branch - - feature-login has been merged into main - - A merge commit exists (three-way merge) - - Login functionality is present on main -#> - -Write-Host "`n=== Verifying Module 04 Solution ===" -ForegroundColor Cyan - -$allChecksPassed = $true - -# Check if challenge directory exists -if (-not (Test-Path "challenge")) { - Write-Host "[FAIL] Challenge directory not found. Did you run setup.ps1?" -ForegroundColor Red - exit 1 -} - -Set-Location "challenge" - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] Not a git repository. Did you run setup.ps1?" -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check current branch is main -$currentBranch = git branch --show-current 2>$null -if ($currentBranch -eq "main") { - Write-Host "[PASS] Currently on main branch" -ForegroundColor Green -} else { - Write-Host "[FAIL] Not on main branch (currently on: $currentBranch)" -ForegroundColor Red - Write-Host "[HINT] Switch to main with: git switch main" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check if feature-login branch exists -$featureLoginBranch = git branch --list "feature-login" 2>$null -if ($featureLoginBranch) { - Write-Host "[PASS] Branch 'feature-login' exists" -ForegroundColor Green -} else { - Write-Host "[INFO] Branch 'feature-login' not found (may have been deleted after merge)" -ForegroundColor Cyan -} - -# Check if login.py exists (should be on main after merge) -if (Test-Path "login.py") { - Write-Host "[PASS] File 'login.py' exists on main (from feature-login merge)" -ForegroundColor Green -} else { - Write-Host "[FAIL] File 'login.py' not found on main" -ForegroundColor Red - Write-Host "[HINT] This file should appear after merging feature-login into main" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check if app.py contains login integration -if (Test-Path "app.py") { - $appContent = Get-Content "app.py" -Raw - if ($appContent -match "login") { - Write-Host "[PASS] app.py contains login integration" -ForegroundColor Green - } else { - Write-Host "[FAIL] app.py doesn't contain login integration" -ForegroundColor Red - Write-Host "[HINT] After merging, app.py should import and use the login module" -ForegroundColor Yellow - $allChecksPassed = $false - } -} else { - Write-Host "[FAIL] app.py not found" -ForegroundColor Red - $allChecksPassed = $false -} - -# Check for merge commit (indicates three-way merge happened) -$mergeCommits = git log --merges --oneline 2>$null -if ($mergeCommits) { - Write-Host "[PASS] Merge commit found (three-way merge completed)" -ForegroundColor Green - - # Get the merge commit message - $mergeMessage = git log --merges --format=%s -1 2>$null - Write-Host "[INFO] Merge commit message: '$mergeMessage'" -ForegroundColor Cyan -} else { - Write-Host "[FAIL] No merge commit found" -ForegroundColor Red - Write-Host "[HINT] Merge feature-login into main with: git merge feature-login" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check if both branches contributed commits (true three-way merge) -$totalCommits = git rev-list --count HEAD 2>$null -if ($totalCommits -ge 4) { - Write-Host "[PASS] Repository has $totalCommits commits (branches diverged properly)" -ForegroundColor Green -} else { - Write-Host "[WARN] Repository has only $totalCommits commits (expected at least 4)" -ForegroundColor Yellow - Write-Host "[INFO] This might still be correct if you deleted commits" -ForegroundColor Cyan -} - -Set-Location .. - -# Final summary -if ($allChecksPassed) { - Write-Host "`n" -NoNewline - Write-Host "=====================================" -ForegroundColor Green - Write-Host " CONGRATULATIONS! CHALLENGE PASSED!" -ForegroundColor Green - Write-Host "=====================================" -ForegroundColor Green - Write-Host "`nYou've successfully completed your first branch merge!" -ForegroundColor Cyan - Write-Host "You now understand:" -ForegroundColor Cyan - Write-Host " - How to merge branches with git merge" -ForegroundColor White - Write-Host " - What a merge commit is and why it's created" -ForegroundColor White - Write-Host " - How divergent branches are combined" -ForegroundColor White - Write-Host " - How to visualize merges with git log --graph" -ForegroundColor White - Write-Host "`nNext up: Module 05 - Merge Conflicts!" -ForegroundColor Yellow - Write-Host "You'll learn what happens when Git can't automatically merge." -ForegroundColor Cyan - Write-Host "" -} else { - Write-Host "`n[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red - Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow - Write-Host "" - exit 1 -} diff --git a/01_essentials/05-merge-conflicts/README.md b/01_essentials/05-merge-conflicts/README.md deleted file mode 100644 index fb8ee67..0000000 --- a/01_essentials/05-merge-conflicts/README.md +++ /dev/null @@ -1,481 +0,0 @@ -# Module 05: Merge Conflicts - -## Learning Objectives - -By the end of this module, you will: -- Understand what merge conflicts are and why they occur -- Identify merge conflicts in your repository -- Read and interpret conflict markers -- Resolve merge conflicts manually step-by-step -- Complete a merge after resolving conflicts - -## Challenge - -### Setup - -Run the setup script to create your challenge environment: - -```powershell -.\setup.ps1 -``` - -This will create a `challenge/` directory with a repository that has a merge conflict waiting to happen! - -### Your Task - -You have a repository with a main branch and a feature branch called `update-config`. Both branches have modified the same configuration file in different ways, creating a merge conflict. - -**Your mission:** -1. Attempt to merge the `update-config` branch into `main` -2. Git will tell you there's a conflict - don't panic! -3. Resolve the conflict by keeping BOTH settings (timeout AND debug) -4. Complete the merge - -**Steps to follow:** - -1. Navigate to the challenge directory: `cd challenge` -2. Make sure you're on main: `git branch` -3. Try to merge: `git merge update-config` -4. Git will report a conflict! -5. Open `config.py` in your text editor -6. Follow the step-by-step guide below to resolve it -7. Save the file, stage it, and commit - -## What Are Merge Conflicts? - -A **merge conflict** occurs when Git cannot automatically combine changes because both branches modified the same lines in the same file. - -**Example scenario:** -``` -main branch changes line 5: TIMEOUT = 30 -feature branch changes line 5: DEBUG = True -``` - -Git doesn't know which one you want! So it asks you to decide. - -**When do conflicts happen?** -- ✅ Two branches modify the same line(s) -- ✅ One branch deletes a file that another branch modifies -- ❌ Different files are changed (no conflict!) -- ❌ Different lines in the same file are changed (no conflict!) - -## Step-by-Step: Resolving Your First Conflict - -### Step 1: Attempt the Merge - -```bash -cd challenge -git merge update-config -``` - -**You'll see:** -``` -Auto-merging config.py -CONFLICT (content): Merge conflict in config.py -Automatic merge failed; fix conflicts and then commit the result. -``` - -**Don't panic!** This is normal. Git is just asking for your help. - ---- - -### Step 2: Check What Happened - -```bash -git status -``` - -**You'll see:** -``` -On branch main -You have unmerged paths. - (fix conflicts and run "git commit") - (use "git merge --abort" to abort the merge) - -Unmerged paths: - (use "git add ..." to mark resolution) - both modified: config.py -``` - -This tells you that `config.py` needs your attention! - ---- - -### Step 3: Open the Conflicted File - -Open `config.py` in your text editor. You'll see something like this: - -```python -# config.py - Application configuration - -APP_NAME = "My Application" -VERSION = "1.0.0" -<<<<<<< HEAD -TIMEOUT = 30 -======= -DEBUG = True ->>>>>>> update-config -``` - -**Let's break down what you're seeing:** - ---- - -## Understanding Conflict Markers - -Git has added special markers to show you both versions: - -```python -<<<<<<< HEAD ← Start of YOUR version (current branch) -TIMEOUT = 30 ← What YOU have on main -======= ← Divider between versions -DEBUG = True ← What THEY have on update-config ->>>>>>> update-config ← End of THEIR version -``` - -**The three parts:** - -1. **`<<<<<<< HEAD`** - Everything between here and `=======` is YOUR current branch's version -2. **`=======`** - This separates the two versions -3. **`>>>>>>> update-config`** - Everything between `=======` and here is the INCOMING branch's version - ---- - -## Step 4: Decide What to Keep - -You have three options: - -**Option A: Keep YOUR version (main)** -```python -# config.py - Application configuration - -APP_NAME = "My Application" -VERSION = "1.0.0" -TIMEOUT = 30 -``` - -**Option B: Keep THEIR version (update-config)** -```python -# config.py - Application configuration - -APP_NAME = "My Application" -VERSION = "1.0.0" -DEBUG = True -``` - -**Option C: Keep BOTH (this is what we want!)** -```python -# config.py - Application configuration - -APP_NAME = "My Application" -VERSION = "1.0.0" -TIMEOUT = 30 -DEBUG = True -``` - ---- - -## Step 5: Edit the File to Resolve the Conflict - -**What the file looks like NOW (with conflict markers):** -```python -# config.py - Application configuration - -APP_NAME = "My Application" -VERSION = "1.0.0" -<<<<<<< HEAD -TIMEOUT = 30 -======= -DEBUG = True ->>>>>>> update-config -``` - -**What you need to do:** - -1. **Delete** the line `<<<<<<< HEAD` -2. **Keep** the line `TIMEOUT = 30` -3. **Delete** the line `=======` -4. **Keep** the line `DEBUG = True` -5. **Delete** the line `>>>>>>> update-config` - -**What the file should look like AFTER (conflict resolved):** -```python -# config.py - Application configuration - -APP_NAME = "My Application" -VERSION = "1.0.0" -TIMEOUT = 30 -DEBUG = True -``` - -**Save the file!** - ---- - -## Step 6: Mark the Conflict as Resolved - -Tell Git you've fixed the conflict by staging the file: - -```bash -git add config.py -``` - -This tells Git: "I've resolved the conflict in this file, it's ready to be committed." - ---- - -## Step 7: Check Your Status - -```bash -git status -``` - -**You should see:** -``` -On branch main -All conflicts fixed but you are still merging. - (use "git commit" to conclude merge) - -Changes to be committed: - modified: config.py -``` - -Great! Git knows the conflict is resolved and is ready for you to complete the merge. - ---- - -## Step 8: Complete the Merge - -```bash -git commit -``` - -Git will open your editor with a default merge message: -``` -Merge branch 'update-config' - -# Conflicts: -# config.py -``` - -You can keep this message or customize it. Save and close the editor. - -**Or use a one-liner:** -```bash -git commit -m "Merge update-config: resolve config conflict" -``` - ---- - -## Step 9: Verify the Merge - -```bash -git log --oneline --graph -``` - -You should see your merge commit! - -```bash -cat config.py -``` - -Verify the file has BOTH settings and NO conflict markers! - ---- - -## Visual Summary: Before and After - -### Before Resolution (WRONG ❌) -```python -# config.py -APP_NAME = "My Application" -VERSION = "1.0.0" -<<<<<<< HEAD -TIMEOUT = 30 -======= -DEBUG = True ->>>>>>> update-config -``` -**This will NOT run! Conflict markers are syntax errors!** - -### After Resolution (CORRECT ✅) -```python -# config.py -APP_NAME = "My Application" -VERSION = "1.0.0" -TIMEOUT = 30 -DEBUG = True -``` -**Clean code, no markers, both settings preserved!** - ---- - -## Common Mistakes to Avoid - -### ❌ Mistake 1: Forgetting to Remove Conflict Markers -```python -TIMEOUT = 30 -======= ← Still has conflict marker! -DEBUG = True -``` -**This is invalid Python code and will crash!** - -### ❌ Mistake 2: Committing Without Staging -```bash -# Wrong order: -git commit # Error! File not staged - -# Correct order: -git add config.py # Stage first -git commit # Then commit -``` - -### ❌ Mistake 3: Only Keeping One Side When You Need Both -```python -# If the task asks for BOTH settings, this is wrong: -TIMEOUT = 30 -# Missing DEBUG = True -``` - ---- - -## Useful Commands Reference - -```bash -# During a conflict -git status # See which files have conflicts -git diff # See the conflict in detail - -# Resolving -git add # Mark file as resolved -git commit # Complete the merge - -# If you want to start over -git merge --abort # Cancel the merge, go back to before - -# After resolving -git log --oneline --graph # See the merge commit -``` - ---- - -## Real-World Conflict Resolution Workflow - -**Here's what you'll do every time you have a conflict:** - -```bash -# 1. Attempt the merge -git merge feature-branch -# → Git says: "CONFLICT! Fix it!" - -# 2. Check what's wrong -git status -# → Shows which files have conflicts - -# 3. Open the file, find the markers -# <<<<<<< HEAD -# your version -# ======= -# their version -# >>>>>>> branch - -# 4. Edit the file to resolve -# Remove the markers, keep what you need - -# 5. Stage the resolved file -git add config.py - -# 6. Complete the merge -git commit -m "Resolve conflict in config.py" - -# 7. Celebrate! 🎉 -``` - ---- - -## What If You Get Stuck? - -### Option 1: Abort the Merge -```bash -git merge --abort -``` -This puts everything back to how it was before the merge. You can try again! - -### Option 2: Ask for Help -```bash -git status # See what's going on -git diff # See the conflict details -``` - ---- - -## Pro Tips - -✅ **Use `git status` constantly** - It tells you exactly what to do next - -✅ **Search for `<<<<<<<`** - Your editor's search function can find all conflicts quickly - -✅ **Test after resolving** - Make sure your code still works! - -✅ **Read both sides carefully** - Sometimes you need parts from each version - -✅ **Take your time** - Conflicts are not a race. Understand what changed and why. - ---- - -## Verification - -Once you've resolved the conflict and completed the merge, verify your solution: - -```powershell -.\verify.ps1 -``` - -The verification will check that: -- ✅ The merge conflict was resolved -- ✅ The merge was completed successfully -- ✅ BOTH settings are in the config file (TIMEOUT and DEBUG) -- ✅ NO conflict markers remain -- ✅ The merge commit exists in history - ---- - -## Need to Start Over? - -If you want to reset the challenge and start fresh: - -```powershell -.\reset.ps1 -``` - ---- - -## What You've Learned - -🎉 **Congratulations!** You can now: -- Recognize when a merge conflict occurs -- Read and understand conflict markers -- Edit files to resolve conflicts -- Stage resolved files with `git add` -- Complete a merge with `git commit` - -**This is a critical skill!** Every developer encounters merge conflicts. Now you know exactly what to do when they happen. - ---- - -## Quick Reference Card - -**When you see a conflict:** - -1. **Don't panic** ✅ -2. **Run `git status`** to see which files -3. **Open the file** in your editor -4. **Find the markers** (`<<<<<<<`, `=======`, `>>>>>>>`) -5. **Decide what to keep** (one side, other side, or both) -6. **Remove ALL markers** (the `<<<<<<<`, `=======`, `>>>>>>>` lines) -7. **Save the file** -8. **Stage it** (`git add filename`) -9. **Commit** (`git commit`) -10. **Verify** (`git status`, `git log --oneline --graph`) - -**Remember:** The conflict markers are NOT valid code - they MUST be removed! diff --git a/01_essentials/05-merge-conflicts/reset.ps1 b/01_essentials/05-merge-conflicts/reset.ps1 deleted file mode 100644 index d9c9ce0..0000000 --- a/01_essentials/05-merge-conflicts/reset.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Resets the merge conflicts challenge environment. - -.DESCRIPTION - Removes the existing challenge directory and runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "Resetting challenge environment..." -ForegroundColor Yellow - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Remove-Item -Path "challenge" -Recurse -Force - Write-Host "Removed existing challenge directory." -ForegroundColor Cyan -} - -# Run setup script -Write-Host "Running setup script...`n" -ForegroundColor Cyan -& ".\setup.ps1" diff --git a/01_essentials/05-merge-conflicts/setup.ps1 b/01_essentials/05-merge-conflicts/setup.ps1 deleted file mode 100644 index a9816c0..0000000 --- a/01_essentials/05-merge-conflicts/setup.ps1 +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Sets up the merge conflicts challenge environment. - -.DESCRIPTION - Creates a Git repository with a merge conflict scenario involving - a configuration file that has been modified differently on two branches. -#> - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Path "challenge" -Recurse -Force -} - -# Create challenge directory -Write-Host "Creating challenge environment..." -ForegroundColor Cyan -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize git repository -git init | Out-Null -git config user.name "Workshop User" | Out-Null -git config user.email "user@workshop.local" | Out-Null - -# Create initial config.json file -$initialConfig = @" -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000 - } -} -"@ - -Set-Content -Path "config.json" -Value $initialConfig -git add config.json -git commit -m "Initial configuration" | Out-Null - -# Create feature branch -git branch update-config | Out-Null - -# On main branch: Add timeout setting -$mainConfig = @" -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, - "timeout": 5000 - } -} -"@ - -Set-Content -Path "config.json" -Value $mainConfig -git add config.json -git commit -m "Add timeout configuration" | Out-Null - -# Switch to feature branch: Add debug setting (conflicting change) -git switch update-config | Out-Null - -$featureConfig = @" -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, - "debug": true - } -} -"@ - -Set-Content -Path "config.json" -Value $featureConfig -git add config.json -git commit -m "Add debug mode configuration" | Out-Null - -# Switch back to main branch -git switch main | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "Challenge environment created!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou are now on the 'main' branch." -ForegroundColor Cyan -Write-Host "There is a branch called 'update-config' with conflicting changes." -ForegroundColor Cyan -Write-Host "`nYour task:" -ForegroundColor Yellow -Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White -Write-Host "2. Try to merge the 'update-config' branch into 'main'" -ForegroundColor White -Write-Host "3. Resolve the merge conflict in config.json" -ForegroundColor White -Write-Host "4. Keep BOTH the timeout setting AND the debug setting" -ForegroundColor White -Write-Host "5. Complete the merge" -ForegroundColor White -Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/01_essentials/05-merge-conflicts/verify.ps1 b/01_essentials/05-merge-conflicts/verify.ps1 deleted file mode 100644 index 2abdca1..0000000 --- a/01_essentials/05-merge-conflicts/verify.ps1 +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Verifies the merge conflicts challenge solution. - -.DESCRIPTION - Checks that the user successfully resolved the merge conflict, - kept both configuration settings, and completed the merge. -#> - -Set-Location "challenge" -ErrorAction SilentlyContinue - -# Check if challenge directory exists -if (-not (Test-Path "../verify.ps1")) { - Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path ".")) { - Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Write-Host "Verifying your solution..." -ForegroundColor Cyan - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] No git repository found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check current branch -$currentBranch = git branch --show-current 2>$null -if ($currentBranch -ne "main") { - Write-Host "[FAIL] You should be on the 'main' branch." -ForegroundColor Red - Write-Host "Current branch: $currentBranch" -ForegroundColor Yellow - Write-Host "Hint: Use 'git checkout main' to switch to main branch" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check if there's an ongoing merge -if (Test-Path ".git/MERGE_HEAD") { - Write-Host "[FAIL] Merge is not complete. There are still unresolved conflicts." -ForegroundColor Red - Write-Host "Hint: After resolving conflicts in config.json, use:" -ForegroundColor Yellow - Write-Host " git add config.json" -ForegroundColor White - Write-Host " git commit" -ForegroundColor White - Set-Location .. - exit 1 -} - -# Check if config.json exists -if (-not (Test-Path "config.json")) { - Write-Host "[FAIL] config.json file not found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Read and parse config.json -try { - $configContent = Get-Content "config.json" -Raw - $config = $configContent | ConvertFrom-Json -} catch { - Write-Host "[FAIL] config.json is not valid JSON." -ForegroundColor Red - Write-Host "Make sure you removed all conflict markers (<<<<<<, ======, >>>>>>)" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check for conflict markers (in case user forgot to remove them) -if ($configContent -match "<<<<<<|======|>>>>>>") { - Write-Host "[FAIL] Conflict markers still present in config.json" -ForegroundColor Red - Write-Host "Remove all lines containing: <<<<<<, ======, >>>>>>" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check if both timeout and debug settings exist -$hasTimeout = $null -ne $config.app.timeout -$hasDebug = $null -ne $config.app.debug - -if (-not $hasTimeout) { - Write-Host "[FAIL] The 'timeout' setting is missing from config.json" -ForegroundColor Red - Write-Host "Hint: The resolved file should include both timeout AND debug settings" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -if (-not $hasDebug) { - Write-Host "[FAIL] The 'debug' setting is missing from config.json" -ForegroundColor Red - Write-Host "Hint: The resolved file should include both timeout AND debug settings" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Validate the values -if ($config.app.timeout -ne 5000) { - Write-Host "[FAIL] The 'timeout' value should be 5000" -ForegroundColor Red - Set-Location .. - exit 1 -} - -if ($config.app.debug -ne $true) { - Write-Host "[FAIL] The 'debug' value should be true" -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check that merge commit exists -$commitCount = (git rev-list --count HEAD 2>$null) -if ($commitCount -lt 4) { - Write-Host "[FAIL] Not enough commits. Expected at least 4 commits (including merge commit)." -ForegroundColor Red - Write-Host "Current commits: $commitCount" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that the latest commit is a merge commit (has 2 parents) -$parentCount = (git rev-list --parents -n 1 HEAD 2>$null).Split(' ').Count - 1 -if ($parentCount -ne 2) { - Write-Host "[FAIL] The latest commit should be a merge commit." -ForegroundColor Red - Write-Host "Hint: Complete the merge with 'git commit' after resolving conflicts" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that both branches are merged -$branches = git branch --merged update-config 2>$null -Write-Host "$branches $($branches -notmatch 'update-config')" -if ($branches -notmatch "update-config") { - Write-Host "[FAIL] The 'update-config' branch was not merged." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Success! -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Identified the merge conflict" -ForegroundColor White -Write-Host "- Resolved the conflict by keeping both settings" -ForegroundColor White -Write-Host "- Completed the merge with a proper merge commit" -ForegroundColor White -Write-Host "`nYou now understand how to handle merge conflicts!" -ForegroundColor Green -Write-Host "This is a critical skill for collaborative development.`n" -ForegroundColor Green - -Set-Location .. -exit 0 diff --git a/01_essentials/07-reset-vs-revert/README.md b/01_essentials/05-reset-vs-revert/README.md similarity index 100% rename from 01_essentials/07-reset-vs-revert/README.md rename to 01_essentials/05-reset-vs-revert/README.md diff --git a/01_essentials/07-reset-vs-revert/reset.ps1 b/01_essentials/05-reset-vs-revert/reset.ps1 similarity index 100% rename from 01_essentials/07-reset-vs-revert/reset.ps1 rename to 01_essentials/05-reset-vs-revert/reset.ps1 diff --git a/01_essentials/07-reset-vs-revert/setup.ps1 b/01_essentials/05-reset-vs-revert/setup.ps1 similarity index 100% rename from 01_essentials/07-reset-vs-revert/setup.ps1 rename to 01_essentials/05-reset-vs-revert/setup.ps1 diff --git a/01_essentials/07-reset-vs-revert/verify.ps1 b/01_essentials/05-reset-vs-revert/verify.ps1 similarity index 100% rename from 01_essentials/07-reset-vs-revert/verify.ps1 rename to 01_essentials/05-reset-vs-revert/verify.ps1 diff --git a/01_essentials/08-stash/README.md b/01_essentials/06-stash/README.md similarity index 100% rename from 01_essentials/08-stash/README.md rename to 01_essentials/06-stash/README.md diff --git a/01_essentials/08-stash/reset.ps1 b/01_essentials/06-stash/reset.ps1 similarity index 100% rename from 01_essentials/08-stash/reset.ps1 rename to 01_essentials/06-stash/reset.ps1 diff --git a/01_essentials/08-stash/setup.ps1 b/01_essentials/06-stash/setup.ps1 similarity index 100% rename from 01_essentials/08-stash/setup.ps1 rename to 01_essentials/06-stash/setup.ps1 diff --git a/01_essentials/08-stash/verify.ps1 b/01_essentials/06-stash/verify.ps1 similarity index 100% rename from 01_essentials/08-stash/verify.ps1 rename to 01_essentials/06-stash/verify.ps1 diff --git a/01_essentials/09-multiplayer/FACILITATOR-SETUP.md b/01_essentials/07-multiplayer/FACILITATOR-SETUP.md similarity index 100% rename from 01_essentials/09-multiplayer/FACILITATOR-SETUP.md rename to 01_essentials/07-multiplayer/FACILITATOR-SETUP.md diff --git a/01_essentials/09-multiplayer/README.md b/01_essentials/07-multiplayer/README.md similarity index 100% rename from 01_essentials/09-multiplayer/README.md rename to 01_essentials/07-multiplayer/README.md diff --git a/README.md b/README.md index 7c32828..7175f89 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,17 @@ Perfect for developers who want to move beyond basic Git usage and master profes The workshop is organized into two tracks: -### 01 Essentials - Core Git Skills (9 modules) +### 01 Essentials - Core Git Skills (7 modules) Master fundamental Git concepts and collaborative workflows: - **Module 01: Git Basics** - Initialize repositories, stage changes, make commits - **Module 02: Viewing History** - Use git log and git diff to explore project history -- **Module 03: Branching Basics** - Create, switch, and manage branches -- **Module 04: Merging** - Combine branches and understand merge workflows -- **Module 05: Merge Conflicts** - Identify, understand, and resolve merge conflicts step-by-step -- **Module 06: Cherry-Pick** - Apply specific commits from one branch to another -- **Module 07: Reset vs Revert** - Understand when to rewrite history vs create new commits -- **Module 08: Stash** - Temporarily save work without committing -- **Module 09: Multiplayer Git** - **The Great Print Project** - Real cloud-based collaboration with teammates +- **Module 03: Branching and Merging** - Create branches, merge them, and resolve conflicts (checkpoint-based) +- **Module 04: Cherry-Pick** - Apply specific commits from one branch to another +- **Module 05: Reset vs Revert** - Understand when to rewrite history vs create new commits +- **Module 06: Stash** - Temporarily save work without committing +- **Module 07: Multiplayer Git** - **The Great Print Project** - Real cloud-based collaboration with teammates ### 02 Advanced - Professional Techniques (6 modules) @@ -46,7 +44,7 @@ Advanced Git workflows for power users: **Quick Reference**: See [GIT-CHEATSHEET.md](GIT-CHEATSHEET.md) for a comprehensive list of all Git commands covered in this workshop. Don't worry about memorizing everything - use this as a reference when you need to look up command syntax! -### For Module 09: Multiplayer Git +### For Module 07: Multiplayer Git **This module is different!** It uses a real Git server for authentic collaboration: @@ -138,9 +136,9 @@ You should see your name and email printed. This is required to make commits in $PSVersionTable.PSVersion ``` -### Python (for Module 09 only) +### Python (for Module 07 only) -Module 09 (Multiplayer Git) uses Python: +Module 07 (Multiplayer Git) uses Python: - **Python 3.6+** required to run the Great Print Project - Check: `python --version` or `python3 --version` @@ -211,7 +209,7 @@ Follow the instructions in each module's README.md file. - **Progressive Difficulty**: Builds from basics to advanced Git techniques - **Reset Anytime**: Each local module includes a reset script for a fresh start - **Self-Paced**: Learn at your own speed with detailed README guides -- **Real Collaboration**: Module 09 uses an actual Git server for authentic teamwork +- **Real Collaboration**: Module 07 uses an actual Git server for authentic teamwork - **Comprehensive Coverage**: From `git init` to advanced rebasing and bisecting ## Learning Path @@ -220,19 +218,19 @@ The modules are designed to build on each other: ### Recommended Progression -**Phase 1: Core Fundamentals (Essentials 01-03)** -- Git Basics, History, Branching -- **Goal**: Understand commits and branches +**Phase 1: Core Fundamentals (Essentials 01-02)** +- Git Basics, History +- **Goal**: Understand commits and history -**Phase 2: Collaboration Basics (Essentials 04-05)** -- Merging, Merge Conflicts -- **Goal**: Work with multiple branches +**Phase 2: Collaboration Basics (Essentials 03)** +- Branching and Merging (checkpoint-based: branching, merging, conflicts) +- **Goal**: Work with multiple branches and resolve conflicts -**Phase 3: Workflow Tools (Essentials 06-08)** +**Phase 3: Workflow Tools (Essentials 04-06)** - Cherry-Pick, Reset vs Revert, Stash - **Goal**: Manage your work effectively -**Phase 4: Real Collaboration (Essentials 09)** +**Phase 4: Real Collaboration (Essentials 07)** - **Multiplayer Git - The Great Print Project** - **Goal**: Apply all skills with real teammates on a cloud server - **Note**: This is a capstone module - bring everything together! @@ -245,14 +243,14 @@ The modules are designed to build on each other: ### Alternative Paths **Fast Track (1 day workshop):** -- Essentials 01-05 + Essentials 09 (Multiplayer) +- Essentials 01-03 + Essentials 07 (Multiplayer) **Solo Learner:** -- Complete Essentials 01-08, skip 09 (requires partners and server) +- Complete Essentials 01-06, skip 07 (requires partners and server) - Or complete all Advanced modules for deep mastery **Team Workshop:** -- Essentials 01-05 then jump to 09 (Multiplayer) for collaborative practice +- Essentials 01-03 then jump to 07 (Multiplayer) for collaborative practice ## Tips for Success @@ -263,7 +261,7 @@ The modules are designed to build on each other: - **Use `git log --oneline --graph --all`** frequently to visualize repository state - **If stuck**, check the Key Concepts section in the module's README - **Consider installing glow** for better markdown reading experience -- **For Module 09**, work with a partner - collaboration is the point! +- **For Module 07**, work with a partner - collaboration is the point! ## Skills You'll Master @@ -307,7 +305,7 @@ Before distributing this workshop to attendees for self-study: 2. Each module's `challenge/` directory will become its own independent git repository when attendees run `setup.ps1` 3. This isolation ensures each module provides a clean learning environment -**Note**: Module 09 (Multiplayer) requires you to set up a Git server - see facilitator guide below. +**Note**: Module 07 (Multiplayer) requires you to set up a Git server - see facilitator guide below. ### Facilitated Workshop @@ -315,13 +313,13 @@ For running this as a full-day instructor-led workshop: 1. **See [WORKSHOP-AGENDA.md](WORKSHOP-AGENDA.md)** - Complete agenda with timing, activities, and facilitation tips 2. **See [PRESENTATION-OUTLINE.md](PRESENTATION-OUTLINE.md)** - Slide deck outline for presentations -3. **Workshop covers:** Essentials 01-05 + Module 09 (Multiplayer collaboration exercise) +3. **Workshop covers:** Essentials 01-05 + Module 07 (Multiplayer collaboration exercise) 4. **Duration:** 6-7 hours including breaks 5. **Format:** Mix of presentation, live demos, and hands-on challenges **Facilitator preparation:** - Review the workshop agenda thoroughly -- Set up Git server for Module 09 (see below) +- Set up Git server for Module 07 (see below) - Ensure all participants have prerequisites installed (Git, PowerShell, Python) - Prepare slides using the presentation outline - Test all modules on a clean machine @@ -329,9 +327,9 @@ For running this as a full-day instructor-led workshop: The workshop format combines instructor-led sessions with self-paced hands-on modules for an engaging learning experience. -### Setting Up Module 09: Multiplayer Git +### Setting Up Module 07: Multiplayer Git -Module 09 requires a Git server for authentic collaboration. You have two options: +Module 07 requires a Git server for authentic collaboration. You have two options: **Option 1: Self-Hosted Gitea Server (Recommended)** @@ -376,18 +374,16 @@ git-workshop/ ├── GITEA-SETUP.md # Self-hosted Git server setup ├── install-glow.ps1 # Install glow markdown renderer │ -├── 01_essentials/ # Core Git skills (9 modules) +├── 01_essentials/ # Core Git skills (7 modules) │ ├── 01-basics/ # Initialize, commit, status │ ├── 02-history/ # Log, diff, show -│ ├── 03-branching/ # Create and switch branches -│ ├── 04-merging/ # Merge branches -│ ├── 05-merge-conflicts/ # Resolve conflicts step-by-step -│ ├── 06-cherry-pick/ # Apply specific commits -│ ├── 07-reset-vs-revert/ # Undo changes safely -│ ├── 08-stash/ # Save work-in-progress -│ └── 09-multiplayer/ # Real collaboration (cloud-based) -│ ├── README.md # Student guide (1,408 lines) -│ └── FACILITATOR-SETUP.md # Server setup guide (904 lines) +│ ├── 03-branching-and-merging/ # Branches, merging, conflicts (checkpoint-based) +│ ├── 04-cherry-pick/ # Apply specific commits +│ ├── 05-reset-vs-revert/ # Undo changes safely +│ ├── 06-stash/ # Save work-in-progress +│ └── 07-multiplayer/ # Real collaboration (cloud-based) +│ ├── README.md # Student guide +│ └── FACILITATOR-SETUP.md # Server setup guide │ └── 02_advanced/ # Professional techniques (6 modules) ├── 01-rebasing/ # Linear history with rebase @@ -400,9 +396,9 @@ git-workshop/ ## What's Unique About This Workshop -### The Great Print Project (Module 09) +### The Great Print Project (Module 07) -Unlike any other Git tutorial, Module 09 provides **real collaborative experience**: +Unlike any other Git tutorial, Module 07 provides **real collaborative experience**: - **Real Git server**: Not simulated - actual cloud repository at https://git.frod.dk/multiplayer - **Real teammates**: Work in pairs on shared branches @@ -429,7 +425,7 @@ This is how professional developers actually work - no simulation, no shortcuts. **Q: Do I need to complete all modules?** A: No! Essentials 01-05 covers what most developers use daily. Complete 06-09 and Advanced modules to deepen your skills. -**Q: Can I do Module 09 (Multiplayer) without a partner?** +**Q: Can I do Module 07 (Multiplayer) without a partner?** A: Not recommended - collaboration is the point. If solo, skip to Advanced modules or wait until you can pair with someone. **Q: How long does the workshop take?** @@ -445,16 +441,16 @@ A: 2. Check [GIT-CHEATSHEET.md](GIT-CHEATSHEET.md) 3. Run `./reset.ps1` to start fresh 4. Use `git status` and `git log --graph` to understand current state -5. For Module 09, ask your partner or facilitator +5. For Module 07, ask your partner or facilitator **Q: Can I use this for a team workshop at my company?** A: Absolutely! See the "For Workshop Facilitators" section above. The materials are designed for both self-study and instructor-led workshops. **Q: Do I need internet access?** -A: Modules 01-08 work completely offline. Module 09 requires internet to access the Git server. +A: Modules 01-08 work completely offline. Module 07 requires internet to access the Git server. **Q: What if I prefer GitHub/GitLab instead of Gitea?** -A: The skills are identical across all Git platforms. Module 09 uses Gitea but everything you learn applies to GitHub, GitLab, Bitbucket, etc. +A: The skills are identical across all Git platforms. Module 07 uses Gitea but everything you learn applies to GitHub, GitLab, Bitbucket, etc. --- From 039debe744e402407e0222bf69716550184a2876 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Sun, 11 Jan 2026 14:52:36 +0100 Subject: [PATCH 02/61] fix: remove accidental subproject addition --- 01_essentials/03-branching-and-merging/challenge | 1 - 1 file changed, 1 deletion(-) delete mode 160000 01_essentials/03-branching-and-merging/challenge diff --git a/01_essentials/03-branching-and-merging/challenge b/01_essentials/03-branching-and-merging/challenge deleted file mode 160000 index 40d5f6c..0000000 --- a/01_essentials/03-branching-and-merging/challenge +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 40d5f6c19ca1ed3a264d326118297304caabc2da From 8d63b2d22e54062cf92cee40be11a4c47e2b47ec Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Sun, 11 Jan 2026 23:02:48 +0100 Subject: [PATCH 03/61] feat: split out reset from the revert. First the safe path then the advanced path --- 01_essentials/05-reset-vs-revert/README.md | 198 ----- 01_essentials/05-reset-vs-revert/reset.ps1 | 22 - 01_essentials/05-reset-vs-revert/setup.ps1 | 190 ----- 01_essentials/05-reset-vs-revert/verify.ps1 | 172 ----- 01_essentials/05-revert/README.md | 638 ++++++++++++++++ 01_essentials/05-revert/reset.ps1 | 24 + 01_essentials/05-revert/setup.ps1 | 373 +++++++++ 01_essentials/05-revert/verify.ps1 | 226 ++++++ 01_essentials/06-reset/README.md | 717 ++++++++++++++++++ 01_essentials/06-reset/reset.ps1 | 24 + 01_essentials/06-reset/setup.ps1 | 348 +++++++++ 01_essentials/06-reset/verify.ps1 | 231 ++++++ .../{06-stash => 07-stash}/README.md | 0 .../{06-stash => 07-stash}/reset.ps1 | 0 .../{06-stash => 07-stash}/setup.ps1 | 0 .../{06-stash => 07-stash}/verify.ps1 | 0 .../FACILITATOR-SETUP.md | 0 .../README.md | 0 GIT-CHEATSHEET.md | 10 + README.md | 52 +- 20 files changed, 2618 insertions(+), 607 deletions(-) delete mode 100644 01_essentials/05-reset-vs-revert/README.md delete mode 100644 01_essentials/05-reset-vs-revert/reset.ps1 delete mode 100644 01_essentials/05-reset-vs-revert/setup.ps1 delete mode 100644 01_essentials/05-reset-vs-revert/verify.ps1 create mode 100644 01_essentials/05-revert/README.md create mode 100644 01_essentials/05-revert/reset.ps1 create mode 100644 01_essentials/05-revert/setup.ps1 create mode 100644 01_essentials/05-revert/verify.ps1 create mode 100644 01_essentials/06-reset/README.md create mode 100644 01_essentials/06-reset/reset.ps1 create mode 100644 01_essentials/06-reset/setup.ps1 create mode 100644 01_essentials/06-reset/verify.ps1 rename 01_essentials/{06-stash => 07-stash}/README.md (100%) rename 01_essentials/{06-stash => 07-stash}/reset.ps1 (100%) rename 01_essentials/{06-stash => 07-stash}/setup.ps1 (100%) rename 01_essentials/{06-stash => 07-stash}/verify.ps1 (100%) rename 01_essentials/{07-multiplayer => 08-multiplayer}/FACILITATOR-SETUP.md (100%) rename 01_essentials/{07-multiplayer => 08-multiplayer}/README.md (100%) diff --git a/01_essentials/05-reset-vs-revert/README.md b/01_essentials/05-reset-vs-revert/README.md deleted file mode 100644 index 2091945..0000000 --- a/01_essentials/05-reset-vs-revert/README.md +++ /dev/null @@ -1,198 +0,0 @@ -# Module 10: Reset vs Revert - -## Learning Objectives - -By the end of this module, you will: -- Understand the difference between `git reset` and `git revert` -- Know when to use reset vs revert -- Understand the three modes of reset (--soft, --mixed, --hard) -- Safely undo commits in both local and shared branches -- Understand the risks of rewriting history - -## Challenge Description - -You have two branches with problematic commits: -1. **local-feature**: A private branch with bad commits that you haven't shared with anyone -2. **shared-feature**: A branch that has been pushed and others might be using - -Your task is to: -1. Use `git reset` to remove the bad commit from the local-feature branch (safe because it's not shared) -2. Use `git revert` to undo the bad commit from the shared-feature branch (safe because it preserves history) - -## Key Concepts - -### Git Reset: Rewriting History - -`git reset` moves the branch pointer backward, effectively erasing commits from history. It has three modes: - -**--soft**: Moves HEAD, keeps changes staged -```bash -git reset --soft HEAD~1 -# Commit is gone, but changes are staged and ready to commit again -``` - -**--mixed** (default): Moves HEAD, keeps changes unstaged -```bash -git reset HEAD~1 -# Commit is gone, changes are in working directory but not staged -``` - -**--hard**: Moves HEAD, discards all changes -```bash -git reset --hard HEAD~1 -# Commit is gone, changes are PERMANENTLY DELETED -``` - -### Git Revert: Safe Undo - -`git revert` creates a NEW commit that undoes the changes from a previous commit. History is preserved. - -```bash -git revert -# Creates a new commit that reverses the specified commit -``` - -### Visual Comparison - -**Before (both branches):** -``` -A---B---C---D (D is the bad commit) -``` - -**After Reset (rewrites history):** -``` -A---B---C -``` -Commit D is gone. If anyone else had D, they'll have problems. - -**After Revert (preserves history):** -``` -A---B---C---D---E -``` -E is a new commit that undoes D. Everyone can pull E safely. - -### When to Use Each - -**Use Reset when:** -- The commits haven't been pushed to a shared repository -- You're cleaning up local commits before pushing -- You made a mistake locally and want to start over -- You're working alone on a branch - -**Use Revert when:** -- The commits have been pushed to a shared repository -- Others might have based work on these commits -- You want to preserve the complete history -- You need a safe, reversible undo operation - -### The Golden Rule - -**Never use `git reset` on commits that have been pushed to a shared branch!** - -This will cause problems for anyone who has pulled those commits. Use `git revert` instead. - -## Useful Commands - -```bash -# Reset (for local-only commits) -git reset --soft HEAD~1 # Undo commit, keep changes staged -git reset HEAD~1 # Undo commit, keep changes unstaged -git reset --hard HEAD~1 # Undo commit, discard changes (DANGEROUS!) - -# Reset to a specific commit -git reset --hard - -# Revert (for shared commits) -git revert -git revert HEAD # Revert the last commit - -# See what would be affected before resetting -git log --oneline -git diff HEAD~1 - -# If you reset by mistake, you can sometimes recover with reflog -git reflog -git reset --hard -``` - -## Verification - -Run the verification script to check your solution: - -```bash -.\verify.ps1 -``` - -The verification will check that: -- local-feature branch has the bad commit removed via reset -- shared-feature branch has the bad commit undone via revert -- shared-feature has a revert commit in the history -- All good commits are preserved - -## Challenge Steps - -1. Navigate to the challenge directory -2. You're on the local-feature branch with a bad commit -3. View commits: `git log --oneline` -4. Use `git reset --hard HEAD~1` to remove the bad commit -5. Switch to shared-feature: `git switch shared-feature` -6. View commits: `git log --oneline` -7. Find the hash of the "Add broken feature" commit -8. Use `git revert ` to undo it safely -9. Run the verification script - -## Tips - -- `HEAD~1` means "one commit before HEAD" -- `HEAD~2` means "two commits before HEAD" -- Always check `git log` before and after reset/revert -- `git reset --hard` is DANGEROUS - it permanently deletes uncommitted changes -- If you're unsure, use `git reset --soft` instead of `--hard` -- Revert will open an editor for the commit message - you can accept the default -- You can always use `.\reset.ps1` to start over if you make a mistake - -## Common Mistakes to Avoid - -### Mistake 1: Using Reset on Pushed Commits -```bash -# DON'T DO THIS if the commit was pushed! -git reset --hard HEAD~1 -git push --force # This will cause problems for others -``` - -### Mistake 2: Using --hard Without Checking -```bash -# This DELETES your work permanently! -git reset --hard HEAD~1 # Uncommitted changes are GONE -``` - -### Mistake 3: Reverting the Wrong Commit -```bash -# Always double-check the commit hash -git log --oneline -git show # Verify it's the right commit -git revert # Now revert it -``` - -## Recovery from Mistakes - -If you reset by accident, Git keeps a reflog: - -```bash -# See recent HEAD movements -git reflog - -# Find the commit you want to restore -# Output looks like: -# abc1234 HEAD@{0}: reset: moving to HEAD~1 -# def5678 HEAD@{1}: commit: The commit you just lost - -# Restore it -git reset --hard def5678 -``` - -The reflog is your safety net, but it only keeps history for about 30 days. - -## What You'll Learn - -Understanding when to use reset versus revert is crucial for safe Git usage. Reset is powerful but dangerous when used on shared commits, while revert is always safe but creates additional history. Mastering both commands and knowing which to use in different situations is a hallmark of Git expertise. The rule is simple: if in doubt, use revert - it's always safe. diff --git a/01_essentials/05-reset-vs-revert/reset.ps1 b/01_essentials/05-reset-vs-revert/reset.ps1 deleted file mode 100644 index 3adb925..0000000 --- a/01_essentials/05-reset-vs-revert/reset.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Resets the reset vs revert challenge environment. - -.DESCRIPTION - Removes the existing challenge directory and runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "Resetting challenge environment..." -ForegroundColor Yellow - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Remove-Item -Path "challenge" -Recurse -Force - Write-Host "Removed existing challenge directory." -ForegroundColor Cyan -} - -# Run setup script -Write-Host "Running setup script...`n" -ForegroundColor Cyan -& ".\setup.ps1" diff --git a/01_essentials/05-reset-vs-revert/setup.ps1 b/01_essentials/05-reset-vs-revert/setup.ps1 deleted file mode 100644 index 4d787d1..0000000 --- a/01_essentials/05-reset-vs-revert/setup.ps1 +++ /dev/null @@ -1,190 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Sets up the reset vs revert challenge environment. - -.DESCRIPTION - Creates a Git repository with two branches: - - local-feature: A private branch where reset should be used - - shared-feature: A pushed branch where revert should be used -#> - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Path "challenge" -Recurse -Force -} - -# Create challenge directory -Write-Host "Creating challenge environment..." -ForegroundColor Cyan -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize git repository -git init | Out-Null -git config user.name "Workshop User" | Out-Null -git config user.email "user@workshop.local" | Out-Null - -# Create initial commits on main -$app = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b -"@ - -Set-Content -Path "calculator.py" -Value $app -git add calculator.py -git commit -m "Initial calculator implementation" | Out-Null - -$readme = @" -# Calculator App - -A simple calculator application. -"@ - -Set-Content -Path "README.md" -Value $readme -git add README.md -git commit -m "Add README" | Out-Null - -# Create local-feature branch (private, not shared) -git checkout -b local-feature | Out-Null - -$appWithMultiply = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b -"@ - -Set-Content -Path "calculator.py" -Value $appWithMultiply -git add calculator.py -git commit -m "Add multiply function" | Out-Null - -# Add a bad commit that should be removed with reset -$appWithBadCode = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - # BUG: This is broken and should never have been committed! - def divide(self, a, b): - # Forgot to check for division by zero - return a / b # This will raise ZeroDivisionError for zero! -"@ - -Set-Content -Path "calculator.py" -Value $appWithBadCode -git add calculator.py -git commit -m "Add broken divide function - DO NOT KEEP" | Out-Null - -# Switch back to main for shared-feature branch -git checkout main | Out-Null - -# Create shared-feature branch (simulating a pushed/shared branch) -git checkout -b shared-feature | Out-Null - -$appWithPower = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def power(self, a, b): - return a ** b -"@ - -Set-Content -Path "calculator.py" -Value $appWithPower -git add calculator.py -git commit -m "Add power function" | Out-Null - -# Add a bad commit that should be reverted (not reset) -$appWithBrokenFeature = @" -import math - -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def power(self, a, b): - return a ** b - - # BUG: This breaks the calculator! - def square_root(self, a): - # This implementation is wrong for negative numbers - return math.sqrt(a) # Raises ValueError for negative numbers without warning! -"@ - -Set-Content -Path "calculator.py" -Value $appWithBrokenFeature -git add calculator.py -git commit -m "Add broken feature" | Out-Null - -# Add another good commit after the bad one (to show that revert preserves subsequent commits) -$appWithMoreFeatures = @" -import math - -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def power(self, a, b): - return a ** b - - # BUG: This breaks the calculator! - def square_root(self, a): - # This implementation is wrong for negative numbers - return math.sqrt(a) # Raises ValueError for negative numbers without warning! - - def modulo(self, a, b): - return a % b -"@ - -Set-Content -Path "calculator.py" -Value $appWithMoreFeatures -git add calculator.py -git commit -m "Add modulo function" | Out-Null - -# Switch to local-feature for the challenge start -git checkout local-feature | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "Challenge environment created!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have two branches with bad commits:" -ForegroundColor Cyan -Write-Host "`n1. local-feature (PRIVATE - not shared):" -ForegroundColor Yellow -Write-Host " - Has a broken divide function commit" -ForegroundColor White -Write-Host " - Safe to use 'git reset' to remove it" -ForegroundColor Green -Write-Host "`n2. shared-feature (PUBLIC - shared with team):" -ForegroundColor Yellow -Write-Host " - Has a broken feature commit" -ForegroundColor White -Write-Host " - Must use 'git revert' to undo it safely" -ForegroundColor Green -Write-Host "`nYour task:" -ForegroundColor Yellow -Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White -Write-Host "2. You're on local-feature - view commits: git log --oneline" -ForegroundColor White -Write-Host "3. Remove the bad commit with: git reset --hard HEAD~1" -ForegroundColor White -Write-Host "4. Switch to shared-feature: git checkout shared-feature" -ForegroundColor White -Write-Host "5. Find the 'Add broken feature' commit hash: git log --oneline" -ForegroundColor White -Write-Host "6. Revert it with: git revert " -ForegroundColor White -Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/01_essentials/05-reset-vs-revert/verify.ps1 b/01_essentials/05-reset-vs-revert/verify.ps1 deleted file mode 100644 index 7cf1bac..0000000 --- a/01_essentials/05-reset-vs-revert/verify.ps1 +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Verifies the reset vs revert challenge solution. - -.DESCRIPTION - Checks that the user correctly used reset on the local branch - and revert on the shared branch. -#> - -Set-Location "challenge" -ErrorAction SilentlyContinue - -# Check if challenge directory exists -if (-not (Test-Path "../verify.ps1")) { - Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path ".")) { - Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Write-Host "Verifying your solution..." -ForegroundColor Cyan - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] No git repository found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Verify local-feature branch -Write-Host "`nChecking local-feature branch..." -ForegroundColor Cyan -git checkout local-feature 2>$null | Out-Null - -# Check commit count on local-feature (should be 3: initial + README + multiply) -$localCommitCount = (git rev-list --count local-feature 2>$null) -if ($localCommitCount -ne 3) { - Write-Host "[FAIL] local-feature should have 3 commits, found $localCommitCount" -ForegroundColor Red - if ($localCommitCount -gt 3) { - Write-Host "Hint: The bad commit should be removed. Use 'git reset --hard HEAD~1'" -ForegroundColor Yellow - } else { - Write-Host "Hint: You may have reset too far. Run ../reset.ps1 to start over." -ForegroundColor Yellow - } - Set-Location .. - exit 1 -} - -# Check that calculator.py exists -if (-not (Test-Path "calculator.py")) { - Write-Host "[FAIL] calculator.py not found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check calculator.py on local-feature -$localCalcContent = Get-Content "calculator.py" -Raw - -# Should have multiply function -if ($localCalcContent -notmatch "multiply") { - Write-Host "[FAIL] calculator.py should have the multiply function." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Should NOT have divide function (it was in the bad commit that should be reset) -if ($localCalcContent -match "divide") { - Write-Host "[FAIL] calculator.py should NOT have the divide function." -ForegroundColor Red - Write-Host "Hint: Use 'git reset --hard HEAD~1' to remove the bad commit" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check commit messages on local-feature -$localCommits = git log --pretty=format:"%s" local-feature 2>$null -if ($localCommits -match "broken divide") { - Write-Host "[FAIL] The 'broken divide' commit should be removed from local-feature." -ForegroundColor Red - Write-Host "Hint: Use 'git reset --hard HEAD~1' to remove it" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -Write-Host "[PASS] local-feature branch correctly reset!" -ForegroundColor Green - -# Verify shared-feature branch -Write-Host "`nChecking shared-feature branch..." -ForegroundColor Cyan -git checkout shared-feature 2>$null | Out-Null - -# Check commit count on shared-feature -# Should be 6: initial + README + power + broken feature + modulo + revert -$sharedCommitCount = (git rev-list --count shared-feature 2>$null) -if ($sharedCommitCount -ne 6) { - Write-Host "[FAIL] shared-feature should have 6 commits, found $sharedCommitCount" -ForegroundColor Red - if ($sharedCommitCount -lt 6) { - Write-Host "Hint: You should REVERT the bad commit, not reset it." -ForegroundColor Yellow - Write-Host " Revert creates a new commit that undoes the bad one." -ForegroundColor Yellow - Write-Host " Use: git revert " -ForegroundColor Yellow - } else { - Write-Host "Hint: You should have exactly 6 commits after reverting." -ForegroundColor Yellow - } - Set-Location .. - exit 1 -} - -# Check that there's a revert commit -$sharedCommits = git log --pretty=format:"%s" shared-feature 2>$null -if ($sharedCommits -notmatch "Revert") { - Write-Host "[FAIL] No revert commit found on shared-feature." -ForegroundColor Red - Write-Host "Hint: Use 'git revert ' to undo the bad commit" -ForegroundColor Yellow - Write-Host " Find the hash with: git log --oneline" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check calculator.py on shared-feature -$sharedCalcContent = Get-Content "calculator.py" -Raw - -# Should have power function -if ($sharedCalcContent -notmatch "power") { - Write-Host "[FAIL] calculator.py should have the power function." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Should have modulo function (commits after the reverted one should be preserved) -if ($sharedCalcContent -notmatch "modulo") { - Write-Host "[FAIL] calculator.py should have the modulo function." -ForegroundColor Red - Write-Host "Hint: Reverting should preserve commits made after the bad one" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Should NOT have square_root function (it was in the bad commit that should be reverted) -if ($sharedCalcContent -match "square_root") { - Write-Host "[FAIL] calculator.py should NOT have the square_root function." -ForegroundColor Red - Write-Host "Hint: The 'Add broken feature' commit should be reverted" -ForegroundColor Yellow - Write-Host " Use: git revert " -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Verify the revert commit specifically reverted the "Add broken feature" commit -$revertCommitMessage = git log --grep="Revert" --pretty=format:"%s" -n 1 2>$null -if ($revertCommitMessage -notmatch "broken feature") { - Write-Host "[FAIL] The revert commit should mention 'broken feature'." -ForegroundColor Red - Write-Host "Hint: Make sure you reverted the correct commit (the one that added square_root)" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -Write-Host "[PASS] shared-feature branch correctly reverted!" -ForegroundColor Green - -# Success! -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Used 'git reset' on local-feature (private branch)" -ForegroundColor White -Write-Host " Removed the bad commit completely from history" -ForegroundColor White -Write-Host "- Used 'git revert' on shared-feature (public branch)" -ForegroundColor White -Write-Host " Created a new commit that undoes the bad one" -ForegroundColor White -Write-Host " Preserved all history and subsequent commits" -ForegroundColor White -Write-Host "`nYou now understand when to use reset vs revert!" -ForegroundColor Green -Write-Host "`nKey takeaway:" -ForegroundColor Yellow -Write-Host "- Reset rewrites history (use only on private commits)" -ForegroundColor White -Write-Host "- Revert preserves history (safe for shared commits)`n" -ForegroundColor White - -Set-Location .. -exit 0 diff --git a/01_essentials/05-revert/README.md b/01_essentials/05-revert/README.md new file mode 100644 index 0000000..5d91d19 --- /dev/null +++ b/01_essentials/05-revert/README.md @@ -0,0 +1,638 @@ +# Module 05: Git Revert - Safe Undoing + +## About This Module + +Welcome to Module 05, where you'll learn the **safe, team-friendly way to undo changes** in Git. Unlike destructive commands that erase history, `git revert` creates new commits that undo previous changes while preserving the complete project history. + +**Why revert is important:** +- ✅ Safe for shared/pushed commits +- ✅ Preserves complete history and audit trail +- ✅ Transparent to your team +- ✅ Can be undone itself if needed +- ✅ Works with any commit in history + +**Key principle:** Revert doesn't erase mistakes—it documents how you fixed them. + +## Learning Objectives + +By completing this module, you will: + +1. Revert regular commits safely while preserving surrounding changes +2. Revert merge commits using the `-m` flag +3. Understand merge commit parent numbering +4. Handle the re-merge problem that occurs after reverting merges +5. Revert multiple commits at once +6. Know when to use revert vs. other undo strategies + +## Prerequisites + +Before starting this module, you should be comfortable with: +- Creating commits (`git commit`) +- Viewing commit history (`git log`) +- Understanding branches and merging (Module 03) + +## Setup + +Run the setup script to create the challenge environment: + +```powershell +./setup.ps1 +``` + +This creates a `challenge/` directory with three branches demonstrating different revert scenarios: +- `regular-revert` - Basic commit reversion +- `merge-revert` - Merge commit reversion +- `multi-revert` - Multiple commit reversion + +## Challenge 1: Reverting a Regular Commit + +### Scenario + +You're working on a calculator application. A developer added a `divide` function that crashes when dividing by zero. The bug was discovered after subsequent commits were made, so you can't just delete it—you need to revert it while keeping the commits that came after. + +### Your Task + +1. Navigate to the challenge directory: + ```bash + cd challenge + ``` + +2. You should be on the `regular-revert` branch. View the commit history: + ```bash + git log --oneline + ``` + +3. Find the commit with the broken divide function (message: "Add broken divide function - needs to be reverted!") + +4. Revert that specific commit: + ```bash + git revert + ``` + +5. Git will open your editor for the revert commit message. The default message is fine—save and close. + +### What to Observe + +After reverting, check: + +```bash +# View the new revert commit +git log --oneline + +# Check that divide function is gone +cat calculator.py | grep "def divide" # Should return nothing + +# Check that modulo function still exists (it came after the bad commit) +cat calculator.py | grep "def modulo" # Should find it + +# Check that multiply function still exists (it came before the bad commit) +cat calculator.py | grep "def multiply" # Should find it +``` + +**Key insight:** Revert creates a new commit that undoes the changes from the target commit, but leaves all other commits intact. + +### Understanding the Timeline + +``` +Before revert: +main.py (initial) → multiply (good) → divide (BAD) → modulo (good) + ↑ + We want to undo THIS + +After revert: +main.py (initial) → multiply (good) → divide (BAD) → modulo (good) → revert divide (new commit) + ↑ + Removes divide, keeps modulo +``` + +The revert commit adds a new point in history that undoes the divide changes. + +## Challenge 2: Reverting a Merge Commit + +### Scenario + +Your team merged a `feature-auth` branch that added authentication functionality. After deployment, you discovered the authentication system has critical security issues. You need to revert the entire merge while the security team redesigns the feature. + +**This is different from reverting a regular commit!** Merge commits have **two parents**, so you must tell Git which parent to keep. + +### Understanding Merge Commit Parents + +When you merge a feature branch into main: + +``` + feature-auth (parent 2) + ↓ + C---D + / \ +A---B-----M ← Merge commit (has TWO parents) + ↑ +parent 1 (main) +``` + +The merge commit `M` has: +- **Parent 1**: The branch you merged INTO (main) +- **Parent 2**: The branch you merged FROM (feature-auth) + +When reverting a merge, you must specify which parent to keep using the `-m` flag: +- `-m 1` means "keep parent 1" (main) - **Most common** +- `-m 2` means "keep parent 2" (feature-auth) - Rare + +**In practice:** You almost always use `-m 1` to keep the main branch and undo the feature branch changes. + +### Your Task + +1. Switch to the merge-revert branch: + ```bash + git switch merge-revert + ``` + +2. View the commit history and find the merge commit: + ```bash + git log --oneline --graph + ``` + + Look for: "Merge feature-auth branch" + +3. Revert the merge commit using `-m 1`: + ```bash + git revert -m 1 + ``` + + **Explanation:** + - `-m 1` tells Git to keep parent 1 (main branch) + - This undoes all changes from the feature-auth branch + - Creates a new "revert merge" commit + +4. Save the default commit message and check the result: + ```bash + # Verify auth.py is gone + ls auth.py # Should not exist + + # Verify calculator.py no longer imports auth + cat calculator.py | grep "from auth" # Should return nothing + ``` + +### What Happens Without -m? + +If you try to revert a merge commit without the `-m` flag: + +```bash +git revert +# Error: commit is a merge but no -m option was given +``` + +Git doesn't know which parent you want to keep, so it refuses to proceed. + +### The Re-Merge Problem + +**Important gotcha:** After reverting a merge, you **cannot simply re-merge** the same branch! + +Here's why: + +``` +Initial merge: +A---B---M (merged feature-auth) + ↑ + All changes from feature-auth are now in main + +After revert: +A---B---M---R (reverted merge) + ↑ + Changes removed, but Git remembers they were merged + +Attempting to re-merge: +A---B---M---R---M2 (try to merge feature-auth again) + ↑ + Git thinks: "I already merged these commits, + nothing new to add!" (Empty merge) +``` + +**Solutions if you need to re-merge:** + +1. **Revert the revert** (recommended): + ```bash + git revert + ``` + This brings back all the feature-auth changes. + +2. **Cherry-pick new commits** from the feature branch: + ```bash + git cherry-pick + ``` + +3. **Merge with --no-ff** and resolve conflicts manually (advanced). + +### When to Revert Merges + +Revert merge commits when: +- ✅ Feature causes production issues +- ✅ Need to temporarily remove a feature +- ✅ Discovered critical bugs after merging +- ✅ Security issues require immediate rollback + +Don't revert merges when: +- ❌ You just need to fix a small bug (fix it with a new commit instead) +- ❌ You plan to re-merge the same branch soon (use reset if local, or revert-the-revert later) + +## Challenge 3: Reverting Multiple Commits + +### Scenario + +Two separate commits added broken mathematical functions (`square_root` and `logarithm`). Both have critical bugs and need to be removed. You can revert multiple commits at once. + +### Your Task + +1. Switch to the multi-revert branch: + ```bash + git switch multi-revert + ``` + +2. View the commit history: + ```bash + git log --oneline + ``` + + Find the two commits: + - "Add broken square_root - REVERT THIS!" + - "Add broken logarithm - REVERT THIS TOO!" + +3. Revert both commits in one command: + ```bash + git revert + ``` + + **Important:** List commits from **oldest to newest** for cleanest history. + + Alternatively, revert them one at a time: + ```bash + git revert + git revert + ``` + +4. Git will prompt for a commit message for each revert. Accept the defaults. + +5. Verify the result: + ```bash + # Check that both bad functions are gone + cat calculator.py | grep "def square_root" # Should return nothing + cat calculator.py | grep "def logarithm" # Should return nothing + + # Check that good functions remain + cat calculator.py | grep "def power" # Should find it + cat calculator.py | grep "def absolute" # Should find it + ``` + +### Multi-Revert Strategies + +**Reverting a range of commits:** + +```bash +# Revert commits from A to B (inclusive) +git revert A^..B + +# Example: Revert last 3 commits +git revert HEAD~3..HEAD +``` + +**Reverting without auto-commit:** + +```bash +# Stage revert changes without committing +git revert --no-commit + +# Review changes +git diff --staged + +# Commit when ready +git commit +``` + +This is useful when reverting multiple commits and you want one combined revert commit. + +## Verification + +Verify your solutions by running the verification script: + +```bash +cd .. # Return to module directory +./verify.ps1 +``` + +The script checks that: +- ✅ Revert commits were created (not destructive deletion) +- ✅ Bad code is removed +- ✅ Good code before and after is preserved +- ✅ Merge commits still exist in history +- ✅ Proper use of `-m` flag for merge reverts + +## Command Reference + +### Basic Revert + +```bash +# Revert a specific commit +git revert + +# Revert the most recent commit +git revert HEAD + +# Revert the second-to-last commit +git revert HEAD~1 +``` + +### Merge Commit Revert + +```bash +# Revert a merge commit (keep parent 1) +git revert -m 1 + +# Revert a merge commit (keep parent 2) - rare +git revert -m 2 +``` + +### Multiple Commits + +```bash +# Revert multiple specific commits +git revert + +# Revert a range of commits (oldest^..newest) +git revert ^.. + +# Revert last 3 commits +git revert HEAD~3..HEAD +``` + +### Revert Options + +```bash +# Revert but don't commit automatically +git revert --no-commit + +# Revert and edit the commit message +git revert --edit + +# Revert without opening editor (use default message) +git revert --no-edit + +# Abort a revert in progress (if conflicts) +git revert --abort + +# Continue revert after resolving conflicts +git revert --continue +``` + +## When to Use Git Revert + +Use `git revert` when: + +- ✅ **Commits are already pushed** - Safe for shared history +- ✅ **Working in a team** - Transparent to everyone +- ✅ **Need audit trail** - Shows what was undone and why +- ✅ **Public repositories** - Can't rewrite public history +- ✅ **Undoing old commits** - Can revert commits from weeks ago +- ✅ **Production hotfixes** - Safe emergency rollback + +**Golden Rule:** If others might have your commits, use revert. + +## When NOT to Use Git Revert + +Consider alternatives when: + +- ❌ **Commits are still local** - Use `git reset` instead (Module 06) +- ❌ **Just want to edit a commit** - Use `git commit --amend` +- ❌ **Haven't pushed yet** - Reset is cleaner for local cleanup +- ❌ **Need to combine commits** - Use interactive rebase +- ❌ **Reverting creates complex conflicts** - Might need manual fix forward + +## Revert vs. Reset vs. Rebase + +| Command | History | Safety | Use Case | +|---------|---------|--------|----------| +| **revert** | Preserves | ✅ Safe | Undo pushed commits | +| **reset** | Erases | ⚠️ Dangerous | Clean up local commits | +| **rebase** | Rewrites | ⚠️ Dangerous | Polish commit history | + +**This module teaches revert.** You'll learn reset in Module 06. + +## Handling Revert Conflicts + +Sometimes reverting causes conflicts if subsequent changes touched the same code: + +```bash +# Start revert +git revert + +# If conflicts occur: +# Conflict in calculator.py +# CONFLICT (content): Merge conflict in calculator.py +``` + +**To resolve:** + +1. Open conflicted files and fix conflicts (look for `<<<<<<<` markers) +2. Stage resolved files: + ```bash + git add + ``` +3. Continue the revert: + ```bash + git revert --continue + ``` + +Or abort if you change your mind: +```bash +git revert --abort +``` + +## Common Mistakes + +### 1. Forgetting -m for Merge Commits + +```bash +# ❌ Wrong - will fail +git revert + +# ✅ Correct +git revert -m 1 +``` + +### 2. Trying to Re-Merge After Revert + +```bash +# After reverting a merge: +git revert -m 1 + +# ❌ This won't work as expected +git merge feature-branch # Empty merge! + +# ✅ Do this instead +git revert # Revert the revert +``` + +### 3. Using Reset on Pushed Commits + +```bash +# ❌ NEVER do this with pushed commits +git reset --hard HEAD~3 + +# ✅ Do this instead +git revert HEAD~3..HEAD +``` + +### 4. Reverting Commits in Wrong Order + +When reverting multiple related commits, revert from newest to oldest: + +```bash +# If you have: A → B → C (and C depends on B) + +# ✅ Correct order +git revert C +git revert B + +# ❌ Wrong order (may cause conflicts) +git revert B # Conflict! C still references B +git revert C +``` + +## Best Practices + +1. **Write clear revert messages:** + ```bash + git revert -m "Revert authentication - security issue #1234" + ``` + +2. **Link to issue tracking:** + ``` + Revert "Add new payment system" + + This reverts commit abc123. + + Critical bug in payment processing. + See bug tracker: ISSUE-1234 + ``` + +3. **Test after reverting:** + - Run your test suite + - Verify the application still works + - Check no unintended changes occurred + +4. **Communicate with team:** + - Announce reverts in team chat + - Explain why the revert was necessary + - Provide timeline for re-introducing the feature + +5. **Keep reverts focused:** + - Revert the minimum necessary + - Don't bundle multiple unrelated reverts + - One problem = one revert commit + +## Troubleshooting + +### "Commit is a merge but no -m option was given" + +**Problem:** Trying to revert a merge commit without `-m`. + +**Solution:** +```bash +git revert -m 1 +``` + +### "Empty Revert / No Changes" + +**Problem:** Revert doesn't seem to do anything. + +**Possible causes:** +- Commit was already reverted +- Subsequent commits already undid the changes +- Wrong commit hash + +**Solution:** +```bash +# Check what the commit actually changed +git show + +# Check if already reverted +git log --grep="Revert" +``` + +### "Conflicts During Revert" + +**Problem:** Revert causes merge conflicts. + +**Why:** Subsequent commits modified the same code. + +**Solution:** +1. Manually resolve conflicts in affected files +2. `git add ` +3. `git revert --continue` + +Or consider fixing forward with a new commit instead of reverting. + +### "Can't Re-Merge After Reverting Merge" + +**Problem:** After reverting a merge, re-merging the branch brings no changes. + +**Solution:** Revert the revert commit: +```bash +# Find the revert commit +git log --oneline + +# Revert the revert (brings changes back) +git revert +``` + +## Advanced: Revert Internals + +Understanding what revert does under the hood: + +```bash +# Revert creates a new commit with inverse changes +git revert + +# This is equivalent to: +git diff ^.. > changes.patch +patch -R < changes.patch # Apply in reverse +git add . +git commit -m "Revert ''" +``` + +**Key insight:** Revert computes the diff of the target commit, inverts it, and applies it as a new commit. + +## Going Further + +Now that you understand revert, you're ready for: + +- **Module 06: Git Reset** - Learn the dangerous but powerful local history rewriting +- **Module 07: Git Stash** - Temporarily set aside uncommitted changes +- **Module 08: Multiplayer Git** - Collaborate with advanced workflows + +## Summary + +You've learned: + +- ✅ `git revert` creates new commits that undo previous changes +- ✅ Revert is safe for shared/pushed commits +- ✅ Merge commits require `-m 1` or `-m 2` flag +- ✅ Parent 1 = branch merged into, Parent 2 = branch merged from +- ✅ Can't simply re-merge after reverting a merge +- ✅ Multiple commits can be reverted in one command +- ✅ Revert preserves complete history for audit trails + +**The Golden Rule of Revert:** Use revert for any commit that might be shared with others. + +## Next Steps + +1. Complete all three challenge scenarios +2. Run `./verify.ps1` to check your solutions +3. Experiment with reverting different commits +4. Move on to Module 06: Git Reset (dangerous but powerful!) + +--- + +**Need Help?** +- Review the command reference above +- Check the troubleshooting section +- Re-run `./setup.ps1` to start fresh +- Practice reverting in different orders to understand the behavior diff --git a/01_essentials/05-revert/reset.ps1 b/01_essentials/05-revert/reset.ps1 new file mode 100644 index 0000000..d40ab84 --- /dev/null +++ b/01_essentials/05-revert/reset.ps1 @@ -0,0 +1,24 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Resets the Module 05 challenge environment to start fresh. + +.DESCRIPTION + This script removes the challenge directory and re-runs setup.ps1 + to create a fresh challenge environment. +#> + +Write-Host "`n=== Resetting Module 05: Git Revert Challenge ===" -ForegroundColor Cyan + +# Check if challenge directory exists +if (Test-Path "challenge") { + Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" + Write-Host "[OK] Challenge directory removed" -ForegroundColor Green +} else { + Write-Host "[INFO] No existing challenge directory found" -ForegroundColor Yellow +} + +# Run setup to create fresh environment +Write-Host "`nRunning setup to create fresh challenge environment..." -ForegroundColor Cyan +& "$PSScriptRoot/setup.ps1" diff --git a/01_essentials/05-revert/setup.ps1 b/01_essentials/05-revert/setup.ps1 new file mode 100644 index 0000000..f3e3911 --- /dev/null +++ b/01_essentials/05-revert/setup.ps1 @@ -0,0 +1,373 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Sets up the Module 05 challenge environment for learning git revert. + +.DESCRIPTION + This script creates a challenge directory with three branches demonstrating + different revert scenarios: + - regular-revert: Basic revert of a single bad commit + - merge-revert: Reverting a merge commit with -m flag + - multi-revert: Reverting multiple commits at once +#> + +Write-Host "`n=== Setting up Module 05: Git Revert Challenge ===" -ForegroundColor Cyan + +# Remove existing challenge directory if it exists +if (Test-Path "challenge") { + Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" +} + +# Create fresh challenge directory +Write-Host "Creating challenge directory..." -ForegroundColor Green +New-Item -ItemType Directory -Path "challenge" | Out-Null +Set-Location "challenge" + +# Initialize Git repository +Write-Host "Initializing Git repository..." -ForegroundColor Green +git init | Out-Null + +# Configure git for this repository +git config user.name "Workshop Student" +git config user.email "student@example.com" + +# ============================================================================ +# SCENARIO 1: Regular Revert (Basic) +# ============================================================================ +Write-Host "`nScenario 1: Creating regular-revert branch..." -ForegroundColor Cyan + +# Initial commit +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Initial calculator implementation" | Out-Null + +# Create regular-revert branch +git switch -c regular-revert | Out-Null + +# Good commit: Add multiply +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def multiply(a, b): + """Multiply two numbers.""" + return a * b +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add multiply function" | Out-Null + +# BAD commit: Add broken divide function +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def multiply(a, b): + """Multiply two numbers.""" + return a * b + +def divide(a, b): + """Divide a by b - BROKEN: doesn't handle division by zero!""" + return a / b # This will crash if b is 0! +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add broken divide function - needs to be reverted!" | Out-Null + +# Good commit: Add modulo (after bad commit) +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def multiply(a, b): + """Multiply two numbers.""" + return a * b + +def divide(a, b): + """Divide a by b - BROKEN: doesn't handle division by zero!""" + return a / b # This will crash if b is 0! + +def modulo(a, b): + """Return remainder of a divided by b.""" + return a % b +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add modulo function" | Out-Null + +Write-Host "[CREATED] regular-revert branch with bad divide commit" -ForegroundColor Green + +# ============================================================================ +# SCENARIO 2: Merge Revert (Merge Commit with -m flag) +# ============================================================================ +Write-Host "`nScenario 2: Creating merge-revert scenario..." -ForegroundColor Cyan + +# Switch back to main +git switch main | Out-Null + +# Create merge-revert branch +git switch -c merge-revert | Out-Null + +# Create a feature branch to merge +git switch -c feature-auth | Out-Null + +# Add auth functionality +$authContent = @" +# auth.py - Authentication module + +def login(username, password): + \"\"\"Login user.\"\"\" + print(f"Logging in {username}...") + return True + +def logout(username): + \"\"\"Logout user.\"\"\" + print(f"Logging out {username}...") + return True +"@ +Set-Content -Path "auth.py" -Value $authContent +git add . +git commit -m "Add authentication module" | Out-Null + +# Add password validation +$authContent = @" +# auth.py - Authentication module + +def validate_password(password): + \"\"\"Validate password strength.\"\"\" + return len(password) >= 8 + +def login(username, password): + \"\"\"Login user.\"\"\" + if not validate_password(password): + print("Password too weak!") + return False + print(f"Logging in {username}...") + return True + +def logout(username): + \"\"\"Logout user.\"\"\" + print(f"Logging out {username}...") + return True +"@ +Set-Content -Path "auth.py" -Value $authContent +git add . +git commit -m "Add password validation" | Out-Null + +# Integrate auth into calculator (part of the feature branch) +$calcContent = @" +# calculator.py - Simple calculator +from auth import login + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def secure_divide(a, b, username): + """Secure divide - requires authentication.""" + if login(username, "password123"): + return a / b + return None +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Integrate auth into calculator" | Out-Null + +# Switch back to merge-revert and merge feature-auth +git switch merge-revert | Out-Null +git merge feature-auth --no-ff -m "Merge feature-auth branch" | Out-Null + +Write-Host "[CREATED] merge-revert branch with merge commit to revert" -ForegroundColor Green + +# ============================================================================ +# SCENARIO 3: Multi Revert (Multiple Bad Commits) +# ============================================================================ +Write-Host "`nScenario 3: Creating multi-revert branch..." -ForegroundColor Cyan + +# Switch back to main +git switch main | Out-Null + +# Create multi-revert branch +git switch -c multi-revert | Out-Null + +# Reset calculator to simple version +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Reset to basic calculator" | Out-Null + +# Good commit: Add power function +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def power(a, b): + """Raise a to the power of b.""" + return a ** b +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add power function" | Out-Null + +# BAD commit 1: Add broken square_root +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def power(a, b): + """Raise a to the power of b.""" + return a ** b + +def square_root(a): + """BROKEN: Returns wrong result for negative numbers!""" + return a ** 0.5 # This returns NaN for negative numbers! +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add broken square_root - REVERT THIS!" | Out-Null + +# BAD commit 2: Add broken logarithm +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def power(a, b): + """Raise a to the power of b.""" + return a ** b + +def square_root(a): + """BROKEN: Returns wrong result for negative numbers!""" + return a ** 0.5 # This returns NaN for negative numbers! + +def logarithm(a): + """BROKEN: Doesn't handle zero or negative numbers!""" + import math + return math.log(a) # This crashes for a <= 0! +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add broken logarithm - REVERT THIS TOO!" | Out-Null + +# Good commit: Add absolute value (after bad commits) +$calcContent = @" +# calculator.py - Simple calculator + +def add(a, b): + """Add two numbers.""" + return a + b + +def subtract(a, b): + """Subtract b from a.""" + return a - b + +def power(a, b): + """Raise a to the power of b.""" + return a ** b + +def square_root(a): + """BROKEN: Returns wrong result for negative numbers!""" + return a ** 0.5 # This returns NaN for negative numbers! + +def logarithm(a): + """BROKEN: Doesn't handle zero or negative numbers!""" + import math + return math.log(a) # This crashes for a <= 0! + +def absolute(a): + """Return absolute value of a.""" + return abs(a) +"@ +Set-Content -Path "calculator.py" -Value $calcContent +git add . +git commit -m "Add absolute value function" | Out-Null + +Write-Host "[CREATED] multi-revert branch with two bad commits to revert" -ForegroundColor Green + +# ============================================================================ +# Return to regular-revert to start +# ============================================================================ +git switch regular-revert | Out-Null + +# Return to module directory +Set-Location .. + +Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green +Write-Host "`nThree revert scenarios have been created:" -ForegroundColor Cyan +Write-Host " 1. regular-revert - Revert a single bad commit (basic)" -ForegroundColor White +Write-Host " 2. merge-revert - Revert a merge commit with -m flag" -ForegroundColor White +Write-Host " 3. multi-revert - Revert multiple bad commits" -ForegroundColor White +Write-Host "`nYou are currently on the 'regular-revert' branch." -ForegroundColor Cyan +Write-Host "`nNext steps:" -ForegroundColor Cyan +Write-Host " 1. cd challenge" -ForegroundColor White +Write-Host " 2. Read the README.md for detailed instructions" -ForegroundColor White +Write-Host " 3. Complete each revert challenge" -ForegroundColor White +Write-Host " 4. Run '..\verify.ps1' to check your solutions" -ForegroundColor White +Write-Host "" diff --git a/01_essentials/05-revert/verify.ps1 b/01_essentials/05-revert/verify.ps1 new file mode 100644 index 0000000..0f32470 --- /dev/null +++ b/01_essentials/05-revert/verify.ps1 @@ -0,0 +1,226 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Verifies the Module 05 challenge solutions. + +.DESCRIPTION + Checks that all three revert scenarios have been completed correctly: + - regular-revert: Single commit reverted + - merge-revert: Merge commit reverted with -m flag + - multi-revert: Multiple commits reverted +#> + +Write-Host "`n=== Verifying Module 05: Git Revert Solutions ===" -ForegroundColor Cyan + +$allChecksPassed = $true +$originalDir = Get-Location + +# Check if challenge directory exists +if (-not (Test-Path "challenge")) { + Write-Host "[FAIL] Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red + exit 1 +} + +Set-Location "challenge" + +# Check if git repository exists +if (-not (Test-Path ".git")) { + Write-Host "[FAIL] Not a git repository. Run setup.ps1 first." -ForegroundColor Red + Set-Location $originalDir + exit 1 +} + +# ============================================================================ +# SCENARIO 1: Regular Revert Verification +# ============================================================================ +Write-Host "`n=== Scenario 1: Regular Revert ===" -ForegroundColor Cyan + +git switch regular-revert 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL] regular-revert branch not found" -ForegroundColor Red + $allChecksPassed = $false +} else { + # Check that a revert commit exists + $revertCommit = git log --oneline --grep="Revert" 2>$null + if ($revertCommit) { + Write-Host "[PASS] Revert commit found" -ForegroundColor Green + } else { + Write-Host "[FAIL] No revert commit found" -ForegroundColor Red + Write-Host "[HINT] Use: git revert " -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that calculator.py exists + if (Test-Path "calculator.py") { + $calcContent = Get-Content "calculator.py" -Raw + + # Check that divide function is NOT in the code (was reverted) + if ($calcContent -notmatch "def divide") { + Write-Host "[PASS] Broken divide function successfully reverted" -ForegroundColor Green + } else { + Write-Host "[FAIL] divide function still exists (should be reverted)" -ForegroundColor Red + Write-Host "[HINT] The bad commit should be reverted, removing the divide function" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that modulo function still exists (should be preserved) + if ($calcContent -match "def modulo") { + Write-Host "[PASS] modulo function preserved (good commit after bad one)" -ForegroundColor Green + } else { + Write-Host "[FAIL] modulo function missing (should still exist)" -ForegroundColor Red + Write-Host "[HINT] Only revert the bad commit, not the good ones after it" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that multiply function exists (should be preserved) + if ($calcContent -match "def multiply") { + Write-Host "[PASS] multiply function preserved (good commit before bad one)" -ForegroundColor Green + } else { + Write-Host "[FAIL] multiply function missing" -ForegroundColor Red + $allChecksPassed = $false + } + } else { + Write-Host "[FAIL] calculator.py not found" -ForegroundColor Red + $allChecksPassed = $false + } +} + +# ============================================================================ +# SCENARIO 2: Merge Revert Verification +# ============================================================================ +Write-Host "`n=== Scenario 2: Merge Revert ===" -ForegroundColor Cyan + +git switch merge-revert 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL] merge-revert branch not found" -ForegroundColor Red + $allChecksPassed = $false +} else { + # Check that a revert commit for the merge exists + $revertMerge = git log --oneline --grep="Revert.*Merge" 2>$null + if ($revertMerge) { + Write-Host "[PASS] Merge revert commit found" -ForegroundColor Green + } else { + Write-Host "[FAIL] No merge revert commit found" -ForegroundColor Red + Write-Host "[HINT] Use: git revert -m 1 " -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that the original merge commit still exists (revert doesn't erase it) + $mergeCommit = git log --merges --oneline --grep="Merge feature-auth" 2>$null + if ($mergeCommit) { + Write-Host "[PASS] Original merge commit still in history (not erased)" -ForegroundColor Green + } else { + Write-Host "[INFO] Original merge commit not found (this is OK if you used a different approach)" -ForegroundColor Yellow + } + + # Check that auth.py no longer exists or its effects are reverted + if (-not (Test-Path "auth.py")) { + Write-Host "[PASS] auth.py removed (merge reverted successfully)" -ForegroundColor Green + } else { + Write-Host "[INFO] auth.py still exists (check if merge was fully reverted)" -ForegroundColor Yellow + } + + # Check that calculator.py exists + if (Test-Path "calculator.py") { + $calcContent = Get-Content "calculator.py" -Raw + + # After reverting the merge, calculator shouldn't import auth + if ($calcContent -notmatch "from auth import") { + Write-Host "[PASS] Auth integration reverted from calculator.py" -ForegroundColor Green + } else { + Write-Host "[FAIL] calculator.py still imports auth (merge not fully reverted)" -ForegroundColor Red + Write-Host "[HINT] Reverting the merge should remove the auth integration" -ForegroundColor Yellow + $allChecksPassed = $false + } + } +} + +# ============================================================================ +# SCENARIO 3: Multi Revert Verification +# ============================================================================ +Write-Host "`n=== Scenario 3: Multi Revert ===" -ForegroundColor Cyan + +git switch multi-revert 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL] multi-revert branch not found" -ForegroundColor Red + $allChecksPassed = $false +} else { + # Count revert commits + $revertCommits = git log --oneline --grep="Revert" 2>$null + $revertCount = ($revertCommits | Measure-Object).Count + + if ($revertCount -ge 2) { + Write-Host "[PASS] Found $revertCount revert commits (expected at least 2)" -ForegroundColor Green + } else { + Write-Host "[FAIL] Found only $revertCount revert commit(s), need at least 2" -ForegroundColor Red + Write-Host "[HINT] Revert both bad commits: git revert " -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check calculator.py content + if (Test-Path "calculator.py") { + $calcContent = Get-Content "calculator.py" -Raw + + # Check that square_root is NOT in code (reverted) + if ($calcContent -notmatch "def square_root") { + Write-Host "[PASS] Broken square_root function reverted" -ForegroundColor Green + } else { + Write-Host "[FAIL] square_root function still exists (should be reverted)" -ForegroundColor Red + $allChecksPassed = $false + } + + # Check that logarithm is NOT in code (reverted) + if ($calcContent -notmatch "def logarithm") { + Write-Host "[PASS] Broken logarithm function reverted" -ForegroundColor Green + } else { + Write-Host "[FAIL] logarithm function still exists (should be reverted)" -ForegroundColor Red + $allChecksPassed = $false + } + + # Check that power function still exists (good commit before bad ones) + if ($calcContent -match "def power") { + Write-Host "[PASS] power function preserved" -ForegroundColor Green + } else { + Write-Host "[FAIL] power function missing (should still exist)" -ForegroundColor Red + $allChecksPassed = $false + } + + # Check that absolute function still exists (good commit after bad ones) + if ($calcContent -match "def absolute") { + Write-Host "[PASS] absolute function preserved" -ForegroundColor Green + } else { + Write-Host "[FAIL] absolute function missing (should still exist)" -ForegroundColor Red + $allChecksPassed = $false + } + } else { + Write-Host "[FAIL] calculator.py not found" -ForegroundColor Red + $allChecksPassed = $false + } +} + +Set-Location $originalDir + +# Final summary +Write-Host "" +if ($allChecksPassed) { + Write-Host "=========================================" -ForegroundColor Green + Write-Host " CONGRATULATIONS! ALL SCENARIOS PASSED!" -ForegroundColor Green + Write-Host "=========================================" -ForegroundColor Green + Write-Host "`nYou've mastered git revert!" -ForegroundColor Cyan + Write-Host "You now understand:" -ForegroundColor Cyan + Write-Host " ✓ Reverting regular commits safely" -ForegroundColor White + Write-Host " ✓ Reverting merge commits with -m flag" -ForegroundColor White + Write-Host " ✓ Reverting multiple commits at once" -ForegroundColor White + Write-Host " ✓ Preserving history while undoing changes" -ForegroundColor White + Write-Host "`nReady for Module 06: Git Reset!" -ForegroundColor Green + Write-Host "" + exit 0 +} else { + Write-Host "[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red + Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow + Write-Host "" + exit 1 +} diff --git a/01_essentials/06-reset/README.md b/01_essentials/06-reset/README.md new file mode 100644 index 0000000..7cd2862 --- /dev/null +++ b/01_essentials/06-reset/README.md @@ -0,0 +1,717 @@ +# Module 06: Git Reset - Dangerous History Rewriting + +## ⚠️ CRITICAL SAFETY WARNING ⚠️ + +**Git reset is DESTRUCTIVE and DANGEROUS when misused!** + +Before using `git reset`, always ask yourself: + +``` +Have I pushed these commits to a remote repository? +├─ YES → ❌ DO NOT USE RESET! +│ Use git revert instead (Module 05) +│ Rewriting pushed history breaks collaboration! +│ +└─ NO → ✅ Proceed with reset (local cleanup only) + Choose your mode carefully: + --soft (safest), --mixed (moderate), --hard (DANGEROUS) +``` + +**The Golden Rule:** NEVER reset commits that have been pushed/shared. + +## About This Module + +Welcome to Module 06, where you'll learn the powerful but dangerous `git reset` command. Unlike `git revert` (Module 05) which safely creates new commits, **reset erases commits from history**. + +**Why reset exists:** +- ✅ Clean up messy local commit history before pushing +- ✅ Undo commits you haven't shared yet +- ✅ Unstage files from the staging area +- ✅ Recover from mistakes (with reflog) + +**Why reset is dangerous:** +- ⚠️ Erases commits permanently (without reflog) +- ⚠️ Breaks repositories if used on pushed commits +- ⚠️ Can lose work if used incorrectly +- ⚠️ Confuses teammates if they have your commits + +**Key principle:** Reset is for polishing LOCAL history before sharing. + +## Learning Objectives + +By completing this module, you will: + +1. Understand the three reset modes: --soft, --mixed, --hard +2. Reset commits while keeping changes staged (--soft) +3. Reset commits and unstage changes (--mixed) +4. Reset commits and discard everything (--hard) +5. Know when reset is appropriate (local only!) +6. Understand when to use revert instead +7. Use reflog to recover from mistakes + +## Prerequisites + +Before starting this module, you should: +- Be comfortable with commits and staging (`git add`, `git commit`) +- Understand `git revert` from Module 05 +- **Know the difference between local and pushed commits!** + +## Setup + +Run the setup script to create the challenge environment: + +```powershell +./setup.ps1 +``` + +This creates a `challenge/` directory with three branches demonstrating different reset modes: +- `soft-reset` - Reset with --soft (keep changes staged) +- `mixed-reset` - Reset with --mixed (unstage changes) +- `hard-reset` - Reset with --hard (discard everything) + +**Remember:** These are all LOCAL commits that have NEVER been pushed! + +## Understanding Reset Modes + +Git reset has three modes that control what happens to your changes: + +| Mode | Commits | Staging Area | Working Directory | +|------|---------|--------------|-------------------| +| **--soft** | ✂️ Removed | ✅ Kept (staged) | ✅ Kept | +| **--mixed** (default) | ✂️ Removed | ✂️ Cleared | ✅ Kept (unstaged) | +| **--hard** | ✂️ Removed | ✂️ Cleared | ✂️ **LOST!** | + +**Visual explanation:** + +``` +Before reset (3 commits): +A → B → C → HEAD + +After git reset --soft HEAD~1: +A → B → HEAD + ↑ + C's changes are staged + +After git reset --mixed HEAD~1 (or just git reset HEAD~1): +A → B → HEAD + ↑ + C's changes are unstaged (in working directory) + +After git reset --hard HEAD~1: +A → B → HEAD + ↑ + C's changes are GONE (discarded completely!) +``` + +## Challenge 1: Soft Reset (Safest) + +### Scenario + +You committed "feature C" but immediately realized the implementation is wrong. You want to undo the commit but keep the changes staged so you can edit and re-commit them properly. + +**Use case:** Fixing the last commit's message or contents. + +### Your Task + +1. Navigate to the challenge directory: + ```bash + cd challenge + ``` + +2. You should be on the `soft-reset` branch. View the commits: + ```bash + git log --oneline + ``` + + You should see: + - "Add feature C - needs better implementation!" + - "Add feature B" + - "Add feature A" + - "Initial project setup" + +3. View the current state: + ```bash + git status + # Should be clean + ``` + +4. Reset the last commit with --soft: + ```bash + git reset --soft HEAD~1 + ``` + +5. Check what happened: + ```bash + # Commit is gone + git log --oneline + # Should only show 3 commits now (feature C commit removed) + + # Changes are still staged + git status + # Should show "Changes to be committed" + + # View the staged changes + git diff --cached + # Should show feature C code ready to be re-committed + ``` + +### What to Observe + +After `--soft` reset: +- ✅ Commit removed from history +- ✅ Changes remain in staging area +- ✅ Working directory unchanged +- ✅ Ready to edit and re-commit + +**When to use --soft:** +- Fix the last commit message (though `commit --amend` is simpler) +- Combine multiple commits into one +- Re-do a commit with better changes + +## Challenge 2: Mixed Reset (Default, Moderate) + +### Scenario + +You committed two experimental features that aren't ready. You want to remove both commits and have the changes back in your working directory (unstaged) so you can review and selectively re-commit them. + +**Use case:** Undoing commits and starting over with more careful staging. + +### Your Task + +1. Switch to the mixed-reset branch: + ```bash + git switch mixed-reset + ``` + +2. View the commits: + ```bash + git log --oneline + ``` + + You should see: + - "Add debug mode - REMOVE THIS TOO!" + - "Add experimental feature X - REMOVE THIS!" + - "Add logging system" + - "Add application lifecycle" + +3. Reset the last TWO commits (default is --mixed): + ```bash + git reset HEAD~2 + # This is equivalent to: git reset --mixed HEAD~2 + ``` + +4. Check what happened: + ```bash + # Commits are gone + git log --oneline + # Should only show 2 commits (lifecycle + logging) + + # NO staged changes + git diff --cached + # Should be empty + + # Changes are in working directory (unstaged) + git status + # Should show "Changes not staged for commit" + + # View the unstaged changes + git diff + # Should show experimental and debug code + ``` + +### What to Observe + +After `--mixed` reset (the default): +- ✅ Commits removed from history +- ✅ Staging area cleared +- ✅ Changes moved to working directory (unstaged) +- ✅ Can selectively stage and re-commit parts + +**When to use --mixed (default):** +- Undo commits and start over with clean staging +- Split one large commit into multiple smaller ones +- Review changes before re-committing +- Most common reset mode for cleanup + +## Challenge 3: Hard Reset (MOST DANGEROUS!) + +### ⚠️ EXTREME CAUTION REQUIRED ⚠️ + +**This will PERMANENTLY DELETE your work!** + +Only use `--hard` when you're absolutely sure you want to throw away changes. + +### Scenario + +You committed completely broken code that you want to discard entirely. There's no salvaging it—you just want it gone. + +**Use case:** Throwing away failed experiments or completely wrong code. + +### Your Task + +1. Switch to the hard-reset branch: + ```bash + git switch hard-reset + ``` + +2. View the commits and the broken code: + ```bash + git log --oneline + # Shows "Add broken helper D - DISCARD COMPLETELY!" + + cat utils.py + # Shows the broken helper_d function + ``` + +3. Reset the last commit with --hard: + ```bash + git reset --hard HEAD~1 + ``` + + **WARNING:** This will permanently discard all changes from that commit! + +4. Check what happened: + ```bash + # Commit is gone + git log --oneline + # Should only show 2 commits + + # NO staged changes + git diff --cached + # Empty + + # NO unstaged changes + git diff + # Empty + + # Working directory clean + git status + # "nothing to commit, working tree clean" + + # File doesn't have broken code + cat utils.py + # helper_d is completely gone + ``` + +### What to Observe + +After `--hard` reset: +- ✅ Commit removed from history +- ✅ Staging area cleared +- ✅ Working directory reset to match +- ⚠️ All changes from that commit PERMANENTLY DELETED + +**When to use --hard:** +- Discarding failed experiments completely +- Throwing away work you don't want (CAREFUL!) +- Cleaning up after mistakes (use reflog to recover if needed) +- Resetting to a known good state + +**⚠️ WARNING:** Files in the discarded commit are NOT gone forever—they're still in reflog for about 90 days. See "Recovery with Reflog" section below. + +## Understanding HEAD~N Syntax + +When resetting, you specify where to reset to: + +```bash +# Reset to the commit before HEAD +git reset HEAD~1 + +# Reset to 2 commits before HEAD +git reset HEAD~2 + +# Reset to 3 commits before HEAD +git reset HEAD~3 + +# Reset to a specific commit hash +git reset abc123 + +# Reset to a branch +git reset main +``` + +**Visualization:** + +``` +HEAD~3 HEAD~2 HEAD~1 HEAD + ↓ ↓ ↓ ↓ +A → B → C → D → E + ↑ + Current commit +``` + +- `git reset HEAD~1` moves HEAD from E to D +- `git reset HEAD~2` moves HEAD from E to C +- `git reset abc123` moves HEAD to that specific commit + +## Verification + +Verify your solutions by running the verification script: + +```bash +cd .. # Return to module directory +./verify.ps1 +``` + +The script checks that: +- ✅ Commits were reset (count decreased) +- ✅ --soft: Changes remain staged +- ✅ --mixed: Changes are unstaged +- ✅ --hard: Everything is clean + +## Recovery with Reflog + +**Good news:** Even `--hard` reset doesn't immediately destroy commits! + +Git keeps a "reflog" (reference log) of where HEAD has been for about 90 days. You can use this to recover "lost" commits. + +### How to Recover from a Reset + +1. View the reflog: + ```bash + git reflog + ``` + + Output example: + ``` + abc123 HEAD@{0}: reset: moving to HEAD~1 + def456 HEAD@{1}: commit: Add broken helper D + ... + ``` + +2. Find the commit you want to recover (def456 in this example) + +3. Reset back to it: + ```bash + git reset def456 + # Or use the reflog reference: + git reset HEAD@{1} + ``` + +4. Your "lost" commit is back! + +### Reflog Safety Net + +**Important:** +- Reflog entries expire after ~90 days (configurable) +- Reflog is LOCAL to your repository (not shared) +- `git gc` can clean up old reflog entries +- If you really lose a commit, check reflog first! + +**Pro tip:** Before doing dangerous operations, note your current commit hash: +```bash +git log --oneline | head -1 +# abc123 Current work +``` + +## When to Use Git Reset + +Use `git reset` when: + +- ✅ **Commits are LOCAL only** (never pushed) +- ✅ **Cleaning up messy history** before sharing +- ✅ **Undoing recent commits** you don't want +- ✅ **Combining commits** into one clean commit +- ✅ **Unstaging files** (mixed mode) +- ✅ **Polishing commit history** before pull request + +**Golden Rule:** Only reset commits that are local to your machine! + +## When NOT to Use Git Reset + +DO NOT use `git reset` when: + +- ❌ **Commits are pushed/shared** with others +- ❌ **Teammates have your commits** (breaks their repos) +- ❌ **In public repositories** (use revert instead) +- ❌ **Unsure if pushed** (check `git log origin/main`) +- ❌ **On main/master branch** after push +- ❌ **Need audit trail** of changes + +**Use git revert instead** (Module 05) for pushed commits! + +## Decision Tree: Reset vs Revert + +``` +Need to undo a commit? +│ +├─ Have you pushed this commit? +│ │ +│ ├─ YES → Use git revert (Module 05) +│ │ Safe for shared history +│ │ Preserves complete audit trail +│ │ +│ └─ NO → Can use git reset (local only) +│ │ +│ ├─ Want to keep changes? +│ │ │ +│ │ ├─ Keep staged → git reset --soft +│ │ └─ Keep unstaged → git reset --mixed +│ │ +│ └─ Discard everything? → git reset --hard +│ (CAREFUL!) +``` + +## Reset vs Revert vs Rebase + +| Command | History | Safety | Use Case | +|---------|---------|--------|----------| +| **reset** | Erases | ⚠️ Dangerous | Local cleanup before push | +| **revert** | Preserves | ✅ Safe | Undo pushed commits | +| **rebase** | Rewrites | ⚠️ Dangerous | Polish history before push | + +**This module teaches reset.** You learned revert in Module 05. + +## Command Reference + +### Basic Reset + +```bash +# Reset last commit, keep changes staged +git reset --soft HEAD~1 + +# Reset last commit, unstage changes (default) +git reset HEAD~1 +git reset --mixed HEAD~1 # Same as above + +# Reset last commit, discard everything (DANGEROUS!) +git reset --hard HEAD~1 + +# Reset multiple commits +git reset --soft HEAD~3 # Last 3 commits + +# Reset to specific commit +git reset --soft abc123 +``` + +### Unstaging Files + +```bash +# Unstage a specific file (common use of reset) +git reset HEAD filename.txt + +# Unstage all files +git reset HEAD . + +# This is the same as: +git restore --staged filename.txt # Modern syntax +``` + +### Reflog and Recovery + +```bash +# View reflog +git reflog + +# Recover from reset +git reset --hard HEAD@{1} +git reset --hard abc123 +``` + +### Check Before Reset + +```bash +# Check if commits are pushed +git log origin/main..HEAD +# If output is empty, commits are pushed (DO NOT RESET) +# If output shows commits, they're local (safe to reset) + +# Another way to check +git log --oneline --graph --all +# Look for origin/main marker +``` + +## Common Mistakes + +### 1. Resetting Pushed Commits + +```bash +# ❌ NEVER do this if you've pushed! +git push +# ... time passes ... +git reset --hard HEAD~3 # BREAKS teammate repos! + +# ✅ Do this instead +git revert HEAD~3..HEAD # Safe for shared history +``` + +### 2. Using --hard Without Thinking + +```bash +# ❌ Dangerous - loses work! +git reset --hard HEAD~1 + +# ✅ Better - keep changes to review +git reset --mixed HEAD~1 +# Now you can review changes and decide +``` + +### 3. Resetting Without Checking If Pushed + +```bash +# ❌ Risky - are these commits pushed? +git reset HEAD~5 + +# ✅ Check first +git log origin/main..HEAD # Local commits only +git reset HEAD~5 # Now safe if output showed commits +``` + +### 4. Forgetting Reflog Exists + +```bash +# ❌ Panic after accidental --hard reset +# "I lost my work!" + +# ✅ Check reflog first! +git reflog # Find the "lost" commit +git reset --hard HEAD@{1} # Recover it +``` + +## Best Practices + +1. **Always check if commits are pushed before reset:** + ```bash + git log origin/main..HEAD + ``` + +2. **Prefer --mixed over --hard:** + - You can always discard changes later + - Hard to recover if you use --hard by mistake + +3. **Commit often locally, reset before push:** + - Make many small local commits + - Reset/squash into clean commits before pushing + +4. **Use descriptive commit messages even for local commits:** + - Helps when reviewing before reset + - Useful when checking reflog + +5. **Know your escape hatch:** + ```bash + git reflog # Your safety net! + ``` + +6. **Communicate with team:** + - NEVER reset shared branches (main, develop, etc.) + - Only reset your personal feature branches + - Only before pushing! + +## Troubleshooting + +### "I accidentally reset with --hard and lost work!" + +**Solution:** Check reflog: +```bash +git reflog +# Find the commit before your reset +git reset --hard HEAD@{1} # Or the commit hash +``` + +**Prevention:** Always use --mixed first, then discard if really needed. + +### "I reset but teammates still have my commits" + +**Problem:** You reset and pushed with --force after they pulled. + +**Impact:** Their repository is now broken/inconsistent. + +**Solution:** Communicate! They need to: +```bash +git fetch +git reset --hard origin/main # Or whatever branch +``` + +**Prevention:** NEVER reset pushed commits! + +### "Reset didn't do what I expected" + +**Issue:** Wrong mode or wrong HEAD~N count. + +**Solution:** Check current state: +```bash +git status +git diff +git diff --cached +git log --oneline +``` + +Undo the reset: +```bash +git reflog +git reset HEAD@{1} # Go back to before your reset +``` + +### "Can't reset - 'fatal: ambiguous argument HEAD~1'" + +**Issue:** No commits to reset (probably first commit). + +**Solution:** You can't reset before the first commit. If you want to remove the first commit entirely: +```bash +rm -rf .git # Nuclear option - deletes entire repo +git init # Start over +``` + +## Advanced: Reset Internals + +Understanding what reset does under the hood: + +```bash +# Reset moves the branch pointer +# Before: +main → A → B → C (HEAD) + +# After git reset --soft HEAD~1: +main → A → B (HEAD) + ↑ + C still exists in reflog, just not in branch history + +# The commit object C is still in .git/objects +# It's just unreachable from any branch +``` + +**Key insight:** Reset moves the HEAD and branch pointers backward. The commits still exist temporarily in reflog until garbage collection. + +## Going Further + +Now that you understand reset, you're ready for: + +- **Module 07: Git Stash** - Temporarily save uncommitted work +- **Module 08: Multiplayer Git** - Collaborate with complex workflows +- **Interactive Rebase** - Advanced history polishing (beyond this workshop) + +## Summary + +You've learned: + +- ✅ `git reset` rewrites history by moving HEAD backward +- ✅ `--soft` keeps changes staged (safest) +- ✅ `--mixed` (default) unstages changes +- ✅ `--hard` discards everything (most dangerous) +- ✅ NEVER reset pushed/shared commits +- ✅ Use reflog to recover from mistakes +- ✅ Check if commits are pushed before resetting +- ✅ Use revert (Module 05) for shared commits + +**The Critical Rule:** Reset is for LOCAL commits ONLY. Once you push, use revert! + +## Next Steps + +1. Complete all three challenge scenarios +2. Run `./verify.ps1` to check your solutions +3. Practice checking if commits are pushed before reset +4. Move on to Module 07: Git Stash + +--- + +**⚠️ FINAL REMINDER ⚠️** + +**Before any `git reset` command, ask yourself:** + +> "Have I pushed these commits?" + +If YES → Use `git revert` instead! + +If NO → Proceed carefully, choose the right mode. + +**When in doubt, use --mixed instead of --hard!** diff --git a/01_essentials/06-reset/reset.ps1 b/01_essentials/06-reset/reset.ps1 new file mode 100644 index 0000000..4cdc736 --- /dev/null +++ b/01_essentials/06-reset/reset.ps1 @@ -0,0 +1,24 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Resets the Module 06 challenge environment to start fresh. + +.DESCRIPTION + This script removes the challenge directory and re-runs setup.ps1 + to create a fresh challenge environment. +#> + +Write-Host "`n=== Resetting Module 06: Git Reset Challenge ===" -ForegroundColor Cyan + +# Check if challenge directory exists +if (Test-Path "challenge") { + Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" + Write-Host "[OK] Challenge directory removed" -ForegroundColor Green +} else { + Write-Host "[INFO] No existing challenge directory found" -ForegroundColor Yellow +} + +# Run setup to create fresh environment +Write-Host "`nRunning setup to create fresh challenge environment..." -ForegroundColor Cyan +& "$PSScriptRoot/setup.ps1" diff --git a/01_essentials/06-reset/setup.ps1 b/01_essentials/06-reset/setup.ps1 new file mode 100644 index 0000000..52ebf47 --- /dev/null +++ b/01_essentials/06-reset/setup.ps1 @@ -0,0 +1,348 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Sets up the Module 06 challenge environment for learning git reset. + +.DESCRIPTION + This script creates a challenge directory with three branches demonstrating + different reset scenarios: + - soft-reset: Reset with --soft (keeps changes staged) + - mixed-reset: Reset with --mixed (unstages changes) + - hard-reset: Reset with --hard (discards everything) + reflog recovery +#> + +Write-Host "`n=== Setting up Module 06: Git Reset Challenge ===" -ForegroundColor Cyan +Write-Host "⚠️ WARNING: Git reset is DANGEROUS - use with extreme caution! ⚠️" -ForegroundColor Red + +# Remove existing challenge directory if it exists +if (Test-Path "challenge") { + Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" +} + +# Create fresh challenge directory +Write-Host "Creating challenge directory..." -ForegroundColor Green +New-Item -ItemType Directory -Path "challenge" | Out-Null +Set-Location "challenge" + +# Initialize Git repository +Write-Host "Initializing Git repository..." -ForegroundColor Green +git init | Out-Null + +# Configure git for this repository +git config user.name "Workshop Student" +git config user.email "student@example.com" + +# ============================================================================ +# Create initial commit (shared by all scenarios) +# ============================================================================ +$readmeContent = @" +# Git Reset Practice + +This repository contains practice scenarios for learning git reset. +"@ +Set-Content -Path "README.md" -Value $readmeContent +git add . +git commit -m "Initial commit" | Out-Null + +# ============================================================================ +# SCENARIO 1: Soft Reset (--soft) +# ============================================================================ +Write-Host "`nScenario 1: Creating soft-reset branch..." -ForegroundColor Cyan + +# Create soft-reset branch from initial commit +git switch -c soft-reset | Out-Null + +# Build up scenario 1 commits +$projectContent = @" +# project.py - Main project file + +def initialize(): + """Initialize the project.""" + print("Project initialized") + +def main(): + initialize() + print("Running application...") +"@ +Set-Content -Path "project.py" -Value $projectContent +git add . +git commit -m "Initial project setup" | Out-Null + +# Good commit: Add feature A +$projectContent = @" +# project.py - Main project file + +def initialize(): + """Initialize the project.""" + print("Project initialized") + +def feature_a(): + """Feature A implementation.""" + print("Feature A is working") + +def main(): + initialize() + feature_a() + print("Running application...") +"@ +Set-Content -Path "project.py" -Value $projectContent +git add . +git commit -m "Add feature A" | Out-Null + +# Good commit: Add feature B +$projectContent = @" +# project.py - Main project file + +def initialize(): + """Initialize the project.""" + print("Project initialized") + +def feature_a(): + """Feature A implementation.""" + print("Feature A is working") + +def feature_b(): + """Feature B implementation.""" + print("Feature B is working") + +def main(): + initialize() + feature_a() + feature_b() + print("Running application...") +"@ +Set-Content -Path "project.py" -Value $projectContent +git add . +git commit -m "Add feature B" | Out-Null + +# BAD commit: Add feature C (wrong implementation) +$projectContent = @" +# project.py - Main project file + +def initialize(): + """Initialize the project.""" + print("Project initialized") + +def feature_a(): + """Feature A implementation.""" + print("Feature A is working") + +def feature_b(): + """Feature B implementation.""" + print("Feature B is working") + +def feature_c(): + """Feature C implementation - WRONG!""" + print("Feature C has bugs!") # This needs to be re-implemented + +def main(): + initialize() + feature_a() + feature_b() + feature_c() + print("Running application...") +"@ +Set-Content -Path "project.py" -Value $projectContent +git add . +git commit -m "Add feature C - needs better implementation!" | Out-Null + +Write-Host "[CREATED] soft-reset branch with commit to reset --soft" -ForegroundColor Green + +# ============================================================================ +# SCENARIO 2: Mixed Reset (--mixed, default) +# ============================================================================ +Write-Host "`nScenario 2: Creating mixed-reset branch..." -ForegroundColor Cyan + +# Switch back to initial commit and create mixed-reset branch +git switch main | Out-Null +git switch -c mixed-reset | Out-Null + +# Build up scenario 2 commits +$appContent = @" +# app.py - Application entry point + +def start(): + """Start the application.""" + print("Application started") + +def stop(): + """Stop the application.""" + print("Application stopped") +"@ +Set-Content -Path "app.py" -Value $appContent +git add . +git commit -m "Add application lifecycle" | Out-Null + +# Good commit: Add logging +$appContent = @" +# app.py - Application entry point + +def log(message): + """Log a message.""" + print(f"[LOG] {message}") + +def start(): + """Start the application.""" + log("Application started") + +def stop(): + """Stop the application.""" + log("Application stopped") +"@ +Set-Content -Path "app.py" -Value $appContent +git add . +git commit -m "Add logging system" | Out-Null + +# BAD commit 1: Add experimental feature X +$appContent = @" +# app.py - Application entry point + +def log(message): + """Log a message.""" + print(f"[LOG] {message}") + +def experimental_feature_x(): + """Experimental feature - NOT READY!""" + log("Feature X is experimental and buggy") + +def start(): + """Start the application.""" + log("Application started") + experimental_feature_x() + +def stop(): + """Stop the application.""" + log("Application stopped") +"@ +Set-Content -Path "app.py" -Value $appContent +git add . +git commit -m "Add experimental feature X - REMOVE THIS!" | Out-Null + +# BAD commit 2: Add debug mode (also not ready) +$appContent = @" +# app.py - Application entry point + +DEBUG_MODE = True # Should not be committed! + +def log(message): + """Log a message.""" + print(f"[LOG] {message}") + +def experimental_feature_x(): + """Experimental feature - NOT READY!""" + log("Feature X is experimental and buggy") + +def start(): + """Start the application.""" + if DEBUG_MODE: + log("DEBUG MODE ACTIVE!") + log("Application started") + experimental_feature_x() + +def stop(): + """Stop the application.""" + log("Application stopped") +"@ +Set-Content -Path "app.py" -Value $appContent +git add . +git commit -m "Add debug mode - REMOVE THIS TOO!" | Out-Null + +Write-Host "[CREATED] mixed-reset branch with commits to reset --mixed" -ForegroundColor Green + +# ============================================================================ +# SCENARIO 3: Hard Reset (--hard) + Reflog Recovery +# ============================================================================ +Write-Host "`nScenario 3: Creating hard-reset branch..." -ForegroundColor Cyan + +# Switch back to main and create hard-reset branch +git switch main | Out-Null +git switch -c hard-reset | Out-Null + +# Reset to basic state +$utilsContent = @" +# utils.py - Utility functions + +def helper_a(): + """Helper function A.""" + return "Helper A" + +def helper_b(): + """Helper function B.""" + return "Helper B" +"@ +Set-Content -Path "utils.py" -Value $utilsContent +git add . +git commit -m "Add utility helpers" | Out-Null + +# Good commit: Add helper C +$utilsContent = @" +# utils.py - Utility functions + +def helper_a(): + """Helper function A.""" + return "Helper A" + +def helper_b(): + """Helper function B.""" + return "Helper B" + +def helper_c(): + """Helper function C.""" + return "Helper C" +"@ +Set-Content -Path "utils.py" -Value $utilsContent +git add . +git commit -m "Add helper C" | Out-Null + +# BAD commit: Add broken helper D (completely wrong) +$utilsContent = @" +# utils.py - Utility functions + +def helper_a(): + """Helper function A.""" + return "Helper A" + +def helper_b(): + """Helper function B.""" + return "Helper B" + +def helper_c(): + """Helper function C.""" + return "Helper C" + +def helper_d(): + """COMPLETELY BROKEN - throw away!""" + # This is all wrong and needs to be discarded + broken_code = "This doesn't even make sense" + return broken_code.nonexistent_method() # Will crash! +"@ +Set-Content -Path "utils.py" -Value $utilsContent +git add . +git commit -m "Add broken helper D - DISCARD COMPLETELY!" | Out-Null + +Write-Host "[CREATED] hard-reset branch with commit to reset --hard" -ForegroundColor Green + +# ============================================================================ +# Return to soft-reset to start +# ============================================================================ +git switch soft-reset | Out-Null + +# Return to module directory +Set-Location .. + +Write-Host "`n=== Setup Complete! ===`n" -ForegroundColor Green +Write-Host "Three reset scenarios have been created:" -ForegroundColor Cyan +Write-Host " 1. soft-reset - Reset --soft (keep changes staged)" -ForegroundColor White +Write-Host " 2. mixed-reset - Reset --mixed (unstage changes)" -ForegroundColor White +Write-Host " 3. hard-reset - Reset --hard (discard everything) + reflog recovery" -ForegroundColor White +Write-Host "`n⚠️ CRITICAL SAFETY REMINDER ⚠️" -ForegroundColor Red +Write-Host "NEVER use git reset on commits that have been PUSHED!" -ForegroundColor Red +Write-Host "These scenarios are LOCAL ONLY for practice." -ForegroundColor Yellow +Write-Host "`nYou are currently on the 'soft-reset' branch." -ForegroundColor Cyan +Write-Host "`nNext steps:" -ForegroundColor Cyan +Write-Host " 1. cd challenge" -ForegroundColor White +Write-Host " 2. Read the README.md for detailed instructions" -ForegroundColor White +Write-Host " 3. Complete each reset challenge" -ForegroundColor White +Write-Host " 4. Run '..\\verify.ps1' to check your solutions" -ForegroundColor White +Write-Host "" diff --git a/01_essentials/06-reset/verify.ps1 b/01_essentials/06-reset/verify.ps1 new file mode 100644 index 0000000..bb0f592 --- /dev/null +++ b/01_essentials/06-reset/verify.ps1 @@ -0,0 +1,231 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Verifies the Module 06 challenge solutions. + +.DESCRIPTION + Checks that all three reset scenarios have been completed correctly: + - soft-reset: Commit reset but changes remain staged + - mixed-reset: Commits reset and changes unstaged + - hard-reset: Everything reset and discarded +#> + +Write-Host "`n=== Verifying Module 06: Git Reset Solutions ===" -ForegroundColor Cyan +Write-Host "⚠️ Remember: NEVER reset pushed commits! ⚠️" -ForegroundColor Red + +$allChecksPassed = $true +$originalDir = Get-Location + +# Check if challenge directory exists +if (-not (Test-Path "challenge")) { + Write-Host "[FAIL] Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red + exit 1 +} + +Set-Location "challenge" + +# Check if git repository exists +if (-not (Test-Path ".git")) { + Write-Host "[FAIL] Not a git repository. Run setup.ps1 first." -ForegroundColor Red + Set-Location $originalDir + exit 1 +} + +# ============================================================================ +# SCENARIO 1: Soft Reset Verification +# ============================================================================ +Write-Host "`n=== Scenario 1: Soft Reset ===`n" -ForegroundColor Cyan + +git switch soft-reset 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL] soft-reset branch not found" -ForegroundColor Red + $allChecksPassed = $false +} else { + # Count commits (should be 4: Initial + project setup + feature A + feature B) + $commitCount = [int](git rev-list --count HEAD 2>$null) + + if ($commitCount -eq 4) { + Write-Host "[PASS] Commit count is 4 (feature C commit was reset)" -ForegroundColor Green + } else { + Write-Host "[FAIL] Expected 4 commits, found $commitCount" -ForegroundColor Red + Write-Host "[HINT] Use: git reset --soft HEAD~1" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check if changes are staged + $stagedChanges = git diff --cached --name-only 2>$null + if ($stagedChanges) { + Write-Host "[PASS] Changes are staged (feature C code in staging area)" -ForegroundColor Green + + # Verify the staged changes contain feature C code + $stagedContent = git diff --cached 2>$null + if ($stagedContent -match "feature_c") { + Write-Host "[PASS] Staged changes contain feature C code" -ForegroundColor Green + } else { + Write-Host "[INFO] Staged changes don't seem to contain feature C" -ForegroundColor Yellow + } + } else { + Write-Host "[FAIL] No staged changes found" -ForegroundColor Red + Write-Host "[HINT] After --soft reset, changes should remain staged" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check working directory has no unstaged changes to tracked files + $unstagedChanges = git diff --name-only 2>$null + if (-not $unstagedChanges) { + Write-Host "[PASS] No unstaged changes (all changes are staged)" -ForegroundColor Green + } else { + Write-Host "[INFO] Found unstaged changes (expected only staged changes)" -ForegroundColor Yellow + } +} + +# ============================================================================ +# SCENARIO 2: Mixed Reset Verification +# ============================================================================ +Write-Host "`n=== Scenario 2: Mixed Reset ===`n" -ForegroundColor Cyan + +git switch mixed-reset 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL] mixed-reset branch not found" -ForegroundColor Red + $allChecksPassed = $false +} else { + # Count commits (should be 3: Initial + lifecycle + logging, bad commits removed) + $commitCount = [int](git rev-list --count HEAD 2>$null) + + if ($commitCount -eq 3) { + Write-Host "[PASS] Commit count is 3 (both bad commits were reset)" -ForegroundColor Green + } elseif ($commitCount -eq 4) { + Write-Host "[INFO] Commit count is 4 (one commit reset, need to reset one more)" -ForegroundColor Yellow + Write-Host "[HINT] Use: git reset HEAD~1 (or git reset --mixed HEAD~1)" -ForegroundColor Yellow + $allChecksPassed = $false + } else { + Write-Host "[FAIL] Expected 3 commits, found $commitCount" -ForegroundColor Red + Write-Host "[HINT] Use: git reset --mixed HEAD~2 to remove both bad commits" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that there are NO staged changes + $stagedChanges = git diff --cached --name-only 2>$null + if (-not $stagedChanges) { + Write-Host "[PASS] No staged changes (--mixed unstages everything)" -ForegroundColor Green + } else { + Write-Host "[FAIL] Found staged changes (--mixed should unstage)" -ForegroundColor Red + Write-Host "[HINT] After --mixed reset, changes should be unstaged" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that there ARE unstaged changes in working directory + $unstagedChanges = git diff --name-only 2>$null + if ($unstagedChanges) { + Write-Host "[PASS] Unstaged changes present in working directory" -ForegroundColor Green + + # Verify unstaged changes contain the experimental/debug code + $workingContent = git diff 2>$null + if ($workingContent -match "experimental|DEBUG") { + Write-Host "[PASS] Unstaged changes contain the reset code" -ForegroundColor Green + } else { + Write-Host "[INFO] Unstaged changes don't contain expected code" -ForegroundColor Yellow + } + } else { + Write-Host "[FAIL] No unstaged changes found" -ForegroundColor Red + Write-Host "[HINT] After --mixed reset, changes should be in working directory (unstaged)" -ForegroundColor Yellow + $allChecksPassed = $false + } +} + +# ============================================================================ +# SCENARIO 3: Hard Reset Verification +# ============================================================================ +Write-Host "`n=== Scenario 3: Hard Reset ===`n" -ForegroundColor Cyan + +git switch hard-reset 2>&1 | Out-Null + +if ($LASTEXITCODE -ne 0) { + Write-Host "[FAIL] hard-reset branch not found" -ForegroundColor Red + $allChecksPassed = $false +} else { + # Count commits (should be 3: Initial + utilities + helper C, bad commit removed) + $commitCount = [int](git rev-list --count HEAD 2>$null) + + if ($commitCount -eq 3) { + Write-Host "[PASS] Commit count is 3 (broken commit was reset)" -ForegroundColor Green + } else { + Write-Host "[FAIL] Expected 3 commits, found $commitCount" -ForegroundColor Red + Write-Host "[HINT] Use: git reset --hard HEAD~1" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that there are NO staged changes + $stagedChanges = git diff --cached --name-only 2>$null + if (-not $stagedChanges) { + Write-Host "[PASS] No staged changes (--hard discards everything)" -ForegroundColor Green + } else { + Write-Host "[FAIL] Found staged changes (--hard should discard all)" -ForegroundColor Red + $allChecksPassed = $false + } + + # Check that there are NO unstaged changes + $unstagedChanges = git diff --name-only 2>$null + if (-not $unstagedChanges) { + Write-Host "[PASS] No unstaged changes (--hard discards everything)" -ForegroundColor Green + } else { + Write-Host "[FAIL] Found unstaged changes (--hard should discard all)" -ForegroundColor Red + $allChecksPassed = $false + } + + # Check working directory is clean + $statusOutput = git status --porcelain 2>$null + if (-not $statusOutput) { + Write-Host "[PASS] Working directory is completely clean" -ForegroundColor Green + } else { + Write-Host "[INFO] Working directory has some changes" -ForegroundColor Yellow + } + + # Verify the file doesn't have the broken code + if (Test-Path "utils.py") { + $utilsContent = Get-Content "utils.py" -Raw + if ($utilsContent -notmatch "helper_d") { + Write-Host "[PASS] Broken helper_d function is gone" -ForegroundColor Green + } else { + Write-Host "[FAIL] Broken helper_d still exists (wasn't reset)" -ForegroundColor Red + $allChecksPassed = $false + } + + if ($utilsContent -match "helper_c") { + Write-Host "[PASS] Good helper_c function is preserved" -ForegroundColor Green + } else { + Write-Host "[FAIL] Good helper_c function missing" -ForegroundColor Red + $allChecksPassed = $false + } + } +} + +Set-Location $originalDir + +# Final summary +Write-Host "" +if ($allChecksPassed) { + Write-Host "==========================================" -ForegroundColor Green + Write-Host " CONGRATULATIONS! ALL SCENARIOS PASSED!" -ForegroundColor Green + Write-Host "==========================================" -ForegroundColor Green + Write-Host "`nYou've mastered git reset!" -ForegroundColor Cyan + Write-Host "You now understand:" -ForegroundColor Cyan + Write-Host " ✓ Resetting commits with --soft (keep staged)" -ForegroundColor White + Write-Host " ✓ Resetting commits with --mixed (unstage)" -ForegroundColor White + Write-Host " ✓ Resetting commits with --hard (discard all)" -ForegroundColor White + Write-Host " ✓ The DANGER of reset on shared history" -ForegroundColor White + Write-Host "`n⚠️ CRITICAL REMINDER ⚠️" -ForegroundColor Red + Write-Host "NEVER use 'git reset' on commits you've already PUSHED!" -ForegroundColor Red + Write-Host "Always use 'git revert' (Module 05) for shared commits!" -ForegroundColor Yellow + Write-Host "`nReady for Module 07: Git Stash!" -ForegroundColor Green + Write-Host "" + exit 0 +} else { + Write-Host "[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red + Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow + Write-Host "[REMINDER] Reset is ONLY for local, un-pushed commits!" -ForegroundColor Yellow + Write-Host "" + exit 1 +} diff --git a/01_essentials/06-stash/README.md b/01_essentials/07-stash/README.md similarity index 100% rename from 01_essentials/06-stash/README.md rename to 01_essentials/07-stash/README.md diff --git a/01_essentials/06-stash/reset.ps1 b/01_essentials/07-stash/reset.ps1 similarity index 100% rename from 01_essentials/06-stash/reset.ps1 rename to 01_essentials/07-stash/reset.ps1 diff --git a/01_essentials/06-stash/setup.ps1 b/01_essentials/07-stash/setup.ps1 similarity index 100% rename from 01_essentials/06-stash/setup.ps1 rename to 01_essentials/07-stash/setup.ps1 diff --git a/01_essentials/06-stash/verify.ps1 b/01_essentials/07-stash/verify.ps1 similarity index 100% rename from 01_essentials/06-stash/verify.ps1 rename to 01_essentials/07-stash/verify.ps1 diff --git a/01_essentials/07-multiplayer/FACILITATOR-SETUP.md b/01_essentials/08-multiplayer/FACILITATOR-SETUP.md similarity index 100% rename from 01_essentials/07-multiplayer/FACILITATOR-SETUP.md rename to 01_essentials/08-multiplayer/FACILITATOR-SETUP.md diff --git a/01_essentials/07-multiplayer/README.md b/01_essentials/08-multiplayer/README.md similarity index 100% rename from 01_essentials/07-multiplayer/README.md rename to 01_essentials/08-multiplayer/README.md diff --git a/GIT-CHEATSHEET.md b/GIT-CHEATSHEET.md index 4755b5e..d8995a6 100644 --- a/GIT-CHEATSHEET.md +++ b/GIT-CHEATSHEET.md @@ -247,6 +247,16 @@ git revert --abort ``` Abort a revert in progress. +```bash +git revert -m 1 +``` +Revert a merge commit (requires -m flag to specify which parent to keep). Use `-m 1` to keep the branch you merged into (most common). + +```bash +git revert -m 2 +``` +Revert a merge commit but keep the branch that was merged in (rare). + ### Cherry-Pick ```bash diff --git a/README.md b/README.md index 7175f89..957191d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Perfect for developers who want to move beyond basic Git usage and master profes The workshop is organized into two tracks: -### 01 Essentials - Core Git Skills (7 modules) +### 01 Essentials - Core Git Skills (8 modules) Master fundamental Git concepts and collaborative workflows: @@ -16,9 +16,10 @@ Master fundamental Git concepts and collaborative workflows: - **Module 02: Viewing History** - Use git log and git diff to explore project history - **Module 03: Branching and Merging** - Create branches, merge them, and resolve conflicts (checkpoint-based) - **Module 04: Cherry-Pick** - Apply specific commits from one branch to another -- **Module 05: Reset vs Revert** - Understand when to rewrite history vs create new commits -- **Module 06: Stash** - Temporarily save work without committing -- **Module 07: Multiplayer Git** - **The Great Print Project** - Real cloud-based collaboration with teammates +- **Module 05: Git Revert** - Safe undoing - preserve history while reversing changes (includes merge commit reversion) +- **Module 06: Git Reset** - Dangerous history rewriting - local cleanup only (NEVER on pushed commits!) +- **Module 07: Stash** - Temporarily save work without committing +- **Module 08: Multiplayer Git** - **The Great Print Project** - Real cloud-based collaboration with teammates ### 02 Advanced - Professional Techniques (6 modules) @@ -44,18 +45,18 @@ Advanced Git workflows for power users: **Quick Reference**: See [GIT-CHEATSHEET.md](GIT-CHEATSHEET.md) for a comprehensive list of all Git commands covered in this workshop. Don't worry about memorizing everything - use this as a reference when you need to look up command syntax! -### For Module 07: Multiplayer Git +### For Module 08: Multiplayer Git **This module is different!** It uses a real Git server for authentic collaboration: -1. Navigate to `01_essentials/09-multiplayer` +1. Navigate to `01_essentials/08-multiplayer` 2. Read the `README.md` for complete instructions 3. **No setup script** - you'll clone from https://git.frod.dk/multiplayer 4. Work with a partner on shared branches 5. Experience real merge conflicts and pull requests 6. **No verify script** - success is visual (your code appears in the final output) -**Facilitators**: See `01_essentials/09-multiplayer/FACILITATOR-SETUP.md` for server setup and workshop guidance. +**Facilitators**: See `01_essentials/08-multiplayer/FACILITATOR-SETUP.md` for server setup and workshop guidance. ## Running PowerShell Scripts @@ -136,9 +137,9 @@ You should see your name and email printed. This is required to make commits in $PSVersionTable.PSVersion ``` -### Python (for Module 07 only) +### Python (for Module 08 only) -Module 07 (Multiplayer Git) uses Python: +Module 08 (Multiplayer Git) uses Python: - **Python 3.6+** required to run the Great Print Project - Check: `python --version` or `python3 --version` @@ -209,7 +210,7 @@ Follow the instructions in each module's README.md file. - **Progressive Difficulty**: Builds from basics to advanced Git techniques - **Reset Anytime**: Each local module includes a reset script for a fresh start - **Self-Paced**: Learn at your own speed with detailed README guides -- **Real Collaboration**: Module 07 uses an actual Git server for authentic teamwork +- **Real Collaboration**: Module 08 uses an actual Git server for authentic teamwork - **Comprehensive Coverage**: From `git init` to advanced rebasing and bisecting ## Learning Path @@ -305,7 +306,7 @@ Before distributing this workshop to attendees for self-study: 2. Each module's `challenge/` directory will become its own independent git repository when attendees run `setup.ps1` 3. This isolation ensures each module provides a clean learning environment -**Note**: Module 07 (Multiplayer) requires you to set up a Git server - see facilitator guide below. +**Note**: Module 08 (Multiplayer) requires you to set up a Git server - see facilitator guide below. ### Facilitated Workshop @@ -313,13 +314,13 @@ For running this as a full-day instructor-led workshop: 1. **See [WORKSHOP-AGENDA.md](WORKSHOP-AGENDA.md)** - Complete agenda with timing, activities, and facilitation tips 2. **See [PRESENTATION-OUTLINE.md](PRESENTATION-OUTLINE.md)** - Slide deck outline for presentations -3. **Workshop covers:** Essentials 01-05 + Module 07 (Multiplayer collaboration exercise) +3. **Workshop covers:** Essentials 01-05 + Module 08 (Multiplayer collaboration exercise) 4. **Duration:** 6-7 hours including breaks 5. **Format:** Mix of presentation, live demos, and hands-on challenges **Facilitator preparation:** - Review the workshop agenda thoroughly -- Set up Git server for Module 07 (see below) +- Set up Git server for Module 08 (see below) - Ensure all participants have prerequisites installed (Git, PowerShell, Python) - Prepare slides using the presentation outline - Test all modules on a clean machine @@ -327,9 +328,9 @@ For running this as a full-day instructor-led workshop: The workshop format combines instructor-led sessions with self-paced hands-on modules for an engaging learning experience. -### Setting Up Module 07: Multiplayer Git +### Setting Up Module 08: Multiplayer Git -Module 07 requires a Git server for authentic collaboration. You have two options: +Module 08 requires a Git server for authentic collaboration. You have two options: **Option 1: Self-Hosted Gitea Server (Recommended)** @@ -344,7 +345,7 @@ Run your own Git server with Gitea using Docker and Cloudflare Tunnel: **Setup:** 1. See [GITEA-SETUP.md](GITEA-SETUP.md) for complete Gitea + Docker + Cloudflare Tunnel instructions -2. See `01_essentials/09-multiplayer/FACILITATOR-SETUP.md` for detailed workshop preparation: +2. See `01_essentials/08-multiplayer/FACILITATOR-SETUP.md` for detailed workshop preparation: - Creating student accounts - Setting up The Great Print Project repository - Pairing students @@ -374,14 +375,15 @@ git-workshop/ ├── GITEA-SETUP.md # Self-hosted Git server setup ├── install-glow.ps1 # Install glow markdown renderer │ -├── 01_essentials/ # Core Git skills (7 modules) +├── 01_essentials/ # Core Git skills (8 modules) │ ├── 01-basics/ # Initialize, commit, status │ ├── 02-history/ # Log, diff, show │ ├── 03-branching-and-merging/ # Branches, merging, conflicts (checkpoint-based) │ ├── 04-cherry-pick/ # Apply specific commits -│ ├── 05-reset-vs-revert/ # Undo changes safely -│ ├── 06-stash/ # Save work-in-progress -│ └── 07-multiplayer/ # Real collaboration (cloud-based) +│ ├── 05-revert/ # Safe undoing (includes merge commits) +│ ├── 06-reset/ # Dangerous local cleanup +│ ├── 07-stash/ # Save work-in-progress +│ └── 08-multiplayer/ # Real collaboration (cloud-based) │ ├── README.md # Student guide │ └── FACILITATOR-SETUP.md # Server setup guide │ @@ -398,7 +400,7 @@ git-workshop/ ### The Great Print Project (Module 07) -Unlike any other Git tutorial, Module 07 provides **real collaborative experience**: +Unlike any other Git tutorial, Module 08 provides **real collaborative experience**: - **Real Git server**: Not simulated - actual cloud repository at https://git.frod.dk/multiplayer - **Real teammates**: Work in pairs on shared branches @@ -425,7 +427,7 @@ This is how professional developers actually work - no simulation, no shortcuts. **Q: Do I need to complete all modules?** A: No! Essentials 01-05 covers what most developers use daily. Complete 06-09 and Advanced modules to deepen your skills. -**Q: Can I do Module 07 (Multiplayer) without a partner?** +**Q: Can I do Module 08 (Multiplayer) without a partner?** A: Not recommended - collaboration is the point. If solo, skip to Advanced modules or wait until you can pair with someone. **Q: How long does the workshop take?** @@ -441,16 +443,16 @@ A: 2. Check [GIT-CHEATSHEET.md](GIT-CHEATSHEET.md) 3. Run `./reset.ps1` to start fresh 4. Use `git status` and `git log --graph` to understand current state -5. For Module 07, ask your partner or facilitator +5. For Module 08, ask your partner or facilitator **Q: Can I use this for a team workshop at my company?** A: Absolutely! See the "For Workshop Facilitators" section above. The materials are designed for both self-study and instructor-led workshops. **Q: Do I need internet access?** -A: Modules 01-08 work completely offline. Module 07 requires internet to access the Git server. +A: Modules 01-07 work completely offline. Module 08 requires internet to access the Git server. **Q: What if I prefer GitHub/GitLab instead of Gitea?** -A: The skills are identical across all Git platforms. Module 07 uses Gitea but everything you learn applies to GitHub, GitLab, Bitbucket, etc. +A: The skills are identical across all Git platforms. Module 08 uses Gitea but everything you learn applies to GitHub, GitLab, Bitbucket, etc. --- From 5f78245734fcb0f0b2ebce232b4c228fb10c48b2 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Sun, 11 Jan 2026 23:05:03 +0100 Subject: [PATCH 04/61] refactor: rename 01_essentials to 01-essentials --- {01_essentials => 01-essentials}/01-basics/README.md | 0 {01_essentials => 01-essentials}/01-basics/reset.ps1 | 0 {01_essentials => 01-essentials}/01-basics/setup.ps1 | 0 {01_essentials => 01-essentials}/01-basics/verify.ps1 | 0 {01_essentials => 01-essentials}/02-history/README.md | 0 {01_essentials => 01-essentials}/02-history/reset.ps1 | 0 {01_essentials => 01-essentials}/02-history/setup.ps1 | 0 {01_essentials => 01-essentials}/02-history/verify.ps1 | 0 .../03-branching-and-merging/README.md | 0 .../03-branching-and-merging/reset.ps1 | 0 .../03-branching-and-merging/setup.ps1 | 0 .../03-branching-and-merging/verify.ps1 | 0 {01_essentials => 01-essentials}/04-cherry-pick/README.md | 0 {01_essentials => 01-essentials}/04-cherry-pick/reset.ps1 | 0 {01_essentials => 01-essentials}/04-cherry-pick/setup.ps1 | 0 {01_essentials => 01-essentials}/04-cherry-pick/verify.ps1 | 0 {01_essentials => 01-essentials}/05-revert/README.md | 0 {01_essentials => 01-essentials}/05-revert/reset.ps1 | 0 {01_essentials => 01-essentials}/05-revert/setup.ps1 | 0 {01_essentials => 01-essentials}/05-revert/verify.ps1 | 0 {01_essentials => 01-essentials}/06-reset/README.md | 0 {01_essentials => 01-essentials}/06-reset/reset.ps1 | 0 {01_essentials => 01-essentials}/06-reset/setup.ps1 | 0 {01_essentials => 01-essentials}/06-reset/verify.ps1 | 0 {01_essentials => 01-essentials}/07-stash/README.md | 0 {01_essentials => 01-essentials}/07-stash/reset.ps1 | 0 {01_essentials => 01-essentials}/07-stash/setup.ps1 | 0 {01_essentials => 01-essentials}/07-stash/verify.ps1 | 0 .../08-multiplayer/FACILITATOR-SETUP.md | 0 {01_essentials => 01-essentials}/08-multiplayer/README.md | 0 {02_advanced => 02-advanced}/01-rebasing/README.md | 0 {02_advanced => 02-advanced}/01-rebasing/reset.ps1 | 0 {02_advanced => 02-advanced}/01-rebasing/setup.ps1 | 0 {02_advanced => 02-advanced}/01-rebasing/verify.ps1 | 0 {02_advanced => 02-advanced}/02-interactive-rebase/README.md | 0 {02_advanced => 02-advanced}/02-interactive-rebase/reset.ps1 | 0 {02_advanced => 02-advanced}/02-interactive-rebase/setup.ps1 | 0 {02_advanced => 02-advanced}/02-interactive-rebase/verify.ps1 | 0 {02_advanced => 02-advanced}/03-worktrees/README.md | 0 {02_advanced => 02-advanced}/03-worktrees/reset.ps1 | 0 {02_advanced => 02-advanced}/03-worktrees/setup.ps1 | 0 {02_advanced => 02-advanced}/03-worktrees/verify.ps1 | 0 {02_advanced => 02-advanced}/04-bisect/README.md | 0 {02_advanced => 02-advanced}/04-bisect/reset.ps1 | 0 {02_advanced => 02-advanced}/04-bisect/setup.ps1 | 0 {02_advanced => 02-advanced}/04-bisect/verify.ps1 | 0 {02_advanced => 02-advanced}/05-blame/README.md | 0 {02_advanced => 02-advanced}/05-blame/reset.ps1 | 0 {02_advanced => 02-advanced}/05-blame/setup.ps1 | 0 {02_advanced => 02-advanced}/05-blame/verify.ps1 | 0 {02_advanced => 02-advanced}/06-merge-strategies/README.md | 0 {02_advanced => 02-advanced}/06-merge-strategies/reset.ps1 | 0 {02_advanced => 02-advanced}/06-merge-strategies/setup.ps1 | 0 {02_advanced => 02-advanced}/06-merge-strategies/verify.ps1 | 0 54 files changed, 0 insertions(+), 0 deletions(-) rename {01_essentials => 01-essentials}/01-basics/README.md (100%) rename {01_essentials => 01-essentials}/01-basics/reset.ps1 (100%) rename {01_essentials => 01-essentials}/01-basics/setup.ps1 (100%) rename {01_essentials => 01-essentials}/01-basics/verify.ps1 (100%) rename {01_essentials => 01-essentials}/02-history/README.md (100%) rename {01_essentials => 01-essentials}/02-history/reset.ps1 (100%) rename {01_essentials => 01-essentials}/02-history/setup.ps1 (100%) rename {01_essentials => 01-essentials}/02-history/verify.ps1 (100%) rename {01_essentials => 01-essentials}/03-branching-and-merging/README.md (100%) rename {01_essentials => 01-essentials}/03-branching-and-merging/reset.ps1 (100%) rename {01_essentials => 01-essentials}/03-branching-and-merging/setup.ps1 (100%) rename {01_essentials => 01-essentials}/03-branching-and-merging/verify.ps1 (100%) rename {01_essentials => 01-essentials}/04-cherry-pick/README.md (100%) rename {01_essentials => 01-essentials}/04-cherry-pick/reset.ps1 (100%) rename {01_essentials => 01-essentials}/04-cherry-pick/setup.ps1 (100%) rename {01_essentials => 01-essentials}/04-cherry-pick/verify.ps1 (100%) rename {01_essentials => 01-essentials}/05-revert/README.md (100%) rename {01_essentials => 01-essentials}/05-revert/reset.ps1 (100%) rename {01_essentials => 01-essentials}/05-revert/setup.ps1 (100%) rename {01_essentials => 01-essentials}/05-revert/verify.ps1 (100%) rename {01_essentials => 01-essentials}/06-reset/README.md (100%) rename {01_essentials => 01-essentials}/06-reset/reset.ps1 (100%) rename {01_essentials => 01-essentials}/06-reset/setup.ps1 (100%) rename {01_essentials => 01-essentials}/06-reset/verify.ps1 (100%) rename {01_essentials => 01-essentials}/07-stash/README.md (100%) rename {01_essentials => 01-essentials}/07-stash/reset.ps1 (100%) rename {01_essentials => 01-essentials}/07-stash/setup.ps1 (100%) rename {01_essentials => 01-essentials}/07-stash/verify.ps1 (100%) rename {01_essentials => 01-essentials}/08-multiplayer/FACILITATOR-SETUP.md (100%) rename {01_essentials => 01-essentials}/08-multiplayer/README.md (100%) rename {02_advanced => 02-advanced}/01-rebasing/README.md (100%) rename {02_advanced => 02-advanced}/01-rebasing/reset.ps1 (100%) rename {02_advanced => 02-advanced}/01-rebasing/setup.ps1 (100%) rename {02_advanced => 02-advanced}/01-rebasing/verify.ps1 (100%) rename {02_advanced => 02-advanced}/02-interactive-rebase/README.md (100%) rename {02_advanced => 02-advanced}/02-interactive-rebase/reset.ps1 (100%) rename {02_advanced => 02-advanced}/02-interactive-rebase/setup.ps1 (100%) rename {02_advanced => 02-advanced}/02-interactive-rebase/verify.ps1 (100%) rename {02_advanced => 02-advanced}/03-worktrees/README.md (100%) rename {02_advanced => 02-advanced}/03-worktrees/reset.ps1 (100%) rename {02_advanced => 02-advanced}/03-worktrees/setup.ps1 (100%) rename {02_advanced => 02-advanced}/03-worktrees/verify.ps1 (100%) rename {02_advanced => 02-advanced}/04-bisect/README.md (100%) rename {02_advanced => 02-advanced}/04-bisect/reset.ps1 (100%) rename {02_advanced => 02-advanced}/04-bisect/setup.ps1 (100%) rename {02_advanced => 02-advanced}/04-bisect/verify.ps1 (100%) rename {02_advanced => 02-advanced}/05-blame/README.md (100%) rename {02_advanced => 02-advanced}/05-blame/reset.ps1 (100%) rename {02_advanced => 02-advanced}/05-blame/setup.ps1 (100%) rename {02_advanced => 02-advanced}/05-blame/verify.ps1 (100%) rename {02_advanced => 02-advanced}/06-merge-strategies/README.md (100%) rename {02_advanced => 02-advanced}/06-merge-strategies/reset.ps1 (100%) rename {02_advanced => 02-advanced}/06-merge-strategies/setup.ps1 (100%) rename {02_advanced => 02-advanced}/06-merge-strategies/verify.ps1 (100%) diff --git a/01_essentials/01-basics/README.md b/01-essentials/01-basics/README.md similarity index 100% rename from 01_essentials/01-basics/README.md rename to 01-essentials/01-basics/README.md diff --git a/01_essentials/01-basics/reset.ps1 b/01-essentials/01-basics/reset.ps1 similarity index 100% rename from 01_essentials/01-basics/reset.ps1 rename to 01-essentials/01-basics/reset.ps1 diff --git a/01_essentials/01-basics/setup.ps1 b/01-essentials/01-basics/setup.ps1 similarity index 100% rename from 01_essentials/01-basics/setup.ps1 rename to 01-essentials/01-basics/setup.ps1 diff --git a/01_essentials/01-basics/verify.ps1 b/01-essentials/01-basics/verify.ps1 similarity index 100% rename from 01_essentials/01-basics/verify.ps1 rename to 01-essentials/01-basics/verify.ps1 diff --git a/01_essentials/02-history/README.md b/01-essentials/02-history/README.md similarity index 100% rename from 01_essentials/02-history/README.md rename to 01-essentials/02-history/README.md diff --git a/01_essentials/02-history/reset.ps1 b/01-essentials/02-history/reset.ps1 similarity index 100% rename from 01_essentials/02-history/reset.ps1 rename to 01-essentials/02-history/reset.ps1 diff --git a/01_essentials/02-history/setup.ps1 b/01-essentials/02-history/setup.ps1 similarity index 100% rename from 01_essentials/02-history/setup.ps1 rename to 01-essentials/02-history/setup.ps1 diff --git a/01_essentials/02-history/verify.ps1 b/01-essentials/02-history/verify.ps1 similarity index 100% rename from 01_essentials/02-history/verify.ps1 rename to 01-essentials/02-history/verify.ps1 diff --git a/01_essentials/03-branching-and-merging/README.md b/01-essentials/03-branching-and-merging/README.md similarity index 100% rename from 01_essentials/03-branching-and-merging/README.md rename to 01-essentials/03-branching-and-merging/README.md diff --git a/01_essentials/03-branching-and-merging/reset.ps1 b/01-essentials/03-branching-and-merging/reset.ps1 similarity index 100% rename from 01_essentials/03-branching-and-merging/reset.ps1 rename to 01-essentials/03-branching-and-merging/reset.ps1 diff --git a/01_essentials/03-branching-and-merging/setup.ps1 b/01-essentials/03-branching-and-merging/setup.ps1 similarity index 100% rename from 01_essentials/03-branching-and-merging/setup.ps1 rename to 01-essentials/03-branching-and-merging/setup.ps1 diff --git a/01_essentials/03-branching-and-merging/verify.ps1 b/01-essentials/03-branching-and-merging/verify.ps1 similarity index 100% rename from 01_essentials/03-branching-and-merging/verify.ps1 rename to 01-essentials/03-branching-and-merging/verify.ps1 diff --git a/01_essentials/04-cherry-pick/README.md b/01-essentials/04-cherry-pick/README.md similarity index 100% rename from 01_essentials/04-cherry-pick/README.md rename to 01-essentials/04-cherry-pick/README.md diff --git a/01_essentials/04-cherry-pick/reset.ps1 b/01-essentials/04-cherry-pick/reset.ps1 similarity index 100% rename from 01_essentials/04-cherry-pick/reset.ps1 rename to 01-essentials/04-cherry-pick/reset.ps1 diff --git a/01_essentials/04-cherry-pick/setup.ps1 b/01-essentials/04-cherry-pick/setup.ps1 similarity index 100% rename from 01_essentials/04-cherry-pick/setup.ps1 rename to 01-essentials/04-cherry-pick/setup.ps1 diff --git a/01_essentials/04-cherry-pick/verify.ps1 b/01-essentials/04-cherry-pick/verify.ps1 similarity index 100% rename from 01_essentials/04-cherry-pick/verify.ps1 rename to 01-essentials/04-cherry-pick/verify.ps1 diff --git a/01_essentials/05-revert/README.md b/01-essentials/05-revert/README.md similarity index 100% rename from 01_essentials/05-revert/README.md rename to 01-essentials/05-revert/README.md diff --git a/01_essentials/05-revert/reset.ps1 b/01-essentials/05-revert/reset.ps1 similarity index 100% rename from 01_essentials/05-revert/reset.ps1 rename to 01-essentials/05-revert/reset.ps1 diff --git a/01_essentials/05-revert/setup.ps1 b/01-essentials/05-revert/setup.ps1 similarity index 100% rename from 01_essentials/05-revert/setup.ps1 rename to 01-essentials/05-revert/setup.ps1 diff --git a/01_essentials/05-revert/verify.ps1 b/01-essentials/05-revert/verify.ps1 similarity index 100% rename from 01_essentials/05-revert/verify.ps1 rename to 01-essentials/05-revert/verify.ps1 diff --git a/01_essentials/06-reset/README.md b/01-essentials/06-reset/README.md similarity index 100% rename from 01_essentials/06-reset/README.md rename to 01-essentials/06-reset/README.md diff --git a/01_essentials/06-reset/reset.ps1 b/01-essentials/06-reset/reset.ps1 similarity index 100% rename from 01_essentials/06-reset/reset.ps1 rename to 01-essentials/06-reset/reset.ps1 diff --git a/01_essentials/06-reset/setup.ps1 b/01-essentials/06-reset/setup.ps1 similarity index 100% rename from 01_essentials/06-reset/setup.ps1 rename to 01-essentials/06-reset/setup.ps1 diff --git a/01_essentials/06-reset/verify.ps1 b/01-essentials/06-reset/verify.ps1 similarity index 100% rename from 01_essentials/06-reset/verify.ps1 rename to 01-essentials/06-reset/verify.ps1 diff --git a/01_essentials/07-stash/README.md b/01-essentials/07-stash/README.md similarity index 100% rename from 01_essentials/07-stash/README.md rename to 01-essentials/07-stash/README.md diff --git a/01_essentials/07-stash/reset.ps1 b/01-essentials/07-stash/reset.ps1 similarity index 100% rename from 01_essentials/07-stash/reset.ps1 rename to 01-essentials/07-stash/reset.ps1 diff --git a/01_essentials/07-stash/setup.ps1 b/01-essentials/07-stash/setup.ps1 similarity index 100% rename from 01_essentials/07-stash/setup.ps1 rename to 01-essentials/07-stash/setup.ps1 diff --git a/01_essentials/07-stash/verify.ps1 b/01-essentials/07-stash/verify.ps1 similarity index 100% rename from 01_essentials/07-stash/verify.ps1 rename to 01-essentials/07-stash/verify.ps1 diff --git a/01_essentials/08-multiplayer/FACILITATOR-SETUP.md b/01-essentials/08-multiplayer/FACILITATOR-SETUP.md similarity index 100% rename from 01_essentials/08-multiplayer/FACILITATOR-SETUP.md rename to 01-essentials/08-multiplayer/FACILITATOR-SETUP.md diff --git a/01_essentials/08-multiplayer/README.md b/01-essentials/08-multiplayer/README.md similarity index 100% rename from 01_essentials/08-multiplayer/README.md rename to 01-essentials/08-multiplayer/README.md diff --git a/02_advanced/01-rebasing/README.md b/02-advanced/01-rebasing/README.md similarity index 100% rename from 02_advanced/01-rebasing/README.md rename to 02-advanced/01-rebasing/README.md diff --git a/02_advanced/01-rebasing/reset.ps1 b/02-advanced/01-rebasing/reset.ps1 similarity index 100% rename from 02_advanced/01-rebasing/reset.ps1 rename to 02-advanced/01-rebasing/reset.ps1 diff --git a/02_advanced/01-rebasing/setup.ps1 b/02-advanced/01-rebasing/setup.ps1 similarity index 100% rename from 02_advanced/01-rebasing/setup.ps1 rename to 02-advanced/01-rebasing/setup.ps1 diff --git a/02_advanced/01-rebasing/verify.ps1 b/02-advanced/01-rebasing/verify.ps1 similarity index 100% rename from 02_advanced/01-rebasing/verify.ps1 rename to 02-advanced/01-rebasing/verify.ps1 diff --git a/02_advanced/02-interactive-rebase/README.md b/02-advanced/02-interactive-rebase/README.md similarity index 100% rename from 02_advanced/02-interactive-rebase/README.md rename to 02-advanced/02-interactive-rebase/README.md diff --git a/02_advanced/02-interactive-rebase/reset.ps1 b/02-advanced/02-interactive-rebase/reset.ps1 similarity index 100% rename from 02_advanced/02-interactive-rebase/reset.ps1 rename to 02-advanced/02-interactive-rebase/reset.ps1 diff --git a/02_advanced/02-interactive-rebase/setup.ps1 b/02-advanced/02-interactive-rebase/setup.ps1 similarity index 100% rename from 02_advanced/02-interactive-rebase/setup.ps1 rename to 02-advanced/02-interactive-rebase/setup.ps1 diff --git a/02_advanced/02-interactive-rebase/verify.ps1 b/02-advanced/02-interactive-rebase/verify.ps1 similarity index 100% rename from 02_advanced/02-interactive-rebase/verify.ps1 rename to 02-advanced/02-interactive-rebase/verify.ps1 diff --git a/02_advanced/03-worktrees/README.md b/02-advanced/03-worktrees/README.md similarity index 100% rename from 02_advanced/03-worktrees/README.md rename to 02-advanced/03-worktrees/README.md diff --git a/02_advanced/03-worktrees/reset.ps1 b/02-advanced/03-worktrees/reset.ps1 similarity index 100% rename from 02_advanced/03-worktrees/reset.ps1 rename to 02-advanced/03-worktrees/reset.ps1 diff --git a/02_advanced/03-worktrees/setup.ps1 b/02-advanced/03-worktrees/setup.ps1 similarity index 100% rename from 02_advanced/03-worktrees/setup.ps1 rename to 02-advanced/03-worktrees/setup.ps1 diff --git a/02_advanced/03-worktrees/verify.ps1 b/02-advanced/03-worktrees/verify.ps1 similarity index 100% rename from 02_advanced/03-worktrees/verify.ps1 rename to 02-advanced/03-worktrees/verify.ps1 diff --git a/02_advanced/04-bisect/README.md b/02-advanced/04-bisect/README.md similarity index 100% rename from 02_advanced/04-bisect/README.md rename to 02-advanced/04-bisect/README.md diff --git a/02_advanced/04-bisect/reset.ps1 b/02-advanced/04-bisect/reset.ps1 similarity index 100% rename from 02_advanced/04-bisect/reset.ps1 rename to 02-advanced/04-bisect/reset.ps1 diff --git a/02_advanced/04-bisect/setup.ps1 b/02-advanced/04-bisect/setup.ps1 similarity index 100% rename from 02_advanced/04-bisect/setup.ps1 rename to 02-advanced/04-bisect/setup.ps1 diff --git a/02_advanced/04-bisect/verify.ps1 b/02-advanced/04-bisect/verify.ps1 similarity index 100% rename from 02_advanced/04-bisect/verify.ps1 rename to 02-advanced/04-bisect/verify.ps1 diff --git a/02_advanced/05-blame/README.md b/02-advanced/05-blame/README.md similarity index 100% rename from 02_advanced/05-blame/README.md rename to 02-advanced/05-blame/README.md diff --git a/02_advanced/05-blame/reset.ps1 b/02-advanced/05-blame/reset.ps1 similarity index 100% rename from 02_advanced/05-blame/reset.ps1 rename to 02-advanced/05-blame/reset.ps1 diff --git a/02_advanced/05-blame/setup.ps1 b/02-advanced/05-blame/setup.ps1 similarity index 100% rename from 02_advanced/05-blame/setup.ps1 rename to 02-advanced/05-blame/setup.ps1 diff --git a/02_advanced/05-blame/verify.ps1 b/02-advanced/05-blame/verify.ps1 similarity index 100% rename from 02_advanced/05-blame/verify.ps1 rename to 02-advanced/05-blame/verify.ps1 diff --git a/02_advanced/06-merge-strategies/README.md b/02-advanced/06-merge-strategies/README.md similarity index 100% rename from 02_advanced/06-merge-strategies/README.md rename to 02-advanced/06-merge-strategies/README.md diff --git a/02_advanced/06-merge-strategies/reset.ps1 b/02-advanced/06-merge-strategies/reset.ps1 similarity index 100% rename from 02_advanced/06-merge-strategies/reset.ps1 rename to 02-advanced/06-merge-strategies/reset.ps1 diff --git a/02_advanced/06-merge-strategies/setup.ps1 b/02-advanced/06-merge-strategies/setup.ps1 similarity index 100% rename from 02_advanced/06-merge-strategies/setup.ps1 rename to 02-advanced/06-merge-strategies/setup.ps1 diff --git a/02_advanced/06-merge-strategies/verify.ps1 b/02-advanced/06-merge-strategies/verify.ps1 similarity index 100% rename from 02_advanced/06-merge-strategies/verify.ps1 rename to 02-advanced/06-merge-strategies/verify.ps1 From 5492f17a5a8d50d36cb496f74cdcc5b8db653812 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Mon, 12 Jan 2026 08:42:20 +0100 Subject: [PATCH 05/61] fix: cleanup README and focus on windows --- 01-essentials/08-multiplayer/README.md | 12 +- 02-advanced/04-bisect/README.md | 24 +- INSTALLATION.md | 303 +++++++++++++++++++++++++ README.md | 107 ++++----- 4 files changed, 365 insertions(+), 81 deletions(-) create mode 100644 INSTALLATION.md diff --git a/01-essentials/08-multiplayer/README.md b/01-essentials/08-multiplayer/README.md index bd1cad7..61ed36f 100644 --- a/01-essentials/08-multiplayer/README.md +++ b/01-essentials/08-multiplayer/README.md @@ -1372,12 +1372,12 @@ You've completed this module when you can check off ALL of these: Continue your Git journey with advanced techniques: -- **02_advanced/01-rebasing**: Learn to rebase instead of merge for cleaner history -- **02_advanced/02-interactive-rebase**: Clean up messy commits before submitting PRs -- **02_advanced/03-worktrees**: Work on multiple branches simultaneously -- **02_advanced/04-bisect**: Find bugs using binary search through commit history -- **02_advanced/05-blame**: Investigate who changed what and when -- **02_advanced/06-merge-strategies**: Master different merge strategies and when to use them +- **02-advanced/01-rebasing**: Learn to rebase instead of merge for cleaner history +- **02-advanced/02-interactive-rebase**: Clean up messy commits before submitting PRs +- **02-advanced/03-worktrees**: Work on multiple branches simultaneously +- **02-advanced/04-bisect**: Find bugs using binary search through commit history +- **02-advanced/05-blame**: Investigate who changed what and when +- **02-advanced/06-merge-strategies**: Master different merge strategies and when to use them ### Practice More diff --git a/02-advanced/04-bisect/README.md b/02-advanced/04-bisect/README.md index e82b1c6..10870b9 100644 --- a/02-advanced/04-bisect/README.md +++ b/02-advanced/04-bisect/README.md @@ -74,7 +74,7 @@ Use bisect when: ## Useful Commands -```bash +```powershell # Start bisect session git bisect start @@ -112,7 +112,7 @@ git bisect log Run the verification script to check your solution: -```bash +```powershell .\verify.ps1 ``` @@ -152,7 +152,7 @@ The verification will check that: ## Manual vs Automated Bisect ### Manual Bisect -```bash +```powershell git bisect start git bisect bad git bisect good HEAD~20 @@ -165,7 +165,7 @@ git bisect reset ``` ### Automated Bisect -```bash +```powershell git bisect start git bisect bad git bisect good HEAD~20 @@ -182,22 +182,28 @@ The test script should exit with: ## Bisect Workflow Example ### Finding a Performance Regression -```bash +```powershell # App is slow now, was fast 50 commits ago git bisect start git bisect bad git bisect good HEAD~50 # Create test script -echo '#!/bin/bash\ntime npm start | grep "Started in"' > test.sh -chmod +x test.sh +@' +$output = npm start 2>&1 | Select-String "Started in" +if ($output -match "Started in (\d+)") { + if ([int]$Matches[1] -gt 5000) { exit 1 } # Slow + else { exit 0 } # Fast +} +exit 1 +'@ | Out-File -FilePath test.ps1 -git bisect run ./test.sh +git bisect run pwsh test.ps1 # Git finds the commit that made it slow ``` ### Finding When a Feature Broke -```bash +```powershell git bisect start git bisect bad git bisect good v1.0.0 # Last known good version diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000..d5c5589 --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,303 @@ +# Installation Guide for Windows 11 + +This guide will help you install everything needed for the Git Workshop on Windows 11. + +## Prerequisites + +You'll need administrator access to install software on your Windows 11 machine. + +## What You'll Install + +1. **PowerShell 7** - Modern cross-platform PowerShell (replaces the older Windows PowerShell 5.1) +2. **Git** - Version control system (2.23 or later) +3. **Visual Studio Code** - Modern code editor with excellent Git integration + +## Installation Steps + +### 1. Install PowerShell 7 + +PowerShell 7 is the modern, cross-platform version of PowerShell. Windows 11 comes with PowerShell 5.1, but we recommend PowerShell 7 for the best experience. + +**Option A: Using winget (Recommended)** + +Open **Windows Terminal** or **Command Prompt** and run: + +```powershell +winget install --id Microsoft.PowerShell --source winget +``` + +**Option B: Manual Download** + +1. Visit https://github.com/PowerShell/PowerShell/releases/latest +2. Download the file ending in `-win-x64.msi` (e.g., `PowerShell-7.4.1-win-x64.msi`) +3. Run the installer +4. Accept all defaults + +**Verify Installation:** + +Open a new terminal and run: + +```powershell +pwsh --version +``` + +You should see version 7.x.x or higher. + +**Important:** After installing PowerShell 7, use it instead of the older "Windows PowerShell 5.1". Look for "PowerShell 7" in your Start menu or Windows Terminal. + +### 2. Install Git + +Git is the version control system you'll learn in this workshop. You need version 2.23 or later. + +**Option A: Using winget (Recommended)** + +```powershell +winget install --id Git.Git -e --source winget +``` + +**Option B: Manual Download** + +1. Visit https://git-scm.com/downloads +2. Click "Windows" +3. Download the 64-bit installer +4. Run the installer with these recommended settings: + - **Default editor**: Choose "Visual Studio Code" (we'll install it next) + - **PATH environment**: Select "Git from the command line and also from 3rd-party software" + - **Line ending conversions**: Choose "Checkout Windows-style, commit Unix-style line endings" + - **Terminal emulator**: Choose "Use Windows' default console window" + - All other settings: Accept defaults + +**Verify Installation:** + +Open a **new** PowerShell window and run: + +```powershell +git --version +``` + +You should see version 2.23 or higher (e.g., `git version 2.43.0`). + +### 3. Install Visual Studio Code + +VS Code is a free, powerful code editor with excellent Git integration. + +**Option A: Using winget (Recommended)** + +```powershell +winget install --id Microsoft.VisualStudioCode --source winget +``` + +**Option B: Manual Download** + +1. Visit https://code.visualstudio.com/ +2. Click "Download for Windows" +3. Run the installer +4. During installation, check these options: + - ✅ Add "Open with Code" action to Windows Explorer file context menu + - ✅ Add "Open with Code" action to Windows Explorer directory context menu + - ✅ Register Code as an editor for supported file types + - ✅ Add to PATH + +**Verify Installation:** + +```powershell +code --version +``` + +You should see version information. + +**Recommended VS Code Extensions:** + +Open VS Code and install these extensions for the best Git experience: + +1. **GitLens** - Supercharge Git capabilities + - Press `Ctrl+Shift+X` to open Extensions + - Search for "GitLens" + - Click Install + +2. **Git Graph** - View Git history visually + - Search for "Git Graph" + - Click Install + +3. **PowerShell** - Better PowerShell support + - Search for "PowerShell" + - Install the one from Microsoft + +## Configure Git + +Before making your first commit, tell Git who you are: + +```powershell +git config --global user.name "Your Name" +git config --global user.email "your.email@example.com" +``` + +**Verify your configuration:** + +```powershell +git config --global user.name +git config --global user.email +``` + +You should see your name and email printed. + +**Optional: Set VS Code as Git's Default Editor** + +If you installed Git before VS Code, configure Git to use VS Code: + +```powershell +git config --global core.editor "code --wait" +``` + +## PowerShell Execution Policy + +When running PowerShell scripts (`.ps1` files) in this workshop, you might encounter an error about execution policies. + +**If you see an error like "script cannot be loaded because running scripts is disabled":** + +Open **PowerShell 7 as Administrator** and run: + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +This allows you to run local scripts while maintaining security for downloaded scripts. + +## Running Scripts in the Workshop + +After installation, you can run workshop scripts using: + +```powershell +.\setup.ps1 +.\verify.ps1 +.\reset.ps1 +``` + +**Example workflow:** + +```powershell +# Navigate to a module +cd 01-essentials\01-basics + +# Run the setup script +.\setup.ps1 + +# Complete the challenge using Git commands +# ... + +# Verify your solution +.\verify.ps1 +``` + +## Optional: Python (for Module 08 only) + +Module 08 (Multiplayer Git) uses Python for "The Great Print Project". You only need this for that specific module. + +**Install Python 3.12:** + +```powershell +winget install --id Python.Python.3.12 --source winget +``` + +**Verify installation:** + +```powershell +python --version +``` + +You should see Python 3.12.x or higher. + +## Optional: Windows Terminal (Highly Recommended) + +Windows Terminal provides a modern terminal experience with tabs, better colors, and PowerShell 7 integration. + +**Install:** + +```powershell +winget install --id Microsoft.WindowsTerminal --source winget +``` + +Or install from the **Microsoft Store** (search for "Windows Terminal"). + +**After installation:** +- Press `Win+X` and select "Windows Terminal" +- Or search "Terminal" in the Start menu +- PowerShell 7 should be the default profile + +## Verify Complete Installation + +Run these commands to verify everything is installed correctly: + +```powershell +# PowerShell version (should be 7.x.x) +pwsh --version + +# Git version (should be 2.23 or higher) +git --version + +# VS Code version +code --version + +# Git configuration +git config --global user.name +git config --global user.email + +# Optional: Python (for Module 08) +python --version +``` + +## Troubleshooting + +### Git command not found + +If `git --version` doesn't work after installation: +1. Close and reopen your terminal (Git needs a new terminal to update PATH) +2. Restart your computer if the problem persists + +### VS Code command not found + +If `code --version` doesn't work: +1. Ensure you checked "Add to PATH" during installation +2. Close and reopen your terminal +3. If still not working, reinstall VS Code with the PATH option enabled + +### PowerShell execution policy errors + +If you can't run `.ps1` scripts: +1. Open PowerShell 7 **as Administrator** +2. Run: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser` +3. Close admin PowerShell and try again in a regular PowerShell window + +### winget command not found + +If `winget` doesn't work: +1. Update Windows 11 to the latest version (Settings → Windows Update) +2. Install "App Installer" from the Microsoft Store +3. Restart your computer + +## You're Ready! + +Once all verification commands work, you're ready to start the workshop! + +```powershell +# Clone or download the git-workshop repository +# Navigate to it +cd path\to\git-workshop + +# Start with Module 01 +cd 01-essentials\01-basics + +# Read the instructions +code README.md + +# Run setup and begin! +.\setup.ps1 +``` + +## Next Steps + +- Read the main [README.md](README.md) for workshop overview +- Check [GIT-CHEATSHEET.md](GIT-CHEATSHEET.md) for Git command reference +- Start with Module 01: `01-essentials\01-basics` + +Happy learning! diff --git a/README.md b/README.md index 957191d..1732ebf 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Advanced Git workflows for power users: ### For Local Modules (01-08 in Essentials, all Advanced modules) -1. Navigate to a module directory (e.g., `01_essentials/01-basics`) +1. Navigate to a module directory (e.g., `01-essentials/01-basics`) 2. Read the `README.md` to understand the challenge 3. Run `./setup.ps1` to create the challenge environment 4. Complete the challenge using git commands @@ -49,20 +49,18 @@ Advanced Git workflows for power users: **This module is different!** It uses a real Git server for authentic collaboration: -1. Navigate to `01_essentials/08-multiplayer` +1. Navigate to `01-essentials/08-multiplayer` 2. Read the `README.md` for complete instructions 3. **No setup script** - you'll clone from https://git.frod.dk/multiplayer 4. Work with a partner on shared branches 5. Experience real merge conflicts and pull requests 6. **No verify script** - success is visual (your code appears in the final output) -**Facilitators**: See `01_essentials/08-multiplayer/FACILITATOR-SETUP.md` for server setup and workshop guidance. +**Facilitators**: See `01-essentials/08-multiplayer/FACILITATOR-SETUP.md` for server setup and workshop guidance. ## Running PowerShell Scripts -Most modules include setup, verification, and reset scripts. Here's how to run them: - -### Windows +Most modules include setup, verification, and reset scripts. If you encounter an "execution policy" error when running scripts, open PowerShell as Administrator and run: @@ -73,76 +71,50 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser Then run scripts using: ```powershell .\setup.ps1 +.\verify.ps1 +.\reset.ps1 ``` -### macOS/Linux - -First, make sure scripts are executable (only needed once): -```bash -chmod +x 01_essentials/*/setup.ps1 01_essentials/*/verify.ps1 01_essentials/*/reset.ps1 -chmod +x 02_advanced/*/setup.ps1 02_advanced/*/verify.ps1 02_advanced/*/reset.ps1 -``` - -Then run scripts using: -```bash -./setup.ps1 -``` - -Or explicitly with PowerShell: -```bash -pwsh setup.ps1 -``` - -**Note:** All examples in this workshop use Windows syntax (`.\script.ps1`). macOS/Linux users should use `./script.ps1` instead. - ## Requirements -### Git +### Installation -You need Git version 2.23 or later (for `git switch` and `git restore` commands). +**Windows 11 Users:** See [INSTALLATION.md](INSTALLATION.md) for complete step-by-step installation instructions including PowerShell 7, Git, and Visual Studio Code. -**Check if Git is installed:** -```powershell -git --version -``` +**Quick Check:** -If you see a version number (e.g., "git version 2.34.1"), you're all set! +You need the following software installed: -If not, download and install from: https://git-scm.com/downloads +- **Git 2.23+** - Version control system + ```powershell + git --version + ``` -**Configure Git (Required - First Time Only):** +- **PowerShell 7+** + ```powershell + pwsh --version + ``` -Before making your first commit, tell Git who you are: +- **Python 3.6+** (for Module 08 only) + ```powershell + python --version + ``` + +**First-Time Git Configuration:** + +Before making your first commit, configure Git with your identity: ```powershell git config --global user.name "Your Name" git config --global user.email "your.email@example.com" ``` -**Verify your configuration:** +Verify your configuration: ```powershell git config --global user.name git config --global user.email ``` -You should see your name and email printed. This is required to make commits in Git. - -### PowerShell - -- **Windows**: PowerShell 5.1+ (already included in Windows 10/11) -- **macOS/Linux**: Install PowerShell Core 7+ from https://github.com/PowerShell/PowerShell - -**Check PowerShell version:** -```powershell -$PSVersionTable.PSVersion -``` - -### Python (for Module 08 only) - -Module 08 (Multiplayer Git) uses Python: -- **Python 3.6+** required to run the Great Print Project -- Check: `python --version` or `python3 --version` - ### Basic Command Line Knowledge You should know how to: @@ -163,11 +135,7 @@ This workshop includes many markdown files with instructions. You can install `g After installation, read markdown files with: ```powershell -# Windows .\bin\glow.exe README.md - -# macOS/Linux -./bin/glow README.md ``` **Pro tip**: Use `glow -p` for pager mode on longer files! @@ -194,10 +162,12 @@ Don't worry if these don't make sense yet - you'll learn them hands-on as you pr ## Getting Started -Start with Essentials Module 01: +**First time?** Make sure you have Git and PowerShell installed - see [INSTALLATION.md](INSTALLATION.md) for Windows 11 setup instructions. + +Once installed, start with Essentials Module 01: ```powershell -cd 01_essentials/01-basics +cd 01-essentials\01-basics .\setup.ps1 ``` @@ -345,7 +315,7 @@ Run your own Git server with Gitea using Docker and Cloudflare Tunnel: **Setup:** 1. See [GITEA-SETUP.md](GITEA-SETUP.md) for complete Gitea + Docker + Cloudflare Tunnel instructions -2. See `01_essentials/08-multiplayer/FACILITATOR-SETUP.md` for detailed workshop preparation: +2. See `01-essentials/08-multiplayer/FACILITATOR-SETUP.md` for detailed workshop preparation: - Creating student accounts - Setting up The Great Print Project repository - Pairing students @@ -369,13 +339,14 @@ You can also use existing cloud Git platforms: ``` git-workshop/ ├── README.md # This file +├── INSTALLATION.md # Windows 11 installation guide (PowerShell 7, Git, VS Code) ├── GIT-CHEATSHEET.md # Quick reference for all Git commands ├── WORKSHOP-AGENDA.md # Facilitator guide for running workshops ├── PRESENTATION-OUTLINE.md # Slide deck outline ├── GITEA-SETUP.md # Self-hosted Git server setup ├── install-glow.ps1 # Install glow markdown renderer │ -├── 01_essentials/ # Core Git skills (8 modules) +├── 01-essentials/ # Core Git skills (8 modules) │ ├── 01-basics/ # Initialize, commit, status │ ├── 02-history/ # Log, diff, show │ ├── 03-branching-and-merging/ # Branches, merging, conflicts (checkpoint-based) @@ -387,7 +358,7 @@ git-workshop/ │ ├── README.md # Student guide │ └── FACILITATOR-SETUP.md # Server setup guide │ -└── 02_advanced/ # Professional techniques (6 modules) +└── 02-advanced/ # Professional techniques (6 modules) ├── 01-rebasing/ # Linear history with rebase ├── 02-interactive-rebase/ # Clean up commits ├── 03-worktrees/ # Multiple branches simultaneously @@ -468,10 +439,14 @@ This workshop is provided as-is for educational purposes. --- -**Ready to master Git? Start with Essentials Module 01 and begin your journey!** +**Ready to master Git?** + +First-time setup (Windows 11): See [INSTALLATION.md](INSTALLATION.md) + +Then start with Essentials Module 01 and begin your journey! ```powershell -cd 01_essentials/01-basics +cd 01-essentials\01-basics .\setup.ps1 ``` From bb01592b6cf0855ba08650cbd440019c31e341ff Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Mon, 12 Jan 2026 08:49:47 +0100 Subject: [PATCH 06/61] feat: initial install-prerequisites script that install vscode, git and powershell 7 --- INSTALLATION.md | 25 +- README.md | 11 +- install-prerequisites.ps1 | 463 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 install-prerequisites.ps1 diff --git a/INSTALLATION.md b/INSTALLATION.md index d5c5589..2d4afac 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -2,6 +2,29 @@ This guide will help you install everything needed for the Git Workshop on Windows 11. +## Quick Start (Automated Installation) + +**Easiest option:** Run our one-shot installation script that automatically installs all required tools using winget. + +1. Open **PowerShell** or **Windows Terminal** +2. Navigate to the git-workshop directory +3. Run the installation script: + +```powershell +.\install-prerequisites.ps1 +``` + +The script will: +- Check if tools are already installed +- Install PowerShell 7, Git 2.23+, and Visual Studio Code +- Prompt you for optional tools (Python 3.12, Windows Terminal) +- Show clear progress and verify each installation +- Display Git configuration instructions when complete + +**If you prefer manual installation**, continue with the detailed steps below. + +--- + ## Prerequisites You'll need administrator access to install software on your Windows 11 machine. @@ -12,7 +35,7 @@ You'll need administrator access to install software on your Windows 11 machine. 2. **Git** - Version control system (2.23 or later) 3. **Visual Studio Code** - Modern code editor with excellent Git integration -## Installation Steps +## Manual Installation Steps ### 1. Install PowerShell 7 diff --git a/README.md b/README.md index 1732ebf..8c83e6b 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,15 @@ Then run scripts using: ### Installation -**Windows 11 Users:** See [INSTALLATION.md](INSTALLATION.md) for complete step-by-step installation instructions including PowerShell 7, Git, and Visual Studio Code. +**Quick Automated Installation (Windows 11):** + +Run our one-shot installation script to automatically install all required tools: + +```powershell +.\install-prerequisites.ps1 +``` + +**Manual Installation:** See [INSTALLATION.md](INSTALLATION.md) for complete step-by-step installation instructions including PowerShell 7, Git, and Visual Studio Code. **Quick Check:** @@ -340,6 +348,7 @@ You can also use existing cloud Git platforms: git-workshop/ ├── README.md # This file ├── INSTALLATION.md # Windows 11 installation guide (PowerShell 7, Git, VS Code) +├── install-prerequisites.ps1 # Automated installation script (one-shot setup) ├── GIT-CHEATSHEET.md # Quick reference for all Git commands ├── WORKSHOP-AGENDA.md # Facilitator guide for running workshops ├── PRESENTATION-OUTLINE.md # Slide deck outline diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 new file mode 100644 index 0000000..5f02dfd --- /dev/null +++ b/install-prerequisites.ps1 @@ -0,0 +1,463 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Installs all prerequisites for the Git Workshop using winget. + +.DESCRIPTION +This script automates the installation of required tools for the Git Workshop: +- PowerShell 7 (cross-platform PowerShell) +- Git 2.23+ (version control system) +- Visual Studio Code (code editor with Git integration) + +Optional tools (with user prompts): +- Python 3.12 (for Module 08: Multiplayer Git) +- Windows Terminal (modern terminal experience) + +The script checks for existing installations, shows clear progress, and verifies +each installation succeeded. At the end, it displays Git configuration instructions. + +.EXAMPLE +PS> .\install-prerequisites.ps1 +Runs the installation script with interactive prompts. + +.NOTES +Requires Windows 11 with winget (App Installer) available. +Some installations may require administrator privileges. +#> + +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Continue' # Continue on errors to show all results + +#region Helper Functions + +function Write-ColorMessage { + param( + [string]$Message, + [string]$Color = 'White' + ) + Write-Host $Message -ForegroundColor $Color +} + +function Write-Step { + param([string]$Message) + Write-ColorMessage "`n=== $Message ===" -Color Cyan +} + +function Write-Success { + param([string]$Message) + Write-ColorMessage " ✓ $Message" -Color Green +} + +function Write-Warning { + param([string]$Message) + Write-ColorMessage " ⚠ $Message" -Color Yellow +} + +function Write-Error { + param([string]$Message) + Write-ColorMessage " ✗ $Message" -Color Red +} + +function Test-CommandExists { + param([string]$Command) + + $oldPreference = $ErrorActionPreference + $ErrorActionPreference = 'SilentlyContinue' + + try { + if (Get-Command $Command -ErrorAction SilentlyContinue) { + return $true + } + return $false + } + finally { + $ErrorActionPreference = $oldPreference + } +} + +function Get-InstalledVersion { + param( + [string]$Command, + [string]$VersionArg = '--version' + ) + + try { + $output = & $Command $VersionArg 2>&1 | Select-Object -First 1 + return $output.ToString().Trim() + } + catch { + return $null + } +} + +function Test-WingetAvailable { + if (-not (Test-CommandExists 'winget')) { + Write-Error "winget is not available on this system." + Write-Host "`nTo fix this:" -ForegroundColor Yellow + Write-Host " 1. Update Windows 11 to the latest version (Settings → Windows Update)" -ForegroundColor White + Write-Host " 2. Install 'App Installer' from the Microsoft Store" -ForegroundColor White + Write-Host " 3. Restart your computer and run this script again" -ForegroundColor White + return $false + } + return $true +} + +function Install-Package { + param( + [string]$Name, + [string]$WingetId, + [string]$CheckCommand, + [string]$MinVersion = $null, + [string]$AdditionalArgs = '' + ) + + Write-Step "Installing $Name" + + # Check if already installed + if (Test-CommandExists $CheckCommand) { + $version = Get-InstalledVersion $CheckCommand + Write-Success "$Name is already installed: $version" + + if ($MinVersion -and $version) { + # Basic version check (not perfect but good enough for common cases) + if ($version -match '(\d+\.[\d.]+)') { + $installedVersion = $matches[1] + if ([version]$installedVersion -lt [version]$MinVersion) { + Write-Warning "Version $installedVersion is below minimum required version $MinVersion" + Write-Host " Attempting to upgrade..." -ForegroundColor Cyan + } + else { + return $true + } + } + } + else { + return $true + } + } + + # Install using winget + Write-Host " Installing via winget: $WingetId" -ForegroundColor Cyan + + $installCmd = "winget install --id $WingetId --source winget --silent $AdditionalArgs".Trim() + Write-Host " Running: $installCmd" -ForegroundColor Gray + + try { + $result = Invoke-Expression $installCmd 2>&1 + + # Check if installation succeeded + Start-Sleep -Seconds 2 # Give the system time to register the new command + + # Refresh environment variables in current session + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + if (Test-CommandExists $CheckCommand) { + $version = Get-InstalledVersion $CheckCommand + Write-Success "$Name installed successfully: $version" + return $true + } + else { + Write-Warning "$Name installation completed, but command '$CheckCommand' not found." + Write-Host " You may need to restart your terminal or computer." -ForegroundColor Yellow + return $false + } + } + catch { + Write-Error "Failed to install $Name`: $_" + return $false + } +} + +function Test-GitVersion { + if (-not (Test-CommandExists 'git')) { + return $false + } + + $version = Get-InstalledVersion 'git' + if ($version -match 'git version (\d+\.\d+)') { + $versionNumber = [decimal]$matches[1] + if ($versionNumber -ge 2.23) { + return $true + } + else { + Write-Warning "Git version $versionNumber is below required version 2.23" + return $false + } + } + + return $false +} + +function Get-UserConfirmation { + param([string]$Prompt) + + while ($true) { + $response = Read-Host "$Prompt (y/n)" + $response = $response.Trim().ToLower() + + if ($response -eq 'y' -or $response -eq 'yes') { + return $true + } + elseif ($response -eq 'n' -or $response -eq 'no') { + return $false + } + else { + Write-Host "Please enter 'y' or 'n'" -ForegroundColor Yellow + } + } +} + +#endregion + +#region Main Script + +Write-Host @" + +╔═══════════════════════════════════════════════════════════╗ +║ ║ +║ Git Workshop - Prerequisites Installation Script ║ +║ ║ +╚═══════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +Write-Host "This script will install the required tools for the Git Workshop:" -ForegroundColor White +Write-Host " • PowerShell 7 (cross-platform PowerShell)" -ForegroundColor White +Write-Host " • Git 2.23+ (version control system)" -ForegroundColor White +Write-Host " • Visual Studio Code (code editor)" -ForegroundColor White +Write-Host "" +Write-Host "You will be prompted for optional tools:" -ForegroundColor White +Write-Host " • Python 3.12 (for Module 08: Multiplayer Git)" -ForegroundColor White +Write-Host " • Windows Terminal (modern terminal experience)" -ForegroundColor White +Write-Host "" + +# Check for winget +Write-Step "Checking Prerequisites" + +if (-not (Test-WingetAvailable)) { + Write-Host "`nInstallation cannot continue without winget." -ForegroundColor Red + exit 1 +} + +Write-Success "winget is available" + +# Track installation results +$results = @{ + PowerShell = $false + Git = $false + VSCode = $false + Python = $null # null = not attempted, true = success, false = failed + WindowsTerminal = $null +} + +Write-Host "`nStarting installation..." -ForegroundColor Cyan +Write-Host "Note: Some installations may take a few minutes." -ForegroundColor Gray +Write-Host "" + +#region Required Installations + +# Install PowerShell 7 +$results.PowerShell = Install-Package ` + -Name "PowerShell 7" ` + -WingetId "Microsoft.PowerShell" ` + -CheckCommand "pwsh" + +# Install Git +$results.Git = Install-Package ` + -Name "Git" ` + -WingetId "Git.Git" ` + -CheckCommand "git" ` + -MinVersion "2.23" ` + -AdditionalArgs "-e" + +# Verify Git version specifically +if ($results.Git) { + if (-not (Test-GitVersion)) { + Write-Warning "Git is installed but version may be below 2.23" + $results.Git = $false + } +} + +# Install Visual Studio Code +$results.VSCode = Install-Package ` + -Name "Visual Studio Code" ` + -WingetId "Microsoft.VisualStudioCode" ` + -CheckCommand "code" + +#endregion + +#region Optional Installations + +# Python 3.12 (optional) +Write-Host "" +if (Get-UserConfirmation "Do you want to install Python 3.12? (Required for Module 08: Multiplayer Git)") { + $results.Python = Install-Package ` + -Name "Python 3.12" ` + -WingetId "Python.Python.3.12" ` + -CheckCommand "python" +} +else { + Write-Host " Skipping Python installation." -ForegroundColor Gray + $results.Python = $null +} + +# Windows Terminal (optional) +Write-Host "" +if (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)") { + $results.WindowsTerminal = Install-Package ` + -Name "Windows Terminal" ` + -WingetId "Microsoft.WindowsTerminal" ` + -CheckCommand "wt" +} +else { + Write-Host " Skipping Windows Terminal installation." -ForegroundColor Gray + $results.WindowsTerminal = $null +} + +#endregion + +#region Installation Summary + +Write-Step "Installation Summary" + +$allRequired = $results.PowerShell -and $results.Git -and $results.VSCode + +Write-Host "" +Write-Host "Required Tools:" -ForegroundColor White + +if ($results.PowerShell) { + Write-Success "PowerShell 7" +} +else { + Write-Error "PowerShell 7 - Installation failed or needs restart" +} + +if ($results.Git) { + Write-Success "Git 2.23+" +} +else { + Write-Error "Git 2.23+ - Installation failed or needs restart" +} + +if ($results.VSCode) { + Write-Success "Visual Studio Code" +} +else { + Write-Error "Visual Studio Code - Installation failed or needs restart" +} + +if ($results.Python -ne $null) { + Write-Host "" + Write-Host "Optional Tools:" -ForegroundColor White + + if ($results.Python) { + Write-Success "Python 3.12" + } + else { + Write-Error "Python 3.12 - Installation failed or needs restart" + } +} + +if ($results.WindowsTerminal -ne $null) { + if ($results.Python -eq $null) { + Write-Host "" + Write-Host "Optional Tools:" -ForegroundColor White + } + + if ($results.WindowsTerminal) { + Write-Success "Windows Terminal" + } + else { + Write-Error "Windows Terminal - Installation failed or needs restart" + } +} + +#endregion + +#region Next Steps + +Write-Step "Next Steps" + +if ($allRequired) { + Write-Host "" + Write-Success "All required tools installed successfully!" + Write-Host "" + + Write-Host "IMPORTANT: Configure Git before your first commit:" -ForegroundColor Yellow + Write-Host "" + Write-Host " git config --global user.name `"Your Name`"" -ForegroundColor White + Write-Host " git config --global user.email `"your.email@example.com`"" -ForegroundColor White + Write-Host "" + + Write-Host "Optional: Set VS Code as Git's default editor:" -ForegroundColor Cyan + Write-Host " git config --global core.editor `"code --wait`"" -ForegroundColor White + Write-Host "" + + Write-Host "Verify your installation:" -ForegroundColor Cyan + Write-Host " pwsh --version" -ForegroundColor White + Write-Host " git --version" -ForegroundColor White + Write-Host " code --version" -ForegroundColor White + if ($results.Python) { + Write-Host " python --version" -ForegroundColor White + } + Write-Host "" + + Write-Host "Set PowerShell execution policy (if needed):" -ForegroundColor Cyan + Write-Host " Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor White + Write-Host "" + + Write-Host "Recommended VS Code Extensions:" -ForegroundColor Cyan + Write-Host " • GitLens - Supercharge Git capabilities" -ForegroundColor White + Write-Host " • Git Graph - View Git history visually" -ForegroundColor White + Write-Host " • PowerShell - Better PowerShell support (from Microsoft)" -ForegroundColor White + Write-Host "" + Write-Host " Install via: Ctrl+Shift+X in VS Code" -ForegroundColor Gray + Write-Host "" + + Write-Host "You're ready to start the workshop!" -ForegroundColor Green + Write-Host " cd path\to\git-workshop" -ForegroundColor White + Write-Host " cd 01-essentials\01-basics" -ForegroundColor White + Write-Host " .\setup.ps1" -ForegroundColor White + Write-Host "" +} +else { + Write-Host "" + Write-Warning "Some required installations failed or need verification." + Write-Host "" + Write-Host "Troubleshooting steps:" -ForegroundColor Yellow + Write-Host " 1. Close and reopen your terminal (or restart your computer)" -ForegroundColor White + Write-Host " 2. Run this script again: .\install-prerequisites.ps1" -ForegroundColor White + Write-Host " 3. If issues persist, try manual installation:" -ForegroundColor White + Write-Host " See INSTALLATION.md for detailed instructions" -ForegroundColor White + Write-Host "" + + if (-not $results.Git) { + Write-Host "For Git issues:" -ForegroundColor Yellow + Write-Host " • Restart terminal after installation (PATH needs to refresh)" -ForegroundColor White + Write-Host " • Manual download: https://git-scm.com/downloads" -ForegroundColor White + Write-Host "" + } + + if (-not $results.VSCode) { + Write-Host "For VS Code issues:" -ForegroundColor Yellow + Write-Host " • Ensure 'Add to PATH' option is enabled during installation" -ForegroundColor White + Write-Host " • Manual download: https://code.visualstudio.com/" -ForegroundColor White + Write-Host "" + } + + if (-not $results.PowerShell) { + Write-Host "For PowerShell 7 issues:" -ForegroundColor Yellow + Write-Host " • Manual download: https://github.com/PowerShell/PowerShell/releases/latest" -ForegroundColor White + Write-Host " • Download the file ending in '-win-x64.msi'" -ForegroundColor White + Write-Host "" + } + + exit 1 +} + +#endregion + +exit 0 From 34f2607b220f17b50e37aed9beca0b693862593b Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:26:29 +0100 Subject: [PATCH 07/61] Add oneshot installation script for easy setup - Created install.ps1 that downloads repository and runs prerequisites installer - Updated README.md and INSTALLATION.md to document the new oneshot option - Script can be run with: irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex - Provides seamless installation experience for new users --- INSTALLATION.md | 8 +- README.md | 10 ++- install.ps1 | 227 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 install.ps1 diff --git a/INSTALLATION.md b/INSTALLATION.md index 2d4afac..9ac3c98 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,7 +4,13 @@ This guide will help you install everything needed for the Git Workshop on Windo ## Quick Start (Automated Installation) -**Easiest option:** Run our one-shot installation script that automatically installs all required tools using winget. +**Easiest option:** Run our oneshot installation script that downloads everything and installs all required tools automatically. + +```powershell +irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex +``` + +**Alternative:** If you've already cloned the repository, you can run the local installation script: 1. Open **PowerShell** or **Windows Terminal** 2. Navigate to the git-workshop directory diff --git a/README.md b/README.md index 8c83e6b..ff20e83 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,15 @@ Then run scripts using: **Quick Automated Installation (Windows 11):** -Run our one-shot installation script to automatically install all required tools: +**Option 1: Oneshot Installation (Recommended)** +Download and run everything in one command: + +```powershell +irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex +``` + +**Option 2: Local Installation** +Clone the repository first, then run: ```powershell .\install-prerequisites.ps1 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..0f83cdf --- /dev/null +++ b/install.ps1 @@ -0,0 +1,227 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS +Oneshot Git Workshop installer - Downloads and runs the prerequisites installation. + +.DESCRIPTION +This script downloads the Git Workshop repository and runs the prerequisites +installation script. It's designed to be run directly from the web: + + irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex + +The script will: +1. Create a temporary working directory +2. Download the Git Workshop repository +3. Run the install-prerequisites.ps1 script +4. Clean up temporary files + +.EXAMPLE +PS> irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex +Downloads and runs the Git Workshop installer. + +.NOTES +Requires Windows 11 with PowerShell and internet access. +#> + +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +#region Helper Functions + +function Write-ColorMessage { + param( + [string]$Message, + [string]$Color = 'White' + ) + Write-Host $Message -ForegroundColor $Color +} + +function Write-Step { + param([string]$Message) + Write-ColorMessage "`n=== $Message ===" -Color Cyan +} + +function Write-Success { + param([string]$Message) + Write-ColorMessage " ✓ $Message" -Color Green +} + +function Write-Warning { + param([string]$Message) + Write-ColorMessage " ⚠ $Message" -Color Yellow +} + +function Write-Error { + param([string]$Message) + Write-ColorMessage " ✗ $Message" -Color Red +} + +#endregion + +#region Main Script + +Write-Host @" +╔═══════════════════════════════════════════════════════════╗ +║ ║ +║ Git Workshop - Oneshot Installation Script ║ +║ ║ +╚═══════════════════════════════════════════════════════════╝ + +"@ -ForegroundColor Cyan + +Write-Host "This script will download and install all prerequisites for the Git Workshop." -ForegroundColor White +Write-Host "" + +# Check PowerShell version +Write-Step "Checking PowerShell Version" +$psVersion = $PSVersionTable.PSVersion +Write-Success "PowerShell $psVersion" + +if ($psVersion.Major -lt 7) { + Write-Warning "PowerShell 7+ is recommended for best compatibility" + Write-Host " Continuing with PowerShell $($psVersion.Major)..." -ForegroundColor Gray +} + +# Create temporary working directory +Write-Step "Creating Working Directory" +$tempDir = Join-Path $env:TEMP "git-workshop-$(Get-Date -Format 'yyyyMMdd-HHmmss')" + +try { + New-Item -Path $tempDir -ItemType Directory -Force | Out-Null + Write-Success "Created temporary directory: $tempDir" +} +catch { + Write-Error "Failed to create temporary directory: $_" + exit 1 +} + +# Download the repository +Write-Step "Downloading Git Workshop Repository" + +$repoUrl = "https://git.frod.dk/floppydiscen/git-workshop/archive/main.zip" +$zipPath = Join-Path $tempDir "git-workshop.zip" + +try { + Write-Host " Downloading from: $repoUrl" -ForegroundColor Gray + Invoke-WebRequest -Uri $repoUrl -OutFile $zipPath -UseBasicParsing + Write-Success "Repository downloaded successfully" +} +catch { + Write-Error "Failed to download repository: $_" + Write-Host "" + Write-Host "Troubleshooting:" -ForegroundColor Yellow + Write-Host " • Check your internet connection" -ForegroundColor White + Write-Host " • Verify the repository URL is correct" -ForegroundColor White + Write-Host " • Try running the script again" -ForegroundColor White + exit 1 +} + +# Extract the repository +Write-Step "Extracting Repository" + +try { + Write-Host " Extracting to: $tempDir" -ForegroundColor Gray + Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force + + # Find the extracted directory (should be git-workshop-main) + $extractedDir = Get-ChildItem -Path $tempDir -Directory | Where-Object { $_.Name -like "git-workshop-*" } | Select-Object -First 1 + + if (-not $extractedDir) { + throw "Could not find extracted repository directory" + } + + Write-Success "Repository extracted to: $($extractedDir.FullName)" +} +catch { + Write-Error "Failed to extract repository: $_" + exit 1 +} + +# Run the prerequisites installer +Write-Step "Running Prerequisites Installation" + +$installerScript = Join-Path $extractedDir.FullName "install-prerequisites.ps1" + +if (-not (Test-Path $installerScript)) { + Write-Error "Installation script not found: $installerScript" + Write-Host "" + Write-Host "Expected file structure:" -ForegroundColor Yellow + Write-Host " git-workshop-main/" -ForegroundColor White + Write-Host " ├── install-prerequisites.ps1" -ForegroundColor White + Write-Host " ├── README.md" -ForegroundColor White + Write-Host " └── ..." -ForegroundColor White + exit 1 +} + +try { + Write-Host " Running: $installerScript" -ForegroundColor Gray + Write-Host "" + + # Change to the extracted directory and run the installer + Push-Location $extractedDir.FullName + & $installerScript + + $installerExitCode = $LASTEXITCODE + Pop-Location + + if ($installerExitCode -eq 0) { + Write-Success "Prerequisites installation completed successfully!" + } + else { + Write-Warning "Prerequisites installation completed with warnings (exit code: $installerExitCode)" + } +} +catch { + Write-Error "Failed to run prerequisites installer: $_" + exit 1 +} +finally { + if (Get-Location -ErrorAction SilentlyContinue) { + Pop-Location -ErrorAction SilentlyContinue + } +} + +# Clean up +Write-Step "Cleaning Up" + +try { + Write-Host " Removing temporary directory: $tempDir" -ForegroundColor Gray + Remove-Item -Path $tempDir -Recurse -Force + Write-Success "Temporary files cleaned up" +} +catch { + Write-Warning "Failed to clean up temporary files: $_" + Write-Host " You can manually delete: $tempDir" -ForegroundColor Yellow +} + +# Final instructions +Write-Step "Installation Complete" + +Write-Host "" +Write-Success "Git Workshop installation is complete!" +Write-Host "" + +Write-Host "Next steps:" -ForegroundColor Cyan +Write-Host " 1. Clone or download the Git Workshop repository" -ForegroundColor White +Write-Host " 2. Configure Git if you haven't already:" -ForegroundColor White +Write-Host " git config --global user.name `"Your Name`"" -ForegroundColor Gray +Write-Host " git config --global user.email `"your.email@example.com`"" -ForegroundColor Gray +Write-Host " 3. Start with the first module:" -ForegroundColor White +Write-Host " cd 01-essentials\01-basics" -ForegroundColor Gray +Write-Host " .\setup.ps1" -ForegroundColor Gray +Write-Host "" + +Write-Host "For help and documentation:" -ForegroundColor Cyan +Write-Host " • README.md - Workshop overview" -ForegroundColor White +Write-Host " • GIT-CHEATSHEET.md - Git command reference" -ForegroundColor White +Write-Host " • AGENDA.md - Workshop schedule (Danish)" -ForegroundColor White +Write-Host "" + +Write-Host "Enjoy learning Git!" -ForegroundColor Green + +#endregion + +exit 0 \ No newline at end of file From e7ce41cbbcafb2e58abe80db849fb36412961782 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:29:09 +0100 Subject: [PATCH 08/61] Update oneshot installation to clone repository locally - Modified install.ps1 to clone git-workshop repository to ~/git-workshop - Added option to remove existing directory before cloning - Updated documentation to reflect the new behavior - Now provides complete setup: install tools + clone repo in one command - Similar to Scoop's get.scoop.sh installation pattern --- INSTALLATION.md | 7 ++++++- README.md | 7 ++++++- install.ps1 | 42 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 9ac3c98..b722b1a 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,12 +4,17 @@ This guide will help you install everything needed for the Git Workshop on Windo ## Quick Start (Automated Installation) -**Easiest option:** Run our oneshot installation script that downloads everything and installs all required tools automatically. +**Easiest option:** Run our oneshot installation script that installs all prerequisites and clones the repository automatically. ```powershell irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex ``` +This will: +- Install PowerShell 7, Git 2.23+, and Visual Studio Code +- Clone the git-workshop repository to `~/git-workshop` +- Leave you ready to start the workshop immediately + **Alternative:** If you've already cloned the repository, you can run the local installation script: 1. Open **PowerShell** or **Windows Terminal** diff --git a/README.md b/README.md index ff20e83..fe98043 100644 --- a/README.md +++ b/README.md @@ -82,12 +82,17 @@ Then run scripts using: **Quick Automated Installation (Windows 11):** **Option 1: Oneshot Installation (Recommended)** -Download and run everything in one command: +Download, install prerequisites, and clone the repository in one command: ```powershell irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex ``` +This will: +- Install PowerShell 7, Git 2.23+, and Visual Studio Code +- Clone the git-workshop repository to `~/git-workshop` +- Leave you ready to start the first module + **Option 2: Local Installation** Clone the repository first, then run: diff --git a/install.ps1 b/install.ps1 index 0f83cdf..41f1ce7 100644 --- a/install.ps1 +++ b/install.ps1 @@ -197,6 +197,38 @@ catch { Write-Host " You can manually delete: $tempDir" -ForegroundColor Yellow } +# Clone the repository locally +Write-Step "Cloning Git Workshop Repository" + +$cloneDir = Join-Path $HOME "git-workshop" + +try { + if (Test-Path $cloneDir) { + Write-Warning "Directory already exists: $cloneDir" + $response = Read-Host " Do you want to remove it and clone fresh? (y/n)" + if ($response.Trim().ToLower() -eq 'y' -or $response.Trim().ToLower() -eq 'yes') { + Remove-Item -Path $cloneDir -Recurse -Force + Write-Host " Removed existing directory" -ForegroundColor Gray + } + else { + Write-Host " Skipping clone - using existing directory" -ForegroundColor Gray + } + } + + if (-not (Test-Path $cloneDir)) { + Write-Host " Cloning to: $cloneDir" -ForegroundColor Gray + git clone "https://git.frod.dk/floppydiscen/git-workshop.git" $cloneDir + Write-Success "Repository cloned successfully!" + } +} +catch { + Write-Error "Failed to clone repository: $_" + Write-Host "" + Write-Host "You can clone manually:" -ForegroundColor Yellow + Write-Host " git clone https://git.frod.dk/floppydiscen/git-workshop.git ~/git-workshop" -ForegroundColor White + exit 1 +} + # Final instructions Write-Step "Installation Complete" @@ -204,11 +236,17 @@ Write-Host "" Write-Success "Git Workshop installation is complete!" Write-Host "" +Write-Host "Repository cloned to: $cloneDir" -ForegroundColor Green +Write-Host "" + Write-Host "Next steps:" -ForegroundColor Cyan -Write-Host " 1. Clone or download the Git Workshop repository" -ForegroundColor White -Write-Host " 2. Configure Git if you haven't already:" -ForegroundColor White +Write-Host " 1. Configure Git if you haven't already:" -ForegroundColor White Write-Host " git config --global user.name `"Your Name`"" -ForegroundColor Gray Write-Host " git config --global user.email `"your.email@example.com`"" -ForegroundColor Gray +Write-Host "" +Write-Host " 2. Navigate to the workshop:" -ForegroundColor White +Write-Host " cd $cloneDir" -ForegroundColor Gray +Write-Host "" Write-Host " 3. Start with the first module:" -ForegroundColor White Write-Host " cd 01-essentials\01-basics" -ForegroundColor Gray Write-Host " .\setup.ps1" -ForegroundColor Gray From f696044461626890a8fa572fe8fb8c07ebb47622 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:35:59 +0100 Subject: [PATCH 09/61] Simplify README installation instructions - Remove complex installation options - List prerequisites with direct winget commands - Keep oneshot installation as recommended option - Add manual setup steps for clarity - Focus on getting users started quickly --- README.md | 47 +++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fe98043..17fcb47 100644 --- a/README.md +++ b/README.md @@ -77,30 +77,45 @@ Then run scripts using: ## Requirements -### Installation +### Prerequisites -**Quick Automated Installation (Windows 11):** +Install these tools before starting: + +**PowerShell 7+** +```powershell +winget install Microsoft.PowerShell +``` + +**Git 2.23+** +```powershell +winget install Git.Git +``` + +**Visual Studio Code** +```powershell +winget install Microsoft.VisualStudioCode +``` + +### Quick Start **Option 1: Oneshot Installation (Recommended)** -Download, install prerequisites, and clone the repository in one command: +Install everything and clone the repository in one command: ```powershell irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex ``` -This will: -- Install PowerShell 7, Git 2.23+, and Visual Studio Code -- Clone the git-workshop repository to `~/git-workshop` -- Leave you ready to start the first module - -**Option 2: Local Installation** -Clone the repository first, then run: - -```powershell -.\install-prerequisites.ps1 -``` - -**Manual Installation:** See [INSTALLATION.md](INSTALLATION.md) for complete step-by-step installation instructions including PowerShell 7, Git, and Visual Studio Code. +**Option 2: Manual Setup** +1. Install the prerequisites above +2. Clone this repository: + ```powershell + git clone https://git.frod.dk/floppydiscen/git-workshop.git + ``` +3. Configure Git: + ```powershell + git config --global user.name "Your Name" + git config --global user.email "your.email@example.com" + ``` **Quick Check:** From 6d2e099eb433c38c59b686554145fbc5d7712fe3 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:37:39 +0100 Subject: [PATCH 10/61] Add progress bars to install-prerequisites.ps1 - Added overall progress tracking for required installations (1/3, 2/3, 3/3) - Added progress indicators for optional installations - Added individual package installation progress with status updates - Progress bars show clear visual feedback during installation process - Helps users understand how far along the installation is --- install-prerequisites.ps1 | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 index 5f02dfd..5635efe 100644 --- a/install-prerequisites.ps1 +++ b/install-prerequisites.ps1 @@ -146,7 +146,12 @@ function Install-Package { Write-Host " Running: $installCmd" -ForegroundColor Gray try { + # Show progress during installation + Write-Progress -Activity "Installing $Name" -Status "Downloading and installing..." -PercentComplete 25 + $result = Invoke-Expression $installCmd 2>&1 + + Write-Progress -Activity "Installing $Name" -Status "Verifying installation..." -PercentComplete 75 # Check if installation succeeded Start-Sleep -Seconds 2 # Give the system time to register the new command @@ -157,16 +162,19 @@ function Install-Package { if (Test-CommandExists $CheckCommand) { $version = Get-InstalledVersion $CheckCommand Write-Success "$Name installed successfully: $version" + Write-Progress -Activity "Installing $Name" -Completed return $true } else { Write-Warning "$Name installation completed, but command '$CheckCommand' not found." Write-Host " You may need to restart your terminal or computer." -ForegroundColor Yellow + Write-Progress -Activity "Installing $Name" -Completed return $false } } catch { Write-Error "Failed to install $Name`: $_" + Write-Progress -Activity "Installing $Name" -Completed return $false } } @@ -253,19 +261,38 @@ $results = @{ WindowsTerminal = $null } +# Progress tracking +$totalSteps = 3 # Required installations +$currentStep = 0 + Write-Host "`nStarting installation..." -ForegroundColor Cyan Write-Host "Note: Some installations may take a few minutes." -ForegroundColor Gray Write-Host "" +# Progress bar helper +function Write-ProgressIndicator { + param( + [string]$Activity, + [string]$Status, + [int]$PercentComplete + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete +} + #region Required Installations # Install PowerShell 7 +$currentStep++ +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing PowerShell 7 (1/3)" -PercentComplete (($currentStep / $totalSteps) * 100) $results.PowerShell = Install-Package ` -Name "PowerShell 7" ` -WingetId "Microsoft.PowerShell" ` -CheckCommand "pwsh" # Install Git +$currentStep++ +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Git (2/3)" -PercentComplete (($currentStep / $totalSteps) * 100) $results.Git = Install-Package ` -Name "Git" ` -WingetId "Git.Git" ` @@ -282,11 +309,16 @@ if ($results.Git) { } # Install Visual Studio Code +$currentStep++ +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Visual Studio Code (3/3)" -PercentComplete (($currentStep / $totalSteps) * 100) $results.VSCode = Install-Package ` -Name "Visual Studio Code" ` -WingetId "Microsoft.VisualStudioCode" ` -CheckCommand "code" +# Clear progress bar +Write-Progress -Activity "Installing Required Tools" -Completed + #endregion #region Optional Installations @@ -294,10 +326,12 @@ $results.VSCode = Install-Package ` # Python 3.12 (optional) Write-Host "" if (Get-UserConfirmation "Do you want to install Python 3.12? (Required for Module 08: Multiplayer Git)") { + Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Python 3.12" -PercentComplete 50 $results.Python = Install-Package ` -Name "Python 3.12" ` -WingetId "Python.Python.3.12" ` -CheckCommand "python" + Write-Progress -Activity "Installing Optional Tools" -Completed } else { Write-Host " Skipping Python installation." -ForegroundColor Gray @@ -307,10 +341,12 @@ else { # Windows Terminal (optional) Write-Host "" if (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)") { + Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Windows Terminal" -PercentComplete 50 $results.WindowsTerminal = Install-Package ` -Name "Windows Terminal" ` -WingetId "Microsoft.WindowsTerminal" ` -CheckCommand "wt" + Write-Progress -Activity "Installing Optional Tools" -Completed } else { Write-Host " Skipping Windows Terminal installation." -ForegroundColor Gray From f55cb444e7b19e9bdf343e77a1cfb602d9b22025 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:49:35 +0100 Subject: [PATCH 11/61] Add VSCode extensions and PowerShell 7 integration setup - Added function to install VSCode extensions via command line - Install PowerShell extension for better PowerShell support in VSCode - Install GitLens and Git Graph extensions for enhanced Git experience - Configure VSCode to use PowerShell 7 as default terminal - Update progress tracking to include extension installation step - Add extension results to installation summary --- install-prerequisites.ps1 | 124 +++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 2 deletions(-) diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 index 5635efe..1d98d80 100644 --- a/install-prerequisites.ps1 +++ b/install-prerequisites.ps1 @@ -257,12 +257,13 @@ $results = @{ PowerShell = $false Git = $false VSCode = $false + VSCodeExtensions = $false Python = $null # null = not attempted, true = success, false = failed WindowsTerminal = $null } # Progress tracking -$totalSteps = 3 # Required installations +$totalSteps = 4 # Required installations + extensions $currentStep = 0 Write-Host "`nStarting installation..." -ForegroundColor Cyan @@ -280,6 +281,91 @@ function Write-ProgressIndicator { Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete } +function Install-VSCodeExtension { + param( + [string]$ExtensionId, + [string]$ExtensionName + ) + + Write-Host " Installing VSCode extension: $ExtensionName" -ForegroundColor Cyan + + try { + # Refresh environment to ensure code command is available + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + $result = & code --install-extension $ExtensionId 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Success "VSCode extension '$ExtensionName' installed successfully" + return $true + } + else { + Write-Warning "Failed to install VSCode extension '$ExtensionName'" + Write-Host " You can install it manually later: Ctrl+Shift+X → Search '$ExtensionName'" -ForegroundColor Gray + return $false + } + } + catch { + Write-Warning "Could not install VSCode extension '$ExtensionName`: $_" + Write-Host " You can install it manually later: Ctrl+Shift+X → Search '$ExtensionName'" -ForegroundColor Gray + return $false + } +} + +function Set-VSCodePowerShellIntegration { + Write-Host " Configuring PowerShell 7 integration with VSCode..." -ForegroundColor Cyan + + try { + # Set PowerShell 7 as the default terminal in VSCode + $vscodeSettingsPath = Join-Path $env:APPDATA "Code\User\settings.json" + $vscodeSettingsDir = Split-Path $vscodeSettingsPath -Parent + + # Create directory if it doesn't exist + if (-not (Test-Path $vscodeSettingsDir)) { + New-Item -Path $vscodeSettingsDir -ItemType Directory -Force | Out-Null + } + + # Read existing settings or create new + if (Test-Path $vscodeSettingsPath) { + $settings = Get-Content $vscodeSettingsPath -Raw | ConvertFrom-Json + } + else { + $settings = @{} + } + + # Set PowerShell 7 as default terminal + if (-not $settings.PSObject.Properties.Name -contains "terminal.integrated.defaultProfile.windows") { + $settings | Add-Member -NotePropertyName "terminal.integrated.defaultProfile.windows" -NotePropertyValue "PowerShell" + } + else { + $settings."terminal.integrated.defaultProfile.windows" = "PowerShell" + } + + # Add terminal profiles if not present + if (-not $settings.PSObject.Properties.Name -contains "terminal.integrated.profiles.windows") { + $profiles = @{ + "PowerShell" = @{ + "source" = "PowerShell" + "icon" = "terminal-powershell" + "path" = "pwsh.exe" + } + } + $settings | Add-Member -NotePropertyName "terminal.integrated.profiles.windows" -NotePropertyValue $profiles + } + + # Save settings + $settings | ConvertTo-Json -Depth 10 | Set-Content $vscodeSettingsPath + + Write-Success "VSCode configured to use PowerShell 7 as default terminal" + return $true + } + catch { + Write-Warning "Could not configure VSCode PowerShell integration automatically" + Write-Host " You can configure it manually in VSCode: Ctrl+Shift+P → Terminal: Select Default Profile → PowerShell" -ForegroundColor Gray + return $false + } +} + #region Required Installations # Install PowerShell 7 @@ -310,12 +396,36 @@ if ($results.Git) { # Install Visual Studio Code $currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Visual Studio Code (3/3)" -PercentComplete (($currentStep / $totalSteps) * 100) +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Visual Studio Code (3/4)" -PercentComplete (($currentStep / $totalSteps) * 100) $results.VSCode = Install-Package ` -Name "Visual Studio Code" ` -WingetId "Microsoft.VisualStudioCode" ` -CheckCommand "code" +# Install VSCode Extensions and configure PowerShell integration +if ($results.VSCode) { + $currentStep++ + Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing VSCode Extensions (4/4)" -PercentComplete (($currentStep / $totalSteps) * 100) + + Write-Host "" + Write-Step "Configuring VSCode" + + # Install PowerShell extension + $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" + + # Install other recommended extensions + $gitLensResult = Install-VSCodeExtension -ExtensionId "eamodio.gitlens" -ExtensionName "GitLens" + $gitGraphResult = Install-VSCodeExtension -ExtensionId "mhutchie.git-graph" -ExtensionName "Git Graph" + + # Configure PowerShell 7 integration + $powershellIntegrationResult = Set-VSCodePowerShellIntegration + + $results.VSCodeExtensions = $powershellExtensionResult -or $gitLensResult -or $gitGraphResult +} +else { + $results.VSCodeExtensions = $false +} + # Clear progress bar Write-Progress -Activity "Installing Required Tools" -Completed @@ -380,6 +490,16 @@ else { if ($results.VSCode) { Write-Success "Visual Studio Code" + + if ($results.VSCodeExtensions) { + Write-Success " • PowerShell extension" + Write-Success " • GitLens extension" + Write-Success " • Git Graph extension" + Write-Success " • PowerShell 7 terminal integration" + } + else { + Write-Warning " • Some VSCode extensions may need manual installation" + } } else { Write-Error "Visual Studio Code - Installation failed or needs restart" From 985c4a0a8ac17a13ea7671f4a6b882b32ce7ee2e Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:51:32 +0100 Subject: [PATCH 12/61] Remove GitLens and GitGraph extensions from VSCode setup - Keep only essential PowerShell extension and PowerShell 7 integration - Simplify VSCode setup to focus on core requirements - Reduce installation time and complexity - Users can install additional Git extensions manually if desired --- install-prerequisites.ps1 | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 index 1d98d80..0eb72bb 100644 --- a/install-prerequisites.ps1 +++ b/install-prerequisites.ps1 @@ -413,14 +413,12 @@ if ($results.VSCode) { # Install PowerShell extension $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" - # Install other recommended extensions - $gitLensResult = Install-VSCodeExtension -ExtensionId "eamodio.gitlens" -ExtensionName "GitLens" - $gitGraphResult = Install-VSCodeExtension -ExtensionId "mhutchie.git-graph" -ExtensionName "Git Graph" + # Configure PowerShell 7 integration $powershellIntegrationResult = Set-VSCodePowerShellIntegration - $results.VSCodeExtensions = $powershellExtensionResult -or $gitLensResult -or $gitGraphResult + $results.VSCodeExtensions = $powershellExtensionResult } else { $results.VSCodeExtensions = $false @@ -493,12 +491,10 @@ if ($results.VSCode) { if ($results.VSCodeExtensions) { Write-Success " • PowerShell extension" - Write-Success " • GitLens extension" - Write-Success " • Git Graph extension" Write-Success " • PowerShell 7 terminal integration" } else { - Write-Warning " • Some VSCode extensions may need manual installation" + Write-Warning " • VSCode PowerShell extension may need manual installation" } } else { From 734b49bc7dea2f65adc7fb6afe93609c5a12c9a0 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:53:25 +0100 Subject: [PATCH 13/61] Add user confirmation for PowerShell 7 terminal integration - Ask user before setting PowerShell 7 as default VSCode terminal - Make PowerShell terminal setup recommended but optional - Add separate tracking for terminal integration result - Update installation summary to show configuration status - Provide clearer feedback on what was configured --- install-prerequisites.ps1 | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 index 0eb72bb..98028c3 100644 --- a/install-prerequisites.ps1 +++ b/install-prerequisites.ps1 @@ -258,6 +258,7 @@ $results = @{ Git = $false VSCode = $false VSCodeExtensions = $false + VSCodePowerShellIntegration = $null # null = not asked, true = configured, false = skipped/failed Python = $null # null = not attempted, true = success, false = failed WindowsTerminal = $null } @@ -313,6 +314,14 @@ function Install-VSCodeExtension { } function Set-VSCodePowerShellIntegration { + # Ask user if they want to set PowerShell 7 as default terminal + $setAsDefault = Get-UserConfirmation "Set PowerShell 7 as the default terminal in VSCode? (Recommended for this workshop)" + + if (-not $setAsDefault) { + Write-Host " Skipping PowerShell 7 terminal configuration." -ForegroundColor Gray + return $false + } + Write-Host " Configuring PowerShell 7 integration with VSCode..." -ForegroundColor Cyan try { @@ -415,8 +424,9 @@ if ($results.VSCode) { - # Configure PowerShell 7 integration + # Configure PowerShell 7 integration (optional but recommended) $powershellIntegrationResult = Set-VSCodePowerShellIntegration + $results.VSCodePowerShellIntegration = $powershellIntegrationResult $results.VSCodeExtensions = $powershellExtensionResult } @@ -491,11 +501,17 @@ if ($results.VSCode) { if ($results.VSCodeExtensions) { Write-Success " • PowerShell extension" - Write-Success " • PowerShell 7 terminal integration" } else { Write-Warning " • VSCode PowerShell extension may need manual installation" } + + if ($results.VSCodePowerShellIntegration -eq $true) { + Write-Success " • PowerShell 7 terminal integration" + } + elseif ($results.VSCodePowerShellIntegration -eq $false) { + Write-Host " • PowerShell 7 terminal integration: Skipped" -ForegroundColor Gray + } } else { Write-Error "Visual Studio Code - Installation failed or needs restart" From 14cfc2feebde33c5a94c53937110a6aa2c543865 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:56:29 +0100 Subject: [PATCH 14/61] Fix Git version check to handle Windows formatting - Update Test-GitVersion to properly parse Git versions with Windows suffixes - Handle formats like '2.52.0.windows.1' correctly - Parse major and minor version numbers separately for accurate comparison - Now correctly identifies versions 2.23+ regardless of Windows-specific suffixes - Add better error message when version parsing fails --- install-prerequisites.ps1 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 index 98028c3..770f5b4 100644 --- a/install-prerequisites.ps1 +++ b/install-prerequisites.ps1 @@ -185,17 +185,24 @@ function Test-GitVersion { } $version = Get-InstalledVersion 'git' - if ($version -match 'git version (\d+\.\d+)') { - $versionNumber = [decimal]$matches[1] - if ($versionNumber -ge 2.23) { + + # Parse Git version from various formats: + # "git version 2.52.0", "git version 2.52.0.windows.1", etc. + if ($version -match 'git version (\d+)\.(\d+)') { + $majorVersion = [int]$matches[1] + $minorVersion = [int]$matches[2] + + # Check if version is 2.23 or higher + if ($majorVersion -gt 2 -or ($majorVersion -eq 2 -and $minorVersion -ge 23)) { return $true } else { - Write-Warning "Git version $versionNumber is below required version 2.23" + Write-Warning "Git version $majorVersion.$minorVersion is below required version 2.23" return $false } } + Write-Warning "Could not parse Git version from: $version" return $false } From 7066e648d5e72bd88e0adb63db5d8f852be9a0ed Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 16:57:23 +0100 Subject: [PATCH 15/61] refactor: do not exit on script finish --- install-prerequisites.ps1 | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 index 770f5b4..74ceec2 100644 --- a/install-prerequisites.ps1 +++ b/install-prerequisites.ps1 @@ -254,7 +254,7 @@ Write-Step "Checking Prerequisites" if (-not (Test-WingetAvailable)) { Write-Host "`nInstallation cannot continue without winget." -ForegroundColor Red - exit 1 + return } Write-Success "winget is available" @@ -630,9 +630,4 @@ else { Write-Host "" } - exit 1 } - -#endregion - -exit 0 From cbefeaf4d2258b48bee6b556ec5f88b9df91063d Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:00:02 +0100 Subject: [PATCH 16/61] refactor: move install-prerequisites to install.ps1 --- install-prerequisites.ps1 | 633 --------------------------------- install.ps1 | 720 ++++++++++++++++++++++++++++---------- 2 files changed, 544 insertions(+), 809 deletions(-) delete mode 100644 install-prerequisites.ps1 diff --git a/install-prerequisites.ps1 b/install-prerequisites.ps1 deleted file mode 100644 index 74ceec2..0000000 --- a/install-prerequisites.ps1 +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS -Installs all prerequisites for the Git Workshop using winget. - -.DESCRIPTION -This script automates the installation of required tools for the Git Workshop: -- PowerShell 7 (cross-platform PowerShell) -- Git 2.23+ (version control system) -- Visual Studio Code (code editor with Git integration) - -Optional tools (with user prompts): -- Python 3.12 (for Module 08: Multiplayer Git) -- Windows Terminal (modern terminal experience) - -The script checks for existing installations, shows clear progress, and verifies -each installation succeeded. At the end, it displays Git configuration instructions. - -.EXAMPLE -PS> .\install-prerequisites.ps1 -Runs the installation script with interactive prompts. - -.NOTES -Requires Windows 11 with winget (App Installer) available. -Some installations may require administrator privileges. -#> - -[CmdletBinding()] -param() - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Continue' # Continue on errors to show all results - -#region Helper Functions - -function Write-ColorMessage { - param( - [string]$Message, - [string]$Color = 'White' - ) - Write-Host $Message -ForegroundColor $Color -} - -function Write-Step { - param([string]$Message) - Write-ColorMessage "`n=== $Message ===" -Color Cyan -} - -function Write-Success { - param([string]$Message) - Write-ColorMessage " ✓ $Message" -Color Green -} - -function Write-Warning { - param([string]$Message) - Write-ColorMessage " ⚠ $Message" -Color Yellow -} - -function Write-Error { - param([string]$Message) - Write-ColorMessage " ✗ $Message" -Color Red -} - -function Test-CommandExists { - param([string]$Command) - - $oldPreference = $ErrorActionPreference - $ErrorActionPreference = 'SilentlyContinue' - - try { - if (Get-Command $Command -ErrorAction SilentlyContinue) { - return $true - } - return $false - } - finally { - $ErrorActionPreference = $oldPreference - } -} - -function Get-InstalledVersion { - param( - [string]$Command, - [string]$VersionArg = '--version' - ) - - try { - $output = & $Command $VersionArg 2>&1 | Select-Object -First 1 - return $output.ToString().Trim() - } - catch { - return $null - } -} - -function Test-WingetAvailable { - if (-not (Test-CommandExists 'winget')) { - Write-Error "winget is not available on this system." - Write-Host "`nTo fix this:" -ForegroundColor Yellow - Write-Host " 1. Update Windows 11 to the latest version (Settings → Windows Update)" -ForegroundColor White - Write-Host " 2. Install 'App Installer' from the Microsoft Store" -ForegroundColor White - Write-Host " 3. Restart your computer and run this script again" -ForegroundColor White - return $false - } - return $true -} - -function Install-Package { - param( - [string]$Name, - [string]$WingetId, - [string]$CheckCommand, - [string]$MinVersion = $null, - [string]$AdditionalArgs = '' - ) - - Write-Step "Installing $Name" - - # Check if already installed - if (Test-CommandExists $CheckCommand) { - $version = Get-InstalledVersion $CheckCommand - Write-Success "$Name is already installed: $version" - - if ($MinVersion -and $version) { - # Basic version check (not perfect but good enough for common cases) - if ($version -match '(\d+\.[\d.]+)') { - $installedVersion = $matches[1] - if ([version]$installedVersion -lt [version]$MinVersion) { - Write-Warning "Version $installedVersion is below minimum required version $MinVersion" - Write-Host " Attempting to upgrade..." -ForegroundColor Cyan - } - else { - return $true - } - } - } - else { - return $true - } - } - - # Install using winget - Write-Host " Installing via winget: $WingetId" -ForegroundColor Cyan - - $installCmd = "winget install --id $WingetId --source winget --silent $AdditionalArgs".Trim() - Write-Host " Running: $installCmd" -ForegroundColor Gray - - try { - # Show progress during installation - Write-Progress -Activity "Installing $Name" -Status "Downloading and installing..." -PercentComplete 25 - - $result = Invoke-Expression $installCmd 2>&1 - - Write-Progress -Activity "Installing $Name" -Status "Verifying installation..." -PercentComplete 75 - - # Check if installation succeeded - Start-Sleep -Seconds 2 # Give the system time to register the new command - - # Refresh environment variables in current session - $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") - - if (Test-CommandExists $CheckCommand) { - $version = Get-InstalledVersion $CheckCommand - Write-Success "$Name installed successfully: $version" - Write-Progress -Activity "Installing $Name" -Completed - return $true - } - else { - Write-Warning "$Name installation completed, but command '$CheckCommand' not found." - Write-Host " You may need to restart your terminal or computer." -ForegroundColor Yellow - Write-Progress -Activity "Installing $Name" -Completed - return $false - } - } - catch { - Write-Error "Failed to install $Name`: $_" - Write-Progress -Activity "Installing $Name" -Completed - return $false - } -} - -function Test-GitVersion { - if (-not (Test-CommandExists 'git')) { - return $false - } - - $version = Get-InstalledVersion 'git' - - # Parse Git version from various formats: - # "git version 2.52.0", "git version 2.52.0.windows.1", etc. - if ($version -match 'git version (\d+)\.(\d+)') { - $majorVersion = [int]$matches[1] - $minorVersion = [int]$matches[2] - - # Check if version is 2.23 or higher - if ($majorVersion -gt 2 -or ($majorVersion -eq 2 -and $minorVersion -ge 23)) { - return $true - } - else { - Write-Warning "Git version $majorVersion.$minorVersion is below required version 2.23" - return $false - } - } - - Write-Warning "Could not parse Git version from: $version" - return $false -} - -function Get-UserConfirmation { - param([string]$Prompt) - - while ($true) { - $response = Read-Host "$Prompt (y/n)" - $response = $response.Trim().ToLower() - - if ($response -eq 'y' -or $response -eq 'yes') { - return $true - } - elseif ($response -eq 'n' -or $response -eq 'no') { - return $false - } - else { - Write-Host "Please enter 'y' or 'n'" -ForegroundColor Yellow - } - } -} - -#endregion - -#region Main Script - -Write-Host @" - -╔═══════════════════════════════════════════════════════════╗ -║ ║ -║ Git Workshop - Prerequisites Installation Script ║ -║ ║ -╚═══════════════════════════════════════════════════════════╝ - -"@ -ForegroundColor Cyan - -Write-Host "This script will install the required tools for the Git Workshop:" -ForegroundColor White -Write-Host " • PowerShell 7 (cross-platform PowerShell)" -ForegroundColor White -Write-Host " • Git 2.23+ (version control system)" -ForegroundColor White -Write-Host " • Visual Studio Code (code editor)" -ForegroundColor White -Write-Host "" -Write-Host "You will be prompted for optional tools:" -ForegroundColor White -Write-Host " • Python 3.12 (for Module 08: Multiplayer Git)" -ForegroundColor White -Write-Host " • Windows Terminal (modern terminal experience)" -ForegroundColor White -Write-Host "" - -# Check for winget -Write-Step "Checking Prerequisites" - -if (-not (Test-WingetAvailable)) { - Write-Host "`nInstallation cannot continue without winget." -ForegroundColor Red - return -} - -Write-Success "winget is available" - -# Track installation results -$results = @{ - PowerShell = $false - Git = $false - VSCode = $false - VSCodeExtensions = $false - VSCodePowerShellIntegration = $null # null = not asked, true = configured, false = skipped/failed - Python = $null # null = not attempted, true = success, false = failed - WindowsTerminal = $null -} - -# Progress tracking -$totalSteps = 4 # Required installations + extensions -$currentStep = 0 - -Write-Host "`nStarting installation..." -ForegroundColor Cyan -Write-Host "Note: Some installations may take a few minutes." -ForegroundColor Gray -Write-Host "" - -# Progress bar helper -function Write-ProgressIndicator { - param( - [string]$Activity, - [string]$Status, - [int]$PercentComplete - ) - - Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete -} - -function Install-VSCodeExtension { - param( - [string]$ExtensionId, - [string]$ExtensionName - ) - - Write-Host " Installing VSCode extension: $ExtensionName" -ForegroundColor Cyan - - try { - # Refresh environment to ensure code command is available - $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") - - $result = & code --install-extension $ExtensionId 2>&1 - - if ($LASTEXITCODE -eq 0) { - Write-Success "VSCode extension '$ExtensionName' installed successfully" - return $true - } - else { - Write-Warning "Failed to install VSCode extension '$ExtensionName'" - Write-Host " You can install it manually later: Ctrl+Shift+X → Search '$ExtensionName'" -ForegroundColor Gray - return $false - } - } - catch { - Write-Warning "Could not install VSCode extension '$ExtensionName`: $_" - Write-Host " You can install it manually later: Ctrl+Shift+X → Search '$ExtensionName'" -ForegroundColor Gray - return $false - } -} - -function Set-VSCodePowerShellIntegration { - # Ask user if they want to set PowerShell 7 as default terminal - $setAsDefault = Get-UserConfirmation "Set PowerShell 7 as the default terminal in VSCode? (Recommended for this workshop)" - - if (-not $setAsDefault) { - Write-Host " Skipping PowerShell 7 terminal configuration." -ForegroundColor Gray - return $false - } - - Write-Host " Configuring PowerShell 7 integration with VSCode..." -ForegroundColor Cyan - - try { - # Set PowerShell 7 as the default terminal in VSCode - $vscodeSettingsPath = Join-Path $env:APPDATA "Code\User\settings.json" - $vscodeSettingsDir = Split-Path $vscodeSettingsPath -Parent - - # Create directory if it doesn't exist - if (-not (Test-Path $vscodeSettingsDir)) { - New-Item -Path $vscodeSettingsDir -ItemType Directory -Force | Out-Null - } - - # Read existing settings or create new - if (Test-Path $vscodeSettingsPath) { - $settings = Get-Content $vscodeSettingsPath -Raw | ConvertFrom-Json - } - else { - $settings = @{} - } - - # Set PowerShell 7 as default terminal - if (-not $settings.PSObject.Properties.Name -contains "terminal.integrated.defaultProfile.windows") { - $settings | Add-Member -NotePropertyName "terminal.integrated.defaultProfile.windows" -NotePropertyValue "PowerShell" - } - else { - $settings."terminal.integrated.defaultProfile.windows" = "PowerShell" - } - - # Add terminal profiles if not present - if (-not $settings.PSObject.Properties.Name -contains "terminal.integrated.profiles.windows") { - $profiles = @{ - "PowerShell" = @{ - "source" = "PowerShell" - "icon" = "terminal-powershell" - "path" = "pwsh.exe" - } - } - $settings | Add-Member -NotePropertyName "terminal.integrated.profiles.windows" -NotePropertyValue $profiles - } - - # Save settings - $settings | ConvertTo-Json -Depth 10 | Set-Content $vscodeSettingsPath - - Write-Success "VSCode configured to use PowerShell 7 as default terminal" - return $true - } - catch { - Write-Warning "Could not configure VSCode PowerShell integration automatically" - Write-Host " You can configure it manually in VSCode: Ctrl+Shift+P → Terminal: Select Default Profile → PowerShell" -ForegroundColor Gray - return $false - } -} - -#region Required Installations - -# Install PowerShell 7 -$currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing PowerShell 7 (1/3)" -PercentComplete (($currentStep / $totalSteps) * 100) -$results.PowerShell = Install-Package ` - -Name "PowerShell 7" ` - -WingetId "Microsoft.PowerShell" ` - -CheckCommand "pwsh" - -# Install Git -$currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Git (2/3)" -PercentComplete (($currentStep / $totalSteps) * 100) -$results.Git = Install-Package ` - -Name "Git" ` - -WingetId "Git.Git" ` - -CheckCommand "git" ` - -MinVersion "2.23" ` - -AdditionalArgs "-e" - -# Verify Git version specifically -if ($results.Git) { - if (-not (Test-GitVersion)) { - Write-Warning "Git is installed but version may be below 2.23" - $results.Git = $false - } -} - -# Install Visual Studio Code -$currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Visual Studio Code (3/4)" -PercentComplete (($currentStep / $totalSteps) * 100) -$results.VSCode = Install-Package ` - -Name "Visual Studio Code" ` - -WingetId "Microsoft.VisualStudioCode" ` - -CheckCommand "code" - -# Install VSCode Extensions and configure PowerShell integration -if ($results.VSCode) { - $currentStep++ - Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing VSCode Extensions (4/4)" -PercentComplete (($currentStep / $totalSteps) * 100) - - Write-Host "" - Write-Step "Configuring VSCode" - - # Install PowerShell extension - $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" - - - - # Configure PowerShell 7 integration (optional but recommended) - $powershellIntegrationResult = Set-VSCodePowerShellIntegration - $results.VSCodePowerShellIntegration = $powershellIntegrationResult - - $results.VSCodeExtensions = $powershellExtensionResult -} -else { - $results.VSCodeExtensions = $false -} - -# Clear progress bar -Write-Progress -Activity "Installing Required Tools" -Completed - -#endregion - -#region Optional Installations - -# Python 3.12 (optional) -Write-Host "" -if (Get-UserConfirmation "Do you want to install Python 3.12? (Required for Module 08: Multiplayer Git)") { - Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Python 3.12" -PercentComplete 50 - $results.Python = Install-Package ` - -Name "Python 3.12" ` - -WingetId "Python.Python.3.12" ` - -CheckCommand "python" - Write-Progress -Activity "Installing Optional Tools" -Completed -} -else { - Write-Host " Skipping Python installation." -ForegroundColor Gray - $results.Python = $null -} - -# Windows Terminal (optional) -Write-Host "" -if (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)") { - Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Windows Terminal" -PercentComplete 50 - $results.WindowsTerminal = Install-Package ` - -Name "Windows Terminal" ` - -WingetId "Microsoft.WindowsTerminal" ` - -CheckCommand "wt" - Write-Progress -Activity "Installing Optional Tools" -Completed -} -else { - Write-Host " Skipping Windows Terminal installation." -ForegroundColor Gray - $results.WindowsTerminal = $null -} - -#endregion - -#region Installation Summary - -Write-Step "Installation Summary" - -$allRequired = $results.PowerShell -and $results.Git -and $results.VSCode - -Write-Host "" -Write-Host "Required Tools:" -ForegroundColor White - -if ($results.PowerShell) { - Write-Success "PowerShell 7" -} -else { - Write-Error "PowerShell 7 - Installation failed or needs restart" -} - -if ($results.Git) { - Write-Success "Git 2.23+" -} -else { - Write-Error "Git 2.23+ - Installation failed or needs restart" -} - -if ($results.VSCode) { - Write-Success "Visual Studio Code" - - if ($results.VSCodeExtensions) { - Write-Success " • PowerShell extension" - } - else { - Write-Warning " • VSCode PowerShell extension may need manual installation" - } - - if ($results.VSCodePowerShellIntegration -eq $true) { - Write-Success " • PowerShell 7 terminal integration" - } - elseif ($results.VSCodePowerShellIntegration -eq $false) { - Write-Host " • PowerShell 7 terminal integration: Skipped" -ForegroundColor Gray - } -} -else { - Write-Error "Visual Studio Code - Installation failed or needs restart" -} - -if ($results.Python -ne $null) { - Write-Host "" - Write-Host "Optional Tools:" -ForegroundColor White - - if ($results.Python) { - Write-Success "Python 3.12" - } - else { - Write-Error "Python 3.12 - Installation failed or needs restart" - } -} - -if ($results.WindowsTerminal -ne $null) { - if ($results.Python -eq $null) { - Write-Host "" - Write-Host "Optional Tools:" -ForegroundColor White - } - - if ($results.WindowsTerminal) { - Write-Success "Windows Terminal" - } - else { - Write-Error "Windows Terminal - Installation failed or needs restart" - } -} - -#endregion - -#region Next Steps - -Write-Step "Next Steps" - -if ($allRequired) { - Write-Host "" - Write-Success "All required tools installed successfully!" - Write-Host "" - - Write-Host "IMPORTANT: Configure Git before your first commit:" -ForegroundColor Yellow - Write-Host "" - Write-Host " git config --global user.name `"Your Name`"" -ForegroundColor White - Write-Host " git config --global user.email `"your.email@example.com`"" -ForegroundColor White - Write-Host "" - - Write-Host "Optional: Set VS Code as Git's default editor:" -ForegroundColor Cyan - Write-Host " git config --global core.editor `"code --wait`"" -ForegroundColor White - Write-Host "" - - Write-Host "Verify your installation:" -ForegroundColor Cyan - Write-Host " pwsh --version" -ForegroundColor White - Write-Host " git --version" -ForegroundColor White - Write-Host " code --version" -ForegroundColor White - if ($results.Python) { - Write-Host " python --version" -ForegroundColor White - } - Write-Host "" - - Write-Host "Set PowerShell execution policy (if needed):" -ForegroundColor Cyan - Write-Host " Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor White - Write-Host "" - - Write-Host "Recommended VS Code Extensions:" -ForegroundColor Cyan - Write-Host " • GitLens - Supercharge Git capabilities" -ForegroundColor White - Write-Host " • Git Graph - View Git history visually" -ForegroundColor White - Write-Host " • PowerShell - Better PowerShell support (from Microsoft)" -ForegroundColor White - Write-Host "" - Write-Host " Install via: Ctrl+Shift+X in VS Code" -ForegroundColor Gray - Write-Host "" - - Write-Host "You're ready to start the workshop!" -ForegroundColor Green - Write-Host " cd path\to\git-workshop" -ForegroundColor White - Write-Host " cd 01-essentials\01-basics" -ForegroundColor White - Write-Host " .\setup.ps1" -ForegroundColor White - Write-Host "" -} -else { - Write-Host "" - Write-Warning "Some required installations failed or need verification." - Write-Host "" - Write-Host "Troubleshooting steps:" -ForegroundColor Yellow - Write-Host " 1. Close and reopen your terminal (or restart your computer)" -ForegroundColor White - Write-Host " 2. Run this script again: .\install-prerequisites.ps1" -ForegroundColor White - Write-Host " 3. If issues persist, try manual installation:" -ForegroundColor White - Write-Host " See INSTALLATION.md for detailed instructions" -ForegroundColor White - Write-Host "" - - if (-not $results.Git) { - Write-Host "For Git issues:" -ForegroundColor Yellow - Write-Host " • Restart terminal after installation (PATH needs to refresh)" -ForegroundColor White - Write-Host " • Manual download: https://git-scm.com/downloads" -ForegroundColor White - Write-Host "" - } - - if (-not $results.VSCode) { - Write-Host "For VS Code issues:" -ForegroundColor Yellow - Write-Host " • Ensure 'Add to PATH' option is enabled during installation" -ForegroundColor White - Write-Host " • Manual download: https://code.visualstudio.com/" -ForegroundColor White - Write-Host "" - } - - if (-not $results.PowerShell) { - Write-Host "For PowerShell 7 issues:" -ForegroundColor Yellow - Write-Host " • Manual download: https://github.com/PowerShell/PowerShell/releases/latest" -ForegroundColor White - Write-Host " • Download the file ending in '-win-x64.msi'" -ForegroundColor White - Write-Host "" - } - -} diff --git a/install.ps1 b/install.ps1 index 41f1ce7..74ceec2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,33 +1,35 @@ #!/usr/bin/env pwsh <# .SYNOPSIS -Oneshot Git Workshop installer - Downloads and runs the prerequisites installation. +Installs all prerequisites for the Git Workshop using winget. .DESCRIPTION -This script downloads the Git Workshop repository and runs the prerequisites -installation script. It's designed to be run directly from the web: +This script automates the installation of required tools for the Git Workshop: +- PowerShell 7 (cross-platform PowerShell) +- Git 2.23+ (version control system) +- Visual Studio Code (code editor with Git integration) - irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex +Optional tools (with user prompts): +- Python 3.12 (for Module 08: Multiplayer Git) +- Windows Terminal (modern terminal experience) -The script will: -1. Create a temporary working directory -2. Download the Git Workshop repository -3. Run the install-prerequisites.ps1 script -4. Clean up temporary files +The script checks for existing installations, shows clear progress, and verifies +each installation succeeded. At the end, it displays Git configuration instructions. .EXAMPLE -PS> irm https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | iex -Downloads and runs the Git Workshop installer. +PS> .\install-prerequisites.ps1 +Runs the installation script with interactive prompts. .NOTES -Requires Windows 11 with PowerShell and internet access. +Requires Windows 11 with winget (App Installer) available. +Some installations may require administrator privileges. #> [CmdletBinding()] param() Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' +$ErrorActionPreference = 'Continue' # Continue on errors to show all results #region Helper Functions @@ -59,207 +61,573 @@ function Write-Error { Write-ColorMessage " ✗ $Message" -Color Red } +function Test-CommandExists { + param([string]$Command) + + $oldPreference = $ErrorActionPreference + $ErrorActionPreference = 'SilentlyContinue' + + try { + if (Get-Command $Command -ErrorAction SilentlyContinue) { + return $true + } + return $false + } + finally { + $ErrorActionPreference = $oldPreference + } +} + +function Get-InstalledVersion { + param( + [string]$Command, + [string]$VersionArg = '--version' + ) + + try { + $output = & $Command $VersionArg 2>&1 | Select-Object -First 1 + return $output.ToString().Trim() + } + catch { + return $null + } +} + +function Test-WingetAvailable { + if (-not (Test-CommandExists 'winget')) { + Write-Error "winget is not available on this system." + Write-Host "`nTo fix this:" -ForegroundColor Yellow + Write-Host " 1. Update Windows 11 to the latest version (Settings → Windows Update)" -ForegroundColor White + Write-Host " 2. Install 'App Installer' from the Microsoft Store" -ForegroundColor White + Write-Host " 3. Restart your computer and run this script again" -ForegroundColor White + return $false + } + return $true +} + +function Install-Package { + param( + [string]$Name, + [string]$WingetId, + [string]$CheckCommand, + [string]$MinVersion = $null, + [string]$AdditionalArgs = '' + ) + + Write-Step "Installing $Name" + + # Check if already installed + if (Test-CommandExists $CheckCommand) { + $version = Get-InstalledVersion $CheckCommand + Write-Success "$Name is already installed: $version" + + if ($MinVersion -and $version) { + # Basic version check (not perfect but good enough for common cases) + if ($version -match '(\d+\.[\d.]+)') { + $installedVersion = $matches[1] + if ([version]$installedVersion -lt [version]$MinVersion) { + Write-Warning "Version $installedVersion is below minimum required version $MinVersion" + Write-Host " Attempting to upgrade..." -ForegroundColor Cyan + } + else { + return $true + } + } + } + else { + return $true + } + } + + # Install using winget + Write-Host " Installing via winget: $WingetId" -ForegroundColor Cyan + + $installCmd = "winget install --id $WingetId --source winget --silent $AdditionalArgs".Trim() + Write-Host " Running: $installCmd" -ForegroundColor Gray + + try { + # Show progress during installation + Write-Progress -Activity "Installing $Name" -Status "Downloading and installing..." -PercentComplete 25 + + $result = Invoke-Expression $installCmd 2>&1 + + Write-Progress -Activity "Installing $Name" -Status "Verifying installation..." -PercentComplete 75 + + # Check if installation succeeded + Start-Sleep -Seconds 2 # Give the system time to register the new command + + # Refresh environment variables in current session + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + if (Test-CommandExists $CheckCommand) { + $version = Get-InstalledVersion $CheckCommand + Write-Success "$Name installed successfully: $version" + Write-Progress -Activity "Installing $Name" -Completed + return $true + } + else { + Write-Warning "$Name installation completed, but command '$CheckCommand' not found." + Write-Host " You may need to restart your terminal or computer." -ForegroundColor Yellow + Write-Progress -Activity "Installing $Name" -Completed + return $false + } + } + catch { + Write-Error "Failed to install $Name`: $_" + Write-Progress -Activity "Installing $Name" -Completed + return $false + } +} + +function Test-GitVersion { + if (-not (Test-CommandExists 'git')) { + return $false + } + + $version = Get-InstalledVersion 'git' + + # Parse Git version from various formats: + # "git version 2.52.0", "git version 2.52.0.windows.1", etc. + if ($version -match 'git version (\d+)\.(\d+)') { + $majorVersion = [int]$matches[1] + $minorVersion = [int]$matches[2] + + # Check if version is 2.23 or higher + if ($majorVersion -gt 2 -or ($majorVersion -eq 2 -and $minorVersion -ge 23)) { + return $true + } + else { + Write-Warning "Git version $majorVersion.$minorVersion is below required version 2.23" + return $false + } + } + + Write-Warning "Could not parse Git version from: $version" + return $false +} + +function Get-UserConfirmation { + param([string]$Prompt) + + while ($true) { + $response = Read-Host "$Prompt (y/n)" + $response = $response.Trim().ToLower() + + if ($response -eq 'y' -or $response -eq 'yes') { + return $true + } + elseif ($response -eq 'n' -or $response -eq 'no') { + return $false + } + else { + Write-Host "Please enter 'y' or 'n'" -ForegroundColor Yellow + } + } +} + #endregion #region Main Script Write-Host @" + ╔═══════════════════════════════════════════════════════════╗ ║ ║ -║ Git Workshop - Oneshot Installation Script ║ +║ Git Workshop - Prerequisites Installation Script ║ ║ ║ ╚═══════════════════════════════════════════════════════════╝ "@ -ForegroundColor Cyan -Write-Host "This script will download and install all prerequisites for the Git Workshop." -ForegroundColor White +Write-Host "This script will install the required tools for the Git Workshop:" -ForegroundColor White +Write-Host " • PowerShell 7 (cross-platform PowerShell)" -ForegroundColor White +Write-Host " • Git 2.23+ (version control system)" -ForegroundColor White +Write-Host " • Visual Studio Code (code editor)" -ForegroundColor White +Write-Host "" +Write-Host "You will be prompted for optional tools:" -ForegroundColor White +Write-Host " • Python 3.12 (for Module 08: Multiplayer Git)" -ForegroundColor White +Write-Host " • Windows Terminal (modern terminal experience)" -ForegroundColor White Write-Host "" -# Check PowerShell version -Write-Step "Checking PowerShell Version" -$psVersion = $PSVersionTable.PSVersion -Write-Success "PowerShell $psVersion" +# Check for winget +Write-Step "Checking Prerequisites" -if ($psVersion.Major -lt 7) { - Write-Warning "PowerShell 7+ is recommended for best compatibility" - Write-Host " Continuing with PowerShell $($psVersion.Major)..." -ForegroundColor Gray +if (-not (Test-WingetAvailable)) { + Write-Host "`nInstallation cannot continue without winget." -ForegroundColor Red + return } -# Create temporary working directory -Write-Step "Creating Working Directory" -$tempDir = Join-Path $env:TEMP "git-workshop-$(Get-Date -Format 'yyyyMMdd-HHmmss')" +Write-Success "winget is available" -try { - New-Item -Path $tempDir -ItemType Directory -Force | Out-Null - Write-Success "Created temporary directory: $tempDir" -} -catch { - Write-Error "Failed to create temporary directory: $_" - exit 1 +# Track installation results +$results = @{ + PowerShell = $false + Git = $false + VSCode = $false + VSCodeExtensions = $false + VSCodePowerShellIntegration = $null # null = not asked, true = configured, false = skipped/failed + Python = $null # null = not attempted, true = success, false = failed + WindowsTerminal = $null } -# Download the repository -Write-Step "Downloading Git Workshop Repository" +# Progress tracking +$totalSteps = 4 # Required installations + extensions +$currentStep = 0 -$repoUrl = "https://git.frod.dk/floppydiscen/git-workshop/archive/main.zip" -$zipPath = Join-Path $tempDir "git-workshop.zip" +Write-Host "`nStarting installation..." -ForegroundColor Cyan +Write-Host "Note: Some installations may take a few minutes." -ForegroundColor Gray +Write-Host "" -try { - Write-Host " Downloading from: $repoUrl" -ForegroundColor Gray - Invoke-WebRequest -Uri $repoUrl -OutFile $zipPath -UseBasicParsing - Write-Success "Repository downloaded successfully" -} -catch { - Write-Error "Failed to download repository: $_" - Write-Host "" - Write-Host "Troubleshooting:" -ForegroundColor Yellow - Write-Host " • Check your internet connection" -ForegroundColor White - Write-Host " • Verify the repository URL is correct" -ForegroundColor White - Write-Host " • Try running the script again" -ForegroundColor White - exit 1 -} - -# Extract the repository -Write-Step "Extracting Repository" - -try { - Write-Host " Extracting to: $tempDir" -ForegroundColor Gray - Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force +# Progress bar helper +function Write-ProgressIndicator { + param( + [string]$Activity, + [string]$Status, + [int]$PercentComplete + ) - # Find the extracted directory (should be git-workshop-main) - $extractedDir = Get-ChildItem -Path $tempDir -Directory | Where-Object { $_.Name -like "git-workshop-*" } | Select-Object -First 1 + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete +} + +function Install-VSCodeExtension { + param( + [string]$ExtensionId, + [string]$ExtensionName + ) - if (-not $extractedDir) { - throw "Could not find extracted repository directory" - } + Write-Host " Installing VSCode extension: $ExtensionName" -ForegroundColor Cyan - Write-Success "Repository extracted to: $($extractedDir.FullName)" -} -catch { - Write-Error "Failed to extract repository: $_" - exit 1 -} - -# Run the prerequisites installer -Write-Step "Running Prerequisites Installation" - -$installerScript = Join-Path $extractedDir.FullName "install-prerequisites.ps1" - -if (-not (Test-Path $installerScript)) { - Write-Error "Installation script not found: $installerScript" - Write-Host "" - Write-Host "Expected file structure:" -ForegroundColor Yellow - Write-Host " git-workshop-main/" -ForegroundColor White - Write-Host " ├── install-prerequisites.ps1" -ForegroundColor White - Write-Host " ├── README.md" -ForegroundColor White - Write-Host " └── ..." -ForegroundColor White - exit 1 -} - -try { - Write-Host " Running: $installerScript" -ForegroundColor Gray - Write-Host "" - - # Change to the extracted directory and run the installer - Push-Location $extractedDir.FullName - & $installerScript - - $installerExitCode = $LASTEXITCODE - Pop-Location - - if ($installerExitCode -eq 0) { - Write-Success "Prerequisites installation completed successfully!" - } - else { - Write-Warning "Prerequisites installation completed with warnings (exit code: $installerExitCode)" - } -} -catch { - Write-Error "Failed to run prerequisites installer: $_" - exit 1 -} -finally { - if (Get-Location -ErrorAction SilentlyContinue) { - Pop-Location -ErrorAction SilentlyContinue - } -} - -# Clean up -Write-Step "Cleaning Up" - -try { - Write-Host " Removing temporary directory: $tempDir" -ForegroundColor Gray - Remove-Item -Path $tempDir -Recurse -Force - Write-Success "Temporary files cleaned up" -} -catch { - Write-Warning "Failed to clean up temporary files: $_" - Write-Host " You can manually delete: $tempDir" -ForegroundColor Yellow -} - -# Clone the repository locally -Write-Step "Cloning Git Workshop Repository" - -$cloneDir = Join-Path $HOME "git-workshop" - -try { - if (Test-Path $cloneDir) { - Write-Warning "Directory already exists: $cloneDir" - $response = Read-Host " Do you want to remove it and clone fresh? (y/n)" - if ($response.Trim().ToLower() -eq 'y' -or $response.Trim().ToLower() -eq 'yes') { - Remove-Item -Path $cloneDir -Recurse -Force - Write-Host " Removed existing directory" -ForegroundColor Gray + try { + # Refresh environment to ensure code command is available + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + + $result = & code --install-extension $ExtensionId 2>&1 + + if ($LASTEXITCODE -eq 0) { + Write-Success "VSCode extension '$ExtensionName' installed successfully" + return $true } else { - Write-Host " Skipping clone - using existing directory" -ForegroundColor Gray + Write-Warning "Failed to install VSCode extension '$ExtensionName'" + Write-Host " You can install it manually later: Ctrl+Shift+X → Search '$ExtensionName'" -ForegroundColor Gray + return $false } } - - if (-not (Test-Path $cloneDir)) { - Write-Host " Cloning to: $cloneDir" -ForegroundColor Gray - git clone "https://git.frod.dk/floppydiscen/git-workshop.git" $cloneDir - Write-Success "Repository cloned successfully!" + catch { + Write-Warning "Could not install VSCode extension '$ExtensionName`: $_" + Write-Host " You can install it manually later: Ctrl+Shift+X → Search '$ExtensionName'" -ForegroundColor Gray + return $false } } -catch { - Write-Error "Failed to clone repository: $_" - Write-Host "" - Write-Host "You can clone manually:" -ForegroundColor Yellow - Write-Host " git clone https://git.frod.dk/floppydiscen/git-workshop.git ~/git-workshop" -ForegroundColor White - exit 1 + +function Set-VSCodePowerShellIntegration { + # Ask user if they want to set PowerShell 7 as default terminal + $setAsDefault = Get-UserConfirmation "Set PowerShell 7 as the default terminal in VSCode? (Recommended for this workshop)" + + if (-not $setAsDefault) { + Write-Host " Skipping PowerShell 7 terminal configuration." -ForegroundColor Gray + return $false + } + + Write-Host " Configuring PowerShell 7 integration with VSCode..." -ForegroundColor Cyan + + try { + # Set PowerShell 7 as the default terminal in VSCode + $vscodeSettingsPath = Join-Path $env:APPDATA "Code\User\settings.json" + $vscodeSettingsDir = Split-Path $vscodeSettingsPath -Parent + + # Create directory if it doesn't exist + if (-not (Test-Path $vscodeSettingsDir)) { + New-Item -Path $vscodeSettingsDir -ItemType Directory -Force | Out-Null + } + + # Read existing settings or create new + if (Test-Path $vscodeSettingsPath) { + $settings = Get-Content $vscodeSettingsPath -Raw | ConvertFrom-Json + } + else { + $settings = @{} + } + + # Set PowerShell 7 as default terminal + if (-not $settings.PSObject.Properties.Name -contains "terminal.integrated.defaultProfile.windows") { + $settings | Add-Member -NotePropertyName "terminal.integrated.defaultProfile.windows" -NotePropertyValue "PowerShell" + } + else { + $settings."terminal.integrated.defaultProfile.windows" = "PowerShell" + } + + # Add terminal profiles if not present + if (-not $settings.PSObject.Properties.Name -contains "terminal.integrated.profiles.windows") { + $profiles = @{ + "PowerShell" = @{ + "source" = "PowerShell" + "icon" = "terminal-powershell" + "path" = "pwsh.exe" + } + } + $settings | Add-Member -NotePropertyName "terminal.integrated.profiles.windows" -NotePropertyValue $profiles + } + + # Save settings + $settings | ConvertTo-Json -Depth 10 | Set-Content $vscodeSettingsPath + + Write-Success "VSCode configured to use PowerShell 7 as default terminal" + return $true + } + catch { + Write-Warning "Could not configure VSCode PowerShell integration automatically" + Write-Host " You can configure it manually in VSCode: Ctrl+Shift+P → Terminal: Select Default Profile → PowerShell" -ForegroundColor Gray + return $false + } } -# Final instructions -Write-Step "Installation Complete" +#region Required Installations -Write-Host "" -Write-Success "Git Workshop installation is complete!" -Write-Host "" +# Install PowerShell 7 +$currentStep++ +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing PowerShell 7 (1/3)" -PercentComplete (($currentStep / $totalSteps) * 100) +$results.PowerShell = Install-Package ` + -Name "PowerShell 7" ` + -WingetId "Microsoft.PowerShell" ` + -CheckCommand "pwsh" -Write-Host "Repository cloned to: $cloneDir" -ForegroundColor Green -Write-Host "" +# Install Git +$currentStep++ +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Git (2/3)" -PercentComplete (($currentStep / $totalSteps) * 100) +$results.Git = Install-Package ` + -Name "Git" ` + -WingetId "Git.Git" ` + -CheckCommand "git" ` + -MinVersion "2.23" ` + -AdditionalArgs "-e" -Write-Host "Next steps:" -ForegroundColor Cyan -Write-Host " 1. Configure Git if you haven't already:" -ForegroundColor White -Write-Host " git config --global user.name `"Your Name`"" -ForegroundColor Gray -Write-Host " git config --global user.email `"your.email@example.com`"" -ForegroundColor Gray -Write-Host "" -Write-Host " 2. Navigate to the workshop:" -ForegroundColor White -Write-Host " cd $cloneDir" -ForegroundColor Gray -Write-Host "" -Write-Host " 3. Start with the first module:" -ForegroundColor White -Write-Host " cd 01-essentials\01-basics" -ForegroundColor Gray -Write-Host " .\setup.ps1" -ForegroundColor Gray -Write-Host "" +# Verify Git version specifically +if ($results.Git) { + if (-not (Test-GitVersion)) { + Write-Warning "Git is installed but version may be below 2.23" + $results.Git = $false + } +} -Write-Host "For help and documentation:" -ForegroundColor Cyan -Write-Host " • README.md - Workshop overview" -ForegroundColor White -Write-Host " • GIT-CHEATSHEET.md - Git command reference" -ForegroundColor White -Write-Host " • AGENDA.md - Workshop schedule (Danish)" -ForegroundColor White -Write-Host "" +# Install Visual Studio Code +$currentStep++ +Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Visual Studio Code (3/4)" -PercentComplete (($currentStep / $totalSteps) * 100) +$results.VSCode = Install-Package ` + -Name "Visual Studio Code" ` + -WingetId "Microsoft.VisualStudioCode" ` + -CheckCommand "code" -Write-Host "Enjoy learning Git!" -ForegroundColor Green +# Install VSCode Extensions and configure PowerShell integration +if ($results.VSCode) { + $currentStep++ + Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing VSCode Extensions (4/4)" -PercentComplete (($currentStep / $totalSteps) * 100) + + Write-Host "" + Write-Step "Configuring VSCode" + + # Install PowerShell extension + $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" + + + + # Configure PowerShell 7 integration (optional but recommended) + $powershellIntegrationResult = Set-VSCodePowerShellIntegration + $results.VSCodePowerShellIntegration = $powershellIntegrationResult + + $results.VSCodeExtensions = $powershellExtensionResult +} +else { + $results.VSCodeExtensions = $false +} + +# Clear progress bar +Write-Progress -Activity "Installing Required Tools" -Completed #endregion -exit 0 \ No newline at end of file +#region Optional Installations + +# Python 3.12 (optional) +Write-Host "" +if (Get-UserConfirmation "Do you want to install Python 3.12? (Required for Module 08: Multiplayer Git)") { + Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Python 3.12" -PercentComplete 50 + $results.Python = Install-Package ` + -Name "Python 3.12" ` + -WingetId "Python.Python.3.12" ` + -CheckCommand "python" + Write-Progress -Activity "Installing Optional Tools" -Completed +} +else { + Write-Host " Skipping Python installation." -ForegroundColor Gray + $results.Python = $null +} + +# Windows Terminal (optional) +Write-Host "" +if (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)") { + Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Windows Terminal" -PercentComplete 50 + $results.WindowsTerminal = Install-Package ` + -Name "Windows Terminal" ` + -WingetId "Microsoft.WindowsTerminal" ` + -CheckCommand "wt" + Write-Progress -Activity "Installing Optional Tools" -Completed +} +else { + Write-Host " Skipping Windows Terminal installation." -ForegroundColor Gray + $results.WindowsTerminal = $null +} + +#endregion + +#region Installation Summary + +Write-Step "Installation Summary" + +$allRequired = $results.PowerShell -and $results.Git -and $results.VSCode + +Write-Host "" +Write-Host "Required Tools:" -ForegroundColor White + +if ($results.PowerShell) { + Write-Success "PowerShell 7" +} +else { + Write-Error "PowerShell 7 - Installation failed or needs restart" +} + +if ($results.Git) { + Write-Success "Git 2.23+" +} +else { + Write-Error "Git 2.23+ - Installation failed or needs restart" +} + +if ($results.VSCode) { + Write-Success "Visual Studio Code" + + if ($results.VSCodeExtensions) { + Write-Success " • PowerShell extension" + } + else { + Write-Warning " • VSCode PowerShell extension may need manual installation" + } + + if ($results.VSCodePowerShellIntegration -eq $true) { + Write-Success " • PowerShell 7 terminal integration" + } + elseif ($results.VSCodePowerShellIntegration -eq $false) { + Write-Host " • PowerShell 7 terminal integration: Skipped" -ForegroundColor Gray + } +} +else { + Write-Error "Visual Studio Code - Installation failed or needs restart" +} + +if ($results.Python -ne $null) { + Write-Host "" + Write-Host "Optional Tools:" -ForegroundColor White + + if ($results.Python) { + Write-Success "Python 3.12" + } + else { + Write-Error "Python 3.12 - Installation failed or needs restart" + } +} + +if ($results.WindowsTerminal -ne $null) { + if ($results.Python -eq $null) { + Write-Host "" + Write-Host "Optional Tools:" -ForegroundColor White + } + + if ($results.WindowsTerminal) { + Write-Success "Windows Terminal" + } + else { + Write-Error "Windows Terminal - Installation failed or needs restart" + } +} + +#endregion + +#region Next Steps + +Write-Step "Next Steps" + +if ($allRequired) { + Write-Host "" + Write-Success "All required tools installed successfully!" + Write-Host "" + + Write-Host "IMPORTANT: Configure Git before your first commit:" -ForegroundColor Yellow + Write-Host "" + Write-Host " git config --global user.name `"Your Name`"" -ForegroundColor White + Write-Host " git config --global user.email `"your.email@example.com`"" -ForegroundColor White + Write-Host "" + + Write-Host "Optional: Set VS Code as Git's default editor:" -ForegroundColor Cyan + Write-Host " git config --global core.editor `"code --wait`"" -ForegroundColor White + Write-Host "" + + Write-Host "Verify your installation:" -ForegroundColor Cyan + Write-Host " pwsh --version" -ForegroundColor White + Write-Host " git --version" -ForegroundColor White + Write-Host " code --version" -ForegroundColor White + if ($results.Python) { + Write-Host " python --version" -ForegroundColor White + } + Write-Host "" + + Write-Host "Set PowerShell execution policy (if needed):" -ForegroundColor Cyan + Write-Host " Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor White + Write-Host "" + + Write-Host "Recommended VS Code Extensions:" -ForegroundColor Cyan + Write-Host " • GitLens - Supercharge Git capabilities" -ForegroundColor White + Write-Host " • Git Graph - View Git history visually" -ForegroundColor White + Write-Host " • PowerShell - Better PowerShell support (from Microsoft)" -ForegroundColor White + Write-Host "" + Write-Host " Install via: Ctrl+Shift+X in VS Code" -ForegroundColor Gray + Write-Host "" + + Write-Host "You're ready to start the workshop!" -ForegroundColor Green + Write-Host " cd path\to\git-workshop" -ForegroundColor White + Write-Host " cd 01-essentials\01-basics" -ForegroundColor White + Write-Host " .\setup.ps1" -ForegroundColor White + Write-Host "" +} +else { + Write-Host "" + Write-Warning "Some required installations failed or need verification." + Write-Host "" + Write-Host "Troubleshooting steps:" -ForegroundColor Yellow + Write-Host " 1. Close and reopen your terminal (or restart your computer)" -ForegroundColor White + Write-Host " 2. Run this script again: .\install-prerequisites.ps1" -ForegroundColor White + Write-Host " 3. If issues persist, try manual installation:" -ForegroundColor White + Write-Host " See INSTALLATION.md for detailed instructions" -ForegroundColor White + Write-Host "" + + if (-not $results.Git) { + Write-Host "For Git issues:" -ForegroundColor Yellow + Write-Host " • Restart terminal after installation (PATH needs to refresh)" -ForegroundColor White + Write-Host " • Manual download: https://git-scm.com/downloads" -ForegroundColor White + Write-Host "" + } + + if (-not $results.VSCode) { + Write-Host "For VS Code issues:" -ForegroundColor Yellow + Write-Host " • Ensure 'Add to PATH' option is enabled during installation" -ForegroundColor White + Write-Host " • Manual download: https://code.visualstudio.com/" -ForegroundColor White + Write-Host "" + } + + if (-not $results.PowerShell) { + Write-Host "For PowerShell 7 issues:" -ForegroundColor Yellow + Write-Host " • Manual download: https://github.com/PowerShell/PowerShell/releases/latest" -ForegroundColor White + Write-Host " • Download the file ending in '-win-x64.msi'" -ForegroundColor White + Write-Host "" + } + +} From 09f25d6eae930ffe97798f39f4e4741763e9213f Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:03:04 +0100 Subject: [PATCH 17/61] Add repository cloning and VSCode opening to install.ps1 - Ask user if they want to clone workshop to Documents/git-workshop - Automatically clone repository to Documents folder for convenience - Open VSCode in the workshop directory after cloning - Handle existing repositories (update if already cloned) - Provide quick start commands for VSCode terminal - Fallback to manual instructions if user declines or cloning fails - Complete end-to-end setup experience --- install.ps1 | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/install.ps1 b/install.ps1 index 74ceec2..576476c 100644 --- a/install.ps1 +++ b/install.ps1 @@ -593,10 +593,78 @@ if ($allRequired) { Write-Host "" Write-Host "You're ready to start the workshop!" -ForegroundColor Green - Write-Host " cd path\to\git-workshop" -ForegroundColor White - Write-Host " cd 01-essentials\01-basics" -ForegroundColor White - Write-Host " .\setup.ps1" -ForegroundColor White Write-Host "" + + # Ask user if they want to clone the workshop repository + Write-Step "Workshop Setup" + + if (Get-UserConfirmation "Clone the Git Workshop repository to Documents\git-workshop and open in VSCode?") { + try { + $documentsPath = [System.Environment]::GetFolderPath("MyDocuments") + $workshopPath = Join-Path $documentsPath "git-workshop" + + Write-Host " Cloning to: $workshopPath" -ForegroundColor Gray + + # Create directory if it doesn't exist + $documentsDir = Split-Path $workshopPath -Parent + if (-not (Test-Path $documentsDir)) { + New-Item -Path $documentsDir -ItemType Directory -Force | Out-Null + } + + # Clone or update the repository + if (Test-Path $workshopPath) { + Write-Host " Directory already exists. Checking if it's a git repository..." -ForegroundColor Yellow + Push-Location $workshopPath + if (Get-Command git -ErrorAction SilentlyContinue) { + $remoteResult = git remote get-url origin 2>$null + if ($LASTEXITCODE -eq 0 -and $remoteResult -like "*git-workshop*") { + Write-Host " Repository already exists. Updating..." -ForegroundColor Cyan + git pull origin main + } else { + Write-Warning " Directory exists but is not the git-workshop repository" + Write-Host " Please remove the directory manually and run again" -ForegroundColor Yellow + Pop-Location + return + } + } + Pop-Location + } else { + git clone "https://git.frod.dk/floppydiscen/git-workshop.git" $workshopPath + Write-Success "Repository cloned successfully!" + } + + # Open in VSCode + Write-Host " Opening in VSCode..." -ForegroundColor Cyan + if (Get-Command code -ErrorAction SilentlyContinue) { + & code $workshopPath + Write-Success "VSCode opened with the workshop repository" + } else { + Write-Warning "VSCode command not found. Please open manually:" + Write-Host " code '$workshopPath'" -ForegroundColor White + } + + Write-Host "" + Write-Host "Quick start commands (run in VSCode terminal):" -ForegroundColor Cyan + Write-Host " cd 01-essentials\01-basics" -ForegroundColor White + Write-Host " .\setup.ps1" -ForegroundColor White + Write-Host "" + + } catch { + Write-Error "Failed to clone repository: $_" + Write-Host "" + Write-Host "You can clone manually:" -ForegroundColor Yellow + Write-Host " git clone https://git.frod.dk/floppydiscen/git-workshop.git ~/Documents/git-workshop" -ForegroundColor White + Write-Host " code ~/Documents/git-workshop" -ForegroundColor White + } + } else { + Write-Host " Skipping repository clone." -ForegroundColor Gray + Write-Host "" + Write-Host "Manual setup:" -ForegroundColor Cyan + Write-Host " cd path\to\git-workshop" -ForegroundColor White + Write-Host " cd 01-essentials\01-basics" -ForegroundColor White + Write-Host " .\setup.ps1" -ForegroundColor White + Write-Host "" + } } else { Write-Host "" From 07faa14b7ac7cc11e30f88db53b87d4ccef83a61 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:12:03 +0100 Subject: [PATCH 18/61] Remove Python installation and add one-shot install instructions - Remove all Python-related installation logic since Module 08 is Git-only - Update synopsis to mention repository cloning capability - Add one-shot installation instructions using Invoke-RestMethod - Remove Python from results tracking, installation prompts, and verification - Simplify installation summary without Python references - Update examples to show both one-shot and local execution methods --- install.ps1 | 64 ++++++++++++++++------------------------------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/install.ps1 b/install.ps1 index 576476c..f329d69 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1,7 +1,7 @@ #!/usr/bin/env pwsh <# .SYNOPSIS -Installs all prerequisites for the Git Workshop using winget. +Installs all prerequisites for Git Workshop using winget and clones the repository. .DESCRIPTION This script automates the installation of required tools for the Git Workshop: @@ -10,14 +10,21 @@ This script automates the installation of required tools for the Git Workshop: - Visual Studio Code (code editor with Git integration) Optional tools (with user prompts): -- Python 3.12 (for Module 08: Multiplayer Git) - Windows Terminal (modern terminal experience) The script checks for existing installations, shows clear progress, and verifies -each installation succeeded. At the end, it displays Git configuration instructions. +each installation succeeded. At the end, it clones the repository and can +open it in VSCode for immediate workshop access. + +One-shot installation: +Invoke-RestMethod -Uri https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | Invoke-Expression .EXAMPLE -PS> .\install-prerequisites.ps1 +PS> Invoke-RestMethod -Uri https://git.frod.dk/floppydiscen/git-workshop/raw/branch/main/install.ps1 | Invoke-Expression +Runs the complete installation and setup in one command. + +.EXAMPLE +PS> .\install.ps1 Runs the installation script with interactive prompts. .NOTES @@ -245,7 +252,7 @@ Write-Host " • Git 2.23+ (version control system)" -ForegroundColor White Write-Host " • Visual Studio Code (code editor)" -ForegroundColor White Write-Host "" Write-Host "You will be prompted for optional tools:" -ForegroundColor White -Write-Host " • Python 3.12 (for Module 08: Multiplayer Git)" -ForegroundColor White + Write-Host " • Windows Terminal (modern terminal experience)" -ForegroundColor White Write-Host "" @@ -266,7 +273,6 @@ $results = @{ VSCode = $false VSCodeExtensions = $false VSCodePowerShellIntegration = $null # null = not asked, true = configured, false = skipped/failed - Python = $null # null = not attempted, true = success, false = failed WindowsTerminal = $null } @@ -448,20 +454,7 @@ Write-Progress -Activity "Installing Required Tools" -Completed #region Optional Installations -# Python 3.12 (optional) -Write-Host "" -if (Get-UserConfirmation "Do you want to install Python 3.12? (Required for Module 08: Multiplayer Git)") { - Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Python 3.12" -PercentComplete 50 - $results.Python = Install-Package ` - -Name "Python 3.12" ` - -WingetId "Python.Python.3.12" ` - -CheckCommand "python" - Write-Progress -Activity "Installing Optional Tools" -Completed -} -else { - Write-Host " Skipping Python installation." -ForegroundColor Gray - $results.Python = $null -} + # Windows Terminal (optional) Write-Host "" @@ -524,24 +517,10 @@ else { Write-Error "Visual Studio Code - Installation failed or needs restart" } -if ($results.Python -ne $null) { +if ($results.WindowsTerminal -ne $null) { Write-Host "" Write-Host "Optional Tools:" -ForegroundColor White - if ($results.Python) { - Write-Success "Python 3.12" - } - else { - Write-Error "Python 3.12 - Installation failed or needs restart" - } -} - -if ($results.WindowsTerminal -ne $null) { - if ($results.Python -eq $null) { - Write-Host "" - Write-Host "Optional Tools:" -ForegroundColor White - } - if ($results.WindowsTerminal) { Write-Success "Windows Terminal" } @@ -571,13 +550,10 @@ if ($allRequired) { Write-Host " git config --global core.editor `"code --wait`"" -ForegroundColor White Write-Host "" - Write-Host "Verify your installation:" -ForegroundColor Cyan - Write-Host " pwsh --version" -ForegroundColor White - Write-Host " git --version" -ForegroundColor White - Write-Host " code --version" -ForegroundColor White - if ($results.Python) { - Write-Host " python --version" -ForegroundColor White - } +Write-Host "Verify your installation:" -ForegroundColor Cyan +Write-Host " pwsh --version" -ForegroundColor White +Write-Host " git --version" -ForegroundColor White +Write-Host " code --version" -ForegroundColor White Write-Host "" Write-Host "Set PowerShell execution policy (if needed):" -ForegroundColor Cyan @@ -585,8 +561,6 @@ if ($allRequired) { Write-Host "" Write-Host "Recommended VS Code Extensions:" -ForegroundColor Cyan - Write-Host " • GitLens - Supercharge Git capabilities" -ForegroundColor White - Write-Host " • Git Graph - View Git history visually" -ForegroundColor White Write-Host " • PowerShell - Better PowerShell support (from Microsoft)" -ForegroundColor White Write-Host "" Write-Host " Install via: Ctrl+Shift+X in VS Code" -ForegroundColor Gray @@ -672,7 +646,7 @@ else { Write-Host "" Write-Host "Troubleshooting steps:" -ForegroundColor Yellow Write-Host " 1. Close and reopen your terminal (or restart your computer)" -ForegroundColor White - Write-Host " 2. Run this script again: .\install-prerequisites.ps1" -ForegroundColor White + Write-Host " 2. Run this script again: .\install.ps1" -ForegroundColor White Write-Host " 3. If issues persist, try manual installation:" -ForegroundColor White Write-Host " See INSTALLATION.md for detailed instructions" -ForegroundColor White Write-Host "" From c28151cc1915169f3a7419aa2086c239607c1b09 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:24:33 +0100 Subject: [PATCH 19/61] Fix version parsing in Install-Package for Windows Git format - Update regex to extract only semantic version numbers (x.y.z) - Prevents matching entire string like '2.52.0.windows.1' - Uses '^(\d+(?:\.\d+){1,2})' to match version at start of string only - Extracts '2.52.0' from '2.52.0.windows.1' for proper version comparison - Handles Windows Git version suffixes correctly - Maintains compatibility with standard version formats --- install.ps1 | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/install.ps1 b/install.ps1 index f329d69..fc9254e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -129,17 +129,29 @@ function Install-Package { Write-Success "$Name is already installed: $version" if ($MinVersion -and $version) { - # Basic version check (not perfect but good enough for common cases) - if ($version -match '(\d+\.[\d.]+)') { + # Extract semantic version numbers only - stop before any non-digit/non-dot characters + # This extracts "2.52.0" from "2.52.0.windows.1" + if ($version -match '^(\d+(?:\.\d+){1,2})') { $installedVersion = $matches[1] - if ([version]$installedVersion -lt [version]$MinVersion) { - Write-Warning "Version $installedVersion is below minimum required version $MinVersion" - Write-Host " Attempting to upgrade..." -ForegroundColor Cyan + try { + if ([version]$installedVersion -lt [version]$MinVersion) { + Write-Warning "Version $installedVersion is below minimum required version $MinVersion" + Write-Host " Attempting to upgrade..." -ForegroundColor Cyan + } + else { + return $true + } } - else { + catch { + Write-Warning "Version comparison failed - assuming sufficient version" return $true } } + else { + Write-Warning "Could not parse version from: $version" + Write-Host " Assuming installed version is sufficient..." -ForegroundColor Cyan + return $true + } } else { return $true From 2633ee2b71ea133a5be746841027fe6b247c33ce Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:29:22 +0100 Subject: [PATCH 20/61] Add clear terminal opening guidance for VSCode - Enhance instructions for opening integrated terminal in VSCode - Provide multiple methods: Ctrl+backtick, menu, and Command Palette - Add visual separation with headers for important information - Emphasize that terminal opening is REQUIRED for next steps - Include detailed fallback instructions if VSCode command fails - Make terminal guidance impossible to miss with multiple clear options - Remove ambiguity about how to access terminal in VSCode --- install.ps1 | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index fc9254e..57ee7c2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -619,14 +619,35 @@ Write-Host " code --version" -ForegroundColor White Write-Success "Repository cloned successfully!" } - # Open in VSCode +# Open in VSCode Write-Host " Opening in VSCode..." -ForegroundColor Cyan if (Get-Command code -ErrorAction SilentlyContinue) { & code $workshopPath Write-Success "VSCode opened with the workshop repository" + Write-Host "" + Write-Host "=== IMPORTANT: Terminal Instructions ===" -ForegroundColor Yellow + Write-Host "Once VSCode opens, you MUST open the integrated terminal:" -ForegroundColor White + Write-Host "" + Write-Host "Option 1 (Recommended): Press Ctrl+` (backtick key)" -ForegroundColor Cyan + Write-Host "Option 2: Use menu: View → Terminal" -ForegroundColor Cyan + Write-Host "Option 3: Use Command Palette: Ctrl+Shift+P → 'Terminal: Create New Terminal'" -ForegroundColor Cyan + Write-Host "" + Write-Host "=== Quick Start (run in VSCode terminal) ===" -ForegroundColor Yellow + Write-Host " cd 01-essentials\01-basics" -ForegroundColor White + Write-Host " .\setup.ps1" -ForegroundColor White + Write-Host "" + Write-Host "The terminal should show 'PowerShell' and open to the correct directory." -ForegroundColor Green + Write-Host "" } else { Write-Warning "VSCode command not found. Please open manually:" Write-Host " code '$workshopPath'" -ForegroundColor White + Write-Host "" + Write-Host "=== Manual Instructions ===" -ForegroundColor Yellow + Write-Host "1. Open VSCode manually" -ForegroundColor White + Write-Host "2. Open integrated terminal (Ctrl+` or View → Terminal)" -ForegroundColor White + Write-Host "3. Navigate: cd 01-essentials\01-basics" -ForegroundColor White + Write-Host "4. Run: .\setup.ps1" -ForegroundColor White + Write-Host "" } Write-Host "" From c99e2388142cb5fc4e9df80611f4eeb86eeec7af Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:40:57 +0100 Subject: [PATCH 21/61] fix: version match --- install.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 57ee7c2..379d535 100644 --- a/install.ps1 +++ b/install.ps1 @@ -131,7 +131,7 @@ function Install-Package { if ($MinVersion -and $version) { # Extract semantic version numbers only - stop before any non-digit/non-dot characters # This extracts "2.52.0" from "2.52.0.windows.1" - if ($version -match '^(\d+(?:\.\d+){1,2})') { + if ($version -match '^\d+(?:\.\d+)*') { $installedVersion = $matches[1] try { if ([version]$installedVersion -lt [version]$MinVersion) { @@ -207,7 +207,7 @@ function Test-GitVersion { # Parse Git version from various formats: # "git version 2.52.0", "git version 2.52.0.windows.1", etc. - if ($version -match 'git version (\d+)\.(\d+)') { + if ($version -match 'git version \d+(?:\.\d+)*') { $majorVersion = [int]$matches[1] $minorVersion = [int]$matches[2] From 91c46718c6375696bda1cb6ca3419ccdb75d3bdf Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Wed, 14 Jan 2026 17:48:57 +0100 Subject: [PATCH 22/61] fix: version regex for git and other semver stuff --- install.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 379d535..92cb841 100644 --- a/install.ps1 +++ b/install.ps1 @@ -131,7 +131,7 @@ function Install-Package { if ($MinVersion -and $version) { # Extract semantic version numbers only - stop before any non-digit/non-dot characters # This extracts "2.52.0" from "2.52.0.windows.1" - if ($version -match '^\d+(?:\.\d+)*') { + if ($version -match '^(\d+)(?:\.(\d+))?(?:\.(\d+))?') { $installedVersion = $matches[1] try { if ([version]$installedVersion -lt [version]$MinVersion) { @@ -207,7 +207,7 @@ function Test-GitVersion { # Parse Git version from various formats: # "git version 2.52.0", "git version 2.52.0.windows.1", etc. - if ($version -match 'git version \d+(?:\.\d+)*') { + if ($version -match 'git version (\d+)(?:\.(\d+))?(?:\.(\d+))?') { $majorVersion = [int]$matches[1] $minorVersion = [int]$matches[2] From 32a0e89f72b254d7cf130c5c9050300e1e3e3a51 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 09:55:54 +0100 Subject: [PATCH 23/61] fix: let's just try to find a version inside the version string --- install.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index 92cb841..b3dfa7e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -131,7 +131,7 @@ function Install-Package { if ($MinVersion -and $version) { # Extract semantic version numbers only - stop before any non-digit/non-dot characters # This extracts "2.52.0" from "2.52.0.windows.1" - if ($version -match '^(\d+)(?:\.(\d+))?(?:\.(\d+))?') { + if ($version -match '(\d+)(?:\.(\d+))?(?:\.(\d+))?') { $installedVersion = $matches[1] try { if ([version]$installedVersion -lt [version]$MinVersion) { @@ -619,7 +619,6 @@ Write-Host " code --version" -ForegroundColor White Write-Success "Repository cloned successfully!" } -# Open in VSCode Write-Host " Opening in VSCode..." -ForegroundColor Cyan if (Get-Command code -ErrorAction SilentlyContinue) { & code $workshopPath From 009a3a910443fb0ae0335a827c7a7b47a1937ba9 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 10:03:21 +0100 Subject: [PATCH 24/61] fix: proper version formatting for install version check --- install.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index b3dfa7e..4dfa96b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -132,7 +132,7 @@ function Install-Package { # Extract semantic version numbers only - stop before any non-digit/non-dot characters # This extracts "2.52.0" from "2.52.0.windows.1" if ($version -match '(\d+)(?:\.(\d+))?(?:\.(\d+))?') { - $installedVersion = $matches[1] + $installedVersion = $matches[1] + "." +$matches[2] try { if ([version]$installedVersion -lt [version]$MinVersion) { Write-Warning "Version $installedVersion is below minimum required version $MinVersion" From a392e8c97db7aeac9a3f1d1fc893a3fef3509726 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 10:09:11 +0100 Subject: [PATCH 25/61] feat: add a "what do you want" step --- install.ps1 | 440 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 345 insertions(+), 95 deletions(-) diff --git a/install.ps1 b/install.ps1 index 4dfa96b..191494a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -246,6 +246,188 @@ function Get-UserConfirmation { #endregion +#region Prerequisites Check Function + +function Get-SystemPrerequisites { + <# + .SYNOPSIS + Checks which required tools are installed and their versions. + + .DESCRIPTION + Scans the system for PowerShell 7, Git 2.23+, and Visual Studio Code. + Returns a hashtable with installation status and version information. + #> + + $prereqs = @{ + PowerShell = @{ + Installed = $false + Version = $null + Command = "pwsh" + MinVersion = "7.0" + Name = "PowerShell 7" + } + Git = @{ + Installed = $false + Version = $null + Command = "git" + MinVersion = "2.23" + Name = "Git" + } + VSCode = @{ + Installed = $false + Version = $null + Command = "code" + MinVersion = $null + Name = "Visual Studio Code" + } + WindowsTerminal = @{ + Installed = $false + Version = $null + Command = "wt" + MinVersion = $null + Name = "Windows Terminal" + } + } + + Write-Host "Checking system for installed tools..." -ForegroundColor Cyan + Write-Host "" + + foreach ($key in $prereqs.Keys) { + $tool = $prereqs[$key] + + if (Test-CommandExists $tool.Command) { + $tool.Installed = $true + $tool.Version = Get-InstalledVersion $tool.Command + + if ($tool.MinVersion -and $tool.Version) { + # Extract semantic version for comparison + if ($tool.Version -match '(\d+)(?:\.(\d+))?(?:\.(\d+))?') { + $major = [int]$matches[1] + $minor = if ($matches[2]) { [int]$matches[2] } else { 0 } + $patch = if ($matches[3]) { [int]$matches[3] } else { 0 } + $installedVersion = "$major.$minor.$patch" + + $minParts = $tool.MinVersion.Split('.') + $minMajor = [int]$minParts[0] + $minMinor = if ($minParts[1]) { [int]$minParts[1] } else { 0 } + $minPatch = if ($minParts[2]) { [int]$minParts[2] } else { 0 } + $minVersion = "$minMajor.$minMinor.$minPatch" + + if ([version]$installedVersion -lt [version]$minVersion) { + Write-Host " ✓ $($tool.Name): $($tool.Version) (⚠ below required $($tool.MinVersion))" -ForegroundColor Yellow + $tool.Sufficient = $false + } else { + Write-Host " ✓ $($tool.Name): $($tool.Version)" -ForegroundColor Green + $tool.Sufficient = $true + } + } else { + Write-Host " ✓ $($tool.Name): $($tool.Version) (⚠ cannot parse version)" -ForegroundColor Yellow + $tool.Sufficient = $true # Assume sufficient if we can't parse + } + } else { + Write-Host " ✓ $($tool.Name): $($tool.Version)" -ForegroundColor Green + $tool.Sufficient = $true + } + } else { + Write-Host " ✗ $($tool.Name): Not installed" -ForegroundColor Red + $tool.Sufficient = $false + } + } + + return $prereqs +} + +function Show-PrerequisitesSummary { + param([hashtable]$Prereqs) + + Write-Host "" + Write-Step "Prerequisites Summary" + + $requiredTools = @("PowerShell", "Git", "VSCode") + $allRequiredInstalled = $true + $allRequiredSufficient = $true + + Write-Host "" + Write-Host "Required Tools:" -ForegroundColor White + + foreach ($toolName in $requiredTools) { + $tool = $Prereqs[$toolName] + if ($tool.Installed -and $tool.Sufficient) { + Write-Success " $($tool.Name): $($tool.Version)" + } elseif ($tool.Installed) { + Write-Warning " $($tool.Name): $($tool.Version) (needs upgrade)" + $allRequiredSufficient = $false + } else { + Write-Error " $($tool.Name): Not installed" + $allRequiredInstalled = $false + $allRequiredSufficient = $false + } + } + + Write-Host "" + Write-Host "Optional Tools:" -ForegroundColor White + + $wt = $Prereqs.WindowsTerminal + if ($wt.Installed) { + Write-Success " $($wt.Name): $($wt.Version)" + } else { + Write-Host " $($wt.Name): Not installed" -ForegroundColor Gray + } + + return @{ + AllRequiredInstalled = $allRequiredInstalled -and $allRequiredSufficient + AnyMissing = -not $allRequiredInstalled + AnyInsufficient = -not $allRequiredSufficient + } +} + +function Get-UserChoice { + param([hashtable]$PrereqStatus) + + Write-Host "" + Write-Step "Choose Your Action" + + if ($PrereqStatus.AllRequiredInstalled) { + Write-Host "All required tools are already installed!" -ForegroundColor Green + Write-Host "" + Write-Host "What would you like to do?" -ForegroundColor White + Write-Host " 1) Just clone the workshop repository" -ForegroundColor Cyan + Write-Host " 2) Install/update missing optional tools" -ForegroundColor Cyan + Write-Host " 3) Reinstall all tools (fresh installation)" -ForegroundColor Yellow + Write-Host "" + + while ($true) { + $choice = Read-Host "Enter your choice (1-3)" + switch ($choice.Trim()) { + "1" { return "CloneOnly" } + "2" { return "OptionalOnly" } + "3" { return "InstallAll" } + default { Write-Host "Please enter 1, 2, or 3" -ForegroundColor Yellow } + } + } + } else { + Write-Host "Some required tools are missing or need updates." -ForegroundColor Yellow + Write-Host "" + Write-Host "What would you like to do?" -ForegroundColor White + Write-Host " 1) Install missing/insufficient tools only" -ForegroundColor Green + Write-Host " 2) Install all required tools" -ForegroundColor Cyan + Write-Host " 3) Just clone the workshop repository (not recommended)" -ForegroundColor Yellow + Write-Host "" + + while ($true) { + $choice = Read-Host "Enter your choice (1-3)" + switch ($choice.Trim()) { + "1" { return "InstallMissing" } + "2" { return "InstallAll" } + "3" { return "CloneOnly" } + default { Write-Host "Please enter 1, 2, or 3" -ForegroundColor Yellow } + } + } + } +} + +#endregion + #region Main Script Write-Host @" @@ -258,18 +440,8 @@ Write-Host @" "@ -ForegroundColor Cyan -Write-Host "This script will install the required tools for the Git Workshop:" -ForegroundColor White -Write-Host " • PowerShell 7 (cross-platform PowerShell)" -ForegroundColor White -Write-Host " • Git 2.23+ (version control system)" -ForegroundColor White -Write-Host " • Visual Studio Code (code editor)" -ForegroundColor White -Write-Host "" -Write-Host "You will be prompted for optional tools:" -ForegroundColor White - -Write-Host " • Windows Terminal (modern terminal experience)" -ForegroundColor White -Write-Host "" - -# Check for winget -Write-Step "Checking Prerequisites" +# Check for winget first +Write-Step "Checking System Requirements" if (-not (Test-WingetAvailable)) { Write-Host "`nInstallation cannot continue without winget." -ForegroundColor Red @@ -278,11 +450,19 @@ if (-not (Test-WingetAvailable)) { Write-Success "winget is available" +# Check system prerequisites +$prereqs = Get-SystemPrerequisites +$prereqStatus = Show-PrerequisitesSummary $prereqs +$userChoice = Get-UserChoice $prereqStatus + +Write-Host "" +Write-Step "User Choice: $userChoice" + # Track installation results $results = @{ - PowerShell = $false - Git = $false - VSCode = $false + PowerShell = $prereqs.PowerShell.Installed -and $prereqs.PowerShell.Sufficient + Git = $prereqs.Git.Installed -and $prereqs.Git.Sufficient + VSCode = $prereqs.VSCode.Installed -and $prereqs.VSCode.Sufficient VSCodeExtensions = $false VSCodePowerShellIntegration = $null # null = not asked, true = configured, false = skipped/failed WindowsTerminal = $null @@ -292,11 +472,6 @@ $results = @{ $totalSteps = 4 # Required installations + extensions $currentStep = 0 -Write-Host "`nStarting installation..." -ForegroundColor Cyan -Write-Host "Note: Some installations may take a few minutes." -ForegroundColor Gray -Write-Host "" - -# Progress bar helper function Write-ProgressIndicator { param( [string]$Activity, @@ -307,6 +482,34 @@ function Write-ProgressIndicator { Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete } +function Should-Install { + param( + [string]$ToolName, + [string]$UserChoice, + [hashtable]$Prereqs + ) + + $tool = $Prereqs[$ToolName] + + switch ($UserChoice) { + "CloneOnly" { + return $false + } + "OptionalOnly" { + return $ToolName -eq "WindowsTerminal" + } + "InstallMissing" { + return -not ($tool.Installed -and $tool.Sufficient) + } + "InstallAll" { + return $true + } + default { + return -not ($tool.Installed -and $tool.Sufficient) + } + } +} + function Install-VSCodeExtension { param( [string]$ExtensionId, @@ -400,87 +603,127 @@ function Set-VSCodePowerShellIntegration { } } -#region Required Installations +#region Installation Based on User Choice -# Install PowerShell 7 -$currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing PowerShell 7 (1/3)" -PercentComplete (($currentStep / $totalSteps) * 100) -$results.PowerShell = Install-Package ` - -Name "PowerShell 7" ` - -WingetId "Microsoft.PowerShell" ` - -CheckCommand "pwsh" - -# Install Git -$currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Git (2/3)" -PercentComplete (($currentStep / $totalSteps) * 100) -$results.Git = Install-Package ` - -Name "Git" ` - -WingetId "Git.Git" ` - -CheckCommand "git" ` - -MinVersion "2.23" ` - -AdditionalArgs "-e" - -# Verify Git version specifically -if ($results.Git) { - if (-not (Test-GitVersion)) { - Write-Warning "Git is installed but version may be below 2.23" - $results.Git = $false - } -} - -# Install Visual Studio Code -$currentStep++ -Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing Visual Studio Code (3/4)" -PercentComplete (($currentStep / $totalSteps) * 100) -$results.VSCode = Install-Package ` - -Name "Visual Studio Code" ` - -WingetId "Microsoft.VisualStudioCode" ` - -CheckCommand "code" - -# Install VSCode Extensions and configure PowerShell integration -if ($results.VSCode) { - $currentStep++ - Write-ProgressIndicator -Activity "Installing Required Tools" -Status "Installing VSCode Extensions (4/4)" -PercentComplete (($currentStep / $totalSteps) * 100) - +if ($userChoice -ne "CloneOnly") { + Write-Host "`nStarting installation based on your choice..." -ForegroundColor Cyan + Write-Host "Note: Some installations may take a few minutes." -ForegroundColor Gray Write-Host "" - Write-Step "Configuring VSCode" - # Install PowerShell extension - $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" + # Calculate steps needed + $neededSteps = 0 + if (Should-Install -ToolName "PowerShell" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } + if (Should-Install -ToolName "Git" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } + if (Should-Install -ToolName "VSCode" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } + if (Should-Install -ToolName "WindowsTerminal" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } + if ($neededSteps -eq 0) { $neededSteps = 1 } # At least for progress bar + if ($prereqs.VSCode.Installed -or Should-Install -ToolName "VSCode" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } # For VSCode extensions + $currentStep = 0 + $totalSteps = $neededSteps + #region Required Installations - # Configure PowerShell 7 integration (optional but recommended) - $powershellIntegrationResult = Set-VSCodePowerShellIntegration - $results.VSCodePowerShellIntegration = $powershellIntegrationResult + # Install PowerShell 7 + if (Should-Install -ToolName "PowerShell" -UserChoice $userChoice -Prereqs $prereqs) { + $currentStep++ + Write-ProgressIndicator -Activity "Installing Tools" -Status "Installing PowerShell 7 ($currentStep/$totalSteps)" -PercentComplete (($currentStep / $totalSteps) * 100) + $results.PowerShell = Install-Package ` + -Name "PowerShell 7" ` + -WingetId "Microsoft.PowerShell" ` + -CheckCommand "pwsh" + } else { + Write-Success "PowerShell 7 already installed and sufficient" + } - $results.VSCodeExtensions = $powershellExtensionResult -} -else { - $results.VSCodeExtensions = $false -} - -# Clear progress bar -Write-Progress -Activity "Installing Required Tools" -Completed - -#endregion - -#region Optional Installations - - - -# Windows Terminal (optional) -Write-Host "" -if (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)") { - Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Windows Terminal" -PercentComplete 50 - $results.WindowsTerminal = Install-Package ` - -Name "Windows Terminal" ` - -WingetId "Microsoft.WindowsTerminal" ` - -CheckCommand "wt" - Write-Progress -Activity "Installing Optional Tools" -Completed -} -else { - Write-Host " Skipping Windows Terminal installation." -ForegroundColor Gray - $results.WindowsTerminal = $null + # Install Git + if (Should-Install -ToolName "Git" -UserChoice $userChoice -Prereqs $prereqs) { + $currentStep++ + Write-ProgressIndicator -Activity "Installing Tools" -Status "Installing Git ($currentStep/$totalSteps)" -PercentComplete (($currentStep / $totalSteps) * 100) + $results.Git = Install-Package ` + -Name "Git" ` + -WingetId "Git.Git" ` + -CheckCommand "git" ` + -MinVersion "2.23" ` + -AdditionalArgs "-e" + + # Verify Git version specifically + if ($results.Git) { + if (-not (Test-GitVersion)) { + Write-Warning "Git is installed but version may be below 2.23" + $results.Git = $false + } + } + } else { + Write-Success "Git already installed and sufficient" + } + + # Install Visual Studio Code + if (Should-Install -ToolName "VSCode" -UserChoice $userChoice -Prereqs $prereqs) { + $currentStep++ + Write-ProgressIndicator -Activity "Installing Tools" -Status "Installing Visual Studio Code ($currentStep/$totalSteps)" -PercentComplete (($currentStep / $totalSteps) * 100) + $results.VSCode = Install-Package ` + -Name "Visual Studio Code" ` + -WingetId "Microsoft.VisualStudioCode" ` + -CheckCommand "code" + } else { + Write-Success "Visual Studio Code already installed" + } + + #endregion + + #region VSCode Extensions and Configuration + + if (($prereqs.VSCode.Installed -or $results.VSCode) -and $userChoice -ne "CloneOnly") { + $currentStep++ + Write-ProgressIndicator -Activity "Installing Tools" -Status "Installing VSCode Extensions ($currentStep/$totalSteps)" -PercentComplete (($currentStep / $totalSteps) * 100) + + Write-Host "" + Write-Step "Configuring VSCode" + + # Install PowerShell extension + $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" + + # Configure PowerShell 7 integration (optional but recommended) + if ($userChoice -eq "InstallAll" -or $userChoice -eq "InstallMissing") { + $powershellIntegrationResult = Set-VSCodePowerShellIntegration + $results.VSCodePowerShellIntegration = $powershellIntegrationResult + } else { + $results.VSCodePowerShellIntegration = $null + } + + $results.VSCodeExtensions = $powershellExtensionResult + } + + # Clear progress bar + Write-Progress -Activity "Installing Tools" -Completed + + #endregion + + #region Optional Installations + + # Windows Terminal (optional) + if (Should-Install -ToolName "WindowsTerminal" -UserChoice $userChoice -Prereqs $prereqs) { + Write-Host "" + if ($userChoice -eq "OptionalOnly" -or (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)")) { + Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Windows Terminal" -PercentComplete 50 + $results.WindowsTerminal = Install-Package ` + -Name "Windows Terminal" ` + -WingetId "Microsoft.WindowsTerminal" ` + -CheckCommand "wt" + Write-Progress -Activity "Installing Optional Tools" -Completed + } else { + Write-Host " Skipping Windows Terminal installation." -ForegroundColor Gray + $results.WindowsTerminal = $null + } + } elseif ($prereqs.WindowsTerminal.Installed) { + Write-Success "Windows Terminal already installed" + } + + #endregion +} else { + Write-Host "`nSkipping tool installation as requested." -ForegroundColor Gray + Write-Host "Proceeding directly to workshop repository setup..." -ForegroundColor Cyan } #endregion @@ -489,7 +732,14 @@ else { Write-Step "Installation Summary" -$allRequired = $results.PowerShell -and $results.Git -and $results.VSCode +# For CloneOnly, check if tools were already installed +if ($userChoice -eq "CloneOnly") { + $allRequired = $prereqs.PowerShell.Installed -and $prereqs.PowerShell.Sufficient -and + $prereqs.Git.Installed -and $prereqs.Git.Sufficient -and + $prereqs.VSCode.Installed -and $prereqs.VSCode.Sufficient +} else { + $allRequired = $results.PowerShell -and $results.Git -and $results.VSCode +} Write-Host "" Write-Host "Required Tools:" -ForegroundColor White From daa787842aa17f371b1db8481e8b1063298be00f Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 10:11:40 +0100 Subject: [PATCH 26/61] fix: remove the statement? --- install.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index 191494a..db30814 100644 --- a/install.ps1 +++ b/install.ps1 @@ -617,7 +617,7 @@ if ($userChoice -ne "CloneOnly") { if (Should-Install -ToolName "VSCode" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } if (Should-Install -ToolName "WindowsTerminal" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } if ($neededSteps -eq 0) { $neededSteps = 1 } # At least for progress bar - if ($prereqs.VSCode.Installed -or Should-Install -ToolName "VSCode" -UserChoice $userChoice -Prereqs $prereqs) { $neededSteps++ } # For VSCode extensions + if ($prereqs.VSCode.Installed -or (Should-Install -ToolName "VSCode" -UserChoice $userChoice -Prereqs $prereqs)) { $neededSteps++ } # For VSCode extensions $currentStep = 0 $totalSteps = $neededSteps From 9e22f84a53974f6985c6ba70a30bbcf8cd79f0c5 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 10:14:15 +0100 Subject: [PATCH 27/61] fix: init of prereqs --- install.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/install.ps1 b/install.ps1 index db30814..a5fde67 100644 --- a/install.ps1 +++ b/install.ps1 @@ -265,6 +265,7 @@ function Get-SystemPrerequisites { Command = "pwsh" MinVersion = "7.0" Name = "PowerShell 7" + Sufficient = $false } Git = @{ Installed = $false @@ -272,6 +273,7 @@ function Get-SystemPrerequisites { Command = "git" MinVersion = "2.23" Name = "Git" + Sufficient = $false } VSCode = @{ Installed = $false @@ -279,6 +281,7 @@ function Get-SystemPrerequisites { Command = "code" MinVersion = $null Name = "Visual Studio Code" + Sufficient = $false } WindowsTerminal = @{ Installed = $false @@ -286,6 +289,7 @@ function Get-SystemPrerequisites { Command = "wt" MinVersion = $null Name = "Windows Terminal" + Sufficient = $false } } From eadf8cfe6a764d205658a0c16ef6b8349b280385 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 10:23:15 +0100 Subject: [PATCH 28/61] fix: version checks --- install.ps1 | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/install.ps1 b/install.ps1 index a5fde67..25f110a 100644 --- a/install.ps1 +++ b/install.ps1 @@ -93,7 +93,10 @@ function Get-InstalledVersion { try { $output = & $Command $VersionArg 2>&1 | Select-Object -First 1 - return $output.ToString().Trim() + if ($null -ne $output) { + return $output.ToString().Trim() + } + return $null } catch { return $null @@ -132,7 +135,10 @@ function Install-Package { # Extract semantic version numbers only - stop before any non-digit/non-dot characters # This extracts "2.52.0" from "2.52.0.windows.1" if ($version -match '(\d+)(?:\.(\d+))?(?:\.(\d+))?') { - $installedVersion = $matches[1] + "." +$matches[2] + $major = $matches[1] + $minor = if ($matches[2]) { $matches[2] } else { "0" } + $patch = if ($matches[3]) { $matches[3] } else { "0" } + $installedVersion = "$major.$minor.$patch" try { if ([version]$installedVersion -lt [version]$MinVersion) { Write-Warning "Version $installedVersion is below minimum required version $MinVersion" @@ -310,11 +316,11 @@ function Get-SystemPrerequisites { $minor = if ($matches[2]) { [int]$matches[2] } else { 0 } $patch = if ($matches[3]) { [int]$matches[3] } else { 0 } $installedVersion = "$major.$minor.$patch" - + $minParts = $tool.MinVersion.Split('.') $minMajor = [int]$minParts[0] - $minMinor = if ($minParts[1]) { [int]$minParts[1] } else { 0 } - $minPatch = if ($minParts[2]) { [int]$minParts[2] } else { 0 } + $minMinor = if ($minParts.Length -gt 1) { [int]$minParts[1] } else { 0 } + $minPatch = if ($minParts.Length -gt 2) { [int]$minParts[2] } else { 0 } $minVersion = "$minMajor.$minMinor.$minPatch" if ([version]$installedVersion -lt [version]$minVersion) { From 7e2f8d64fbef941f89b20ff5a0a8b97f8cd4054a Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 10:29:34 +0100 Subject: [PATCH 29/61] refactor: simplify user choice. Install and clone split --- install.ps1 | 79 ++++++++++++++++++++--------------------------------- 1 file changed, 29 insertions(+), 50 deletions(-) diff --git a/install.ps1 b/install.ps1 index 25f110a..df689b2 100644 --- a/install.ps1 +++ b/install.ps1 @@ -393,45 +393,29 @@ function Show-PrerequisitesSummary { function Get-UserChoice { param([hashtable]$PrereqStatus) - + Write-Host "" Write-Step "Choose Your Action" - + + Write-Host "" + Write-Host "What would you like to do?" -ForegroundColor White + Write-Host " 1) Install/update tools and clone workshop repository" -ForegroundColor Cyan + Write-Host " 2) Just clone the workshop repository (skip tool installation)" -ForegroundColor Yellow + Write-Host "" + if ($PrereqStatus.AllRequiredInstalled) { - Write-Host "All required tools are already installed!" -ForegroundColor Green - Write-Host "" - Write-Host "What would you like to do?" -ForegroundColor White - Write-Host " 1) Just clone the workshop repository" -ForegroundColor Cyan - Write-Host " 2) Install/update missing optional tools" -ForegroundColor Cyan - Write-Host " 3) Reinstall all tools (fresh installation)" -ForegroundColor Yellow - Write-Host "" - - while ($true) { - $choice = Read-Host "Enter your choice (1-3)" - switch ($choice.Trim()) { - "1" { return "CloneOnly" } - "2" { return "OptionalOnly" } - "3" { return "InstallAll" } - default { Write-Host "Please enter 1, 2, or 3" -ForegroundColor Yellow } - } - } + Write-Host "Note: All required tools are already installed" -ForegroundColor Green } else { - Write-Host "Some required tools are missing or need updates." -ForegroundColor Yellow - Write-Host "" - Write-Host "What would you like to do?" -ForegroundColor White - Write-Host " 1) Install missing/insufficient tools only" -ForegroundColor Green - Write-Host " 2) Install all required tools" -ForegroundColor Cyan - Write-Host " 3) Just clone the workshop repository (not recommended)" -ForegroundColor Yellow - Write-Host "" - - while ($true) { - $choice = Read-Host "Enter your choice (1-3)" - switch ($choice.Trim()) { - "1" { return "InstallMissing" } - "2" { return "InstallAll" } - "3" { return "CloneOnly" } - default { Write-Host "Please enter 1, 2, or 3" -ForegroundColor Yellow } - } + Write-Host "Note: Some required tools are missing or need updates" -ForegroundColor Yellow + } + Write-Host "" + + while ($true) { + $choice = Read-Host "Enter your choice (1-2)" + switch ($choice.Trim()) { + "1" { return "InstallTools" } + "2" { return "CloneOnly" } + default { Write-Host "Please enter 1 or 2" -ForegroundColor Yellow } } } } @@ -498,24 +482,19 @@ function Should-Install { [string]$UserChoice, [hashtable]$Prereqs ) - + $tool = $Prereqs[$ToolName] - + switch ($UserChoice) { - "CloneOnly" { - return $false + "CloneOnly" { + return $false } - "OptionalOnly" { - return $ToolName -eq "WindowsTerminal" - } - "InstallMissing" { + "InstallTools" { + # Install only if missing or insufficient (smart install) return -not ($tool.Installed -and $tool.Sufficient) } - "InstallAll" { - return $true - } - default { - return -not ($tool.Installed -and $tool.Sufficient) + default { + return $false } } } @@ -695,7 +674,7 @@ if ($userChoice -ne "CloneOnly") { $powershellExtensionResult = Install-VSCodeExtension -ExtensionId "ms-vscode.PowerShell" -ExtensionName "PowerShell" # Configure PowerShell 7 integration (optional but recommended) - if ($userChoice -eq "InstallAll" -or $userChoice -eq "InstallMissing") { + if ($userChoice -eq "InstallTools") { $powershellIntegrationResult = Set-VSCodePowerShellIntegration $results.VSCodePowerShellIntegration = $powershellIntegrationResult } else { @@ -715,7 +694,7 @@ if ($userChoice -ne "CloneOnly") { # Windows Terminal (optional) if (Should-Install -ToolName "WindowsTerminal" -UserChoice $userChoice -Prereqs $prereqs) { Write-Host "" - if ($userChoice -eq "OptionalOnly" -or (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)")) { + if (Get-UserConfirmation "Do you want to install Windows Terminal? (Highly recommended for better terminal experience)") { Write-ProgressIndicator -Activity "Installing Optional Tools" -Status "Installing Windows Terminal" -PercentComplete 50 $results.WindowsTerminal = Install-Package ` -Name "Windows Terminal" ` From b2b8a2cfffcd6dfd3cf98481a3f7c5d6e2caf003 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 11:45:58 +0100 Subject: [PATCH 30/61] feat: make the steps more explicit --- 01-essentials/01-basics/README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/01-essentials/01-basics/README.md b/01-essentials/01-basics/README.md index f2eac28..c6c39ce 100644 --- a/01-essentials/01-basics/README.md +++ b/01-essentials/01-basics/README.md @@ -29,20 +29,25 @@ Your goal is to commit both `welcome.txt` and `instructions.txt` to a git reposi 1. Navigate into the `challenge` directory: `cd challenge` 2. **Initialize a new git repository**: `git init` (this is your first step!) 3. Check the status of your repository: `git status` -4. Stage the files you want to commit: `git add welcome.txt` (or `git add .` to stage all files) -5. Create a commit: `git commit -m "Your commit message"` -6. Verify both files are committed: `git ls-tree -r HEAD --name-only` +4. Stage the file you want to commit: `git add welcome.txt` (or `git add .` to stage all files) +5. Check the status again and see the difference `git status`. Notice the file is now *staged* and ready to be committed. +6. Create a commit: `git commit -m "add welcome.txt"` +5. Check the status again and see the difference `git status`. Notice the file no longer appears in the output. +7. Stage the next file: `git add instructions.txt` (or `git add .` to stage all files) +8. Check the status again and see the difference `git status`. Notice the file is now *staged* and ready to be committed. +9. Create a commit: `git commit -m "add instructions.txt"` +10. Check the status again and see the difference `git status`. Notice that the files are now not shown in status. If and when you change something about the file you will once again see it in the `git status` command. **Important Notes**: - The challenge directory is NOT a git repository until you run `git init`. This is intentional - you're learning to start from scratch! -- You can commit both files together in one commit, or separately in multiple commits - it's up to you! +- You can commit both files together in one commit, or separately in multiple commits (use `git add .` to add all files in the folder) - it's up to you! - The verification script checks that both files are committed, not the specific commit messages or order ### Key Concepts - **Repository**: A directory tracked by git, containing your project files and their history - **Working Directory**: The files you see and edit -- **Staging Area (Index)**: A preparation area for your next commit +- **Staging Area**: A preparation area for your next commit, you first add the files to the stage, and then you commit the files to repository. - **Commit**: A snapshot of your staged changes ### Useful Commands From 9e03a9624a8a0b172022c55516b79be23d55006c Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:01:07 +0100 Subject: [PATCH 31/61] fix: verification of answers for history --- 01-essentials/02-history/verify.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/01-essentials/02-history/verify.ps1 b/01-essentials/02-history/verify.ps1 index 08b08a6..529af2e 100644 --- a/01-essentials/02-history/verify.ps1 +++ b/01-essentials/02-history/verify.ps1 @@ -42,7 +42,7 @@ if (-not (Test-Path "answers.md")) { $answersLower = $answers.ToLower() # Check 1: Contains "5" or "five" for commit count - if ($answersLower -match "5|five") { + if ($answersLower -match "5|five|fem") { Write-Host "[PASS] Correct commit count found" -ForegroundColor Green } else { Write-Host "[FAIL] Commit count not found or incorrect" -ForegroundColor Red @@ -76,7 +76,7 @@ if (-not (Test-Path "answers.md")) { } # Check 4: Contains "config" keyword for staged file - if ($answersLower -match "config") { + if ($answersLower -match "config.py") { Write-Host "[PASS] Staged file identified" -ForegroundColor Green } else { Write-Host "[FAIL] Staged file not identified" -ForegroundColor Red From 8b3ba808d1839b063c4715159a97dbecad639fa5 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:01:23 +0100 Subject: [PATCH 32/61] feat: add azure devops ssh setup guidelines --- AZURE-DEVOPS-SSH-SETUP.md | 628 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 AZURE-DEVOPS-SSH-SETUP.md diff --git a/AZURE-DEVOPS-SSH-SETUP.md b/AZURE-DEVOPS-SSH-SETUP.md new file mode 100644 index 0000000..81f0326 --- /dev/null +++ b/AZURE-DEVOPS-SSH-SETUP.md @@ -0,0 +1,628 @@ +# Azure DevOps SSH Setup - Best Practices Guide + +This guide provides comprehensive instructions for setting up SSH authentication with Azure DevOps. SSH is the recommended authentication method for secure Git operations. + +## Why SSH is Best Practice + +SSH (Secure Shell) keys provide a secure way to authenticate with Azure DevOps without exposing passwords or tokens. Here's why SSH is the security best practice: + +**Security Benefits:** +- **No Password Exposure**: Your credentials never travel over the network +- **Strong Encryption**: Uses RSA cryptographic algorithms +- **No Credential Prompts**: Seamless authentication after initial setup +- **Better for Automation**: Scripts and CI/CD pipelines benefit from passwordless authentication +- **Revocable**: Individual keys can be removed without changing passwords +- **Auditable**: Track which key was used for each operation + +**Comparison with HTTPS/PAT:** +- HTTPS with Personal Access Tokens (PAT) requires storing tokens, which can be accidentally committed to repositories +- SSH keys separate your authentication (private key stays on your machine) from the service +- SSH connections are faster after initial setup (no token validation on every request) + +--- + +## Prerequisites + +Before starting, ensure you have: + +- **Git 2.23 or higher** installed + ```powershell + git --version + ``` + +- **Azure DevOps account** with access to your organization/project + - If you don't have one, create a free account at [dev.azure.com](https://dev.azure.com) + +- **PowerShell 7+ or Bash terminal** for running commands + ```powershell + pwsh --version + ``` + +--- + +## Step 1: Generate SSH Key Pair + +SSH authentication uses a key pair: a private key (stays on your computer) and a public key (uploaded to Azure DevOps). + +### Generate RSA Key + +Open your terminal and run: + +```powershell +ssh-keygen -t rsa -b 4096 -C "your.email@example.com" +``` + +**Important notes:** +- Replace `your.email@example.com` with your actual email address +- The `-C` flag adds a comment to help identify the key later +- The `-b 4096` flag specifies a 4096-bit key size for enhanced security + +**Note about RSA:** Azure DevOps currently only supports RSA SSH keys. While newer algorithms like Ed25519 offer better security and performance, they are not yet supported by Azure DevOps. See the note at the end of this guide for more information. + +### Save Location + +When prompted for the file location, press `Enter` to accept the default: + +``` +Enter file in which to save the key (/Users/yourname/.ssh/id_rsa): +``` + +**Default locations:** +- **Linux/Mac**: `~/.ssh/id_rsa` +- **Windows**: `C:\Users\YourName\.ssh\id_rsa` + +### Passphrase (Optional but Recommended) + +You'll be prompted to enter a passphrase, just press `Enter` no password is needed: + +``` +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +``` + +**Passphrase pros and cons:** +- **With passphrase**: Extra security layer - even if someone steals your private key, they can't use it without the passphrase +- **Without passphrase**: More convenient - no prompt when pushing/pulling (but less secure if your machine is compromised) + +**Recommendation**: Use a passphrase, especially on laptops or shared machines. + +### Verify Key Generation + +Check that your keys were created: + +**Linux/Mac:** +**Windows PowerShell:** +```powershell +dir $HOME\.ssh\ +``` + +You should see two files: +- `id_rsa` - Private key (NEVER share this) +- `id_rsa.pub` - Public key (safe to share) + +--- + +## Step 2: Add SSH Public Key to Azure DevOps + +Now you'll upload your public key to Azure DevOps. + +### Navigate to SSH Public Keys Settings + +1. Sign in to Azure DevOps at [https://dev.azure.com](https://dev.azure.com) +2. Click your **profile icon** in the top-right corner +3. Select **User settings** from the dropdown menu +4. Click **SSH Public Keys** + +![Azure DevOps - User Settings Menu](./images/azure-devops-user-settings.png) +*Navigate to your user settings by clicking the profile icon in the top-right corner* + +### Add New SSH Key + +5. Click the **+ New Key** button + +![Azure DevOps - Add SSH Public Key Dialog](./images/azure-devops-add-ssh-key.png) +*Click '+ New Key' to begin adding your SSH public key* + +### Copy Your Public Key + +Open your terminal and display your public key: + +**Linux/Mac:** +```bash +cat ~/.ssh/id_rsa.pub +``` + +**Windows PowerShell:** +```powershell +type $HOME\.ssh\id_rsa.pub +``` + +**Windows Command Prompt:** +```cmd +type %USERPROFILE%\.ssh\id_rsa.pub +``` + +The output will look like this: +``` +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2YbXnrSK5TTflZSwUv9KUedvI4p3JJ4dHgwp/SeJGqMNWnOMDbzQQzYT7E39w9Q8ItrdWsK4vRLGY2B1rQ+BpS6nn4KhTanMXLTaUFDlg6I1Yn5S3cTTe8dMAoa14j3CZfoSoRRgK8E+ktNb0o0nBMuZJlLkgEtPIz28fwU1vcHoSK7jFp5KL0pjf37RYZeHkbpI7hdCG2qHtdrC35gzdirYPJOekErF5VFRrLZaIRSSsX0V4XzwY2k1hxM037o/h6qcTLWfi5ugbyrdscL8BmhdGNH4Giwqd1k3MwSyiswRuAuclYv27oKnFVBRT+n649px4g3Vqa8dh014wM2HDjMGENIkHx0hcV9BWdfBfTSCJengmosGW+wQfmaNUo4WpAbwZD73ALNsoLg5Yl1tB6ZZ5mHwLRY3LG2BbQZMZRCELUyvbh8ZsRksNN/2zcS44RIQdObV8/4hcLse30+NQ7GRaMnJeAMRz4Rpzbb02y3w0wNQFp/evj1nN4WTz6l8= your@email.com +``` + +**Copy the entire output** (from `ssh-rsa` to your email address). + +### Paste and Name Your Key + +6. In the Azure DevOps dialog: + - **Name**: Give your key a descriptive name (e.g., "Workshop Laptop 2026", "Home Desktop", "Work MacBook") + - **Public Key Data**: Paste the entire public key you just copied +7. Click **Save** + +![Azure DevOps - SSH Key Added Successfully](./images/azure-devops-ssh-key-success.png) +*Your SSH key has been successfully added and is ready to use* + +**Naming tip**: Use names that help you identify which machine uses each key. This makes it easier to revoke keys later if needed. + +--- + +## Step 3: Configure SSH (Optional but Recommended) + +Create or edit your SSH configuration file to specify which key to use with Azure DevOps. + +### Create/Edit SSH Config File + +**Linux/Mac:** +```bash +mkdir -p ~/.ssh +nano ~/.ssh/config +``` + +**Windows PowerShell:** +```powershell +if (!(Test-Path "$HOME\.ssh")) { New-Item -ItemType Directory -Path "$HOME\.ssh" } +notepad $HOME\.ssh\config +``` + +### Add Azure DevOps Host Configuration + +Add these lines to your `~/.ssh/config` file: + +``` +Host ssh.dev.azure.com + IdentityFile ~/.ssh/id_rsa + IdentitiesOnly yes +``` + +**For Windows users**, use backslashes in the path: +``` +Host ssh.dev.azure.com + IdentityFile C:\Users\YourName\.ssh\id_rsa + IdentitiesOnly yes +``` + +**What this does:** +- `Host ssh.dev.azure.com` - Applies these settings only to Azure DevOps +- `IdentityFile` - Specifies which private key to use (your RSA key) +- `IdentitiesOnly yes` - Prevents SSH from trying other keys + +### Save the Configuration + +Save and close the file: +- **Nano**: Press `Ctrl+X`, then `Y`, then `Enter` +- **Notepad**: Click File → Save, then close + +--- + +## Step 4: Test SSH Connection + +Verify that your SSH key is working correctly. + +### Test Command + +Run this command to test your connection: + +```bash +ssh -T git@ssh.dev.azure.com +``` + +### Expected Output + +**First-time connection** will show a host key verification prompt: + +``` +The authenticity of host 'ssh.dev.azure.com (20.42.134.1)' can't be established. +RSA key fingerprint is SHA256:ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og. +Are you sure you want to continue connecting (yes/no)? +``` + +Type `yes` and press Enter to add Azure DevOps to your known hosts. + +**Successful authentication** will show: + +``` +remote: Shell access is not supported. +shell request failed on channel 0 +``` + +![Azure DevOps - Successful SSH Test](./images/azure-devops-ssh-test-success.png) +*Successful SSH test output showing authenticated connection* + +**This is normal!** Azure DevOps doesn't provide shell access, but this message confirms your SSH key authentication worked. + +### Troubleshooting Connection Issues + +If the connection fails, see the [Troubleshooting section](#troubleshooting) below. + +--- + +## Step 5: Using SSH with Git + +Now that SSH is configured, you can use it for all Git operations. + +### Clone a Repository with SSH + +To clone a repository using SSH: + +```bash +git clone git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} +``` + +**Example** (replace placeholders with your actual values): +```bash +git clone git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project +``` + +**How to find your SSH URL:** +1. Navigate to your repository in Azure DevOps +2. Click **Clone** in the top-right +3. Select **SSH** from the dropdown +4. Copy the SSH URL + +![Azure DevOps - Get SSH Clone URL](./images/azure-devops-clone-ssh.png) +*Select SSH from the clone dialog to get your repository's SSH URL* + +### Convert Existing HTTPS Repository to SSH + +If you already cloned a repository using HTTPS, you can switch it to SSH: + +```bash +cd /path/to/your/repository +git remote set-url origin git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} +``` + +**Verify the change:** +```bash +git remote -v +``` + +You should see SSH URLs: +``` +origin git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project (fetch) +origin git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project (push) +``` + +### Daily Git Operations + +All standard Git commands now work seamlessly with SSH: + +```bash +# Pull latest changes +git pull + +# Push your commits +git push + +# Fetch from remote +git fetch + +# Push a new branch +git push -u origin feature-branch +``` + +**No more credential prompts!** SSH authentication happens automatically. + +--- + +## Troubleshooting + +### Permission Denied (publickey) + +**Error:** +``` +git@ssh.dev.azure.com: Permission denied (publickey). +fatal: Could not read from remote repository. +``` + +**Causes and solutions:** + +1. **SSH key not added to Azure DevOps** + - Go back to [Step 2](#step-2-add-ssh-public-key-to-azure-devops) and verify your public key is uploaded + - Check you copied the **entire** public key (from `ssh-rsa` to your email) + +2. **Wrong private key being used** + - Verify your SSH config file points to the correct key + - Test with: `ssh -vT git@ssh.dev.azure.com` (verbose output shows which keys are tried) + +3. **SSH agent not running** (if you used a passphrase) + - Start the SSH agent: + ```bash + eval "$(ssh-agent -s)" + ssh-add ~/.ssh/id_rsa + ``` + +### Connection Timeout + +**Error:** +``` +ssh: connect to host ssh.dev.azure.com port 22: Connection timed out +``` + +**Causes and solutions:** + +1. **Firewall blocking SSH port (22)** + - Check if your organization's firewall blocks port 22 + - Try using HTTPS as a fallback + +2. **Network restrictions** + - Try from a different network (mobile hotspot, home network) + - Contact your IT department about SSH access + +3. **Proxy configuration** + - If behind a corporate proxy, you may need to configure SSH to use it + - Add to `~/.ssh/config`: + ``` + Host ssh.dev.azure.com + ProxyCommand nc -X connect -x proxy.company.com:3128 %h %p + ``` + +### Host Key Verification Failed + +**Error:** +``` +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! +``` + +**Causes and solutions:** + +1. **Azure DevOps updated their host keys** (rare but happens) + - Check [Azure DevOps SSH key fingerprints](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate#verify-the-host-key-fingerprint) + - If fingerprint matches, remove old key and re-add: + ```bash + ssh-keygen -R ssh.dev.azure.com + ``` + +2. **Man-in-the-middle attack** (security risk!) + - If fingerprint doesn't match Microsoft's published keys, **DO NOT PROCEED** + - Contact your security team + +### SSH Key Not Working After Creation + +**Symptoms:** +- Created key successfully +- Added to Azure DevOps +- Still getting "Permission denied" + +**Solutions:** + +1. **Check file permissions** (Linux/Mac only) + ```bash + chmod 700 ~/.ssh + chmod 600 ~/.ssh/id_rsa + chmod 644 ~/.ssh/id_rsa.pub + ``` + +2. **Verify key format** + - Ensure you copied the **public key** (.pub file) to Azure DevOps, not the private key + - Public key starts with `ssh-rsa` + +3. **Test with verbose output** + ```bash + ssh -vvv git@ssh.dev.azure.com + ``` + - Look for lines like "Offering public key" to see which keys are tried + - Check for "Authentication succeeded" message + +--- + +## Security Best Practices + +Follow these security guidelines to keep your SSH keys safe: + +### Use Passphrase Protection + +**Always use a passphrase for your SSH keys**, especially on: +- Laptops (risk of theft) +- Shared machines +- Devices that leave your office/home + +**How to add a passphrase to an existing key:** +```bash +ssh-keygen -p -f ~/.ssh/id_rsa +``` + +### Never Share Your Private Key + +**Critical security rule:** +- **NEVER** share your private key (`~/.ssh/id_rsa`) +- **NEVER** commit private keys to Git repositories +- **NEVER** send private keys via email or chat + +**Only share:** +- Public key (`~/.ssh/id_rsa.pub`) - This is safe and intended to be shared + +### Use Different Keys for Different Purposes + +Consider creating separate SSH keys for: +- Work projects +- Personal projects +- Different organizations + +**Benefits:** +- Limit blast radius if one key is compromised +- Easier to revoke access to specific services +- Better audit trail + +**Example: Create a work-specific key:** +```bash +ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_work -C "work.email@company.com" +``` + +Then add to `~/.ssh/config`: +``` +Host ssh.dev.azure.com-work + HostName ssh.dev.azure.com + IdentityFile ~/.ssh/id_rsa_work +``` + +### Rotate Keys Periodically + +**Recommended schedule:** +- Personal projects: Annually +- Work projects: Every 6 months +- High-security projects: Every 3 months + +**How to rotate:** +1. Generate new SSH key pair +2. Add new public key to Azure DevOps +3. Test the new key works +4. Remove old public key from Azure DevOps +5. Delete old private key from your machine + +### Revoke Compromised Keys Immediately + +If your private key is exposed: +1. **Immediately** remove the public key from Azure DevOps + - User Settings → SSH Public Keys → Click the key → Delete +2. Generate a new key pair +3. Update all repositories to use the new key + +### Protect Your Private Key File + +Ensure correct file permissions: + +**Linux/Mac:** +```bash +chmod 600 ~/.ssh/id_rsa +``` + +**Windows:** +```powershell +icacls "$HOME\.ssh\id_rsa" /inheritance:r /grant:r "$($env:USERNAME):F" +``` + +### Use SSH Agent Forwarding Carefully + +SSH agent forwarding (`-A` flag) can be convenient but risky: +- Only use with trusted servers +- Prefer ProxyJump instead when possible + +### Enable Two-Factor Authentication (2FA) + +While SSH keys are secure, enable 2FA on your Azure DevOps account for additional security: +1. Azure DevOps → User Settings → Security → Two-factor authentication +2. Use an authenticator app (Microsoft Authenticator, Google Authenticator) + +--- + +## Additional Resources + +- **Azure DevOps SSH Documentation**: [https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate) +- **SSH Key Best Practices**: [https://security.stackexchange.com/questions/tagged/ssh-keys](https://security.stackexchange.com/questions/tagged/ssh-keys) +- **Git with SSH**: [https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key) + +--- + +## Quick Reference + +### Common Commands + +```bash +# Generate RSA key +ssh-keygen -t rsa -b 4096 -C "your.email@example.com" + +# Display public key (Linux/Mac) +cat ~/.ssh/id_rsa.pub + +# Display public key (Windows) +type $HOME\.ssh\id_rsa.pub + +# Test SSH connection +ssh -T git@ssh.dev.azure.com + +# Clone with SSH +git clone git@ssh.dev.azure.com:v3/{org}/{project}/{repo} + +# Convert HTTPS to SSH +git remote set-url origin git@ssh.dev.azure.com:v3/{org}/{project}/{repo} + +# Check remote URL +git remote -v +``` + +### SSH URL Format + +``` +git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} +``` + +**Example:** +``` +git@ssh.dev.azure.com:v3/mycompany/git-workshop/great-print-project +``` + +--- + +## Important Note: RSA and Modern SSH Key Algorithms + +**Why This Guide Uses RSA:** + +This guide exclusively uses RSA keys because **Azure DevOps currently only supports RSA SSH keys**. As of January 2026, Azure DevOps does not support modern SSH key algorithms like Ed25519, ECDSA, or other newer formats. + +**About RSA Security:** + +RSA is an older cryptographic algorithm that has been the industry standard for decades. While RSA with 4096-bit keys (as used in this guide) is still considered secure for most use cases, it has some limitations compared to modern alternatives: + +**RSA Drawbacks:** +- **Larger key sizes**: RSA requires 4096 bits for strong security, resulting in larger keys +- **Slower performance**: Key generation and signature operations are slower than modern algorithms +- **Older cryptographic foundation**: Based on mathematical principles from the 1970s +- **More CPU-intensive**: Authentication operations require more computational resources + +**Modern Alternatives (Not Supported by Azure DevOps):** + +If Azure DevOps supported modern algorithms, we would recommend: + +**Ed25519:** +- **Faster**: Significantly faster key generation and authentication +- **Smaller keys**: 256-bit keys (much smaller than RSA 4096-bit) +- **Modern cryptography**: Based on elliptic curve cryptography (ECC) with strong security guarantees +- **Better performance**: Less CPU usage, faster operations +- **Widely supported**: GitHub, GitLab, Bitbucket, and most modern Git platforms support Ed25519 + +**ECDSA:** +- Also based on elliptic curve cryptography +- Faster than RSA but slightly slower than Ed25519 +- Supported by many platforms + +**Current State:** + +RSA with 4096-bit keys remains secure and is acceptable for Git authentication, despite being outdated compared to modern algorithms. The Azure DevOps team has not provided a timeline for supporting Ed25519 or other modern key types. + +**For Other Platforms:** + +If you're using GitHub, GitLab, Bitbucket, or other Git hosting services, we strongly recommend using Ed25519 instead of RSA: + +```bash +# For platforms that support Ed25519 (GitHub, GitLab, Bitbucket, etc.) +ssh-keygen -t ed25519 -C "your.email@example.com" +``` + +**References:** +- [Ed25519 Wikipedia](https://en.wikipedia.org/wiki/EdDSA#Ed25519) +- [SSH Key Algorithm Comparison](https://security.stackexchange.com/questions/5096/rsa-vs-dsa-for-ssh-authentication-keys) +- [Azure DevOps SSH Documentation](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate) + +--- + +**You're all set!** SSH authentication with RSA keys is now configured for secure, passwordless Git operations with Azure DevOps. From 25077cd5894e1ea0a4367e9590f1894e2e3d0e74 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:02:13 +0100 Subject: [PATCH 33/61] feat: add a simple best practices for when working together --- BEST-PRACTICES.md | 146 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 BEST-PRACTICES.md diff --git a/BEST-PRACTICES.md b/BEST-PRACTICES.md new file mode 100644 index 0000000..bb7ed0b --- /dev/null +++ b/BEST-PRACTICES.md @@ -0,0 +1,146 @@ +# Best Practices for Cloud-Based Git Collaboration + +When multiple people work on the same project, merge conflicts are inevitable. But with good habits, you can dramatically reduce how often they happen and how painful they are to resolve. + +## Pull Early, Pull Often + +The most common cause of merge conflicts is working on outdated code. The longer you work without syncing, the more your code drifts from what others are doing. + +**Do this:** +```bash +# Start every work session by pulling +git pull + +# Pull again before pushing +git pull +git push +``` + +**Why it works:** Small, frequent syncs mean small, manageable conflicts. A conflict in 3 lines is easy to fix. A conflict in 300 lines is a nightmare. + +## Keep Commits Small and Focused + +Large commits that touch many files are conflict magnets. + +**Instead of this:** +``` +"Implemented user authentication, fixed navbar, updated styles, refactored database" +``` + +**Do this:** +``` +"Add login form to auth page" +"Add password validation" +"Connect login form to auth API" +"Add logout button to navbar" +``` + +**Why it works:** +- Smaller changes = smaller chance of overlap with others +- If a conflict does happen, it's easier to understand and resolve +- Easier to review, revert, or cherry-pick specific changes + +## Communicate About Shared Files + +Some files are conflict hotspots because everyone needs to edit them: +- Configuration files +- Route definitions +- Database schemas +- Shared constants or types + +**Do this:** +- Tell your team when you're editing shared files +- Make those changes in dedicated commits +- Push changes to shared files quickly - don't let them sit + +## Use Short-Lived Branches + +Long-running branches drift further from the main branch every day. + +``` +main: A───B───C───D───E───F───G───H + \ +feature: X───────────────────────Y + (2 weeks of drift = painful merge) +``` + +**Better approach:** +``` +main: A───B───C───D───E───F───G───H + \ \ \ +feature: X───────Y───────Z + (merge main frequently) +``` + +**Do this:** +- Merge main into your branch regularly (daily if active) +- Keep features small enough to complete in days, not weeks +- Break large features into smaller incremental changes + +## Organize Code to Minimize Overlap + +How you structure your code affects how often people collide. + +**Conflict-prone structure:** +``` +src/ + app.py # 2000 lines, everyone edits this + utils.py # 500 lines of mixed utilities +``` + +**Better structure:** +``` +src/ + auth/ + login.py + logout.py + users/ + profile.py + settings.py + utils/ + dates.py + strings.py +``` + +**Why it works:** When each file has a clear purpose, different team members naturally work in different files. + +## Avoid Reformatting Wars + +Nothing creates unnecessary conflicts like two people reformatting the same file differently. + +**Do this:** +- Agree on code formatting standards as a team +- Use automatic formatters (Prettier, Black, etc.) +- Configure your editor to format on save +- Run formatters before committing + +**Important:** If you need to reformat a file, do it in a dedicated commit with no other changes. This keeps the reformatting separate from your actual work. + +## Coordinate Large Refactors + +Renaming a widely-used function or moving files around will conflict with almost everyone's work. + +**Do this:** +- Announce refactors to the team before starting +- Do them quickly and push immediately +- Consider doing them when others aren't actively working +- Keep refactoring commits separate from feature work + +## The Golden Rules + +1. **Sync frequently** - Pull before you start, pull before you push +2. **Commit small** - Many small commits beat one large commit +3. **Talk to your team** - A quick message prevents hours of conflict resolution +4. **Stay focused** - One branch = one purpose +5. **Push promptly** - Don't sit on finished work + +## When Conflicts Do Happen + +Even with best practices, conflicts will occur. When they do: + +1. **Don't panic** - Conflicts are normal, not failures +2. **Read carefully** - Understand both sides before choosing +3. **Test after resolving** - Make sure the merged code actually works +4. **Ask if unsure** - If you don't understand the other person's code, ask them + +Remember: merge conflicts are a communication problem as much as a technical one. The best tool for reducing conflicts is talking to your team. From 74a23dbbcaeb8bcd381f14224a1befdca6710a23 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:02:25 +0100 Subject: [PATCH 34/61] feat: add commit message best practices --- COMMIT-MESSAGES.md | 123 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 COMMIT-MESSAGES.md diff --git a/COMMIT-MESSAGES.md b/COMMIT-MESSAGES.md new file mode 100644 index 0000000..09e71ae --- /dev/null +++ b/COMMIT-MESSAGES.md @@ -0,0 +1,123 @@ +# Writing Good Commit Messages + +A good commit message explains **what** the change does and **why** it matters. Your future self (and your teammates) will thank you. + +## The Golden Rule: Write the Intent + +Write your message as a command - what will this commit **do** when applied? + +**Good (imperative, present tense):** +``` +Add login button to navbar +Fix crash when username is empty +Remove unused database connection +``` + +**Avoid (past tense, describing what you did):** +``` +Added login button to navbar +Fixed crash when username is empty +Removed unused database connection +``` + +**Why?** Think of it as completing this sentence: +> "If applied, this commit will... **add login button to navbar**" + +## Use Prefixes to Categorize + +Start your message with a prefix that tells readers what kind of change this is: + +| Prefix | Use For | Example | +|--------|---------|---------| +| `feat:` | New features | `feat: add password reset flow` | +| `fix:` | Bug fixes | `fix: prevent duplicate form submission` | +| `docs:` | Documentation only | `docs: add API examples to README` | +| `style:` | Formatting, no code change | `style: fix indentation in auth module` | +| `refactor:` | Code change that doesn't fix or add | `refactor: extract validation logic` | +| `test:` | Adding or fixing tests | `test: add unit tests for login` | +| `chore:` | Maintenance, dependencies | `chore: update pytest to 8.0` | + +## Keep It Short + +The first line should be **50 characters or less**. This ensures it displays properly in: +- Git log output +- GitHub/Azure DevOps commit lists +- Email notifications + +``` +fix: resolve memory leak in image processing +│ │ +└──────────── 45 characters ─────────────────┘ +``` + +## Add Details When Needed + +For complex changes, add a blank line and then more context: + +``` +fix: prevent crash when user uploads empty file + +The application crashed because we tried to read the first byte +of an empty file. Now we check file size before processing. + +Closes #142 +``` + +Use the body to explain: +- **Why** this change was necessary +- **What** was the problem or context +- **How** does this approach solve it (if not obvious) + +## Examples + +**Simple bug fix:** +``` +fix: correct tax calculation for EU customers +``` + +**New feature:** +``` +feat: add dark mode toggle to settings +``` + +**With context:** +``` +refactor: split user service into smaller modules + +The user service had grown to 800 lines and was handling +authentication, profile management, and notifications. +Split into three focused modules for maintainability. +``` + +**Documentation:** +``` +docs: add setup instructions for Windows +``` + +## Quick Reference + +``` +: + +[optional body: why and how] + +[optional footer: references issues] +``` + +**Checklist:** +- [ ] Starts with a prefix (`feat:`, `fix:`, etc.) +- [ ] Uses imperative mood ("add" not "added") +- [ ] First line under 50 characters +- [ ] Explains why, not just what (for complex changes) + +## Common Mistakes + +| Instead of... | Write... | +|---------------|----------| +| `fixed bug` | `fix: prevent null pointer in search` | +| `updates` | `feat: add email notifications` | +| `WIP` | `feat: add basic form validation` | +| `stuff` | `chore: clean up unused imports` | +| `fix: fix the bug` | `fix: handle empty input in calculator` | + +Your commit history tells the story of your project. Make it a story worth reading. From 5b4c544fee7b19a149ad8e6e98d3700f74bb63da Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:03:12 +0100 Subject: [PATCH 35/61] feat: add a WHAT-IS-GIT file that describes the essentials of what GIT is --- WHAT-IS-GIT.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 WHAT-IS-GIT.md diff --git a/WHAT-IS-GIT.md b/WHAT-IS-GIT.md new file mode 100644 index 0000000..0768e17 --- /dev/null +++ b/WHAT-IS-GIT.md @@ -0,0 +1,124 @@ +# What is Git? + +Git is a tool that tracks changes to your files over time. Think of it as an "undo history" for your entire project that you can browse, search, and share with others. + +## Commits: Snapshots of Your Project + +A **commit** is like taking a photo of your entire project at a specific moment in time. + +Every time you make a commit, Git: +1. Records what **all** your files look like right now +2. Adds a message describing what changed +3. Notes who made the change and when +4. Links back to the previous commit + +### Every Commit Contains Everything + +This is important: each commit is a complete snapshot of **all** files in your project - not just the files you changed. You can check out any commit and see the entire project exactly as it was. + +But wait - doesn't that waste a lot of space? No! Git is clever about this. + +### Unchanged Files Are Reused + +If a file hasn't changed since the last commit, Git doesn't store a new copy. Instead, it simply points to the version it already has: + +``` +Commit 1 Commit 2 Commit 3 +───────── ───────── ───────── +README.md ─────────────────────────────> (same) +app.py ────────> app.py (v2) ───────> (same) +config.json ──────> (same) ────────────> config.json (v3) +``` + +In this example: +- `README.md` never changed - all three commits refer to the same stored version +- `app.py` changed in Commit 2, so a new version was stored +- `config.json` changed in Commit 3, so a new version was stored + +This means: +- Every commit gives you the **complete picture** of your project +- Git only stores **new content** when files actually change +- Going back to any point in history is instant - no need to "replay" changes + +``` +Commit 3 Commit 2 Commit 1 + | | | + v v v +[Add login] <-- [Fix bug] <-- [First version] +``` + +Each commit points back to its parent, creating a chain of history. You can always go back and see exactly what your project looked like at any point. + +## The Magic of Checksums + +Here's where Git gets clever. Every commit gets a unique ID called a **checksum** (or "hash"). It looks like this: + +``` +a1b2c3d4e5f6g7h8i9j0... +``` + +This ID is calculated from the **contents** of the commit - the files, the message, the author, and the parent commit's ID. + +Why does this matter? + +### Verification + +If even one character changes in a file, the checksum becomes completely different. This means: +- Git instantly knows if something has been corrupted or tampered with +- You can trust that what you downloaded is exactly what was uploaded + +### Finding Differences + +When you connect to another copy of the repository, Git compares checksums: + +``` +Your computer: Server: + Commit A Commit A (same checksum = identical) + Commit B Commit B (same checksum = identical) + Commit C (missing) (you have something new!) +``` + +Git doesn't need to compare every file. It just compares the short checksums to instantly know what's different. + +## Distributed: Everyone Has a Full Copy + +Unlike older systems where one central server held all the history, Git is **distributed**. This means: + +- Every person has a complete copy of the entire project history +- You can work offline - commit, browse history, create branches +- If the server disappears, anyone's copy can restore everything +- You sync with others by exchanging commits + +``` + [Alice's Computer] [Bob's Computer] + | | + Full history Full history + All branches All branches + | | + +---- [Shared Server] ------+ + | + Full history + All branches +``` + +When Alice pushes her new commits to the server, Bob can pull them down. The checksums ensure nothing gets lost or corrupted in transit. + +## Putting It All Together + +1. **You work** - Edit files, create new ones, delete old ones +2. **You commit** - Take a snapshot with a descriptive message +3. **Git calculates** - Creates a unique checksum for this commit +4. **You push** - Send your commits to a shared server +5. **Others pull** - Download your commits using checksums to verify +6. **History grows** - The chain of commits gets longer + +That's it! Git is essentially a distributed database of snapshots, connected together and verified by checksums. Everything else - branches, merges, rebasing - builds on these simple ideas. + +## Key Takeaways + +- **Commit** = A snapshot of your project at one moment +- **Checksum** = A unique fingerprint calculated from the content +- **Distributed** = Everyone has a full copy, not just the server +- **History** = A chain of commits, each pointing to its parent + +You don't need to understand every detail to use Git effectively. Just remember: commit often, write clear messages, and sync with your team regularly. From 76c418218653926c1b33a8f751e986ecbec213db Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:03:50 +0100 Subject: [PATCH 36/61] feat: update README to include SSH guidelines --- README.md | 76 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 17fcb47..7dba45a 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,15 @@ Advanced Git workflows for power users: ### For Module 08: Multiplayer Git -**This module is different!** It uses a real Git server for authentic collaboration: +**This module is different!** It uses Azure DevOps for authentic cloud-based collaboration: 1. Navigate to `01-essentials/08-multiplayer` 2. Read the `README.md` for complete instructions -3. **No setup script** - you'll clone from https://git.frod.dk/multiplayer +3. **No setup script** - you'll clone from Azure DevOps (URL provided by facilitator) 4. Work with a partner on shared branches 5. Experience real merge conflicts and pull requests -6. **No verify script** - success is visual (your code appears in the final output) +6. Use SSH keys for secure authentication (best practice) +7. **No verify script** - success is visual (your code appears in the final output) **Facilitators**: See `01-essentials/08-multiplayer/FACILITATOR-SETUP.md` for server setup and workshop guidance. @@ -285,7 +286,8 @@ By completing this workshop, you'll be able to: - ✅ **Collaborate with teammates on shared repositories** - ✅ **Resolve real merge conflicts in a team environment** - ✅ **Create and review pull requests** -- ✅ **Use Git on a real cloud server (Gitea)** +- ✅ **Use Git on a real cloud server (Azure DevOps)** +- ✅ **Use SSH keys for secure Git authentication** ### From Advanced Track: - ✅ Rebase to maintain clean, linear history @@ -336,37 +338,46 @@ The workshop format combines instructor-led sessions with self-paced hands-on mo ### Setting Up Module 08: Multiplayer Git -Module 08 requires a Git server for authentic collaboration. You have two options: +Module 08 requires a Git server for authentic collaboration using **Azure DevOps**. -**Option 1: Self-Hosted Gitea Server (Recommended)** +**Azure DevOps Setup** -Run your own Git server with Gitea using Docker and Cloudflare Tunnel: +Use Azure DevOps as the cloud-based Git platform for this module: **Benefits:** -- 💰 Completely free (no cloud costs) -- 🔒 Full control over your data -- 🌐 Accessible from anywhere via Cloudflare Tunnel -- 🚀 Quick setup with Docker Compose -- 👥 Perfect for workshops with 2-24 students +- 💰 Free tier supports up to 5 users with full access +- 🌐 Cloud-hosted - no server maintenance required +- 🔒 Enterprise-grade security and reliability +- 🔑 Built-in SSH key support (industry best practice) +- 👥 Perfect for workshops with any number of students (use Stakeholder licenses for >5 users) +- 📊 Built-in pull request workflows and code review tools -**Setup:** -1. See [GITEA-SETUP.md](GITEA-SETUP.md) for complete Gitea + Docker + Cloudflare Tunnel instructions -2. See `01-essentials/08-multiplayer/FACILITATOR-SETUP.md` for detailed workshop preparation: - - Creating student accounts - - Setting up The Great Print Project repository - - Pairing students - - Monitoring progress - - Troubleshooting common issues +**Setup Steps:** -**Option 2: Azure DevOps / GitHub / GitLab** +1. **Create Azure DevOps Organization** (if you don't have one): + - Sign up at [dev.azure.com](https://dev.azure.com) with a Microsoft account + - Create a new organization for your workshop -You can also use existing cloud Git platforms: -- Create organization/group for the workshop -- Set up repository with starter code (see facilitator guide) -- Create user accounts for students -- Configure permissions +2. **Set up SSH authentication** (recommended for all users): + - See [AZURE-DEVOPS-SSH-SETUP.md](AZURE-DEVOPS-SSH-SETUP.md) for complete SSH key setup instructions + - SSH provides secure, passwordless authentication (industry standard) -**Both options work - Gitea gives you more control and is free for any number of students.** +3. **Configure workshop repository and users**: + - See `01-essentials/08-multiplayer/FACILITATOR-SETUP.md` for detailed workshop preparation: + - Adding student accounts to Azure DevOps + - Creating The Great Print Project repository + - Configuring branch policies + - Pairing students + - Monitoring progress + - Troubleshooting SSH and authentication issues + +**Alternative: GitHub / GitLab / Bitbucket** + +While this workshop uses Azure DevOps, the skills learned apply to any Git platform: +- The workflow is identical across all platforms +- SSH authentication works the same way everywhere +- Pull request concepts transfer directly +- Students can apply these skills to any Git hosting service --- @@ -380,7 +391,7 @@ git-workshop/ ├── GIT-CHEATSHEET.md # Quick reference for all Git commands ├── WORKSHOP-AGENDA.md # Facilitator guide for running workshops ├── PRESENTATION-OUTLINE.md # Slide deck outline -├── GITEA-SETUP.md # Self-hosted Git server setup +├── AZURE-DEVOPS-SSH-SETUP.md # SSH authentication best practices for Azure DevOps ├── install-glow.ps1 # Install glow markdown renderer │ ├── 01-essentials/ # Core Git skills (8 modules) @@ -406,15 +417,16 @@ git-workshop/ ## What's Unique About This Workshop -### The Great Print Project (Module 07) +### The Great Print Project (Module 08) Unlike any other Git tutorial, Module 08 provides **real collaborative experience**: -- **Real Git server**: Not simulated - actual cloud repository at https://git.frod.dk/multiplayer +- **Real Git server**: Not simulated - actual Azure DevOps cloud repository - **Real teammates**: Work in pairs on shared branches - **Real conflicts**: Both partners edit the same code and must resolve conflicts together - **Real pull requests**: Create PRs, review code, merge to main - **Real success**: When all pairs merge, run `python main.py` and see everyone's contributions! +- **Real security**: Use SSH keys for authentication (industry best practice) **The challenge**: Each pair implements 3 Python functions (e.g., `print_b()`, `print_c()`, `print_d()`) in a shared repository. When complete, the program prints the alphabet A-Z and numbers 0-9. @@ -459,8 +471,8 @@ A: Absolutely! See the "For Workshop Facilitators" section above. The materials **Q: Do I need internet access?** A: Modules 01-07 work completely offline. Module 08 requires internet to access the Git server. -**Q: What if I prefer GitHub/GitLab instead of Gitea?** -A: The skills are identical across all Git platforms. Module 08 uses Gitea but everything you learn applies to GitHub, GitLab, Bitbucket, etc. +**Q: What if I prefer GitHub/GitLab instead of Azure DevOps?** +A: The skills are identical across all Git platforms. Module 08 uses Azure DevOps but everything you learn applies directly to GitHub, GitLab, Bitbucket, and any other Git hosting service. The SSH authentication, pull request workflow, and collaboration patterns are the same everywhere. --- From ea5cbccc75a65c544f4c984fc73d57e4996ed4b8 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:04:20 +0100 Subject: [PATCH 37/61] refactor: simplify the multiplayer part of Git --- .../08-multiplayer/01_FACILITATOR.md | 254 +++ 01-essentials/08-multiplayer/02_README.md | 1303 +++++++++++++++ 01-essentials/08-multiplayer/03_TASKS.md | 395 +++++ .../08-multiplayer/FACILITATOR-SETUP.md | 904 ----------- 01-essentials/08-multiplayer/README.md | 1408 ----------------- 5 files changed, 1952 insertions(+), 2312 deletions(-) create mode 100644 01-essentials/08-multiplayer/01_FACILITATOR.md create mode 100644 01-essentials/08-multiplayer/02_README.md create mode 100644 01-essentials/08-multiplayer/03_TASKS.md delete mode 100644 01-essentials/08-multiplayer/FACILITATOR-SETUP.md delete mode 100644 01-essentials/08-multiplayer/README.md diff --git a/01-essentials/08-multiplayer/01_FACILITATOR.md b/01-essentials/08-multiplayer/01_FACILITATOR.md new file mode 100644 index 0000000..5f76172 --- /dev/null +++ b/01-essentials/08-multiplayer/01_FACILITATOR.md @@ -0,0 +1,254 @@ +# Facilitator Setup Guide + +This guide helps workshop facilitators set up the cloud-based multiplayer Git module using Azure DevOps. + +## Overview + +The Number Challenge is a collaborative Git exercise where students work together on a shared repository hosted on **Azure DevOps**. + +**What participants will do:** +- Clone a real repository from Azure DevOps +- Collaborate to sort numbers 0-20 into the correct order +- Experience push/pull workflow and merge conflicts +- Learn to communicate and coordinate with teammates +- Use SSH keys for secure authentication + +--- + +## Prerequisites + +### Azure DevOps Setup + +You need: +- **Azure DevOps Organization** - Free tier is sufficient + - Sign up at [dev.azure.com](https://dev.azure.com) +- **Project created** within your organization +- **Admin access** to create repositories and manage users + +### Workshop Materials + +Participants need: +- Git installed (version 2.23+) +- VS Code (or any text editor) +- SSH keys configured + +--- + +## Pre-Workshop Setup + +### Step 1: Add User Accounts + +Add workshop participants to your Azure DevOps organization. + +1. Navigate to **Organization Settings** → **Users** +2. Click **Add users** +3. Enter participant email addresses (Microsoft accounts) +4. Select your workshop project +5. Select **Access level**: Stakeholder (free) or Basic +6. Click **Add** + +### Step 2: Create the Repository + +Create the shared repository: **number-challenge** + +1. Sign in to Azure DevOps at [dev.azure.com](https://dev.azure.com) +2. Navigate to your **Project** +3. Click **Repos** in the left navigation +4. Click the repo dropdown → **New repository** +5. Fill in details: + - **Name:** `number-challenge` + - **Add a README:** Checked +6. Click **Create** + +### Step 3: Add the Starter File + +Create `numbers.txt` with numbers 0-20 in random order. + +**Option A: Via Azure DevOps web UI** + +1. In your repository, click **+ New** → **File** +2. Name it `numbers.txt` +3. Add this content (numbers 0-20 shuffled): + +``` +17 +3 +12 +8 +19 +1 +14 +6 +11 +0 +20 +9 +4 +16 +2 +18 +7 +13 +5 +15 +10 +``` + +4. Click **Commit** + +**Option B: Via command line** + +```powershell +git clone git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge +cd number-challenge + +# Create numbers.txt with shuffled numbers +@" +17 +3 +12 +8 +19 +1 +14 +6 +11 +0 +20 +9 +4 +16 +2 +18 +7 +13 +5 +15 +10 +"@ | Out-File -FilePath numbers.txt -Encoding UTF8 + +git add numbers.txt +git commit -m "feat: add shuffled numbers for challenge" +git push +``` + +### Step 4: Verify Student Access + +Students added to the project automatically have access. Verify: + +1. Go to **Project Settings** → **Repositories** → **number-challenge** +2. Click **Security** tab +3. Verify project team has **Contribute** permission + +--- + +## During the Workshop + +### Getting Started + +1. Ensure all students have cloned the repository +2. Have everyone open `numbers.txt` to see the shuffled numbers +3. Explain the goal: sort numbers 0-20 into correct order + +### The Exercise Flow + +1. **Students pull** the latest changes +2. **One person** moves a number to its correct position +3. **They commit and push** +4. **Others pull** and see the change +5. **Repeat** until sorted + +### Creating Conflicts (The Learning Moment) + +Conflicts happen naturally when multiple people edit at once. You can encourage this: + +- Have two students deliberately edit at the same time +- Watch them experience the push rejection +- Guide them through pulling and resolving the conflict + +### Monitoring Progress + +Check progress in Azure DevOps: + +- **Repos → Commits**: See who's contributing +- **Repos → Files → numbers.txt**: See current state + +### Common Issues + +**"I can't push!"** +- Did they pull first? Run `git pull` +- Is SSH set up? Check with `ssh -T git@ssh.dev.azure.com` + +**"Merge conflict!"** +- Walk them through removing conflict markers +- Help them understand both sides of the conflict + +**"Numbers are duplicated/missing!"** +- Someone resolved a conflict incorrectly +- Have the team review and fix together + +--- + +## Success + +When complete, `numbers.txt` should contain: + +``` +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +``` + +Celebrate the team's success! + +--- + +## Post-Workshop Cleanup + +To reuse the repository: + +1. Reset `numbers.txt` to shuffled state +2. Or delete and recreate the repository + +--- + +## Tips + +- **Keep groups small** (4-8 people) for more interaction +- **Encourage communication** - the exercise works best when people talk +- **Let conflicts happen** - they're the best learning opportunity +- **Walk the room** - help students who get stuck +- **Point students to 03_TASKS.md** - Simple explanations of clone, push, pull, and fetch for beginners + +--- + +## Troubleshooting + +### SSH Issues +- Verify SSH key added to Azure DevOps (User Settings → SSH Public Keys) +- Test: `ssh -T git@ssh.dev.azure.com` + +### Permission Issues +- Check user is added to project +- Verify Contribute permission on repository + +### Service Issues +- Check status: https://status.dev.azure.com diff --git a/01-essentials/08-multiplayer/02_README.md b/01-essentials/08-multiplayer/02_README.md new file mode 100644 index 0000000..ff8a010 --- /dev/null +++ b/01-essentials/08-multiplayer/02_README.md @@ -0,0 +1,1303 @@ +# Module 08: Multiplayer Git - The Number Challenge + +## Learning Objectives + +By the end of this module, you will: +- Clone and work with remote repositories on a cloud server +- Collaborate with teammates using a shared repository +- Experience push rejections when your local repository is out of sync +- Resolve merge conflicts in a real team environment +- Practice the fundamental push/pull workflow +- Apply all the Git skills you've learned in a collaborative setting + +## Welcome to Real Collaboration! + +Congratulations on making it this far! You've learned Git basics: committing, branching, merging, and even resolving conflicts solo. But here's where it gets real - **working with actual teammates on a shared codebase**. + +This module is different from all the others. There's no `setup.ps1` script creating a simulated environment. Instead, you'll work with: +- A real Git server: **Azure DevOps** (your facilitator will provide the specific URL) +- Real teammates +- A shared repository where everyone works together +- Real merge conflicts when multiple people edit the same file +- Real push rejections when your local repository falls out of sync + +**This is exactly how professional developers collaborate every day on GitHub, GitLab, Bitbucket, Azure DevOps, and company Git servers.** + +**New to remote Git commands?** Check out [GIT-BASICS.md](./GIT-BASICS.md) for simple explanations of clone, push, pull, and fetch! + +Ready? Let's collaborate! + +--- + +## The Number Challenge + +### What You'll Do + +Your team will work together to sort a jumbled list of numbers (0-20) into the correct order. The repository contains a file called `numbers.txt` with numbers in random order: + +``` +17 +3 +12 +8 +19 +... +``` + +**Your goal:** Work as a team to rearrange the numbers so they appear in order from 0 to 20: + +``` +0 +1 +2 +3 +4 +... +20 +``` + +**The rules:** +- Each person moves **ONE number per commit** +- You **MUST pull before making changes** to get the latest version +- **Communicate with your team** - coordination is key! + +**The challenge:** You'll experience merge conflicts when two people edit the file at the same time, and push rejections when your local copy is out of sync with the server. + +### Why This Exercise? + +This exercise teaches collaboration in a safe, structured way: + +1. **Simple task:** Moving a number is easy. The hard part is Git, not the work itself. +2. **Clear success:** You can instantly see when all numbers are sorted. +3. **Guaranteed conflicts:** Multiple people editing the same file creates conflicts to practice resolving. +4. **Push rejections:** You'll experience what happens when your database goes out of sync with the remote. +5. **Team coordination:** Success requires communication and collaboration. +6. **Safe experimentation:** It's okay to make mistakes - you can always pull a fresh copy! + +### Repository Structure + +``` +number-challenge/ +├── numbers.txt # The file everyone edits - contains numbers 0-20 +└── README.md # Quick reference for the challenge +``` + +**Note:** The repository is already set up on the server. You'll clone it and start collaborating! + +--- + +## Prerequisites + +Before starting, ensure you have: + +### 1. Your Azure DevOps Account + +Your facilitator will provide: +- **Organization and Project URLs** for the workshop +- **Azure DevOps account credentials** (Microsoft Account or Azure AD) +- **SSH Key Setup** (Recommended - see below) + +**First-time setup:** Visit the Azure DevOps URL provided by your facilitator and sign in to verify your account works. + +### 2. Git Configuration + +Verify your Git identity is configured: + +```bash +git config --global user.name +git config --global user.email +``` + +If these are empty, set them now: + +```bash +git config --global user.name "Your Name" +git config --global user.email "your.email@example.com" +``` + +**Why this matters:** Every commit you make will be tagged with this information. + +### 3. Authentication Setup: SSH Keys (Recommended) + +**SSH is the best practice for secure Git authentication.** It provides secure, passwordless access to Azure DevOps without exposing credentials. + +#### Quick SSH Setup + +**If you haven't set up SSH keys yet, follow these steps:** + +1. **Generate SSH key:** + ```bash + ssh-keygen -t rsa -b 4096 -C "your.email@example.com" + ``` + Press Enter to accept default location, optionally add a passphrase for extra security. + + **Note:** Azure DevOps requires RSA keys. See [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md) for details on why we use RSA. + +2. **Copy your public key:** + + **Linux/Mac:** + ```bash + cat ~/.ssh/id_rsa.pub + ``` + + **Windows PowerShell:** + ```powershell + type $HOME\.ssh\id_rsa.pub + ``` + +3. **Add to Azure DevOps:** + - Sign in to Azure DevOps + - Click your profile icon (top-right) → **User settings** + - Select **SSH Public Keys** + - Click **+ New Key** + - Paste your public key and give it a name (e.g., "Workshop Laptop 2026") + - Click **Save** + + ![Azure DevOps - SSH Public Keys](./images/azure-devops-ssh-keys.png) + *Navigate to User Settings → SSH Public Keys to add your SSH key* + +4. **Test your SSH connection:** + ```bash + ssh -T git@ssh.dev.azure.com + ``` + + Expected output: `remote: Shell access is not supported.` - This is normal and means authentication worked! + +**For detailed SSH setup instructions including troubleshooting, see:** [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md) + +#### Alternative: HTTPS with Personal Access Token (PAT) + +If you cannot use SSH (firewall restrictions, etc.), you can use HTTPS with a Personal Access Token: + +1. Sign in to Azure DevOps +2. Click your profile icon → **Personal access tokens** +3. Click **+ New Token** +4. Give it a name, set expiration, and select **Code (Read & Write)** scope +5. Click **Create** and **copy the token** (you won't see it again!) +6. Use the token as your password when Git prompts for credentials + +**Note:** SSH is recommended for security and convenience. With SSH, you won't need to enter credentials for every push/pull. + +--- + +## Part 1: Getting Started (15 minutes) + +### Step 1: Get Ready + +Your facilitator will explain the exercise. Everyone on the team will work together on the same repository. + +**Important:** Everyone will work on the **same branch (`main`)**. This simulates real team development where multiple developers collaborate on a shared codebase. + +### Step 2: Understand the Exercise + +Your team will collaborate to sort the numbers in `numbers.txt` from 0 to 20. + +**The approach:** +- Everyone works on the same file (`numbers.txt`) +- Everyone works on the same branch (`main`) +- Each person moves one number per commit +- Communication is key to avoid too many conflicts! + +### Step 3: Clone the Repository + +Your facilitator will provide the exact repository URL. The format depends on your authentication method: + +**Using SSH (Recommended):** + +```bash +# Replace {organization}, {project}, and {repository} with values from your facilitator +git clone git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge +cd number-challenge +``` + +**Example:** +```bash +git clone git@ssh.dev.azure.com:v3/workshoporg/git-workshop/number-challenge +cd number-challenge +``` + +**Using HTTPS (with PAT):** + +```bash +# Replace {organization} and {project} with values from your facilitator +git clone https://dev.azure.com/{organization}/{project}/_git/number-challenge +cd number-challenge +``` + +**Note:** Use the exact URL provided by your facilitator to ensure you're cloning the correct repository. + +**Expected output:** + +``` +Cloning into 'number-challenge'... +remote: Enumerating objects: 10, done. +remote: Counting objects: 100% (10/10), done. +remote: Compressing objects: 100% (7/7), done. +remote: Total 10 (delta 2), reused 0 (delta 0), pack-reused 0 +Receiving objects: 100% (10/10), done. +Resolving deltas: 100% (2/2), done. +``` + +Success! You now have a local copy of the shared repository. + +### Step 4: Explore the Repository + +Let's see what we're working with: + +```bash +# List files +ls -la + +# View the numbers file +cat numbers.txt +``` + +**What you'll see in `numbers.txt`:** + +``` +17 +3 +12 +8 +19 +1 +14 +6 +11 +0 +20 +9 +4 +16 +2 +18 +7 +13 +5 +15 +10 +``` + +The numbers are all jumbled up! Your team's goal is to sort them from 0 to 20. + +### Step 5: Understanding the Workflow + +**For this exercise, everyone works on the `main` branch together.** + +Unlike typical Git workflows where you create feature branches, this exercise intentionally has everyone work on the same branch to experience: +- **Push rejections** when someone else pushed before you +- **Merge conflicts** when two people edit the same line +- **The pull-before-push cycle** that's fundamental to Git collaboration + +**There's no need to create a branch** - you'll work directly on `main` after cloning. + +--- + +## Part 2: Your First Contribution (20 minutes) + +Now you'll practice the basic collaborative workflow: make a change, commit, push, and help others pull your changes. + +### Step 1: Decide Who Goes First + +Pick someone to go first. They'll move one number to its correct position. + +### Step 2: First Person - Move ONE Number + +**First person:** Open `numbers.txt` in your text editor. + +Look at the file and find a number that's in the wrong position. Let's say you decide to move the `0` to the top. + +**Before:** +``` +17 +3 +12 +8 +19 +1 +14 +6 +11 +0 ← Let's move this to the top! +20 +... +``` + +**After editing:** +``` +0 ← Moved to the top! +17 +3 +12 +8 +19 +1 +14 +6 +11 +20 +... +``` + +**Key point:** Move ONLY ONE number per commit. This keeps things simple and helps everyone track changes. + +### Step 3: Commit Your Change + +```bash +git status +# You should see: modified: numbers.txt + +git add numbers.txt +git commit -m "Move 0 to its correct position" +``` + +**Expected output:** + +``` +[main abc1234] Move 0 to its correct position + 1 file changed, 1 insertion(+), 1 deletion(-) +``` + +### Step 4: Push to Remote + +This uploads your commit to the shared server: + +```bash +git push origin main +``` + +**Expected output:** + +``` +Enumerating objects: 5, done. +Counting objects: 100% (5/5), done. +Delta compression using up to 8 threads +Compressing objects: 100% (3/3), done. +Writing objects: 100% (3/3), 345 bytes | 345.00 KiB/s, done. +Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 +remote: Analyzing objects... (100%) (3/3) (X ms) +remote: Storing packfile... done (X ms) +remote: Storing index... done (X ms) +To ssh.dev.azure.com:v3/{organization}/{project}/number-challenge + abc1234..def5678 main -> main +``` + +Success! Your change is now on the server for everyone to see. + +### Step 5: Others - Pull the Change + +**Everyone else:** Make sure you get the latest changes: + +```bash +# Pull the changes from the server +git pull origin main +``` + +**Expected output:** + +``` +From ssh.dev.azure.com:v3/{organization}/{project}/number-challenge + * branch main -> FETCH_HEAD +Updating 123abc..456def +Fast-forward + numbers.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) +``` + +**Verify you have the update:** + +```bash +cat numbers.txt +# Should show 0 at the top +``` + +You now have the first person's change! + +### Step 6: Next Person's Turn + +**Next person:** Now it's your turn! Pick a different number and move it to its correct position. + +Follow the same cycle: +1. Pull first: `git pull origin main` (always get the latest!) +2. Edit `numbers.txt` - move ONE number +3. Commit: `git add numbers.txt && git commit -m "Move X to correct position"` +4. Push: `git push origin main` +5. Tell the team you pushed! + +### Step 7: Keep Taking Turns + +Continue this pattern: +- **Always pull before editing** +- Move one number per person +- Commit with a clear message +- Push to share with the team +- Communicate when you've pushed + +**As you work, the file gradually becomes more sorted:** + +``` +After a few rounds: +0 +1 +2 +3 +17 ← Still needs to be moved +12 ← Still needs to be moved +... +``` + +**Congratulations! You've completed your first collaborative Git workflow!** You've learned the core cycle: pull → work → commit → push. This is what professional developers do hundreds of times per day. + +--- + +## Part 3: Deliberate Conflict Exercise (30 minutes) + +Now for the **real** learning: merge conflicts! You'll deliberately create a conflict, then resolve it together. + +### The Scenario + +Merge conflicts happen when two people edit the same lines in the same file. Git can't automatically decide which version to keep, so it asks you to resolve it manually. + +**What you'll do:** +1. Two people will BOTH edit the same line in `numbers.txt` +2. Person A pushes first (succeeds) +3. Person B tries to push (gets rejected!) +4. Person B pulls (sees conflict markers) +5. You resolve the conflict together +6. Person B pushes the resolution + +This is a **deliberate practice** scenario. In real projects, conflicts happen by accident - now you'll know how to handle them! + +### Setup: Choose Two People + +Pick two people to create the conflict. Let's call them **Person A** and **Person B**. + +Everyone else can watch and learn - you'll create your own conflicts later! + +### Step 1: Both People Start Fresh + +Make sure you both have the latest code: + +```bash +git pull origin main + +# Check status - should be clean +git status +``` + +Both people should see: "Your branch is up to date with 'origin/main'" and "nothing to commit, working tree clean" + +### Step 2: Person A - Make Your Change First + +**Person A:** Open `numbers.txt` and move a specific number. Let's say you decide to move `17` down a few lines. + +**Before:** +``` +17 ← Let's move this +3 +12 +8 +``` + +**After (Person A's version):** +``` +3 +17 ← Moved here +12 +8 +``` + +**Commit and push IMMEDIATELY:** + +```bash +git add numbers.txt +git commit -m "Person A: Move 17 down" +git push origin main +``` + +Person A should see the push succeed. + +### Step 3: Person B - Make DIFFERENT Change (DON'T PULL YET!) + +**Person B:** This is critical - do NOT pull Person A's changes yet! + +Instead, edit the SAME lines with a DIFFERENT change. Move `17` to a different position: + +**Before:** +``` +17 ← Let's move this somewhere else +3 +12 +8 +``` + +**After (Person B's version):** +``` +3 +12 +17 ← Moved to a different position than Person A! +8 +``` + +**Commit (but don't push yet):** + +```bash +git add numbers.txt +git commit -m "Person B: Move 17 down" +``` + +### Step 4: Person B - Try to Push (This Will Fail!) + +```bash +git push origin main +``` + +**You'll see an error like this:** + +``` +To ssh.dev.azure.com:v3/{organization}/{project}/number-challenge + ! [rejected] main -> main (fetch first) +error: failed to push some refs to 'git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge' +hint: Updates were rejected because the remote contains work that you do +hint: not have locally. This is usually caused by another repository pushing +hint: to the same ref. You may want to first integrate the remote changes +hint: (e.g., 'git pull ...') before pushing again. +hint: See the 'Note about fast-forwards' in 'git push --help' for details. +``` + +**Don't panic!** This is completely normal and expected. Git is protecting you from overwriting Person A's work. + +**What happened:** Person A pushed commits that you don't have. Git requires you to pull first and integrate their changes before you can push yours. + +### Step 5: Person B - Pull and See the Conflict + +```bash +git pull origin main +``` + +**You'll see:** + +``` +From ssh.dev.azure.com:v3/{organization}/{project}/number-challenge + * branch main -> FETCH_HEAD +Auto-merging numbers.txt +CONFLICT (content): Merge conflict in numbers.txt +Automatic merge failed; fix conflicts and then commit the result. +``` + +**This is a merge conflict!** Git tried to merge Person A's changes with yours, but couldn't automatically combine them because you both edited the same lines. + +### Step 6: Check Git Status + +```bash +git status +``` + +**Output:** + +``` +On branch main +You have unmerged paths. + (fix conflicts and run "git commit") + (use "git merge --abort" to abort the merge) + +Unmerged paths: + (use "git add ..." to mark resolution) + both modified: numbers.txt + +no changes added to commit (use "git add" and/or "git commit -a") +``` + +Git is telling you: "The file `numbers.txt` has conflicts. Both of you modified it. Please resolve and commit." + +### Step 7: Open the Conflicted File + +```bash +cat numbers.txt +# Or open in your text editor: code numbers.txt, vim numbers.txt, nano numbers.txt +``` + +**Find the conflict markers:** + +``` +3 +<<<<<<< HEAD +12 +17 +======= +17 +12 +>>>>>>> abc1234567890abcdef1234567890abcdef12 +8 +``` + +### Understanding Conflict Markers + +``` +<<<<<<< HEAD # Start marker +12 # YOUR version (Person B's order) +17 +======= # Divider +17 # THEIR version (Person A's order from remote) +12 +>>>>>>> abc1234... # End marker (shows commit hash) +``` + +**The three sections:** +1. `<<<<<<< HEAD` to `=======`: Your current changes (what you committed locally) +2. `=======` to `>>>>>>>`: Their changes (what Person A pushed to the server) +3. You must choose one, combine them, or write something new + +### Step 8: Resolve the Conflict TOGETHER + +**Talk with the group!** Look at both versions and decide which order makes sense. + +**Person A's version:** +``` +3 +17 +12 +8 +``` + +**Person B's version:** +``` +3 +12 +17 +8 +``` + +**Decide together:** Which is closer to the correct sorted order (0-20)? Or is there a better way? + +**Edit the file to:** +1. Remove ALL conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) +2. Keep the agreed-upon order +3. Make sure the file is clean + +**Example resolved version:** + +``` +3 +12 +17 +8 +``` + +**Save the file!** + +### Step 9: Verify the Resolution + +```bash +cat numbers.txt +``` + +Make sure you don't see any conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). The file should just contain numbers. + +### Step 10: Mark as Resolved and Commit + +Tell Git you've resolved the conflict: + +```bash +git add numbers.txt +``` + +This stages the resolved file. + +Now commit the resolution: + +```bash +git commit -m "Resolve conflict - agreed on number order" +``` + +**Note:** Git may open an editor with a default merge commit message. You can keep it or customize it. + +**Expected output:** + +``` +[main def5678] Resolve conflict - agreed on number order +``` + +### Step 11: Person B - Push the Resolution + +```bash +git push origin main +``` + +This time it should succeed! The conflict is resolved and the agreed-upon order is on the server. + +### Step 12: Everyone - Pull the Resolved Version + +**Everyone else:** Get the resolved version: + +```bash +git pull origin main +``` + +Check the file - you should see the agreed-upon resolution. + +**You've successfully resolved your first merge conflict together!** In real projects, this is a daily occurrence. You now know exactly what to do when you see those conflict markers. + +--- + +## Part 4: Continue Sorting (Until Complete) + +Now that you understand the pull-push cycle and how to resolve conflicts, continue working as a team to sort all the numbers! + +### The Goal + +Keep working until `numbers.txt` contains all numbers sorted from 0 to 20: + +``` +0 +1 +2 +3 +4 +5 +... +18 +19 +20 +``` + +### The Workflow + +Continue the pattern you've learned: + +1. **Pull first:** Always start with `git pull origin main` +2. **Check the file:** Look at `numbers.txt` to see what still needs sorting +3. **Move one number:** Edit the file to move ONE number closer to its correct position +4. **Commit:** `git add numbers.txt && git commit -m "Move X to position Y"` +5. **Push:** `git push origin main` +6. **Communicate:** Tell the team you've pushed so they can pull +7. **Repeat!** + +### Tips for Success + +**Coordinate with your team:** +- Decide who goes next to avoid too many conflicts +- Call out what number you're working on +- Pull frequently to stay in sync + +**Handle conflicts calmly:** +- If you get a push rejection, don't panic - just pull first +- If you get a merge conflict, work through it together +- Remember: conflicts are normal and you know how to resolve them! + +**Check your progress:** +- View the file regularly: `cat numbers.txt` +- Count how many numbers are in the right position +- Celebrate as the file gets more sorted! + +### When You're Done + +When all numbers are sorted correctly, verify your success: + +```bash +git pull origin main +cat numbers.txt +``` + +**Expected final result:** +``` +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +``` + +**Congratulations!** Your team successfully collaborated using Git to complete the challenge! + +--- + +## Part 5: What You've Learned + +You've now experienced the fundamental Git collaboration workflow that professional developers use every day! + +--- + +## Commands Reference + +### Essential Git Commands for Collaboration + +**Cloning and Setup:** +```bash +git clone # Create local copy of remote repo +git config user.name "Your Name" # Set your name (one-time setup) +git config user.email "your@email.com" # Set your email (one-time setup) +``` + +**Branching:** +```bash +git switch -c # Create and switch to new branch +git switch # Switch to existing branch +git branch # List local branches (* = current) +git branch -a # List all branches (local + remote) +git branch -d # Delete branch (safe - prevents data loss) +``` + +**Making Changes:** +```bash +git status # See current state and changed files +git add # Stage specific file +git add . # Stage all changed files +git commit -m "message" # Commit staged changes with message +git commit # Commit and open editor for message +``` + +**Synchronizing with Remote:** +```bash +git pull origin # Fetch and merge from remote branch +git push origin # Push commits to remote branch +git push -u origin # Push and set upstream tracking +git fetch origin # Download changes without merging +git remote -v # Show configured remotes +``` + +**Conflict Resolution:** +```bash +git status # See which files have conflicts +# Edit files to remove <<<<<<, =======, >>>>>>> markers +git add # Mark file as resolved +git commit -m "Resolve conflict in ..." # Commit the resolution +git merge --abort # Abort merge and go back to before +``` + +**Merging and Integration:** +```bash +git merge # Merge branch into current branch +git merge main # Common: merge main into feature branch +git log --oneline --graph --all # Visualize branch history +``` + +**Viewing Changes:** +```bash +git diff # See unstaged changes +git diff --staged # See staged changes +git show # Show last commit +git log --oneline # See commit history (concise) +git log --oneline --graph # See branch structure visually +``` + +--- + +## Common Scenarios & Solutions + +### "My push was rejected!" + +**Error:** +``` +! [rejected] main -> main (fetch first) +error: failed to push some refs to 'git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge' +``` + +**What it means:** Someone else pushed commits to the branch since you last pulled. + +**Solution:** +```bash +# Pull their changes first +git pull origin main + +# If conflicts, resolve them (see Part 3) +# If no conflicts, you can now push +git push origin main +``` + +--- + +### "I have merge conflicts!" + +**What you see:** +``` +CONFLICT (content): Merge conflict in numbers.txt +Automatic merge failed; fix conflicts and then commit the result. +``` + +**Solution:** +1. Don't panic - this is normal! +2. Run `git status` to see which files have conflicts +3. Open `numbers.txt` in your editor +4. Find the conflict markers: `<<<<<<<`, `=======`, `>>>>>>>` +5. **Talk with your team** - decide which version makes sense +6. Remove ALL markers and keep the agreed order +7. Verify: `cat numbers.txt` (make sure no markers remain!) +8. Stage: `git add numbers.txt` +9. Commit: `git commit -m "Resolve conflict in numbers.txt"` +10. Push: `git push origin main` + +--- + +### "Someone else pushed, how do I get their changes?" + +**Solution:** +```bash +# Pull the latest changes +git pull origin main +``` + +If you have uncommitted changes, Git might ask you to commit or stash first: +```bash +# Option 1: Commit your changes first +git add numbers.txt +git commit -m "Move number X" +git pull origin main + +# Option 2: Stash your changes temporarily +git stash +git pull origin main +git stash pop # Restore your changes after pull +``` + +--- + +### "Two people edited the same numbers!" + +**This creates a conflict - which is exactly what we want to practice!** + +**Solution:** +Follow the complete conflict resolution workflow from Part 3. The key steps: +1. Person who tries to push second gets rejection +2. They pull (sees conflict) +3. Everyone looks at the conflict markers together +4. Decide which version to keep (or create new version) +5. Remove markers, verify the file, commit, push + +--- + +### "How do I see what changed?" + +**Before committing:** +```bash +git diff # See unstaged changes +git diff --staged # See staged changes (after git add) +``` + +**After committing:** +```bash +git show # Show last commit's changes +git log --oneline # See commit history (one line per commit) +git log --oneline --graph # See branch structure with commits +``` + +--- + +### "I want to start over on this file!" + +**Scenario:** You made a mess and want to restore the file to the last committed version. + +**Solution:** +```bash +# Discard all changes to the file (CAREFUL: can't undo this!) +git restore numbers.txt + +# Or restore to a specific commit +git restore --source=abc1234 numbers.txt +``` + +**If you want to keep your changes but try a different approach:** +```bash +# Save your work temporarily +git stash + +# Work is saved, file is back to clean state +# Later, restore your work: +git stash pop +``` + +--- + +### "I accidentally deleted all the numbers!" + +**Don't worry - Git has your back!** + +**Solution:** +```bash +# If you haven't committed the deletion: +git restore numbers.txt + +# If you already committed the deletion but haven't pushed: +git log --oneline # Find the commit before deletion +git reset --hard abc1234 # Replace abc1234 with the good commit hash + +# If you already pushed: +# Ask your facilitator for help, or let someone else pull and fix it! +``` + +--- + +## Troubleshooting + +### Authentication Issues + +**Problem:** "Authentication failed" or "Permission denied" when pushing or pulling + +**Solution (SSH - Recommended):** + +1. **Verify your SSH key is added to Azure DevOps:** + - Sign in to Azure DevOps + - User Settings (profile icon) → SSH Public Keys + - Confirm your key is listed + +2. **Test SSH connection:** + ```bash + ssh -T git@ssh.dev.azure.com + ``` + + Expected: `remote: Shell access is not supported.` (This is normal!) + + If you get "Permission denied (publickey)": + - Your SSH key is not added or Azure DevOps can't find it + - See [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md) for detailed troubleshooting + +3. **Check your remote URL uses SSH:** + ```bash + git remote -v + ``` + + Should show: `git@ssh.dev.azure.com:v3/...` (not `https://`) + + If it shows HTTPS, switch to SSH: + ```bash + git remote set-url origin git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge + ``` + +**Solution (HTTPS with PAT):** + +1. **Verify you're using a Personal Access Token** (not your account password) + - Azure DevOps → User Settings → Personal access tokens + - Create new token with **Code (Read & Write)** scope + - Use token as password when Git prompts for credentials + +2. **Check token permissions:** + - Token must have **Code (Read & Write)** scope + - Verify token hasn't expired + +3. **Update stored credentials** (if cached incorrectly): + + **Windows:** + ```powershell + git credential-manager erase https://dev.azure.com + ``` + + **Mac:** + ```bash + git credential-osxkeychain erase https://dev.azure.com + ``` + + **Linux:** + ```bash + git config --global --unset credential.helper + ``` + +**Recommendation:** Use SSH to avoid credential management issues. See [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md). + +--- + +### Can't Pull or Push - "Unrelated Histories" + +**Problem:** +``` +fatal: refusing to merge unrelated histories +``` + +**What happened:** Your local branch and remote branch don't share a common ancestor (rare, but happens if branches were created independently). + +**Solution:** +```bash +git pull origin main --allow-unrelated-histories +``` + +Then resolve any conflicts if they appear. + +--- + +### Accidentally Deleted Numbers + +**Problem:** "I deleted numbers and committed it!" + +**Solution:** + +**If not pushed yet:** +```bash +# Find the commit before deletion +git log --oneline + +# Example: abc1234 was the last good commit +git reset --hard abc1234 +``` + +**If already pushed:** +```bash +# Find the commit with the correct numbers +git log --oneline + +# Restore the file from that commit +git checkout abc1234 -- numbers.txt + +# Commit the restoration +git add numbers.txt +git commit -m "Restore accidentally deleted numbers" +git push origin main +``` + +**Pro tip:** Use `git log --all --full-history -- numbers.txt` to see all commits that touched that file. + +--- + +### File Still Has Conflict Markers + +**Problem:** You thought you resolved the conflict, but when you look at the file: +``` +3 +<<<<<<< HEAD +12 +17 +======= +17 +12 +>>>>>>> abc1234 +8 +``` + +**What happened:** You forgot to remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). + +**Solution:** +```bash +# Open the file +nano numbers.txt # or vim, code, etc. + +# Search for "<<<<<<<" and remove ALL markers +# Keep only the numbers you want + +# Verify it's clean +cat numbers.txt + +# If it looks good, commit the fix +git add numbers.txt +git commit -m "Remove remaining conflict markers" +``` + +--- + +## Success Criteria + +You've completed this module when you can check off ALL of these: + +**Basic Collaboration:** +- [ ] Cloned the repository from Azure DevOps using SSH +- [ ] Successfully pushed at least one commit to the shared repository +- [ ] Successfully pulled changes from other team members +- [ ] Contributed to sorting the numbers file + +**Conflict Resolution:** +- [ ] Experienced or witnessed a merge conflict +- [ ] Saw the conflict markers in the file (`<<<<<<<`, `=======`, `>>>>>>>`) +- [ ] Resolved a conflict (or helped someone resolve one) +- [ ] Successfully pushed the resolution + +**Push/Pull Cycle:** +- [ ] Experienced a push rejection when someone else pushed first +- [ ] Understood why the push was rejected +- [ ] Pulled changes before pushing again +- [ ] Successfully pushed after pulling + +**Final Result:** +- [ ] The `numbers.txt` file contains all numbers from 0 to 20 in sorted order +- [ ] No conflict markers remain in the file +- [ ] Everyone on the team contributed at least one commit + +**Bonus (if time permits):** +- [ ] Helped someone else resolve a conflict +- [ ] Created multiple merge conflicts and resolved them +- [ ] Experimented with `git stash` when needing to pull with local changes + +--- + +## What You've Learned + +**Collaborative Git Skills:** +- ✅ Cloning repositories from remote Git servers +- ✅ Working with teammates on a shared repository +- ✅ The push/pull cycle for synchronizing work +- ✅ Experiencing and resolving real merge conflicts +- ✅ Understanding push rejections and why they happen +- ✅ Communicating with teammates during collaborative work +- ✅ Using Git in a realistic team environment +- ✅ SSH authentication for secure Git operations + +**Real-World Applications:** + +**These skills are exactly what you'll use at work:** +- This workflow is identical across GitHub, GitLab, Bitbucket, Azure DevOps, and any Git server +- Professional teams do this hundreds of times per day +- Understanding merge conflicts is critical for team collaboration +- Push rejections happen constantly in real teams - you now know how to handle them +- SSH authentication is the industry standard for secure Git operations + +**You're now ready to:** +- Contribute to open source projects on GitHub +- Join a development team and collaborate effectively +- Handle merge conflicts without panic +- Understand the fundamental push/pull workflow +- Work on distributed teams across time zones + +--- + +## What's Next? + +### More Advanced Git Modules + +Continue your Git journey with advanced techniques: + +- **02-advanced/01-rebasing**: Learn to rebase instead of merge for cleaner history +- **02-advanced/02-interactive-rebase**: Clean up messy commits before submitting PRs +- **02-advanced/03-worktrees**: Work on multiple branches simultaneously +- **02-advanced/04-bisect**: Find bugs using binary search through commit history +- **02-advanced/05-blame**: Investigate who changed what and when +- **02-advanced/06-merge-strategies**: Master different merge strategies and when to use them + +### Practice More + +- Try contributing to a real open source project on GitHub +- Practice more complex workflows (multiple feature branches, rebasing, etc.) +- Help teammates at work or school with Git issues + +--- + +## Congratulations! + +**You've completed the Multiplayer Git module!** + +You started this workshop learning basic Git commands like `git init` and `git commit`. Now you're collaborating with teammates, resolving conflicts, and handling push rejections like a professional developer. + +**What makes you different from most beginners:** +- You've experienced REAL merge conflicts and resolved them +- You've worked on a REAL shared repository with teammates +- You've experienced REAL push rejections and learned how to handle them +- You've practiced the entire workflow professionals use daily + +**Most importantly:** You're no longer afraid of merge conflicts or push rejections. You know exactly what to do when you see those `<<<<<<<` markers or get a "rejected" error. + +**Keep practicing, keep collaborating, and welcome to the world of professional Git!** + +--- + +**Happy Collaborating!** diff --git a/01-essentials/08-multiplayer/03_TASKS.md b/01-essentials/08-multiplayer/03_TASKS.md new file mode 100644 index 0000000..b682bdb --- /dev/null +++ b/01-essentials/08-multiplayer/03_TASKS.md @@ -0,0 +1,395 @@ +# Multiplayer Git Tasks + +These tasks walk you through collaborating with Git in the cloud. You'll clone a shared repository, make changes, and sync with your teammates. + +## Prerequisites + +Before starting, make sure you have: +- [ ] An account on the team's Azure DevOps project +- [ ] SSH key configured (ask your facilitator if you need help) +- [ ] Git installed on your computer + +--- + +## Task 1: Clone the Repository + +Cloning creates a local copy of a remote repository on your computer. + +### Steps + +1. Get the SSH URL from Azure DevOps: + - Navigate to the repository + - Click **Clone** + - Select **SSH** + - Copy the URL + +2. Open PowerShell and run: + ```powershell + git clone + ``` + +3. Open the folder in VS Code: + ```powershell + code + ``` + +4. Open the VS Code terminal (`` Ctrl+` ``) and verify the clone worked: + ```powershell + git status + git log --oneline --graph --all + ``` + +### What Just Happened? + +``` +Azure DevOps Your Computer +┌─────────────┐ ┌─────────────┐ +│ Repository │ ───── clone ──> │ Repository │ +│ (original) │ │ (copy) │ +└─────────────┘ └─────────────┘ +``` + +You now have: +- A complete copy of all files +- The entire commit history +- A connection back to the original (called "origin") + +--- + +## Task 2: Make Changes and Push + +Pushing sends your local commits to the remote repository. + +### Steps + +1. In VS Code, create a new file: + - Click **File → New File** (or `Ctrl+N`) + - Add some content, for example: `Hello from ` + - Save as `hello-.txt` (use `Ctrl+S`) + +2. In the VS Code terminal, stage and commit your change: + ```powershell + git add . + git commit -m "feat: add greeting from " + ``` + +3. Push to the remote: + ```powershell + git push + ``` + +### What Just Happened? + +``` +Your Computer Azure DevOps +┌─────────────┐ ┌─────────────┐ +│ Commit A │ │ Commit A │ +│ Commit B │ ───── push ───> │ Commit B │ +│ Commit C │ (new!) │ Commit C │ +└─────────────┘ └─────────────┘ +``` + +Your new commit is now on the server. Others can see it and download it. + +--- + +## Task 3: Pull Changes from Others + +Pulling downloads new commits from the remote and merges them into your branch. + +### Steps + +1. Check if there are new changes: + ```powershell + git status + ``` + Look for "Your branch is behind..." + +2. Pull the changes: + ```powershell + git pull + ``` + +3. See what's new: + ```powershell + git log --oneline -10 + ``` + +### What Just Happened? + +``` +Azure DevOps Your Computer +┌─────────────┐ ┌─────────────┐ +│ Commit A │ │ Commit A │ +│ Commit B │ │ Commit B │ +│ Commit C │ ───── pull ───> │ Commit C │ +│ Commit D │ (new!) │ Commit D │ +└─────────────┘ └─────────────┘ +``` + +Your local repository now has all the commits from the remote. + +--- + +## Task 4: The Push-Pull Dance + +When working with others, you'll often need to pull before you can push. + +### The Scenario + +You made a commit, but someone else pushed while you were working: + +``` +Azure DevOps: A ── B ── C ── D (teammate's commit) +Your Computer: A ── B ── C ── E (your commit) +``` + +### Steps + +1. Try to push: + ```powershell + git push + ``` + This will fail with: "Updates were rejected because the remote contains work that you do not have locally" + +2. Pull first: + ```powershell + git pull + ``` + +3. Now push: + ```powershell + git push + ``` + +### What Happened? + +``` +Before pull: + Remote: A ── B ── C ── D + Local: A ── B ── C ── E + +After pull (Git merges automatically): + Local: A ── B ── C ── D ── M + \ / + E ───┘ + +After push: + Remote: A ── B ── C ── D ── M + \ / + E ───┘ +``` + +--- + +## Task 5: Understanding Fetch + +Fetch downloads changes but does **not** merge them. This lets you see what's new before deciding what to do. + +### Steps + +1. Fetch updates from the remote: + ```powershell + git fetch + ``` + +2. See what's different: + ```powershell + git log HEAD..origin/main --oneline + ``` + This shows commits on the remote that you don't have locally. + +3. When ready, merge: + ```powershell + git merge origin/main + ``` + +### Fetch vs Pull + +| Command | Downloads | Merges | Safe to run anytime? | +|---------|-----------|--------|----------------------| +| `git fetch` | Yes | No | Yes | +| `git pull` | Yes | Yes | Usually | + +**Think of it this way:** +- `fetch` = "Show me what's new" +- `pull` = "Give me what's new" (same as `fetch` + `merge`) + +--- + +## Task 6: Working with Branches + +Branches let you work on features without affecting the main code. + +### Steps + +1. Create and switch to a new branch: + ```powershell + git switch -c feature/-greeting + ``` + +2. In VS Code, create a new file: + - Click **File → New File** (or `Ctrl+N`) + - Add some content, for example: `A special greeting` + - Save as `special.txt` (use `Ctrl+S`) + +3. Stage and commit: + ```powershell + git add . + git commit -m "feat: add special greeting" + ``` + +4. Push your branch to the remote: + ```powershell + git push -u origin feature/-greeting + ``` + The `-u` flag sets up tracking so future pushes are simpler. + +5. Go back to main: + ```powershell + git switch main + ``` + +--- + +## Task 7: The Number Challenge + +This is the main collaborative exercise. Your team will work together to sort numbers 0-20 into the correct order. + +### The Setup + +The repository contains a file called `numbers.txt` with numbers 0-20 in random order: + +``` +17 +3 +12 +8 +... +``` + +Your goal: Work as a team to rearrange the numbers so they appear in order from 0 to 20. + +### The Rules + +1. **Each person moves ONE number per commit** +2. **You must pull before making changes** +3. **Communicate with your team** - decide who moves which number + +### Steps + +1. Pull the latest changes: + ```powershell + git pull + ``` + +2. Open `numbers.txt` in VS Code + +3. Find a number that's out of place and move it to the correct position + - For example, if `5` is at the bottom, move it between `4` and `6` + +4. Save the file (`Ctrl+S`) + +5. Commit your change with a clear message: + ```powershell + git add numbers.txt + git commit -m "fix: move 5 to correct position" + ``` + +6. Push your change: + ```powershell + git push + ``` + +7. If push fails (someone else pushed first): + ```powershell + git pull + ``` + Resolve any conflicts, then push again. + +8. Repeat until all numbers are in order! + +### Handling Conflicts + +When two people edit the same part of the file, you'll see conflict markers: + +``` +<<<<<<< HEAD +4 +5 +6 +======= +4 +6 +>>>>>>> origin/main +``` + +To resolve: +1. Decide what the correct order should be +2. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) +3. Keep only the correct content: + ``` + 4 + 5 + 6 + ``` +4. Save, commit, and push + +### Success + +When complete, `numbers.txt` should look like: + +``` +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +``` + +Celebrate with your team! + +--- + +## Quick Reference + +| Command | What It Does | +|---------|--------------| +| `git clone ` | Download a repository | +| `git push` | Upload your commits | +| `git pull` | Download and merge commits | +| `git fetch` | Download commits (don't merge) | +| `git switch -c ` | Create and switch to a branch | +| `git push -u origin ` | Push a new branch | + +--- + +## Common Issues + +### "Permission denied (publickey)" +Your SSH key isn't set up correctly. See the SSH setup guide or ask your facilitator. + +### "Updates were rejected" +Someone pushed before you. Run `git pull` first, then `git push`. + +### "Merge conflict" +Two people edited the same lines. See BEST-PRACTICES.md for how to handle this. + +### "There is no tracking information" +Run `git push -u origin ` to set up tracking. diff --git a/01-essentials/08-multiplayer/FACILITATOR-SETUP.md b/01-essentials/08-multiplayer/FACILITATOR-SETUP.md deleted file mode 100644 index 21117c0..0000000 --- a/01-essentials/08-multiplayer/FACILITATOR-SETUP.md +++ /dev/null @@ -1,904 +0,0 @@ -# Facilitator Setup Guide - The Great Print Project - -This guide helps workshop facilitators set up the cloud-based multiplayer Git module. - -## Overview - -The Great Print Project is a collaborative Git exercise where pairs of students work together on a shared repository hosted on your Gitea server at **https://git.frod.dk/multiplayer**. - -**What participants will do:** -- Clone a real repository from your server -- Collaborate with partners on shared branches -- Deliberately create and resolve merge conflicts -- Create pull requests and review code -- Experience the full collaborative Git workflow - ---- - -## Prerequisites - -### Gitea Server Setup - -You should have: -- Gitea running at https://git.frod.dk/multiplayer -- Admin access to create repositories and users -- HTTPS or SSH access enabled for Git operations - -**Need to set up Gitea?** See the main workshop's `GITEA-SETUP.md` for Docker + Cloudflare Tunnel instructions. - -### Workshop Materials - -Participants need: -- Access to the module README in `01_essentials/09-multiplayer/README.md` -- Git installed (version 2.23+) -- Python 3.6+ (to run the print project) -- Text editor - ---- - -## Pre-Workshop Setup - -### Step 1: Create User Accounts - -Create individual Gitea accounts for each participant. - -**Recommended naming:** -- `student01`, `student02`, `student03`, etc. -- Or use their real names/emails if preferred - -**Two approaches:** - -**Option A: Manual account creation** -1. Go to Gitea admin panel -2. Create users one by one -3. Set initial passwords (students can change later) -4. Provide credentials to students - -**Option B: Self-registration** (if you trust your network) -1. Enable self-registration in Gitea settings -2. Provide registration URL to students -3. They create their own accounts -4. You verify and approve accounts - -**Access tokens (recommended for HTTPS):** -- Have students create personal access tokens after logging in -- Settings → Applications → Generate New Token -- Token needs `repo` scope -- Students use token as password when pushing/pulling - -### Step 2: Create the Repository - -Create the shared repository: **great-print-project** - -**Via Gitea web UI:** -1. Log in as admin or organization account -2. Click "+" → "New Repository" -3. **Name:** `great-print-project` -4. **Owner:** `multiplayer` (organization) or your admin account -5. **Visibility:** Private (only visible to students you add) -6. **Initialize:** Check "Initialize this repository with selected files" -7. **README:** Yes -8. **License:** None -9. **.gitignore:** Python -10. Click "Create Repository" - -**Via command line (alternative):** -```bash -# Create local directory -mkdir great-print-project -cd great-print-project - -# Initialize git -git init - -# Add files (see Step 3) -git add . -git commit -m "Initial commit: The Great Print Project" - -# Create bare repo on server -ssh user@git.frod.dk -cd /path/to/gitea/repositories/multiplayer -git init --bare great-print-project.git -exit - -# Push to server -git remote add origin git@git.frod.dk:multiplayer/great-print-project.git -git push -u origin main -``` - -### Step 3: Add Starter Code to Repository - -Commit these four files to the repository: - -#### File 1: main.py - -```python -#!/usr/bin/env python3 -""" -The Great Print Project -A collaborative Git exercise - -When everyone completes their assigned functions, -this program will print the complete alphabet and numbers! -""" - -from letters import print_letters -from numbers import print_numbers - -def main(): - print("=" * 50) - print(" THE GREAT PRINT PROJECT") - print("=" * 50) - print("\nLetters:") - print_letters() - print() # New line after letters - - print("\nNumbers:") - print_numbers() - print() # New line after numbers - - print("\n" + "=" * 50) - print(" PROJECT COMPLETE!") - print("=" * 50) - -if __name__ == "__main__": - main() -``` - -#### File 2: letters.py - -```python -""" -Letter Printing Functions -Each pair completes their assigned functions. - -Expected output: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z -""" - -def print_a(): - """Print letter A - EXAMPLE (already completed)""" - print("A", end=" ") - -def print_b(): - # TODO: Pair 1 - implement this function - pass - -def print_c(): - # TODO: Pair 1 - implement this function - pass - -def print_d(): - # TODO: Pair 1 - implement this function - pass - -def print_e(): - # TODO: Pair 2 - implement this function - pass - -def print_f(): - # TODO: Pair 2 - implement this function - pass - -def print_g(): - # TODO: Pair 2 - implement this function - pass - -def print_h(): - # TODO: Pair 3 - implement this function - pass - -def print_i(): - # TODO: Pair 3 - implement this function - pass - -def print_j(): - # TODO: Pair 3 - implement this function - pass - -def print_k(): - # TODO: Pair 4 - implement this function - pass - -def print_l(): - # TODO: Pair 4 - implement this function - pass - -def print_m(): - # TODO: Pair 4 - implement this function - pass - -def print_n(): - # TODO: Pair 5 - implement this function - pass - -def print_o(): - # TODO: Pair 5 - implement this function - pass - -def print_p(): - # TODO: Pair 5 - implement this function - pass - -def print_q(): - # TODO: Pair 6 - implement this function - pass - -def print_r(): - # TODO: Pair 6 - implement this function - pass - -def print_s(): - # TODO: Pair 6 - implement this function - pass - -def print_t(): - # TODO: Pair 7 - implement this function - pass - -def print_u(): - # TODO: Pair 7 - implement this function - pass - -def print_v(): - # TODO: Pair 7 - implement this function - pass - -def print_w(): - # TODO: Pair 8 - implement this function - pass - -def print_x(): - # TODO: Pair 8 - implement this function - pass - -def print_y(): - # TODO: Pair 8 - implement this function - pass - -def print_z(): - # TODO: Pair 9 - implement this function - pass - - -def print_letters(): - """Print all letters A-Z""" - print_a() - print_b() - print_c() - print_d() - print_e() - print_f() - print_g() - print_h() - print_i() - print_j() - print_k() - print_l() - print_m() - print_n() - print_o() - print_p() - print_q() - print_r() - print_s() - print_t() - print_u() - print_v() - print_w() - print_x() - print_y() - print_z() -``` - -#### File 3: numbers.py - -```python -""" -Number Printing Functions -Each pair completes their assigned functions. - -Expected output: 0 1 2 3 4 5 6 7 8 9 -""" - -def print_0(): - # TODO: Pair 9 - implement this function - pass - -def print_1(): - # TODO: Pair 10 - implement this function - pass - -def print_2(): - # TODO: Pair 10 - implement this function - pass - -def print_3(): - # TODO: Pair 10 - implement this function - pass - -def print_4(): - # TODO: Pair 11 - implement this function - pass - -def print_5(): - # TODO: Pair 11 - implement this function - pass - -def print_6(): - # TODO: Pair 11 - implement this function - pass - -def print_7(): - # TODO: Pair 12 - implement this function - pass - -def print_8(): - # TODO: Pair 12 - implement this function - pass - -def print_9(): - # TODO: Pair 12 - implement this function - pass - - -def print_numbers(): - """Print all numbers 0-9""" - print_0() - print_1() - print_2() - print_3() - print_4() - print_5() - print_6() - print_7() - print_8() - print_9() -``` - -#### File 4: assignments.md - -```markdown -# Pair Assignments - -## How This Works - -Each pair is assigned 3 functions to implement. You'll work together on a shared branch. - -**Important:** Check with your facilitator for your pair number and assignment! - ---- - -## Assignments - -### Pair 1 -- **Functions:** `print_b()`, `print_c()`, `print_d()` -- **File:** `letters.py` -- **Branch:** `pair-1-bcd` - -### Pair 2 -- **Functions:** `print_e()`, `print_f()`, `print_g()` -- **File:** `letters.py` -- **Branch:** `pair-2-efg` - -### Pair 3 -- **Functions:** `print_h()`, `print_i()`, `print_j()` -- **File:** `letters.py` -- **Branch:** `pair-3-hij` - -### Pair 4 -- **Functions:** `print_k()`, `print_l()`, `print_m()` -- **File:** `letters.py` -- **Branch:** `pair-4-klm` - -### Pair 5 -- **Functions:** `print_n()`, `print_o()`, `print_p()` -- **File:** `letters.py` -- **Branch:** `pair-5-nop` - -### Pair 6 -- **Functions:** `print_q()`, `print_r()`, `print_s()` -- **File:** `letters.py` -- **Branch:** `pair-6-qrs` - -### Pair 7 -- **Functions:** `print_t()`, `print_u()`, `print_v()` -- **File:** `letters.py` -- **Branch:** `pair-7-tuv` - -### Pair 8 -- **Functions:** `print_w()`, `print_x()`, `print_y()` -- **File:** `letters.py` -- **Branch:** `pair-8-wxy` - -### Pair 9 -- **Functions:** `print_z()`, `print_0()`, `print_1()` -- **Files:** `letters.py`, `numbers.py` -- **Branch:** `pair-9-z01` - -### Pair 10 -- **Functions:** `print_2()`, `print_3()`, `print_4()` -- **File:** `numbers.py` -- **Branch:** `pair-10-234` - -### Pair 11 -- **Functions:** `print_5()`, `print_6()`, `print_7()` -- **File:** `numbers.py` -- **Branch:** `pair-11-567` - -### Pair 12 -- **Functions:** `print_8()`, `print_9()` -- **File:** `numbers.py` -- **Branch:** `pair-12-89` - ---- - -## Example Implementation - -```python -def print_a(): - """Print letter A - EXAMPLE (already completed)""" - print("A", end=" ") -``` - -**Your functions should follow the same pattern:** - -```python -def print_x(): - """Print letter/number X""" - print("X", end=" ") -``` - ---- - -## Testing - -After implementing your functions, test with: - -```bash -python main.py -``` - -You should see your letters/numbers in the output! - ---- - -## Questions? - -Ask your facilitator for: -- Your pair number -- Your Gitea credentials -- Help with authentication setup -- Any Git or Python issues -``` - -#### File 5: README.md (in repository) - -```markdown -# The Great Print Project 🎯 - -A collaborative Git exercise for learning teamwork with version control! - -## Goal - -When everyone completes their assigned functions, running `python main.py` will print: - -``` -================================================== - THE GREAT PRINT PROJECT -================================================== - -Letters: -A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - -Numbers: -0 1 2 3 4 5 6 7 8 9 - -================================================== - PROJECT COMPLETE! -================================================== -``` - -## Your Mission - -1. Find your pair assignment in `assignments.md` -2. Clone this repository -3. Create your feature branch -4. Implement your assigned functions -5. Practice collaboration: push, pull, resolve conflicts -6. Create a pull request -7. Celebrate when your code is merged! - -## Quick Start - -```bash -# Clone the repository -git clone https://git.frod.dk/multiplayer/great-print-project.git -cd great-print-project - -# Check your assignment -cat assignments.md - -# Create your branch (replace X with your pair number) -git switch -c pair-X-feature - -# Edit your file (letters.py or numbers.py) -# Implement your functions - -# Test it -python main.py - -# Commit and push -git add . -git commit -m "Implement print_x() functions" -git push -u origin pair-X-feature -``` - -## Files - -- **main.py** - Orchestrator (runs the whole program) -- **letters.py** - Functions for printing A-Z -- **numbers.py** - Functions for printing 0-9 -- **assignments.md** - See which functions your pair should implement - -## Need Help? - -See the module README in the workshop repository for detailed step-by-step instructions! - -**Happy Collaborating! 🚀** -``` - -**Commit these files:** - -```bash -git add main.py letters.py numbers.py assignments.md README.md -git commit -m "Add Great Print Project starter code" -git push origin main -``` - -### Step 4: Grant Student Access - -Add students as collaborators with write access: - -**Via Gitea web UI:** -1. Go to repository → Settings → Collaborators -2. Add each student account -3. Set permission level: **Write** (allows push, pull, branch creation) - -**Important:** Students need **Write** access to: -- Create branches -- Push commits -- Create pull requests - -### Step 5: Configure Branch Protection (Optional) - -To prevent accidental pushes to main: - -1. Repository → Settings → Branches -2. Add protection rule for `main` branch -3. Settings: - - **Block direct pushes:** Yes (requires pull requests) - - **Require PR reviews:** Optional (you can review PRs yourself) - - **Auto-merge:** Disabled (you merge manually or students do) - -This ensures students: -- MUST use feature branches -- MUST create pull requests -- Can't accidentally break main - -**For beginners:** Consider allowing students to merge their own PRs after approval to complete the full workflow. - -### Step 6: Test the Setup - -Before the workshop, test as a student would: - -```bash -# Clone as a test student -git clone https://git.frod.dk/multiplayer/great-print-project.git -cd great-print-project - -# Run the program -python main.py -# Should show only "A" with missing letters/numbers - -# Create test branch -git switch -c test-branch - -# Edit letters.py, add print_b() -def print_b(): - print("B", end=" ") - -# Commit and push -git add letters.py -git commit -m "Test commit" -git push -u origin test-branch - -# Create test pull request -# (Do this via web UI) - -# Clean up test branch after -git push origin --delete test-branch -``` - ---- - -## During the Workshop - -### Pairing Students - -**Strategies for assigning pairs:** - -**Option 1: Random pairing** -- Use a random number generator -- Pair students as they arrive - -**Option 2: Skill-based pairing** -- Mix experienced and beginner students -- Balance pair capabilities - -**Option 3: Let them choose** -- Students pick their own partners -- Good for building team dynamics - -**Announce pairs clearly:** -- Write on board/screen: "Pair 1: Alice & Bob" -- Provide printed assignment sheet -- Update `assignments.md` in repo if needed - -### Timeline - -**Suggested schedule for 2-hour session:** - -- **0:00-0:10** (10 min): Introduction, distribute credentials -- **0:10-0:20** (10 min): Students clone repo, verify access -- **0:20-0:35** (15 min): Part 1 - Getting Started -- **0:35-0:55** (20 min): Part 2 - First Contribution -- **0:55-1:25** (30 min): Part 3 - Conflict Exercise (key learning!) -- **1:25-1:45** (20 min): Part 4 - Pull Requests -- **1:45-2:00** (15 min): Part 5 - Syncing, Q&A, wrap-up - -### Monitoring Progress - -**Use Gitea to track:** - -1. **Branches created:** Repository → Branches - - Should see `pair-1-bcd`, `pair-2-efg`, etc. - -2. **Commits:** Repository → Commits - - Each pair should have multiple commits - -3. **Pull requests:** Repository → Pull Requests - - Should see one PR per pair - -**Walk around the room:** -- Check screens for conflict markers -- Ask pairs how they're resolving conflicts -- Ensure both partners are engaged - -**Common issues to watch for:** -- Partners not using the same branch name -- Forgetting to pull before pushing -- Not removing conflict markers completely -- Committing to main instead of feature branch - -### Managing Pull Requests - -**Your role:** - -**Option A: Review and merge yourself** -- Teaches students what good reviews look like -- Ensures quality before merging -- More facilitator work - -**Option B: Students merge their own** -- More autonomous learning -- Students experience complete workflow -- Risk of messy main branch - -**Recommended approach:** -1. First 2-3 PRs: You review and merge (demonstrate good practices) -2. Remaining PRs: Students review each other, you approve -3. Students can merge after approval - -**What to check in PR reviews:** -- Functions implemented correctly -- No conflict markers in code -- Code follows pattern (e.g., `print("X", end=" ")`) -- Meaningful commit messages - -### Handling Problems - -**Common issues and solutions:** - -**Problem: "I can't push!"** -- Check they're authenticated (token or SSH key) -- Check they pulled latest changes first -- Check branch name matches their partner's - -**Problem: "Merge conflict won't resolve!"** -- Walk through Part 3 step-by-step with them -- Show them the conflict markers -- Verify they removed ALL markers -- Run `python main.py` together to test - -**Problem: "We both committed to main!"** -- Have them create proper feature branch -- Use `git cherry-pick` to move commits -- Reset main to origin/main - -**Problem: "GitHub Desktop / GUI tool shows something different"** -- Recommend command line for this exercise -- GUIs can hide important details during conflicts - -### Celebrating Success - -When all pairs have merged: - -```bash -git pull origin main -python main.py -``` - -**Everyone should see:** -``` -================================================== - THE GREAT PRINT PROJECT -================================================== - -Letters: -A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - -Numbers: -0 1 2 3 4 5 6 7 8 9 - -================================================== - PROJECT COMPLETE! -================================================== -``` - -**Take a screenshot!** Share it with the class. This is a genuine collaborative achievement. - ---- - -## Post-Workshop - -### Cleanup (Optional) - -**Keep the repository for future workshops:** -- Delete all feature branches: `git push origin --delete pair-1-bcd` (etc.) -- Reset main to initial state -- Reuse for next cohort - -**Archive the session:** -- Export final repository state -- Take screenshots of successful PRs -- Save for portfolio/examples - -### Student Takeaways - -Provide students: -- Link to repository (they can clone for reference) -- Completion certificate (if applicable) -- Next steps: contributing to open source, Git resources - ---- - -## Troubleshooting - -### Gitea Server Issues - -**Problem: Server unreachable** -- Check Cloudflare Tunnel is running: `cloudflared tunnel info` -- Verify Gitea container is up: `docker ps` -- Check firewall rules - -**Problem: SSH not working** -- Verify SSH port is exposed in docker-compose.yml -- Check Cloudflare Tunnel config includes SSH -- Test: `ssh -T git@git.frod.dk` - -**Problem: HTTPS clone fails** -- Check certificate validity -- Try `GIT_SSL_NO_VERIFY=true git clone ...` (temporary workaround) -- Configure Gitea to use proper HTTPS certificates - -### Authentication Issues - -**Problem: Students can't log in** -- Verify accounts created and active -- Reset passwords if needed -- Check email verification isn't blocking (disable for workshop) - -**Problem: Push fails with authentication error** -- HTTPS: Ensure students use access token, not password -- SSH: Verify keys added to Gitea account -- Check repo permissions (must be Write, not Read) - -### Git Workflow Issues - -**Problem: Students create PR but can't merge** -- Check branch protection rules -- Verify they have Write access -- Ensure PR doesn't have conflicts - -**Problem: Main branch gets messy** -- Reset to last good commit: `git reset --hard ` -- Force push: `git push --force origin main` (CAREFUL!) -- Or start fresh: delete repo, recreate with starter code - ---- - -## Tips for Success - -### Before Workshop - -- Test the entire flow yourself as a student -- Prepare credential sheets for each student -- Have backup plan if server goes down (local git exercise) -- Prepare slides explaining merge conflicts visually - -### During Workshop - -- **Start on time** - respect everyone's schedule -- **Pair programming** - ensure both partners engage -- **Encourage talking** - best conflicts are resolved by discussion -- **Celebrate small wins** - first push, first conflict resolution -- **Walk the room** - see screens, answer questions live - -### After Workshop - -- Gather feedback - what worked, what didn't -- Note timing - were parts too rushed or too slow? -- Archive successful PRs as examples -- Plan improvements for next session - ---- - -## Scaling Considerations - -### Small Groups (4-8 students, 2-4 pairs) - -- More hands-on facilitator time -- Can review all PRs in detail -- Easier to monitor progress - -### Medium Groups (10-20 students, 5-10 pairs) - -- Recommended size -- Good mix of collaboration and individual attention -- Helps if you have a teaching assistant - -### Large Groups (20+ students, 10+ pairs) - -- Consider multiple repositories (split into groups of 12 pairs max) -- Recruit teaching assistants to help monitor -- Use breakout rooms (if online) -- Automate more (less PR review, more self-merging) - ---- - -## Additional Resources - -### For You (Facilitator) - -- Gitea documentation: https://docs.gitea.io/ -- Pro Git book (free): https://git-scm.com/book/en/v2 -- Teaching Git: https://git-scm.com/doc - -### For Students - -- Git cheatsheet (included in workshop repo) -- Interactive Git tutorial: https://learngitbranching.js.org/ -- Oh Shit Git: https://ohshitgit.com/ (recovering from mistakes) - ---- - -## Questions or Issues? - -This guide should cover most scenarios. If you encounter issues not listed here: - -1. Check Gitea logs: `docker logs gitea-container-name` -2. Test with minimal setup (single test student) -3. Consult Gitea documentation -4. Reach out to workshop repository maintainers - -**Good luck with your workshop! The multiplayer module is where Git skills really come alive.** diff --git a/01-essentials/08-multiplayer/README.md b/01-essentials/08-multiplayer/README.md deleted file mode 100644 index 61ed36f..0000000 --- a/01-essentials/08-multiplayer/README.md +++ /dev/null @@ -1,1408 +0,0 @@ -# Module 09: Multiplayer Git - The Great Print Project - -## Learning Objectives - -By the end of this module, you will: -- Clone and work with remote repositories on a cloud server -- Collaborate with a partner using shared branches -- Resolve merge conflicts in a real team environment -- Create and review pull requests on Gitea -- Synchronize your work with teammates -- Apply all the Git skills you've learned in a collaborative setting - -## Welcome to Real Collaboration! - -Congratulations on making it this far! You've learned Git basics: committing, branching, merging, and even resolving conflicts solo. But here's where it gets real - **working with actual teammates on a shared codebase**. - -This module is different from all the others. There's no `setup.ps1` script creating a simulated environment. Instead, you'll work with: -- A real Git server: **https://git.frod.dk/multiplayer** -- Real teammates (your pair partner) -- Real branches on a shared repository -- Real merge conflicts when you both push to the same branch -- Real pull requests that others will review - -**This is exactly how professional developers collaborate every day on GitHub, GitLab, Bitbucket, and company Git servers.** - -Ready? Let's build something together! - ---- - -## The Great Print Project - -### What You'll Build - -You and your partner are joining a team project to build a Python program that prints the complete alphabet (A-Z) and numbers (0-9). When finished and all pairs have completed their work, running `python main.py` will output: - -``` -================================================== - THE GREAT PRINT PROJECT -================================================== - -Letters: -A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - -Numbers: -0 1 2 3 4 5 6 7 8 9 - -================================================== - PROJECT COMPLETE! -================================================== -``` - -**Your role:** Each pair implements 3 functions (e.g., `print_b()`, `print_c()`, `print_d()`). - -**The challenge:** You'll need to collaborate with your partner, handle merge conflicts when you both edit the same code, and integrate your work with other pairs through pull requests. - -### Why This Project? - -This project teaches collaboration in a safe, structured way: - -1. **Simple code:** Implementing `print("B", end=" ")` is easy. The hard part is Git, not Python. -2. **Clear success:** You can visually verify your letters appear in the output. -3. **Guaranteed conflicts:** You'll deliberately create merge conflicts to practice resolving them. -4. **Team integration:** Your work depends on others, just like real software projects. -5. **Safe experimentation:** It's okay to make mistakes - you can always pull a fresh copy! - -### Repository Structure - -``` -great-print-project/ -├── main.py # Orchestrator - runs the complete program -├── letters.py # Functions for A-Z (pairs 1-9 work here) -├── numbers.py # Functions for 0-9 (pairs 9-12 work here) -├── assignments.md # See which functions your pair should implement -└── README.md # Quick reference for the project -``` - -**Note:** The repository is already set up on the server with starter code. You'll clone it and add your parts! - ---- - -## Prerequisites - -Before starting, ensure you have: - -### 1. Your Gitea Account - -Your facilitator will provide: -- **Username:** (e.g., `student01`, `student02`) -- **Password or Access Token:** For HTTPS authentication -- OR **SSH Key Setup:** If using SSH - -**First-time setup:** Visit https://git.frod.dk/multiplayer and log in to verify your account works. - -### 2. Git Configuration - -Verify your Git identity is configured: - -```bash -git config --global user.name -git config --global user.email -``` - -If these are empty, set them now: - -```bash -git config --global user.name "Your Name" -git config --global user.email "your.email@example.com" -``` - -**Why this matters:** Every commit you make will be tagged with this information. - -### 3. Authentication Setup - -Choose ONE method for authenticating with the Git server: - -**Method A: HTTPS with Access Token (Recommended)** - -1. Log in to https://git.frod.dk/multiplayer -2. Go to Settings → Applications → Tokens -3. Generate a new token with `repo` permissions -4. Save the token securely (you'll use it as your password when pushing/pulling) - -**Method B: SSH Key** - -1. Generate SSH key (if you don't have one): - ```bash - ssh-keygen -t ed25519 -C "your.email@example.com" - ``` -2. Copy your public key: - ```bash - cat ~/.ssh/id_ed25519.pub - ``` -3. Add it to Gitea: Settings → SSH Keys → Add Key -4. Test connection: - ```bash - ssh -T git@git.frod.dk - ``` - ---- - -## Part 1: Getting Started (15 minutes) - -### Step 1: Find Your Pair Partner - -Your facilitator will assign pairs. Find your partner and sit together (or connect on chat if remote). - -**Important:** Both of you will work on the **same branch**. This simulates real team development where multiple developers collaborate on a feature branch. - -### Step 2: Check Your Assignment - -Your facilitator will tell you your pair number (1-12). Remember this number! - -**Example assignments:** -- **Pair 1:** Functions `print_b()`, `print_c()`, `print_d()` in `letters.py` -- **Pair 2:** Functions `print_e()`, `print_f()`, `print_g()` in `letters.py` -- **Pair 9:** Functions `print_z()` in `letters.py`, `print_0()`, `print_1()` in `numbers.py` -- **Pair 12:** Functions `print_8()`, `print_9()` in `numbers.py` - -### Step 3: Clone the Repository - -**Using HTTPS (recommended):** - -```bash -git clone https://git.frod.dk/multiplayer/great-print-project.git -cd great-print-project -``` - -**Using SSH:** - -```bash -git clone git@git.frod.dk:multiplayer/great-print-project.git -cd great-print-project -``` - -**Expected output:** - -``` -Cloning into 'great-print-project'... -remote: Enumerating objects: 10, done. -remote: Counting objects: 100% (10/10), done. -remote: Compressing objects: 100% (7/7), done. -remote: Total 10 (delta 2), reused 0 (delta 0), pack-reused 0 -Receiving objects: 100% (10/10), done. -Resolving deltas: 100% (2/2), done. -``` - -Success! You now have a local copy of the shared repository. - -### Step 4: Explore the Code - -Let's see what we're working with: - -```bash -# List files -ls -la - -# Run the program in its current state -python main.py - -# View your pair's assignment -cat assignments.md -``` - -**What you'll see when running `python main.py`:** - -``` -================================================== - THE GREAT PRINT PROJECT -================================================== - -Letters: -A - -Numbers: - - -================================================== - PROJECT COMPLETE! -================================================== -``` - -Most letters and numbers are missing! Only `print_a()` is implemented as an example. Your job is to add your assigned functions. - -### Step 5: Create Your Feature Branch - -**Both partners should create the SAME branch name:** - -```bash -# Replace X with your pair number (1-12) -# Example: pair-1-bcd, pair-2-efg, pair-3-hij -git switch -c pair-X-feature -``` - -**Example for Pair 1:** - -```bash -git switch -c pair-1-bcd -``` - -**Expected output:** - -``` -Switched to a new branch 'pair-1-bcd' -``` - -**Verify you're on the right branch:** - -```bash -git branch -``` - -You should see `* pair-1-bcd` (or your pair's branch name) with an asterisk. - -> **Important:** Both partners MUST use the exact same branch name! This allows you to push and pull each other's work. - ---- - -## Part 2: Your First Contribution (20 minutes) - -Now you'll practice the basic collaborative workflow: one partner pushes, the other pulls, then you switch roles. - -### Step 1: Decide Who Goes First - -Choose one partner to be **Partner A** (goes first) and one to be **Partner B** (goes second). - -- **Partner A:** Will implement the first function and push to the server -- **Partner B:** Will pull Partner A's work, then implement the second function - -Later you'll switch roles for the third function. - -### Step 2: Partner A - Complete ONE Function - -**Partner A:** Open the appropriate file (`letters.py` or `numbers.py`) in your text editor. - -**Example for Pair 1 (editing `letters.py`):** - -Find this: - -```python -def print_b(): - # TODO: Pair 1 - implement this function - pass -``` - -Change it to: - -```python -def print_b(): - """Print letter B""" - print("B", end=" ") -``` - -**Key points:** -- Use `end=" "` to add a space after the letter/number -- Follow the pattern from `print_a()` (already implemented as an example) -- Keep it simple! - -### Step 3: Test Your Change - -```bash -python main.py -``` - -**Expected output:** - -``` -Letters: -A B -``` - -Great! The B now appears. (C and D still missing because you haven't implemented them yet.) - -### Step 4: Commit Your Work - -```bash -git status -# You should see: modified: letters.py - -git add letters.py -git commit -m "Add print_b() implementation" -``` - -**Expected output:** - -``` -[pair-1-bcd abc1234] Add print_b() implementation - 1 file changed, 2 insertions(+), 2 deletions(-) -``` - -### Step 5: Push to Remote - -This uploads your commit to the shared server: - -```bash -git push -u origin pair-1-bcd -``` - -**The `-u` flag:** Sets up tracking so future pushes can just use `git push`. - -**Expected output:** - -``` -Enumerating objects: 5, done. -Counting objects: 100% (5/5), done. -Delta compression using up to 8 threads -Compressing objects: 100% (3/3), done. -Writing objects: 100% (3/3), 345 bytes | 345.00 KiB/s, done. -Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 -remote: . Processing 1 references -remote: Processed 1 references in total -To https://git.frod.dk/multiplayer/great-print-project.git - * [new branch] pair-1-bcd -> pair-1-bcd -``` - -Success! Your code is now on the server. - -### Step 6: Partner B - Pull Partner A's Work - -**Partner B:** Make sure you're on the same branch, then pull: - -```bash -# Verify branch (should match Partner A's) -git branch - -# Pull Partner A's changes from the server -git pull origin pair-1-bcd -``` - -**Expected output:** - -``` -From https://git.frod.dk/multiplayer/great-print-project - * branch pair-1-bcd -> FETCH_HEAD -Updating 123abc..456def -Fast-forward - letters.py | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) -``` - -**Verify you have Partner A's code:** - -```bash -cat letters.py | grep -A 2 "def print_b" -# Should show Partner A's implementation - -python main.py -# Should show "A B" in the output -``` - -You now have Partner A's work! - -### Step 7: Partner B - Add Your Function - -**Partner B:** Now it's your turn! Implement the SECOND assigned function. - -**Example for Pair 1:** - -```python -def print_c(): - """Print letter C""" - print("C", end=" ") -``` - -**Test, commit, and push:** - -```bash -python main.py -# Should show: A B C - -git add letters.py -git commit -m "Add print_c() implementation" -git push origin pair-1-bcd -``` - -### Step 8: Partner A - Pull Partner B's Work - -**Partner A:** Pull the latest changes: - -```bash -git pull origin pair-1-bcd -``` - -**Verify you have both functions:** - -```bash -python main.py -# Should show: A B C -``` - -### Step 9: Complete Your Third Function Together - -For your third and final function, EITHER partner can implement it. Follow the same cycle: -1. Implement the function -2. Test with `python main.py` -3. Commit and push -4. Other partner pulls - -**When you're done, all three assigned functions should be complete!** - -```bash -python main.py -# Pair 1 should see: A B C D -``` - -**Congratulations! You've completed your first collaborative Git workflow!** You've learned the core cycle: pull → work → commit → push → pull. This is what professional developers do hundreds of times per day. - ---- - -## Part 3: Deliberate Conflict Exercise (30 minutes) - -Now for the **real** learning: merge conflicts! You'll deliberately create a conflict with your partner, then resolve it together. - -### The Scenario - -Merge conflicts happen when two people edit the same lines in the same file. Git can't automatically decide which version to keep, so it asks you to resolve it manually. - -**What you'll do:** -1. Partner A and Partner B will BOTH edit the same function -2. Partner A pushes first (succeeds) -3. Partner B tries to push (gets rejected!) -4. Partner B pulls (sees conflict markers) -5. You resolve the conflict together -6. Partner B pushes the resolution - -This is a **deliberate practice** scenario. In real projects, conflicts happen by accident - now you'll know how to handle them! - -### Setup: Choose a Conflict Function - -Look at your three assigned functions. Pick the **LAST** one for this exercise. - -- **Pair 1:** Use `print_d()` -- **Pair 2:** Use `print_g()` -- **Pair 9:** Use `print_1()` (in numbers.py) - -### Step 1: Both Partners Start Fresh - -Make sure you both have the latest code: - -```bash -git switch pair-1-bcd -git pull origin pair-1-bcd - -# Check status - should be clean -git status -``` - -Both partners should see: "Your branch is up to date" and "nothing to commit, working tree clean" - -### Step 2: Partner A - Make Your Change First - -**Partner A:** Edit the chosen function with a SPECIFIC implementation. - -**Example for `print_d()`:** - -```python -def print_d(): - """Print letter D""" - print("D", end=" ") # Partner A's version -``` - -**Commit and push IMMEDIATELY:** - -```bash -git add letters.py -git commit -m "Partner A: Add print_d()" -git push origin pair-1-bcd -``` - -Partner A should see: "Everything up-to-date" or the commit being pushed successfully. - -### Step 3: Partner B - Make DIFFERENT Change (DON'T PULL YET!) - -**Partner B:** This is critical - do NOT pull Partner A's changes yet! - -Instead, edit the SAME function with a DIFFERENT implementation: - -```python -def print_d(): - """Print letter D""" - print("D ", end="") # Partner B's version (extra space, different quote style) -``` - -**Commit (but don't push yet):** - -```bash -git add letters.py -git commit -m "Partner B: Add print_d()" -``` - -### Step 4: Partner B - Try to Push (This Will Fail!) - -```bash -git push origin pair-1-bcd -``` - -**You'll see an error like this:** - -``` -To https://git.frod.dk/multiplayer/great-print-project.git - ! [rejected] pair-1-bcd -> pair-1-bcd (fetch first) -error: failed to push some refs to 'https://git.frod.dk/multiplayer/great-print-project.git' -hint: Updates were rejected because the remote contains work that you do -hint: not have locally. This is usually caused by another repository pushing -hint: to the same ref. You may want to first integrate the remote changes -hint: (e.g., 'git pull ...') before pushing again. -hint: See the 'Note about fast-forwards' in 'git push --help' for details. -``` - -**Don't panic!** This is completely normal and expected. Git is protecting you from overwriting Partner A's work. - -**What happened:** Partner A pushed commits that you don't have. Git requires you to pull first and integrate their changes before you can push yours. - -### Step 5: Partner B - Pull and See the Conflict - -```bash -git pull origin pair-1-bcd -``` - -**You'll see:** - -``` -From https://git.frod.dk/multiplayer/great-print-project - * branch pair-1-bcd -> FETCH_HEAD -Auto-merging letters.py -CONFLICT (content): Merge conflict in letters.py -Automatic merge failed; fix conflicts and then commit the result. -``` - -**This is a merge conflict!** Git tried to merge Partner A's changes with yours, but couldn't automatically combine them because you both edited the same lines. - -### Step 6: Check Git Status - -```bash -git status -``` - -**Output:** - -``` -On branch pair-1-bcd -You have unmerged paths. - (fix conflicts and run "git commit") - (use "git merge --abort" to abort the merge) - -Unmerged paths: - (use "git add ..." to mark resolution) - both modified: letters.py - -no changes added to commit (use "git add" and/or "git commit -a") -``` - -Git is telling you: "The file `letters.py` has conflicts. Both of you modified it. Please resolve and commit." - -### Step 7: Open the Conflicted File - -```bash -cat letters.py -# Or open in your text editor: code letters.py, vim letters.py, nano letters.py -``` - -**Find the conflict markers around `print_d()`:** - -```python -def print_d(): - """Print letter D""" -<<<<<<< HEAD - print("D ", end="") # Partner B's version -======= - print("D", end=" ") # Partner A's version ->>>>>>> abc1234567890abcdef1234567890abcdef12 -``` - -### Understanding Conflict Markers - -```python -<<<<<<< HEAD # Start marker - print("D ", end="") # YOUR version (Partner B's code) -======= # Divider - print("D", end=" ") # THEIR version (Partner A's code from remote) ->>>>>>> abc1234... # End marker (shows commit hash) -``` - -**The three sections:** -1. `<<<<<<< HEAD` to `=======`: Your current changes (what you committed locally) -2. `=======` to `>>>>>>>`: Their changes (what Partner A pushed to the server) -3. You must choose one, combine them, or write something new - -### Step 8: Resolve the Conflict TOGETHER - -**Talk with your partner!** Look at both versions and decide: - -**Option 1: Keep Partner A's version** -```python -def print_d(): - """Print letter D""" - print("D", end=" ") -``` - -**Option 2: Keep Partner B's version** -```python -def print_d(): - """Print letter D""" - print("D ", end="") -``` - -**Option 3: Agree on a third option** -```python -def print_d(): - """Print letter D""" - print("D", end=" ") # Agreed version after discussion -``` - -**Edit the file to:** -1. Remove ALL conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) -2. Keep the agreed-upon code -3. Make sure the syntax is valid Python - -**Example resolved version:** - -```python -def print_d(): - """Print letter D""" - print("D", end=" ") # Resolved: using Partner A's version -``` - -**Save the file!** - -### Step 9: Test the Resolution - -```bash -python main.py -``` - -Make sure the program runs without errors and shows the expected output. If you see syntax errors, you probably left conflict markers in the file - open it again and remove them. - -### Step 10: Mark as Resolved and Commit - -Tell Git you've resolved the conflict: - -```bash -git add letters.py -``` - -This stages the resolved file. - -Now commit the resolution: - -```bash -git commit -m "Resolve conflict in print_d() - used Partner A's version" -``` - -**Note:** Git may open an editor with a default merge commit message. You can keep it or customize it. - -**Expected output:** - -``` -[pair-1-bcd def5678] Resolve conflict in print_d() - used Partner A's version -``` - -### Step 11: Partner B - Push the Resolution - -```bash -git push origin pair-1-bcd -``` - -This time it should succeed! The conflict is resolved and your unified code is on the server. - -### Step 12: Partner A - Pull the Resolved Version - -**Partner A:** Get the resolved code: - -```bash -git pull origin pair-1-bcd -``` - -Check the file - you should see the agreed-upon resolution. - -**You've successfully resolved your first merge conflict together!** In real projects, this is a daily occurrence. You now know exactly what to do when you see those conflict markers. - ---- - -## Part 4: Pull Requests (20 minutes) - -Now that your functions are complete and tested, it's time to integrate your work into the main branch through a **Pull Request (PR)**. - -Pull requests are how professional teams review code before merging it. Someone proposes changes, others review and comment, and when approved, the changes are merged. - -### Step 1: Push All Your Work - -Make sure everything is committed and pushed: - -```bash -git status -# Should show: "Your branch is up to date" and "nothing to commit" - -# If you have uncommitted changes, commit them now -git add . -git commit -m "Complete all assigned functions" - -# Push to make sure server has latest -git push origin pair-1-bcd -``` - -### Step 2: Create Pull Request on Gitea - -1. **Open browser:** Navigate to https://git.frod.dk/multiplayer/great-print-project - -2. **Go to Pull Requests tab:** Click "Pull Requests" in the top navigation - -3. **Click "New Pull Request"** - -4. **Set the branches:** - - **Base branch:** `main` (where your code will be merged) - - **Compare branch:** `pair-1-bcd` (your feature branch) - -5. **Fill in the pull request form:** - -**Title:** Clear, concise description -``` -Implement functions for Pair 1 (B, C, D) -``` - -**Description:** Use this template: - -```markdown -## What This PR Does - -Implements the following functions for Pair 1: -- `print_b()` - prints letter B -- `print_c()` - prints letter C -- `print_d()` - prints letter D - -## Testing - -- [x] Ran `python main.py` locally -- [x] All assigned letters print correctly (A B C D) -- [x] No Python syntax errors -- [x] No conflict markers left in code - -## Pair Members - -- Partner A: [Your Name Here] -- Partner B: [Partner's Name Here] - -## Notes - -Resolved a merge conflict in `print_d()` during development. Both partners agreed on the final implementation. -``` - -6. **Click "Create Pull Request"** - -Your PR is now created! Others can see it, comment on your code, and approve it. - -### Step 3: Review Another Pair's PR - -Learning to review code is just as important as writing it! - -**Find a PR to review:** - -1. Go to "Pull Requests" tab -2. Look for PRs from other pairs -3. Click on one that interests you - -**Review the code:** - -1. Click "Files Changed" tab -2. Look at the code they wrote -3. Check for: - - Do the functions follow the pattern (`print("X", end=" ")`)? - - Are there any syntax errors? - - Did they implement their assigned functions? - - Is the code clean and readable? - -**Leave comments:** - -**Positive feedback:** -``` -✅ Looks good! LGTM (Looks Good To Me) -Great work on the implementation! -``` - -**Suggestions:** -``` -💡 Suggestion: Consider adding a docstring to this function for clarity -``` - -**Questions:** -``` -❓ Question: Why did you use double quotes instead of single quotes? -(Either is fine in Python, just curious about your choice!) -``` - -**Approve the PR (if allowed):** -- Click "Approve" or "LGTM" (Looks Good To Me) - -### Step 4: Address Feedback on Your PR - -Check your own PR for comments: - -1. Go to "Pull Requests" → Your PR -2. Read any comments left by reviewers -3. If changes requested: - - Make edits locally - - Commit and push (PR updates automatically!) - - Reply to comments - -### Step 5: Merge Your PR (When Approved) - -Depending on your facilitator's setup: - -**Option A: Wait for facilitator to merge** -- Facilitator reviews all PRs -- Merges them in order -- You'll get a notification when merged - -**Option B: Merge yourself (if you have permissions)** -1. Click "Merge Pull Request" -2. Confirm the merge -3. Optionally delete your branch (server will prompt) - -**After merge:** - -Your code is now in `main`! Run this to see it: - -```bash -git switch main -git pull origin main -python main.py -``` - -You should see your letters in the final output along with letters from other pairs who have merged! - ---- - -## Part 5: Staying in Sync (15 minutes) - -As other pairs merge their PRs, the `main` branch updates. You need to stay synchronized to avoid conflicts later. - -### When to Sync - -Sync your branch with `main` when: -- You see other pairs' PRs getting merged -- Before starting new work -- When you get push rejections - -### How to Sync - -**Step 1: Update your local `main` branch** - -```bash -# Switch to main -git switch main - -# Pull latest changes -git pull origin main -``` - -You should see new commits from other pairs! - -**Step 2: Update your feature branch** - -```bash -# Switch back to your branch -git switch pair-1-bcd - -# Merge main into your branch -git merge main -``` - -**If there are NO conflicts:** - -``` -Updating abc1234..def5678 -Fast-forward - letters.py | 15 +++++++++++++++ - 2 files changed, 15 insertions(+) -``` - -Great! Your branch now has all the latest changes. - -```bash -# Push the updated branch -git push origin pair-1-bcd -``` - -**If there ARE conflicts:** - -``` -Auto-merging letters.py -CONFLICT (content): Merge conflict in letters.py -Automatic merge failed; fix conflicts and then commit the result. -``` - -Follow the conflict resolution steps from Part 3: -1. Open the file -2. Find conflict markers -3. Discuss with partner which version to keep -4. Remove markers -5. Test the code -6. `git add` the resolved file -7. `git commit` the merge -8. `git push` the resolution - -### Viewing What Changed - -Before merging, see what's new in `main`: - -```bash -# See commits in main that you don't have -git log main..pair-1-bcd --oneline - -# See code changes -git diff main..pair-1-bcd -``` - ---- - -## Commands Reference - -### Essential Git Commands for Collaboration - -**Cloning and Setup:** -```bash -git clone # Create local copy of remote repo -git config user.name "Your Name" # Set your name (one-time setup) -git config user.email "your@email.com" # Set your email (one-time setup) -``` - -**Branching:** -```bash -git switch -c # Create and switch to new branch -git switch # Switch to existing branch -git branch # List local branches (* = current) -git branch -a # List all branches (local + remote) -git branch -d # Delete branch (safe - prevents data loss) -``` - -**Making Changes:** -```bash -git status # See current state and changed files -git add # Stage specific file -git add . # Stage all changed files -git commit -m "message" # Commit staged changes with message -git commit # Commit and open editor for message -``` - -**Synchronizing with Remote:** -```bash -git pull origin # Fetch and merge from remote branch -git push origin # Push commits to remote branch -git push -u origin # Push and set upstream tracking -git fetch origin # Download changes without merging -git remote -v # Show configured remotes -``` - -**Conflict Resolution:** -```bash -git status # See which files have conflicts -# Edit files to remove <<<<<<, =======, >>>>>>> markers -git add # Mark file as resolved -git commit -m "Resolve conflict in ..." # Commit the resolution -git merge --abort # Abort merge and go back to before -``` - -**Merging and Integration:** -```bash -git merge # Merge branch into current branch -git merge main # Common: merge main into feature branch -git log --oneline --graph --all # Visualize branch history -``` - -**Viewing Changes:** -```bash -git diff # See unstaged changes -git diff --staged # See staged changes -git show # Show last commit -git log --oneline # See commit history (concise) -git log --oneline --graph # See branch structure visually -``` - ---- - -## Common Scenarios & Solutions - -### "My push was rejected!" - -**Error:** -``` -! [rejected] pair-1-bcd -> pair-1-bcd (fetch first) -error: failed to push some refs to 'https://git.frod.dk/...' -``` - -**What it means:** Someone else (probably your partner) pushed commits to this branch since you last pulled. - -**Solution:** -```bash -# Pull their changes first -git pull origin pair-1-bcd - -# If conflicts, resolve them (see Part 3) -# If no conflicts, you can now push -git push origin pair-1-bcd -``` - ---- - -### "I have merge conflicts!" - -**What you see:** -``` -CONFLICT (content): Merge conflict in letters.py -Automatic merge failed; fix conflicts and then commit the result. -``` - -**Solution:** -1. Don't panic - this is normal! -2. Run `git status` to see which files have conflicts -3. Open the conflicted file(s) in your editor -4. Find the conflict markers: `<<<<<<<`, `=======`, `>>>>>>>` -5. **Talk with your partner** - decide which version to keep -6. Remove ALL markers and keep the agreed code -7. Test: `python main.py` (make sure it works!) -8. Stage: `git add letters.py` -9. Commit: `git commit -m "Resolve conflict in letters.py"` -10. Push: `git push origin pair-1-bcd` - ---- - -### "My partner pushed, how do I get their changes?" - -**Solution:** -```bash -# Make sure you're on the right branch -git branch -# Should show: * pair-1-bcd - -# Pull their changes -git pull origin pair-1-bcd -``` - -If you have uncommitted changes, Git might ask you to commit or stash first: -```bash -# Option 1: Commit your changes first -git add . -git commit -m "Work in progress" -git pull origin pair-1-bcd - -# Option 2: Stash your changes temporarily -git stash -git pull origin pair-1-bcd -git stash pop # Restore your changes after pull -``` - ---- - -### "We both edited the same line!" - -**This creates a conflict - which is exactly what we want to practice!** - -**Solution:** -Follow the complete conflict resolution workflow from Part 3. The key steps: -1. Partner who tries to push second gets rejection -2. They pull (sees conflict) -3. Both partners look at the conflict markers together -4. Decide which version to keep (or create new version) -5. Remove markers, test, commit, push - ---- - -### "I committed to the wrong branch!" - -**Scenario:** You meant to commit to `pair-1-bcd` but you're on `main`. - -**Solution (if you haven't pushed yet):** -```bash -# Find the commit hash -git log --oneline -n 1 -# Example output: abc1234 Add print_b() - -# Switch to correct branch -git switch pair-1-bcd - -# Cherry-pick the commit -git cherry-pick abc1234 - -# Go back to main and undo the wrong commit -git switch main -git reset --hard origin/main # Dangerous: only if you haven't pushed! -``` - -**If you already pushed:** Ask facilitator for help or create a revert commit. - ---- - -### "How do I see what changed?" - -**Before committing:** -```bash -git diff # See unstaged changes -git diff --staged # See staged changes (after git add) -``` - -**After committing:** -```bash -git show # Show last commit's changes -git log --oneline # See commit history (one line per commit) -git log --oneline --graph # See branch structure with commits -git log -p # See commits WITH their code changes -``` - -**Compare branches:** -```bash -# See what's in main that you don't have -git log pair-1-bcd..main --oneline - -# See code differences between branches -git diff pair-1-bcd..main -``` - ---- - -### "I want to start over on this file!" - -**Scenario:** You made a mess and want to restore the file to the last committed version. - -**Solution:** -```bash -# Discard all changes to the file (CAREFUL: can't undo this!) -git restore letters.py - -# Or restore to a specific commit -git restore --source=abc1234 letters.py -``` - -**If you want to keep your changes but try a different approach:** -```bash -# Save your work temporarily -git stash - -# Work is saved, file is back to clean state -# Later, restore your work: -git stash pop -``` - ---- - -## Troubleshooting - -### Authentication Issues - -**Problem:** "Authentication failed" when pushing or pulling - -**Solution (HTTPS):** -1. Make sure you're using your **access token** as the password, not your account password -2. Check token has `repo` permissions in Gitea settings -3. Try cloning with token in URL (not recommended for security, but useful for debugging): - ```bash - git clone https://username:TOKEN@git.frod.dk/multiplayer/great-print-project.git - ``` - -**Solution (SSH):** -1. Verify your SSH key is added to Gitea: Settings → SSH Keys -2. Test SSH connection: - ```bash - ssh -T git@git.frod.dk - ``` -3. Make sure you cloned with SSH URL (`git@git.frod.dk:...`) not HTTPS URL - ---- - -### Can't Pull or Push - "Unrelated Histories" - -**Problem:** -``` -fatal: refusing to merge unrelated histories -``` - -**What happened:** Your local branch and remote branch don't share a common ancestor (rare, but happens if branches were created independently). - -**Solution:** -```bash -git pull origin pair-1-bcd --allow-unrelated-histories -``` - -Then resolve any conflicts if they appear. - ---- - -### Accidentally Deleted Important Code - -**Problem:** "I deleted something important and committed it!" - -**Solution:** - -**If not pushed yet:** -```bash -# Find the commit before deletion -git log --oneline - -# Example: abc1234 was the last good commit -git reset --hard abc1234 -``` - -**If already pushed:** -```bash -# Find the commit with the deleted code -git log --oneline - -# Restore the file from that commit -git checkout abc1234 -- letters.py - -# Commit the restoration -git add letters.py -git commit -m "Restore accidentally deleted code" -git push origin pair-1-bcd -``` - -**Pro tip:** Use `git log --all --full-history -- letters.py` to see all commits that touched that file. - ---- - -### File Has Conflict Markers After "Resolving" - -**Problem:** You thought you resolved the conflict, but when you run `python main.py`: -``` - File "letters.py", line 42 - <<<<<<< HEAD - ^ -SyntaxError: invalid syntax -``` - -**What happened:** You forgot to remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). - -**Solution:** -```bash -# Open the file -nano letters.py # or vim, code, etc. - -# Search for "<<<<<<<" and remove ALL markers -# Keep only the code you want - -# Test again -python main.py - -# If it works, commit the fix -git add letters.py -git commit -m "Remove remaining conflict markers" -``` - ---- - -### Wrong Pair Assignment - -**Problem:** "We implemented the wrong functions!" - -**Solution:** -1. **Don't panic** - your Git skills are still valid! -2. Check with facilitator about your correct assignment -3. Either: - - **Option A:** Keep your current work and implement correct functions in a new commit - - **Option B:** Revert your commits and start fresh with correct functions -4. Communicate with other pairs to avoid duplicate work - ---- - -## Success Criteria - -You've completed this module when you can check off ALL of these: - -**Basic Collaboration:** -- [ ] Cloned the repository from https://git.frod.dk/multiplayer -- [ ] Created a feature branch with your pair (both using same branch name) -- [ ] Completed your assigned functions (all 2-3 functions) -- [ ] Successfully pushed and pulled changes with your partner - -**Conflict Resolution:** -- [ ] Deliberately created a merge conflict with your partner -- [ ] Saw the conflict markers in the file (`<<<<<<<`, `=======`, `>>>>>>>`) -- [ ] Resolved the conflict by editing the file -- [ ] Successfully pushed the resolution - -**Pull Requests:** -- [ ] Created a pull request from your branch to `main` -- [ ] Wrote a meaningful PR description -- [ ] Reviewed at least one other pair's pull request -- [ ] Your pull request was merged (or is approved and waiting for merge) - -**Integration:** -- [ ] When you run `python main.py` on the `main` branch, your letters/numbers appear in the output -- [ ] No conflict markers remain in any files -- [ ] All tests pass (if applicable) - -**Bonus (if time permits):** -- [ ] Synced your branch with latest `main` after other pairs merged -- [ ] Helped another pair resolve a conflict -- [ ] Left constructive code review comments on 2+ pull requests - ---- - -## What You've Learned - -**Collaborative Git Skills:** -- ✅ Cloning repositories from remote Git servers -- ✅ Working with teammates on shared branches -- ✅ The push/pull cycle for synchronizing work -- ✅ Creating and resolving real merge conflicts -- ✅ Creating meaningful pull requests -- ✅ Reviewing others' code and leaving feedback -- ✅ Staying synchronized with team changes -- ✅ Using Git in a realistic team environment - -**Real-World Applications:** - -**These skills are exactly what you'll use at work:** -- This workflow works on GitHub, GitLab, Bitbucket, Azure DevOps, and any Git server -- Professional teams do this hundreds of times per day -- Open source projects use pull requests for all contributions -- Companies require code reviews before merging to production - -**You're now ready to:** -- Contribute to open source projects on GitHub -- Join a development team and collaborate effectively -- Handle merge conflicts without panic -- Review teammates' code professionally -- Work on distributed teams across time zones - ---- - -## What's Next? - -### More Advanced Git Modules - -Continue your Git journey with advanced techniques: - -- **02-advanced/01-rebasing**: Learn to rebase instead of merge for cleaner history -- **02-advanced/02-interactive-rebase**: Clean up messy commits before submitting PRs -- **02-advanced/03-worktrees**: Work on multiple branches simultaneously -- **02-advanced/04-bisect**: Find bugs using binary search through commit history -- **02-advanced/05-blame**: Investigate who changed what and when -- **02-advanced/06-merge-strategies**: Master different merge strategies and when to use them - -### Practice More - -- Try contributing to a real open source project on GitHub -- Practice more complex workflows (multiple feature branches, rebasing, etc.) -- Help teammates at work or school with Git issues - ---- - -## Congratulations! - -**You've completed the Multiplayer Git module!** - -You started this workshop learning basic Git commands like `git init` and `git commit`. Now you're collaborating with teammates, resolving conflicts, and reviewing code like a professional developer. - -**What makes you different from most beginners:** -- You've experienced REAL merge conflicts and resolved them -- You've worked on a REAL shared repository with teammates -- You've created REAL pull requests and reviewed code -- You've practiced the entire workflow professionals use daily - -**Most importantly:** You're no longer afraid of merge conflicts. You know exactly what to do when you see those `<<<<<<<` markers. - -**Keep practicing, keep collaborating, and welcome to the world of professional Git!** - ---- - -**Happy Collaborating!** From 7b638d27de80eff8d6077ccc8befe3c54f037094 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 12:51:43 +0100 Subject: [PATCH 38/61] refactor: we're breaking out merge-conflicts --- .../03-branching-and-merging/README.md | 991 +++++------------- .../03-branching-and-merging/challenge | 1 + .../03-branching-and-merging/reset.ps1 | 221 +--- .../03-branching-and-merging/setup.ps1 | 301 +++--- .../03-branching-and-merging/verify.ps1 | 337 ++---- 01-essentials/04-merge-conflict/README.md | 487 +++++++++ 01-essentials/04-merge-conflict/challenge | 1 + 01-essentials/04-merge-conflict/reset.ps1 | 23 + 01-essentials/04-merge-conflict/setup.ps1 | 125 +++ 01-essentials/04-merge-conflict/verify.ps1 | 208 ++++ .../README.md | 0 .../reset.ps1 | 0 .../setup.ps1 | 0 .../verify.ps1 | 0 .../{05-revert => 06-revert}/README.md | 0 .../{05-revert => 06-revert}/reset.ps1 | 0 .../{05-revert => 06-revert}/setup.ps1 | 0 .../{05-revert => 06-revert}/verify.ps1 | 0 .../{06-reset => 07-reset}/README.md | 0 .../{06-reset => 07-reset}/reset.ps1 | 0 .../{06-reset => 07-reset}/setup.ps1 | 0 .../{06-reset => 07-reset}/verify.ps1 | 0 .../{07-stash => 08-stash}/README.md | 0 .../{07-stash => 08-stash}/reset.ps1 | 0 .../{07-stash => 08-stash}/setup.ps1 | 0 .../{07-stash => 08-stash}/verify.ps1 | 0 .../01_FACILITATOR.md | 0 .../02_README.md | 0 .../03_TASKS.md | 0 29 files changed, 1385 insertions(+), 1310 deletions(-) create mode 160000 01-essentials/03-branching-and-merging/challenge create mode 100644 01-essentials/04-merge-conflict/README.md create mode 160000 01-essentials/04-merge-conflict/challenge create mode 100644 01-essentials/04-merge-conflict/reset.ps1 create mode 100644 01-essentials/04-merge-conflict/setup.ps1 create mode 100644 01-essentials/04-merge-conflict/verify.ps1 rename 01-essentials/{04-cherry-pick => 05-cherry-pick}/README.md (100%) rename 01-essentials/{04-cherry-pick => 05-cherry-pick}/reset.ps1 (100%) rename 01-essentials/{04-cherry-pick => 05-cherry-pick}/setup.ps1 (100%) rename 01-essentials/{04-cherry-pick => 05-cherry-pick}/verify.ps1 (100%) rename 01-essentials/{05-revert => 06-revert}/README.md (100%) rename 01-essentials/{05-revert => 06-revert}/reset.ps1 (100%) rename 01-essentials/{05-revert => 06-revert}/setup.ps1 (100%) rename 01-essentials/{05-revert => 06-revert}/verify.ps1 (100%) rename 01-essentials/{06-reset => 07-reset}/README.md (100%) rename 01-essentials/{06-reset => 07-reset}/reset.ps1 (100%) rename 01-essentials/{06-reset => 07-reset}/setup.ps1 (100%) rename 01-essentials/{06-reset => 07-reset}/verify.ps1 (100%) rename 01-essentials/{07-stash => 08-stash}/README.md (100%) rename 01-essentials/{07-stash => 08-stash}/reset.ps1 (100%) rename 01-essentials/{07-stash => 08-stash}/setup.ps1 (100%) rename 01-essentials/{07-stash => 08-stash}/verify.ps1 (100%) rename 01-essentials/{08-multiplayer => 09-multiplayer}/01_FACILITATOR.md (100%) rename 01-essentials/{08-multiplayer => 09-multiplayer}/02_README.md (100%) rename 01-essentials/{08-multiplayer => 09-multiplayer}/03_TASKS.md (100%) diff --git a/01-essentials/03-branching-and-merging/README.md b/01-essentials/03-branching-and-merging/README.md index 55c028b..f40ef70 100644 --- a/01-essentials/03-branching-and-merging/README.md +++ b/01-essentials/03-branching-and-merging/README.md @@ -1,22 +1,16 @@ # Module 03: Branching and Merging -## About This Module +## Learning Objectives -Welcome to Module 03! This module is different from the others - it uses a **checkpoint system** that lets you work through three related concepts in one continuous repository: +By the end of this module, you will: +- Understand what branches are and why they're useful +- Create and switch between branches +- Make commits on different branches +- Merge branches together +- Visualize branch history with `git log --graph` +- Understand merge commits and how they work -1. **Branching Basics** - Create and work with feature branches -2. **Merging Branches** - Combine branches together -3. **Resolving Merge Conflicts** - Fix conflicts when Git can't merge automatically - -Instead of three separate modules, you'll progress through checkpoints in a single Git repository, building on each previous section. You can jump between checkpoints, skip ahead, or restart any section at any time! - -### Why Checkpoints? - -Branching, merging, and conflict resolution are naturally connected - you can't understand merging without branches, and you can't master conflicts without trying to merge. The checkpoint system lets you learn these concepts as a continuous workflow, just like real development. - -## Quick Start - -### Setup +## Setup Create the challenge environment: @@ -24,816 +18,403 @@ Create the challenge environment: .\setup.ps1 ``` -This creates a complete Git repository with all checkpoints ready. +This creates a repository with a realistic project history showing multiple merged feature branches. -### Working with Checkpoints +## Overview -**View available checkpoints:** -```bash -.\reset.ps1 -``` +**Branching** lets you create independent lines of development. Think of it like parallel universes for your code - you can experiment on one branch without affecting others. -**Jump to a specific checkpoint:** -```bash -.\reset.ps1 start # Checkpoint 1: Branching Basics -.\reset.ps1 merge # Checkpoint 2: Merging Branches -.\reset.ps1 merge-conflict # Checkpoint 3: Resolving Conflicts -``` - -**Verify your progress:** -```bash -.\verify.ps1 # Verify all checkpoints complete -.\verify.ps1 start # Verify Checkpoint 1 only -.\verify.ps1 merge # Verify Checkpoint 2 only -.\verify.ps1 merge-conflict # Verify Checkpoint 3 only -``` - -### Recommended Workflow - -Complete checkpoints in order: -1. Start with Checkpoint 1 (Branching Basics) -2. Progress to Checkpoint 2 (Merging) -3. Finish with Checkpoint 3 (Merge Conflicts) - -Or skip to any checkpoint if you already know the earlier concepts! - ---- - -## Checkpoint 1: Branching Basics - -### Learning Objectives - -- Understand what a branch is in Git -- Create new branches with `git switch -c` -- Switch between branches with `git switch` -- View all branches with `git branch` -- Understand that branches are independent lines of development - -### Your Task - -Create a feature branch called `feature-login`, add a `login.py` file, and make commits to demonstrate that branches allow independent development. - -**Steps:** - -1. Navigate to the challenge directory: `cd challenge` -2. Create a new branch: `git switch -c feature-login` -3. Create a file: `login.py` (with any content you like) -4. Commit your file: `git add login.py && git commit -m "Add login module"` -5. Make another change to `login.py` and commit it -6. Switch back to main: `git switch main` -7. Notice that `login.py` doesn't exist on main! -8. Switch back to your feature: `git switch feature-login` -9. Notice that `login.py` exists again! - -**Verify:** Run `.\verify.ps1 start` to check your solution. - -### What is a Branch? - -A **branch** in Git is an independent line of development. Think of it as a parallel universe for your code - you can make changes without affecting the main timeline. - -**Visual representation:** - -``` -main: A---B---C - \ -feature-login: D---E -``` - -- Both branches share commits A and B -- Branch `main` continues with commit C -- Branch `feature-login` goes in a different direction with commits D and E -- Changes in one branch don't affect the other! +**Merging** combines work from different branches back together. ### Why Use Branches? -Branches let you: -- **Experiment safely** - Try new ideas without breaking main +- **Experiment safely** - Try new ideas without breaking main code - **Work in parallel** - Multiple features can be developed simultaneously - **Organize work** - Each feature/fix gets its own branch - **Collaborate better** - Team members work on separate branches -### Key Concepts +## Your Task -- **Branch**: A lightweight movable pointer to a commit -- **HEAD**: A pointer showing which branch you're currently on -- **main**: The default branch (formerly called "master") -- **Feature branch**: A branch created for a specific feature or task +### Part 1: Explore the Repository -### Useful Commands - -```bash -# View all branches (current branch marked with *) -git branch - -# Create a new branch -git branch feature-login - -# Switch to a branch -git switch feature-login - -# Create AND switch in one command -git switch -c feature-login - -# Switch back to previous branch -git switch - - -# Delete a branch (only if merged) -git branch -d feature-login - -# Force delete a branch -git branch -D feature-login -``` - -### Understanding HEAD - -`HEAD` is Git's way of saying "you are here." It points to your current branch. - -When you run `git switch main`, HEAD moves to point to main. -When you run `git switch feature-login`, HEAD moves to point to feature-login. - ---- - -## Checkpoint 2: Merging Branches - -**Prerequisites:** Complete Checkpoint 1 OR run `.\reset.ps1 merge` - -### Learning Objectives - -- Understand what merging means in Git -- Merge a feature branch back into main -- Use `git merge` to combine branches -- Understand merge commits -- Visualize merged branches with `git log --graph` - -### Your Task - -You've completed work on your `feature-login` branch. Now merge it back into `main` to include the login functionality in your main codebase. - -**Scenario:** -- You created the `feature-login` branch and added login functionality -- Meanwhile, development continued on `main` (README and app.py were added) -- Now you need to merge your login feature into main - -**Steps:** - -1. Make sure you're in the challenge directory: `cd challenge` -2. Check which branch you're on: `git branch` -3. Switch to main if needed: `git switch main` -4. View the branch structure: `git log --oneline --graph --all` -5. Merge feature-login into main: `git merge feature-login` -6. View the result: `git log --oneline --graph --all` - -**Verify:** Run `.\verify.ps1 merge` to check your solution. - -### What is Merging? - -**Merging** is the process of combining changes from one branch into another. - -Think of it like combining two streams into one river - all the water (code) flows together. - -#### Before Merging - -You have two branches with different work: - -``` -main: A---B---C---D - \ -feature-login: E---F -``` - -- Main branch progressed with commits C and D -- Feature-login branch has commits E and F -- They diverged at commit B - -#### After Merging - -You bring the feature branch into main: - -``` -main: A---B---C---D---M - \ / -feature-login: E-----F -``` - -- Commit M is a **merge commit** - it combines both branches -- Main now has all the work from both branches -- Your login feature is now part of main! - -### How to Merge - -Merging is simple - just two steps: - -**1. Switch to the branch you want to merge INTO:** -```bash -git switch main -``` -This is the branch that will receive the changes. - -**2. Merge the other branch:** -```bash -git merge feature-login -``` -This brings changes from `feature-login` into `main`. - -**That's it!** Git automatically combines the changes. - -### Understanding Merge Commits - -When you merge, Git creates a special commit called a **merge commit**. - -**What makes it special?** -- It has TWO parent commits (one from each branch) -- It represents the point where branches come back together -- The message typically says "Merge branch 'feature-login'" - -**See your merge commit:** -```bash -git log --oneline -``` - -Look for the merge commit at the top - it will say something like: -``` -abc1234 Merge branch 'feature-login' -``` - -### Types of Merges - -**Three-way merge** (what you just did): -- Both branches have new commits -- Git creates a merge commit -- History shows both branches clearly - -**Fast-forward merge**: -- Main hasn't changed since the branch was created -- Git just moves the main pointer forward -- No merge commit needed! - -``` -# Before (fast-forward merge) -main: A---B - \ -feature: C---D - -# After (main just moves forward) -main: A---B---C---D -``` - -### Visualizing Branches - -The `--graph` flag is your best friend: - -```bash -git log --oneline --graph --all -``` - -**What the graph shows:** -- `*` = A commit -- `|` = A branch line -- `/` and `\` = Branches splitting/joining -- Branch names in parentheses - -**Example output:** -``` -* a1b2c3d (HEAD -> main) Merge branch 'feature-login' -|\ -| * e4f5g6h (feature-login) Add password validation -| * i7j8k9l Add login module -* | m1n2o3p Add README documentation -* | q4r5s6t Add app.py entry point -|/ -* u7v8w9x Add main functionality -* y1z2a3b Initial commit -``` - -### Useful Commands - -```bash -# Merge a branch into your current branch -git merge - -# Abort a merge if something goes wrong -git merge --abort - -# View merge commits only -git log --merges - -# View branch structure -git log --oneline --graph --all - -# See which branches have been merged into main -git branch --merged main - -# See which branches haven't been merged -git branch --no-merged main -``` - ---- - -## Checkpoint 3: Resolving Merge Conflicts - -**Prerequisites:** Complete Checkpoint 2 OR run `.\reset.ps1 merge-conflict` - -### Learning Objectives - -- Understand what merge conflicts are and why they occur -- Identify merge conflicts in your repository -- Read and interpret conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) -- Resolve merge conflicts manually -- Complete a merge after resolving conflicts - -### Your Task - -You have an `update-config` branch that modified `config.json`, and the main branch also modified `config.json` in a different way. When you try to merge, Git can't automatically combine them - you'll need to resolve the conflict manually. - -**Your mission:** -1. Attempt to merge the `update-config` branch into `main` -2. Git will tell you there's a conflict - don't panic! -3. Resolve the conflict by keeping BOTH settings (timeout AND debug) -4. Complete the merge - -**Steps:** - -1. Make sure you're in challenge directory: `cd challenge` -2. Verify you're on main: `git branch` -3. Try to merge: `git merge update-config` -4. Git will report a conflict! -5. Open `config.json` in your text editor -6. Follow the resolution guide below -7. Save the file -8. Stage the resolved file: `git add config.json` -9. Complete the merge: `git commit` - -**Verify:** Run `.\verify.ps1 merge-conflict` to check your solution. - -### What Are Merge Conflicts? - -A **merge conflict** occurs when Git cannot automatically combine changes because both branches modified the same part of the same file. - -**Example scenario:** -``` -main branch: changes line 5 to: "timeout": 5000 -update-config: changes line 5 to: "debug": true -``` - -Git doesn't know which one you want (or if you want both)! So it asks you to decide. - -**When do conflicts happen?** -- ✅ Two branches modify the same lines in a file -- ✅ One branch deletes a file that another branch modifies -- ✅ Complex changes Git can't merge automatically -- ❌ Different files are changed (no conflict!) -- ❌ Different parts of the same file are changed (no conflict!) - -**Don't fear conflicts!** They're a normal part of collaborative development. Git just needs your help to decide what the final code should look like. - -### Step-by-Step: Resolving Your First Conflict - -#### Step 1: Attempt the Merge +First, explore the existing history to see what merging looks like: ```bash cd challenge -git merge update-config +git log --oneline --graph --all ``` -**You'll see:** -``` -Auto-merging config.json -CONFLICT (content): Merge conflict in config.json -Automatic merge failed; fix conflicts and then commit the result. +You'll see a visual graph showing: +- The `main` branch timeline +- Feature branches that split off from main +- Merge points where branches come back together + +**Study the graph:** +- Look for the `*` symbols (commits) +- Notice the `|`, `/`, and `\` characters (branch lines) +- Find the merge commits (they have two parent lines converging) + +**Explore the branches:** +```bash +# See all branches +git branch --all + +# Check which files exist on different branches +git ls-tree --name-only feature-login +git ls-tree --name-only feature-api +git ls-tree --name-only main ``` -**Don't panic!** This is normal. Git is just asking for your help. +**View specific merges:** +```bash +# See all merge commits +git log --merges --oneline -#### Step 2: Check What Happened +# See details of a specific merge +git show +``` + +### Part 2: Create Your Own Branch + +Now practice creating your own branch: ```bash -git status +# Create and switch to a new branch +git switch -c my-feature + +# Verify you're on the new branch +git branch ``` -**You'll see:** -``` -On branch main -You have unmerged paths. - (fix conflicts and run "git commit") - (use "git merge --abort" to abort the merge) +The `*` shows which branch you're currently on. -Unmerged paths: - (use "git add ..." to mark resolution) - both modified: config.json -``` +### Part 3: Make Commits on Your Branch -This tells you that `config.json` needs your attention! - -#### Step 3: Open the Conflicted File - -Open `config.json` in your text editor. You'll see special **conflict markers**: - -```json -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, -<<<<<<< HEAD - "timeout": 5000 -======= - "debug": true ->>>>>>> update-config - } -} -``` - -#### Step 4: Understand the Conflict Markers - -``` -<<<<<<< HEAD - "timeout": 5000 ← Your current branch (main) -======= - "debug": true ← The branch you're merging (update-config) ->>>>>>> update-config -``` - -**What each marker means:** -- `<<<<<<< HEAD` - Start of your changes (current branch) -- `=======` - Separator between the two versions -- `>>>>>>> update-config` - End of their changes (branch being merged) - -#### Step 5: Decide What to Keep - -You have three options: - -**Option 1: Keep ONLY your changes (timeout)** -```json - "timeout": 5000 -``` - -**Option 2: Keep ONLY their changes (debug)** -```json - "debug": true -``` - -**Option 3: Keep BOTH changes** ← This is what we want! -```json - "timeout": 5000, - "debug": true -``` - -For this challenge, choose **Option 3** - keep both settings! - -#### Step 6: Edit the File - -Delete ALL the conflict markers and keep both settings: - -**Before (with conflict markers):** -```json -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, -<<<<<<< HEAD - "timeout": 5000 -======= - "debug": true ->>>>>>> update-config - } -} -``` - -**After (resolved):** -```json -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, - "timeout": 5000, - "debug": true - } -} -``` - -**Important:** -- Remove `<<<<<<< HEAD` -- Remove `=======` -- Remove `>>>>>>> update-config` -- Keep both the timeout and debug settings -- Ensure valid JSON syntax (notice the comma after timeout!) - -#### Step 7: Save the File - -Save `config.json` with your changes. - -#### Step 8: Stage the Resolved File - -Tell Git you've resolved the conflict: +Add some changes to your branch: ```bash -git add config.json +# Create a new file or modify an existing one +echo "# My Feature" > my-feature.md + +# Stage and commit +git add . +git commit -m "Add my feature" + +# Make another commit +echo "More details" >> my-feature.md +git add . +git commit -m "Expand feature documentation" ``` -#### Step 9: Check Status +**Important:** Changes on your branch don't affect main! ```bash -git status +# Switch to main +git switch main + +# Notice your my-feature.md doesn't exist here +ls + +# Switch back to your branch +git switch my-feature + +# Now it exists again! +ls ``` -**You'll see:** -``` -On branch main -All conflicts fixed but you are still merging. - (use "git commit" to conclude merge) -``` +### Part 4: Merge Your Branch -Perfect! Git confirms the conflict is resolved. - -#### Step 10: Complete the Merge - -Commit the merge: +Bring your work into main: ```bash -git commit +# Switch to the branch you want to merge INTO +git switch main + +# Merge your feature branch +git merge my-feature + +# View the result +git log --oneline --graph --all ``` -Git will open an editor with a default merge message. You can accept it or customize it. +You should see your feature branch merged into main! -**Done!** Your merge is complete! +### Part 5: Practice More -### Common Mistakes to Avoid - -❌ **Forgetting to remove conflict markers** -```json -<<<<<<< HEAD ← Don't leave these in! - "timeout": 5000, - "debug": true ->>>>>>> update-config ← Don't leave these in! -``` -This breaks your code! Always remove ALL markers. - -❌ **Committing without staging** -```bash -git commit # Error! You didn't add the file -``` -Always `git add` the resolved file first! - -❌ **Keeping only one side when both are needed** -If you delete one setting, you lose that work. For this challenge, you need BOTH! - -❌ **Breaking syntax** -```json -"timeout": 5000 ← Missing comma! -"debug": true -``` -Always verify your file is valid after resolving! - -### Aborting a Merge - -Changed your mind? You can abort the merge anytime: +Create additional branches to practice: ```bash -git merge --abort +# Create another feature +git switch -c another-feature + +# Add changes and commits +# ... your work ... + +# Merge it +git switch main +git merge another-feature ``` -This returns your repository to the state before you started the merge. No harm done! +**Verify your work:** +```bash +# From the module directory (not inside challenge/) +.\verify.ps1 +``` -### Useful Commands +## Understanding Branches + +### What is a Branch? + +A **branch** is a lightweight movable pointer to a commit. When you create a branch, Git creates a new pointer - it doesn't copy all your files! + +``` +main: A---B---C + \ +my-feature: D---E +``` + +- Both branches share commits A and B +- Main has commit C +- My-feature has commits D and E +- They're independent! + +### What is HEAD? + +`HEAD` points to your current branch. It's Git's way of saying "you are here." ```bash -# Attempt a merge -git merge +# HEAD points to main +git switch main -# Check which files have conflicts -git status - -# Abort the merge and start over -git merge --abort - -# After resolving conflicts: -git add -git commit - -# View conflicts in a different style -git diff --ours # Your changes -git diff --theirs # Their changes -git diff --base # Original version +# Now HEAD points to my-feature +git switch my-feature ``` -### Pro Tips +## Understanding Merging -💡 **Prevent conflicts** -- Pull changes frequently: `git pull` -- Communicate with your team about who's working on what -- Keep branches short-lived and merge often +### Types of Merges -💡 **Make conflicts easier** -- Work on different files when possible -- If you must edit the same file, coordinate with teammates -- Make small, focused commits +**Three-way merge** (most common): +``` +Before: +main: A---B---C + \ +feature: D---E -💡 **When stuck** -- Read the conflict markers carefully -- Look at `git log` to understand what each side changed -- Ask a teammate to review your resolution -- Use a merge tool: `git mergetool` +After merge: +main: A---B---C---M + \ / +feature: D---E +``` ---- +Git creates a merge commit `M` that has two parents (C and E). -## Complete Command Reference +**Fast-forward merge**: +``` +Before: +main: A---B + \ +feature: C---D + +After merge: +main: A---B---C---D +``` + +If main hasn't changed, Git just moves the pointer forward. No merge commit needed! + +## Key Commands ### Branching ```bash -git branch # List all branches -git branch feature-name # Create a new branch -git switch branch-name # Switch to a branch -git switch -c feature-name # Create and switch -git switch - # Switch to previous branch -git branch -d feature-name # Delete branch (if merged) -git branch -D feature-name # Force delete branch +# List all branches (* shows current branch) +git branch + +# Create a new branch +git branch feature-name + +# Create AND switch to new branch +git switch -c feature-name + +# Switch to existing branch +git switch branch-name + +# Switch to previous branch +git switch - + +# Delete a branch (only if merged) +git branch -d feature-name + +# Force delete a branch +git branch -D feature-name ``` ### Merging ```bash -git merge branch-name # Merge a branch into current branch -git merge --no-ff branch-name # Force a merge commit -git merge --abort # Abort a merge in progress -git log --merges # View only merge commits +# Merge a branch into your current branch +git merge branch-name + +# Abort a merge if something goes wrong +git merge --abort + +# Force a merge commit (even if fast-forward possible) +git merge --no-ff branch-name ``` ### Viewing History ```bash -git log --oneline --graph --all # Visual branch structure -git log --oneline # Compact commit list -git log --graph --decorate --all # Detailed branch view -git log main..feature-login # Commits in feature not in main -git diff main...feature-login # Changes between branches +# Visual branch graph +git log --oneline --graph --all + +# Compact history +git log --oneline + +# Only merge commits +git log --merges + +# Show which branches have been merged into main +git branch --merged main + +# Show which branches haven't been merged +git branch --no-merged main ``` -### Conflict Resolution +## Common Workflows + +### Creating a Feature ```bash -git status # See conflicted files -git diff # View conflicts -git add resolved-file # Mark file as resolved -git commit # Complete the merge -git merge --abort # Give up and start over +# Start from main +git switch main + +# Create feature branch +git switch -c feature-awesome + +# Make changes +echo "cool stuff" > feature.txt +git add . +git commit -m "Add awesome feature" + +# More changes... +echo "more cool stuff" >> feature.txt +git add . +git commit -m "Improve awesome feature" ``` -### Checkpoint Commands (This Module) +### Merging a Feature ```bash -.\reset.ps1 # Show available checkpoints -.\reset.ps1 start # Jump to Checkpoint 1 -.\reset.ps1 merge # Jump to Checkpoint 2 -.\reset.ps1 merge-conflict # Jump to Checkpoint 3 -.\verify.ps1 # Verify all complete -.\verify.ps1 start # Verify Checkpoint 1 -.\verify.ps1 merge # Verify Checkpoint 2 -.\verify.ps1 merge-conflict # Verify Checkpoint 3 +# Switch to main +git switch main + +# Merge your feature +git merge feature-awesome + +# Delete the feature branch (optional) +git branch -d feature-awesome ``` ---- +### Keeping Main Updated While Working + +```bash +# You're on feature-awesome +git switch feature-awesome + +# Main branch has new commits from others +# Bring those into your feature branch +git switch main +git pull +git switch feature-awesome +git merge main + +# Or in one command (more advanced): +git pull origin main +``` ## Troubleshooting ### "I'm on the wrong branch!" ```bash -git switch main # Switch to main -git branch # Verify current branch +# Switch to the correct branch +git switch correct-branch + +# Check current branch anytime +git branch ``` ### "I made commits on the wrong branch!" -Don't panic! You can move them: +Don't panic! You can move commits to another branch: ```bash -# You're on main but should be on feature-login -git switch feature-login # Switch to correct branch -git merge main # Bring the commits over -git switch main -git reset --hard HEAD~1 # Remove from main (careful!) +# Create the correct branch from current state +git branch correct-branch + +# Switch to wrong branch and remove the commits +git switch wrong-branch +git reset --hard HEAD~2 # Remove last 2 commits (adjust number) + +# Switch to correct branch - commits are there! +git switch correct-branch ``` -Or use cherry-pick (covered in a later module). - -### "The merge created a mess!" - -Abort and try again: +### "The merge created unexpected results!" ```bash +# Undo the merge git merge --abort -git status # Verify you're back to clean state + +# Or if already committed: +git reset --hard HEAD~1 ``` -### "I want to start this checkpoint over!" - -Use the reset script: +### "I want to see what changed in a merge!" ```bash -.\reset.ps1 start # Go back to Checkpoint 1 +# Show the merge commit +git show + +# Compare two branches before merging +git diff main..feature-branch ``` -This resets your repository to the beginning of that checkpoint. +## Tips for Success -### "I can't find my branch!" +💡 **Branch often** - Branches are cheap! Create one for each feature or experiment. -List all branches: +💡 **Commit before switching** - Always commit (or stash) changes before switching branches. -```bash -git branch --all # Shows all branches including remote -``` +💡 **Keep branches focused** - One feature per branch makes merging easier. -The branch might have been deleted after merging (this is normal!). +💡 **Delete merged branches** - Clean up with `git branch -d branch-name` after merging. -### "How do I know which checkpoint I'm on?" +💡 **Use descriptive names** - `feature-login` is better than `stuff` or `branch1`. -```bash -.\reset.ps1 # Shows current checkpoint -git log --oneline --graph --all --decorate # Shows all tags/branches -``` - ---- - -## Real-World Workflow Example - -Here's how professional developers use these skills: - -**Day 1: Start a new feature** -```bash -git switch main -git pull # Get latest changes -git switch -c feature-dark-mode # New feature branch -# ... make changes ... -git add . -git commit -m "Add dark mode toggle" -``` - -**Day 2: Continue work** -```bash -git switch feature-dark-mode # Resume work -# ... make more changes ... -git add . -git commit -m "Add dark mode styles" -``` - -**Day 3: Ready to merge** -```bash -git switch main -git pull # Get latest main -git switch feature-dark-mode -git merge main # Bring main's changes into feature -# Resolve any conflicts -git switch main -git merge feature-dark-mode # Merge feature into main -git push # Share with team -git branch -d feature-dark-mode # Clean up -``` - -**This is exactly what you just practiced!** - ---- +💡 **Visualize often** - Run `git log --oneline --graph --all` to understand your history. ## What You've Learned -By completing all three checkpoints, you now understand: +After completing this module, you understand: -### Checkpoint 1: Branching Basics - ✅ Branches create independent lines of development -- ✅ `git switch -c` creates and switches to a new branch +- ✅ `git switch -c` creates a new branch - ✅ Changes in one branch don't affect others -- ✅ Branches are lightweight and easy to create - -### Checkpoint 2: Merging Branches -- ✅ Merging combines work from two branches +- ✅ `git merge` combines branches - ✅ Merge commits have two parent commits -- ✅ `git merge` brings changes into your current branch -- ✅ Three-way merges create a merge commit - -### Checkpoint 3: Resolving Merge Conflicts -- ✅ Conflicts happen when the same lines are changed differently -- ✅ Conflict markers show both versions -- ✅ You choose what the final code should look like -- ✅ Conflicts are normal and easy to resolve with practice - ---- +- ✅ `git log --graph` visualizes branch history +- ✅ Branches are pointers, not copies of files ## Next Steps -**Completed the module?** Great work! You're ready to move on. +Ready to continue? The next module covers **merge conflicts** - what happens when Git can't automatically merge changes. -**Want more practice?** Jump to any checkpoint and try again: +To start over: ```bash -.\reset.ps1 start # Practice branching -.\reset.ps1 merge # Practice merging -.\reset.ps1 merge-conflict # Practice conflict resolution +.\reset.ps1 +.\setup.ps1 ``` -**Ready for the next module?** -Continue to Module 04 to learn about cherry-picking specific commits! - ---- - -**Need help?** Review the relevant checkpoint section above, or run `git status` to see what Git suggests! +**Need help?** Review the commands above, or run `git status` to see what Git suggests! diff --git a/01-essentials/03-branching-and-merging/challenge b/01-essentials/03-branching-and-merging/challenge new file mode 160000 index 0000000..8ae52cc --- /dev/null +++ b/01-essentials/03-branching-and-merging/challenge @@ -0,0 +1 @@ +Subproject commit 8ae52cc25c2a5417b1d6b5316949394da02f6914 diff --git a/01-essentials/03-branching-and-merging/reset.ps1 b/01-essentials/03-branching-and-merging/reset.ps1 index 7027dc4..664d9c5 100755 --- a/01-essentials/03-branching-and-merging/reset.ps1 +++ b/01-essentials/03-branching-and-merging/reset.ps1 @@ -1,216 +1,23 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Resets the challenge environment to a specific checkpoint. + Resets the Module 03 challenge environment. .DESCRIPTION - This script allows you to jump to any checkpoint in the module, - resetting your repository to that state. Useful for skipping ahead, - starting over, or practicing specific sections. - -.PARAMETER Checkpoint - The checkpoint to reset to: start, merge, or merge-conflict. - If not specified, displays help information. - -.EXAMPLE - .\reset.ps1 - Shows available checkpoints and current status. - -.EXAMPLE - .\reset.ps1 start - Resets to the beginning (branching basics section). - -.EXAMPLE - .\reset.ps1 merge - Jumps to the merging section (feature-login branch already exists). - -.EXAMPLE - .\reset.ps1 merge-conflict - Jumps to the conflict resolution section (merge already complete). + This script removes the challenge directory, allowing you to start fresh. + Run setup.ps1 again after resetting to recreate the environment. #> -param( - [ValidateSet('start', 'merge', 'merge-conflict', '')] - [string]$Checkpoint = '' -) +Write-Host "`n=== Resetting Module 03 Challenge ===" -ForegroundColor Cyan -# Checkpoint to tag mapping -$checkpointTags = @{ - 'start' = 'checkpoint-start' - 'merge' = 'checkpoint-merge' - 'merge-conflict' = 'checkpoint-merge-conflict' +if (Test-Path "challenge") { + Write-Host "Removing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" + Write-Host "`n[SUCCESS] Challenge environment reset complete!" -ForegroundColor Green + Write-Host "`nRun .\setup.ps1 to create a fresh challenge environment." -ForegroundColor Cyan + Write-Host "" +} else { + Write-Host "`n[INFO] No challenge directory found. Nothing to reset." -ForegroundColor Yellow + Write-Host "Run .\setup.ps1 to create the challenge environment." -ForegroundColor Cyan + Write-Host "" } - -# Checkpoint descriptions -$checkpointDescriptions = @{ - 'start' = 'Branching Basics - Create and work with feature branches' - 'merge' = 'Merging Branches - Merge feature-login into main' - 'merge-conflict' = 'Resolving Conflicts - Fix merge conflicts in config.json' -} - -# ============================================================================ -# Display help if no checkpoint specified -# ============================================================================ -if ($Checkpoint -eq '') { - Write-Host "`n=== Module 03: Branching and Merging - Checkpoints ===" -ForegroundColor Cyan - Write-Host "`nAvailable checkpoints:" -ForegroundColor White - Write-Host "" - - foreach ($key in @('start', 'merge', 'merge-conflict')) { - $desc = $checkpointDescriptions[$key] - Write-Host " $key" -ForegroundColor Green -NoNewline - Write-Host " - $desc" -ForegroundColor White - } - - Write-Host "`nUsage:" -ForegroundColor Cyan - Write-Host " .\reset.ps1 " -ForegroundColor White - Write-Host "" - Write-Host "Examples:" -ForegroundColor Cyan - Write-Host " .\reset.ps1 start # Start from the beginning" -ForegroundColor White - Write-Host " .\reset.ps1 merge # Jump to merging section" -ForegroundColor White - Write-Host " .\reset.ps1 merge-conflict # Jump to conflict resolution" -ForegroundColor White - Write-Host "" - - # Try to detect current checkpoint - if (Test-Path "challenge/.git") { - Push-Location "challenge" - $currentBranch = git branch --show-current 2>$null - $currentCommit = git rev-parse HEAD 2>$null - - # Check which checkpoint we're at - $currentCheckpoint = $null - foreach ($cp in @('start', 'merge', 'merge-conflict')) { - $tagCommit = git rev-parse $checkpointTags[$cp] 2>$null - if ($currentCommit -eq $tagCommit) { - $currentCheckpoint = $cp - break - } - } - - if ($currentCheckpoint) { - Write-Host "Current checkpoint: " -ForegroundColor Yellow -NoNewline - Write-Host "$currentCheckpoint" -ForegroundColor Green -NoNewline - Write-Host " (on branch $currentBranch)" -ForegroundColor Yellow - } else { - Write-Host "Current status: " -ForegroundColor Yellow -NoNewline - Write-Host "In progress (on branch $currentBranch)" -ForegroundColor White - } - - Pop-Location - } - - Write-Host "" - exit 0 -} - -# ============================================================================ -# Validate challenge directory exists -# ============================================================================ -if (-not (Test-Path "challenge")) { - Write-Host "[ERROR] Challenge directory not found." -ForegroundColor Red - Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow - exit 1 -} - -if (-not (Test-Path "challenge/.git")) { - Write-Host "[ERROR] No git repository found in challenge directory." -ForegroundColor Red - Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow - exit 1 -} - -# Navigate to challenge directory -Push-Location "challenge" - -# ============================================================================ -# Verify the checkpoint tag exists -# ============================================================================ -$targetTag = $checkpointTags[$Checkpoint] -$tagExists = git tag -l $targetTag - -if (-not $tagExists) { - Write-Host "[ERROR] Checkpoint tag '$targetTag' not found." -ForegroundColor Red - Write-Host "Run ..\setup.ps1 to recreate the challenge environment." -ForegroundColor Yellow - Pop-Location - exit 1 -} - -# ============================================================================ -# Check for uncommitted changes -# ============================================================================ -$statusOutput = git status --porcelain 2>$null - -if ($statusOutput) { - Write-Host "`n[WARNING] You have uncommitted changes!" -ForegroundColor Yellow - Write-Host "The following changes will be lost:" -ForegroundColor Yellow - Write-Host "" - git status --short - Write-Host "" - - $response = Read-Host "Continue and discard all changes? (y/N)" - if ($response -ne 'y' -and $response -ne 'Y') { - Write-Host "`nReset cancelled." -ForegroundColor Cyan - Pop-Location - exit 0 - } -} - -# ============================================================================ -# Reset to checkpoint -# ============================================================================ -Write-Host "`nResetting to checkpoint: $Checkpoint" -ForegroundColor Cyan -Write-Host "Description: $($checkpointDescriptions[$Checkpoint])" -ForegroundColor White -Write-Host "" - -try { - # Reset to the checkpoint tag - git reset --hard $targetTag 2>&1 | Out-Null - - # Clean untracked files - git clean -fd 2>&1 | Out-Null - - # Ensure we're on main branch - $currentBranch = git branch --show-current - if ($currentBranch -ne 'main') { - git switch main 2>&1 | Out-Null - git reset --hard $targetTag 2>&1 | Out-Null - } - - Write-Host "[SUCCESS] Reset to checkpoint '$Checkpoint' complete!" -ForegroundColor Green - Write-Host "" - - # Show what to do next - switch ($Checkpoint) { - 'start' { - Write-Host "Next steps:" -ForegroundColor Cyan - Write-Host " 1. Create a new branch: git switch -c feature-login" -ForegroundColor White - Write-Host " 2. Create login.py and make 2+ commits" -ForegroundColor White - Write-Host " 3. Verify: ..\verify.ps1 start" -ForegroundColor White - } - 'merge' { - Write-Host "Next steps:" -ForegroundColor Cyan - Write-Host " 1. View branch structure: git log --oneline --graph --all" -ForegroundColor White - Write-Host " 2. Merge feature-login: git merge feature-login" -ForegroundColor White - Write-Host " 3. Verify: ..\verify.ps1 merge" -ForegroundColor White - } - 'merge-conflict' { - Write-Host "Next steps:" -ForegroundColor Cyan - Write-Host " 1. Attempt merge: git merge update-config" -ForegroundColor White - Write-Host " 2. Resolve conflicts in config.json" -ForegroundColor White - Write-Host " 3. Complete merge: git add config.json && git commit" -ForegroundColor White - Write-Host " 4. Verify: ..\verify.ps1 merge-conflict" -ForegroundColor White - } - } - - Write-Host "" - Write-Host "View current state: git log --oneline --graph --all" -ForegroundColor Cyan - Write-Host "" - -} catch { - Write-Host "[ERROR] Failed to reset to checkpoint." -ForegroundColor Red - Write-Host $_.Exception.Message -ForegroundColor Red - Pop-Location - exit 1 -} - -Pop-Location -exit 0 diff --git a/01-essentials/03-branching-and-merging/setup.ps1 b/01-essentials/03-branching-and-merging/setup.ps1 index 8d15534..a9ca504 100755 --- a/01-essentials/03-branching-and-merging/setup.ps1 +++ b/01-essentials/03-branching-and-merging/setup.ps1 @@ -1,17 +1,13 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Sets up the Module 03 checkpoint-based challenge environment. + Sets up the Module 03 challenge environment for branching and merging. .DESCRIPTION - This script creates a challenge directory with a complete Git repository - containing all commits and checkpoints for learning branching, merging, - and merge conflict resolution in one continuous workflow. - - The script creates three checkpoints: - - checkpoint-start: Beginning of branching basics - - checkpoint-merge: Beginning of merging section - - checkpoint-merge-conflict: Beginning of conflict resolution + This script creates a challenge directory with a Git repository containing + a realistic project history with multiple merged branches. Students will see + what branching and merging looks like in practice, then create their own + branches to experiment. #> Write-Host "`n=== Setting up Module 03: Branching and Merging ===" -ForegroundColor Cyan @@ -36,113 +32,75 @@ git config user.name "Workshop Student" git config user.email "student@example.com" # ============================================================================ -# PHASE 1: Branching Basics - Initial commits on main +# Create a realistic project history with multiple merged branches # ============================================================================ -Write-Host "`nPhase 1: Creating initial project structure..." -ForegroundColor Cyan +Write-Host "Creating project history with multiple branches..." -ForegroundColor Cyan -# Commit 1: Initial commit -$mainContent = @" -# main.py - Main application file +# Initial commits on main +$readmeContent = @" +# My Application -def main(): - print("Welcome to the Application!") - print("This is the main branch") - -if __name__ == "__main__": - main() +A sample application for learning Git branching and merging. "@ -Set-Content -Path "main.py" -Value $mainContent +Set-Content -Path "README.md" -Value $readmeContent git add . git commit -m "Initial commit" | Out-Null -# Commit 2: Add main functionality $mainContent = @" # main.py - Main application file def main(): print("Welcome to the Application!") - print("This is the main branch") - run_application() - -def run_application(): - print("Application is running...") - print("Ready for new features!") if __name__ == "__main__": main() "@ Set-Content -Path "main.py" -Value $mainContent git add . -git commit -m "Add main functionality" | Out-Null - -# Tag checkpoint-start (students begin here - will create feature-login) -Write-Host "Creating checkpoint: start" -ForegroundColor Green -git tag checkpoint-start +git commit -m "Add main application file" | Out-Null # ============================================================================ -# PHASE 2: Create feature-login branch (what students will do in checkpoint 1) +# Branch 1: feature-login (will be merged) # ============================================================================ -Write-Host "Phase 2: Creating feature-login branch..." -ForegroundColor Cyan - -# Create and switch to feature-login branch +Write-Host "Creating feature-login branch..." -ForegroundColor Cyan git switch -c feature-login | Out-Null -# Commit 3: Add login module $loginContent = @" -# login.py - User login module +# login.py - User authentication def login(username, password): """Authenticate a user.""" - print(f"Authenticating user: {username}") - # TODO: Add actual authentication logic - return True - -def logout(username): - """Log out a user.""" - print(f"Logging out user: {username}") + print(f"Logging in user: {username}") return True "@ Set-Content -Path "login.py" -Value $loginContent git add . git commit -m "Add login module" | Out-Null -# Commit 4: Add password validation $loginContent = @" -# login.py - User login module +# login.py - User authentication def validate_password(password): """Validate password strength.""" - if len(password) < 8: - return False - return True + return len(password) >= 8 def login(username, password): """Authenticate a user.""" if not validate_password(password): print("Password too weak!") return False - print(f"Authenticating user: {username}") - # TODO: Add actual authentication logic - return True - -def logout(username): - """Log out a user.""" - print(f"Logging out user: {username}") + print(f"Logging in user: {username}") return True "@ Set-Content -Path "login.py" -Value $loginContent git add . git commit -m "Add password validation" | Out-Null -# Switch back to main +# Switch back to main and make more commits git switch main | Out-Null -# Now create divergence - add commits to main while feature-login exists -Write-Host "Creating divergent history on main..." -ForegroundColor Cyan - -# Commit 5: Add app.py with basic functionality $appContent = @" -# app.py - Main application entry point +# app.py - Application entry point from main import main @@ -150,7 +108,6 @@ def run(): """Run the application.""" print("Starting application...") main() - print("Application finished.") if __name__ == "__main__": run() @@ -159,16 +116,65 @@ Set-Content -Path "app.py" -Value $appContent git add . git commit -m "Add app.py entry point" | Out-Null -# Commit 6: Add README +# Merge feature-login into main +Write-Host "Merging feature-login into main..." -ForegroundColor Green +git merge feature-login --no-edit | Out-Null + +# ============================================================================ +# Branch 2: feature-api (will be merged) +# ============================================================================ +Write-Host "Creating feature-api branch..." -ForegroundColor Cyan +git switch -c feature-api | Out-Null + +$apiContent = @" +# api.py - API endpoints + +def get_data(): + """Retrieve data from API.""" + return {"status": "ok", "data": []} + +def post_data(data): + """Send data to API.""" + print(f"Posting data: {data}") + return {"status": "ok"} +"@ +Set-Content -Path "api.py" -Value $apiContent +git add . +git commit -m "Add API module" | Out-Null + +$apiContent = @" +# api.py - API endpoints + +def get_data(): + """Retrieve data from API.""" + return {"status": "ok", "data": []} + +def post_data(data): + """Send data to API.""" + print(f"Posting data: {data}") + return {"status": "ok"} + +def delete_data(id): + """Delete data by ID.""" + print(f"Deleting data: {id}") + return {"status": "ok"} +"@ +Set-Content -Path "api.py" -Value $apiContent +git add . +git commit -m "Add delete endpoint to API" | Out-Null + +# Switch back to main and add documentation +git switch main | Out-Null + $readmeContent = @" # My Application -Welcome to my application! +A sample application for learning Git branching and merging. ## Features -- Main functionality -- More features coming soon +- User authentication +- Main application logic ## Setup @@ -176,104 +182,107 @@ Run: python app.py "@ Set-Content -Path "README.md" -Value $readmeContent git add . -git commit -m "Add README documentation" | Out-Null +git commit -m "Update README with setup instructions" | Out-Null -# Tag checkpoint-merge (students begin merging here - divergent branches ready) -Write-Host "Creating checkpoint: merge" -ForegroundColor Green -git tag checkpoint-merge +# Merge feature-api into main +Write-Host "Merging feature-api into main..." -ForegroundColor Green +git merge feature-api --no-edit | Out-Null # ============================================================================ -# PHASE 3: Merge feature-login into main (what students will do in checkpoint 2) +# Branch 3: feature-database (will be merged) # ============================================================================ -Write-Host "Phase 3: Merging feature-login into main..." -ForegroundColor Cyan +Write-Host "Creating feature-database branch..." -ForegroundColor Cyan +git switch -c feature-database | Out-Null -# Merge feature-login into main (will create three-way merge commit) -git merge feature-login --no-edit | Out-Null +$dbContent = @" +# database.py - Database operations -# ============================================================================ -# PHASE 4: Create conflict scenario (what students will do in checkpoint 3) -# ============================================================================ -Write-Host "Phase 4: Creating merge conflict scenario..." -ForegroundColor Cyan +class Database: + def __init__(self): + self.connection = None -# Create config.json file on main -$initialConfig = @" -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000 - } -} + def connect(self): + """Connect to database.""" + print("Connecting to database...") + self.connection = True + + def query(self, sql): + """Execute SQL query.""" + print(f"Executing: {sql}") + return [] "@ -Set-Content -Path "config.json" -Value $initialConfig -git add config.json -git commit -m "Add initial configuration" | Out-Null +Set-Content -Path "database.py" -Value $dbContent +git add . +git commit -m "Add database module" | Out-Null -# On main branch: Add timeout setting -$mainConfig = @" -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, - "timeout": 5000 - } -} +$dbContent = @" +# database.py - Database operations + +class Database: + def __init__(self): + self.connection = None + + def connect(self): + """Connect to database.""" + print("Connecting to database...") + self.connection = True + + def disconnect(self): + """Disconnect from database.""" + print("Disconnecting from database...") + self.connection = None + + def query(self, sql): + """Execute SQL query.""" + print(f"Executing: {sql}") + return [] "@ -Set-Content -Path "config.json" -Value $mainConfig -git add config.json -git commit -m "Add timeout configuration" | Out-Null +Set-Content -Path "database.py" -Value $dbContent +git add . +git commit -m "Add disconnect method" | Out-Null -# Create update-config branch from the commit before timeout was added -git switch -c update-config HEAD~1 | Out-Null - -# On update-config branch: Add debug setting (conflicting change) -$featureConfig = @" -{ - "app": { - "name": "MyApp", - "version": "1.0.0", - "port": 3000, - "debug": true - } -} -"@ -Set-Content -Path "config.json" -Value $featureConfig -git add config.json -git commit -m "Add debug mode configuration" | Out-Null - -# Switch back to main +# Switch to main and merge git switch main | Out-Null +Write-Host "Merging feature-database into main..." -ForegroundColor Green +git merge feature-database --no-edit | Out-Null -# Tag checkpoint-merge-conflict (students begin conflict resolution here - on main with timeout, update-config has debug) -Write-Host "Creating checkpoint: merge-conflict" -ForegroundColor Green -git tag checkpoint-merge-conflict +# Final update on main +$readmeContent = @" +# My Application -# ============================================================================ -# Reset to checkpoint-start so students begin at the beginning -# ============================================================================ -Write-Host "`nResetting to checkpoint-start..." -ForegroundColor Yellow -git reset --hard checkpoint-start | Out-Null -git clean -fd | Out-Null +A sample application for learning Git branching and merging. + +## Features + +- User authentication +- API endpoints +- Database operations +- Main application logic + +## Setup + +Run: python app.py + +## Requirements + +- Python 3.6+ +"@ +Set-Content -Path "README.md" -Value $readmeContent +git add . +git commit -m "Update README with all features" | Out-Null # Return to module directory Set-Location .. Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan -Write-Host "`nThis module uses a CHECKPOINT SYSTEM:" -ForegroundColor Yellow -Write-Host " You'll work through 3 sections in one continuous repository:" -ForegroundColor White -Write-Host " 1. Branching Basics (checkpoint: start)" -ForegroundColor White -Write-Host " 2. Merging Branches (checkpoint: merge)" -ForegroundColor White -Write-Host " 3. Resolving Merge Conflicts (checkpoint: merge-conflict)" -ForegroundColor White -Write-Host "`nCommands:" -ForegroundColor Cyan -Write-Host " .\reset.ps1 - Show available checkpoints" -ForegroundColor White -Write-Host " .\reset.ps1 start - Jump to branching section" -ForegroundColor White -Write-Host " .\reset.ps1 merge - Jump to merging section" -ForegroundColor White -Write-Host " .\verify.ps1 - Verify all sections complete" -ForegroundColor White -Write-Host " .\verify.ps1 start - Verify only branching section" -ForegroundColor White +Write-Host "`nThe repository contains a realistic project history:" -ForegroundColor Yellow +Write-Host " - Multiple feature branches (login, api, database)" -ForegroundColor White +Write-Host " - All branches have been merged into main" -ForegroundColor White +Write-Host " - View the history: git log --oneline --graph --all" -ForegroundColor White Write-Host "`nNext steps:" -ForegroundColor Cyan Write-Host " 1. Read the README.md for detailed instructions" -ForegroundColor White Write-Host " 2. cd challenge" -ForegroundColor White -Write-Host " 3. Start with Checkpoint 1: Branching Basics" -ForegroundColor White +Write-Host " 3. Explore the repository history: git log --oneline --graph --all" -ForegroundColor White +Write-Host " 4. Create your own branches and practice merging!" -ForegroundColor White Write-Host "" diff --git a/01-essentials/03-branching-and-merging/verify.ps1 b/01-essentials/03-branching-and-merging/verify.ps1 index 8311d87..246f8fe 100755 --- a/01-essentials/03-branching-and-merging/verify.ps1 +++ b/01-essentials/03-branching-and-merging/verify.ps1 @@ -1,34 +1,15 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Verifies the Module 03 challenge solution (checkpoint-aware). + Verifies the Module 03 challenge solution. .DESCRIPTION - This script can verify completion of individual checkpoints or - the entire module. Without arguments, it verifies all checkpoints. - -.PARAMETER Checkpoint - The checkpoint to verify: start, merge, or merge-conflict. - If not specified, verifies all checkpoints. - -.EXAMPLE - .\verify.ps1 - Verifies all three checkpoints are complete. - -.EXAMPLE - .\verify.ps1 start - Verifies only the branching basics checkpoint. - -.EXAMPLE - .\verify.ps1 merge - Verifies only the merging checkpoint. + This script checks that you've practiced branching and merging by: + - Creating at least one new branch (beyond the example branches) + - Making commits on your branch + - Merging your branch into main #> -param( - [ValidateSet('start', 'merge', 'merge-conflict', '')] - [string]$Checkpoint = '' -) - $script:allChecksPassed = $true # ============================================================================ @@ -51,198 +32,15 @@ function Write-Hint { Write-Host "[HINT] $Message" -ForegroundColor Yellow } -# ============================================================================ -# Checkpoint 1: Branching Basics Verification -# ============================================================================ - -function Verify-Branching { - Write-Host "`n=== Checkpoint 1: Branching Basics ===" -ForegroundColor Cyan - - # Save current branch - $originalBranch = git branch --show-current 2>$null - - # Check if feature-login branch exists - $branchExists = git branch --list "feature-login" 2>$null - if ($branchExists) { - Write-Pass "Branch 'feature-login' exists" - } else { - Write-Fail "Branch 'feature-login' not found" - Write-Hint "Create the branch with: git switch -c feature-login" - return - } - - # Check if feature-login has commits beyond main (or if they've been merged) - $commitCount = git rev-list main..feature-login --count 2>$null - $mergeCommitExists = (git log --merges --oneline 2>$null | Select-String "Merge.*feature-login") - - if ($mergeCommitExists -and $commitCount -eq 0) { - # Commits were merged into main - this is correct! - Write-Pass "Branch 'feature-login' commits have been merged into main" - } elseif ($commitCount -ge 2) { - Write-Pass "Branch 'feature-login' has $commitCount new commits" - } else { - Write-Fail "Branch 'feature-login' needs at least 2 new commits (found: $commitCount)" - Write-Hint "Make sure you've committed login.py and made at least one more commit" - } - - # Switch to feature-login and check for login.py - git switch feature-login 2>$null | Out-Null - if (Test-Path "login.py") { - Write-Pass "File 'login.py' exists in feature-login branch" - } else { - Write-Fail "File 'login.py' not found in feature-login branch" - Write-Hint "Create login.py and commit it to the feature-login branch" - } - - # Switch to main and verify login.py doesn't exist there yet (unless merged) - git switch main 2>$null | Out-Null - - # Check if merge happened - if so, login.py can exist on main - $mergeCommitExists = (git log --merges --oneline 2>$null | Select-String "Merge.*feature-login") - - if (-not $mergeCommitExists) { - # No merge yet - login.py should NOT be on main - if (-not (Test-Path "login.py")) { - Write-Pass "File 'login.py' does NOT exist in main branch (branches are independent!)" - } else { - Write-Fail "File 'login.py' should not exist in main branch yet (before merge)" - Write-Hint "Make sure you created login.py only on the feature-login branch" - } - } - - # Switch back to original branch - if ($originalBranch) { - git switch $originalBranch 2>$null | Out-Null - } +function Write-Info { + param([string]$Message) + Write-Host "[INFO] $Message" -ForegroundColor Cyan } # ============================================================================ -# Checkpoint 2: Merging Verification +# Check challenge directory exists # ============================================================================ -function Verify-Merging { - Write-Host "`n=== Checkpoint 2: Merging Branches ===" -ForegroundColor Cyan - - # Check current branch is main - $currentBranch = git branch --show-current 2>$null - if ($currentBranch -eq "main") { - Write-Pass "Currently on main branch" - } else { - Write-Fail "Should be on main branch (currently on: $currentBranch)" - Write-Hint "Switch to main with: git switch main" - } - - # Check if login.py exists on main (indicates merge happened) - if (Test-Path "login.py") { - Write-Pass "File 'login.py' exists on main branch (merged successfully)" - } else { - Write-Fail "File 'login.py' not found on main branch" - Write-Hint "Merge feature-login into main with: git merge feature-login" - } - - # Check for merge commit - $mergeCommitExists = (git log --merges --oneline 2>$null | Select-String "Merge.*feature-login") - - if ($mergeCommitExists) { - Write-Pass "Merge commit exists" - } else { - Write-Fail "No merge commit found" - Write-Hint "Create a merge commit with: git merge feature-login" - } - - # Check commit count (should have both branches' commits) - $commitCount = [int](git rev-list --count HEAD 2>$null) - if ($commitCount -ge 6) { - Write-Pass "Repository has $commitCount commits (merge complete)" - } else { - Write-Fail "Repository should have at least 6 commits after merge (found: $commitCount)" - } -} - -# ============================================================================ -# Checkpoint 3: Merge Conflicts Verification -# ============================================================================ - -function Verify-MergeConflicts { - Write-Host "`n=== Checkpoint 3: Resolving Merge Conflicts ===" -ForegroundColor Cyan - - # Check current branch is main - $currentBranch = git branch --show-current 2>$null - if ($currentBranch -eq "main") { - Write-Pass "Currently on main branch" - } else { - Write-Fail "Should be on main branch (currently on: $currentBranch)" - Write-Hint "Switch to main with: git switch main" - } - - # Check that merge is not in progress - if (Test-Path ".git/MERGE_HEAD") { - Write-Fail "Merge is still in progress (conflicts not resolved)" - Write-Hint "Resolve conflicts in config.json, then: git add config.json && git commit" - return - } else { - Write-Pass "No merge in progress (conflicts resolved)" - } - - # Check if config.json exists - if (Test-Path "config.json") { - Write-Pass "File 'config.json' exists" - } else { - Write-Fail "File 'config.json' not found" - Write-Hint "Merge update-config branch with: git merge update-config" - return - } - - # Verify config.json is valid JSON - try { - $configContent = Get-Content "config.json" -Raw - $config = $configContent | ConvertFrom-Json -ErrorAction Stop - Write-Pass "File 'config.json' is valid JSON" - } catch { - Write-Fail "File 'config.json' is not valid JSON" - Write-Hint "Make sure you removed all conflict markers (<<<<<<<, =======, >>>>>>>)" - return - } - - # Check for conflict markers - if ($configContent -match '<<<<<<<|=======|>>>>>>>') { - Write-Fail "Conflict markers still present in config.json" - Write-Hint "Remove all conflict markers (<<<<<<<, =======, >>>>>>>)" - return - } else { - Write-Pass "No conflict markers in config.json" - } - - # Verify both settings are present (timeout and debug) - if ($config.app.timeout -eq 5000) { - Write-Pass "Timeout setting preserved (5000)" - } else { - Write-Fail "Timeout setting missing or incorrect" - Write-Hint "Keep the timeout: 5000 setting from main branch" - } - - if ($config.app.debug -eq $true) { - Write-Pass "Debug setting preserved (true)" - } else { - Write-Fail "Debug setting missing or incorrect" - Write-Hint "Keep the debug: true setting from update-config branch" - } - - # Verify merge commit exists for update-config - $updateConfigMerge = (git log --merges --oneline 2>$null | Select-String "Merge.*update-config") - if ($updateConfigMerge) { - Write-Pass "Merge commit exists for update-config branch" - } else { - Write-Fail "No merge commit found for update-config" - Write-Hint "Complete the merge with: git commit (after resolving conflicts)" - } -} - -# ============================================================================ -# Main Script Logic -# ============================================================================ - -# Check if challenge directory exists if (-not (Test-Path "challenge")) { Write-Host "[ERROR] Challenge directory not found." -ForegroundColor Red Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow @@ -251,7 +49,6 @@ if (-not (Test-Path "challenge")) { Push-Location "challenge" -# Check if git repository exists if (-not (Test-Path ".git")) { Write-Host "[ERROR] Not a git repository." -ForegroundColor Red Write-Host "Run ..\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow @@ -259,62 +56,98 @@ if (-not (Test-Path ".git")) { exit 1 } -# Run appropriate verification -if ($Checkpoint -eq '') { - # Verify all checkpoints - Write-Host "`n=== Verifying All Checkpoints ===" -ForegroundColor Cyan +Write-Host "`n=== Verifying Module 03: Branching and Merging ===" -ForegroundColor Cyan - Verify-Branching - Verify-Merging - Verify-MergeConflicts +# ============================================================================ +# Count initial setup commits (should be 13 commits from setup) +# ============================================================================ +$initialCommitCount = 13 +# ============================================================================ +# Check for new commits beyond setup +# ============================================================================ +Write-Host "`nChecking your work..." -ForegroundColor Cyan + +$totalCommits = [int](git rev-list --count HEAD 2>$null) + +if ($totalCommits -gt $initialCommitCount) { + $newCommits = $totalCommits - $initialCommitCount + Write-Pass "Found $newCommits new commit(s) beyond the initial setup" } else { - # Verify specific checkpoint - switch ($Checkpoint) { - 'start' { Verify-Branching } - 'merge' { Verify-Merging } - 'merge-conflict' { Verify-MergeConflicts } - } + Write-Fail "No new commits found" + Write-Hint "Create a branch, make some commits, and merge it into main" + Write-Hint "Example: git switch -c my-feature" +} + +# ============================================================================ +# Check for branches (excluding the example branches) +# ============================================================================ +$allBranches = git branch --list 2>$null | ForEach-Object { $_.Trim('* ') } +$exampleBranches = @('main', 'feature-login', 'feature-api', 'feature-database') +$studentBranches = $allBranches | Where-Object { $_ -notin $exampleBranches } + +if ($studentBranches.Count -gt 0) { + Write-Pass "Created $($studentBranches.Count) new branch(es): $($studentBranches -join ', ')" +} else { + Write-Info "No new branches found (it's OK if you deleted them after merging)" + Write-Hint "To practice: git switch -c your-branch-name" +} + +# ============================================================================ +# Check for merge commits by the student +# ============================================================================ +$setupUser = "Workshop Student" +$mergeCommits = git log --merges --format="%s" 2>$null + +# Count how many merge commits exist beyond the initial 3 +$totalMerges = ($mergeCommits | Measure-Object).Count +$setupMerges = 3 # feature-login, feature-api, feature-database + +if ($totalMerges -gt $setupMerges) { + $studentMerges = $totalMerges - $setupMerges + Write-Pass "Performed $studentMerges merge(s) of your own work" +} else { + Write-Fail "No merge commits found beyond the example merges" + Write-Hint "Create a branch, add commits, then merge it: git merge your-branch-name" +} + +# ============================================================================ +# Check current branch +# ============================================================================ +$currentBranch = git branch --show-current 2>$null +if ($currentBranch -eq "main") { + Write-Pass "Currently on main branch" +} else { + Write-Info "Currently on '$currentBranch' branch" + Write-Hint "Typically you merge feature branches INTO main" } Pop-Location +# ============================================================================ # Final summary +# ============================================================================ Write-Host "" if ($script:allChecksPassed) { Write-Host "=========================================" -ForegroundColor Green Write-Host " CONGRATULATIONS! CHALLENGE PASSED!" -ForegroundColor Green Write-Host "=========================================" -ForegroundColor Green - - if ($Checkpoint -eq '') { - Write-Host "`nYou've completed the entire module!" -ForegroundColor Cyan - Write-Host "You've mastered:" -ForegroundColor Cyan - Write-Host " ✓ Creating and working with branches" -ForegroundColor White - Write-Host " ✓ Merging branches together" -ForegroundColor White - Write-Host " ✓ Resolving merge conflicts" -ForegroundColor White - Write-Host "`nReady for the next module!" -ForegroundColor Green - } else { - Write-Host "`nCheckpoint '$Checkpoint' complete!" -ForegroundColor Cyan - - switch ($Checkpoint) { - 'start' { - Write-Host "Next: Move to the merging checkpoint" -ForegroundColor White - Write-Host " ..\reset.ps1 merge OR continue to merge feature-login" -ForegroundColor Yellow - } - 'merge' { - Write-Host "Next: Move to the conflict resolution checkpoint" -ForegroundColor White - Write-Host " ..\reset.ps1 merge-conflict" -ForegroundColor Yellow - } - 'merge-conflict' { - Write-Host "Module complete! Ready for the next module!" -ForegroundColor Green - } - } - } + Write-Host "`nYou've successfully practiced:" -ForegroundColor Cyan + Write-Host " ✓ Creating branches" -ForegroundColor White + Write-Host " ✓ Making commits on branches" -ForegroundColor White + Write-Host " ✓ Merging branches together" -ForegroundColor White + Write-Host "`nReady for the next module!" -ForegroundColor Green Write-Host "" exit 0 } else { - Write-Host "[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red - Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow + Write-Host "[SUMMARY] Some checks failed. Review the hints above." -ForegroundColor Red + Write-Host "" + Write-Host "Quick guide:" -ForegroundColor Cyan + Write-Host " 1. Create a branch: git switch -c my-feature" -ForegroundColor White + Write-Host " 2. Make changes and commit them" -ForegroundColor White + Write-Host " 3. Switch to main: git switch main" -ForegroundColor White + Write-Host " 4. Merge your branch: git merge my-feature" -ForegroundColor White + Write-Host " 5. Run this verify script again" -ForegroundColor White Write-Host "" exit 1 } diff --git a/01-essentials/04-merge-conflict/README.md b/01-essentials/04-merge-conflict/README.md new file mode 100644 index 0000000..7cdc640 --- /dev/null +++ b/01-essentials/04-merge-conflict/README.md @@ -0,0 +1,487 @@ +# Module 04: Merge Conflicts + +## Learning Objectives + +By the end of this module, you will: +- Understand what merge conflicts are and why they occur +- Use `git diff` to discover changes between branches +- Identify merge conflicts in your repository +- Read and interpret conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) +- Resolve merge conflicts manually +- Complete a merge after resolving conflicts + +## Setup + +Create the challenge environment: + +```bash +.\setup.ps1 +``` + +This creates a repository with two feature branches that have conflicting changes. + +## Overview + +A **merge conflict** occurs when Git cannot automatically combine changes because both branches modified the same part of the same file in different ways. + +**When do conflicts happen?** +- ✅ Two branches modify the same lines in a file +- ✅ One branch deletes a file that another branch modifies +- ✅ Complex changes Git can't merge automatically +- ❌ Different files are changed (no conflict!) +- ❌ Different parts of the same file are changed (no conflict!) + +**Don't fear conflicts!** They're a normal part of collaborative development. Git just needs your help to decide what the final code should look like. + +## Your Task + +### Part 1: Discover the Changes + +Before merging, it's good practice to see what each branch changed: + +```bash +cd challenge + +# Check which branch you're on +git branch + +# View all branches +git branch --all +``` + +You'll see three branches: `main`, `add-timeout`, and `add-debug`. + +**Discover what each branch changed:** + +```bash +# Compare main with add-timeout +git diff main add-timeout + +# Compare main with add-debug +git diff main add-debug + +# Compare the two feature branches directly +git diff add-timeout add-debug +``` + +**What did you discover?** +- Both branches modified `config.json` +- They both added a line in the same location (after `"port": 3000`) +- One adds `"timeout": 5000` +- The other adds `"debug": true` + +This is a recipe for a conflict! + +### Part 2: Merge the First Branch (No Conflict) + +Let's merge `add-timeout` first: + +```bash +# Make sure you're on main +git switch main + +# Merge the first branch +git merge add-timeout +``` + +✅ **Success!** This merge works because main hasn't changed since add-timeout was created. + +```bash +# View the updated config +cat config.json + +# Check the history +git log --oneline --graph --all +``` + +### Part 3: Try to Merge the Second Branch (Conflict!) + +Now let's try to merge `add-debug`: + +```bash +# Still on main +git merge add-debug +``` + +💥 **Boom!** You'll see: + +``` +Auto-merging config.json +CONFLICT (content): Merge conflict in config.json +Automatic merge failed; fix conflicts and then commit the result. +``` + +**Don't panic!** This is expected. Git is asking for your help. + +### Part 4: Check the Status + +```bash +git status +``` + +You'll see: + +``` +On branch main +You have unmerged paths. + (fix conflicts and run "git commit") + (use "git merge --abort" to abort the merge) + +Unmerged paths: + (use "git add ..." to mark resolution) + both modified: config.json +``` + +This tells you that `config.json` needs your attention! + +### Part 5: Open and Examine the Conflicted File + +Open `config.json` in your text editor: + +```bash +# On Windows +notepad config.json + +# Or use VS Code +code config.json +``` + +You'll see special **conflict markers**: + +```json +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, +<<<<<<< HEAD + "timeout": 5000 +======= + "debug": true +>>>>>>> add-debug + } +} +``` + +### Part 6: Understand the Conflict Markers + +Let's break down what you're seeing: + +``` +<<<<<<< HEAD + "timeout": 5000 ← Your current branch (main, which has add-timeout merged) +======= + "debug": true ← The branch you're merging (add-debug) +>>>>>>> add-debug +``` + +**What each marker means:** +- `<<<<<<< HEAD` - Start of your changes (current branch) +- `=======` - Separator between the two versions +- `>>>>>>> add-debug` - End of their changes (branch being merged) + +### Part 7: Resolve the Conflict + +You have three options: + +**Option 1: Keep ONLY your changes (timeout)** +```json + "timeout": 5000 +``` + +**Option 2: Keep ONLY their changes (debug)** +```json + "debug": true +``` + +**Option 3: Keep BOTH changes** ← **Do this!** +```json + "timeout": 5000, + "debug": true +``` + +### Part 8: Edit the File + +For this challenge, we want **both settings**, so: + +1. Delete ALL the conflict markers: + - Remove `<<<<<<< HEAD` + - Remove `=======` + - Remove `>>>>>>> add-debug` + +2. Keep both settings: + +**Before (with conflict markers):** +```json +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, +<<<<<<< HEAD + "timeout": 5000 +======= + "debug": true +>>>>>>> add-debug + } +} +``` + +**After (resolved):** +```json +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, + "timeout": 5000, + "debug": true + } +} +``` + +**Important:** +- Remove ALL markers +- Add a comma after `"timeout": 5000` (for valid JSON) +- Ensure the file is valid JSON + +3. Save the file + +### Part 9: Mark the Conflict as Resolved + +Tell Git you've resolved the conflict: + +```bash +# Stage the resolved file +git add config.json + +# Check status +git status +``` + +You should see: + +``` +On branch main +All conflicts fixed but you are still merging. + (use "git commit" to conclude merge) +``` + +Perfect! Git confirms the conflict is resolved. + +### Part 10: Complete the Merge + +Commit the merge: + +```bash +git commit +``` + +Git will open an editor with a default merge message. You can accept it or customize it, then save and close. + +**Done!** Your merge is complete! + +```bash +# View the final result +cat config.json + +# View the history +git log --oneline --graph --all +``` + +You should see both `timeout` and `debug` in the config! + +### Part 11: Verify Your Solution + +From the module directory (not inside challenge/): + +```bash +.\verify.ps1 +``` + +## Understanding Conflict Markers + +### Anatomy of a Conflict + +``` +<<<<<<< HEAD ← Marker: Start of your version +Your changes here +======= ← Marker: Separator +Their changes here +>>>>>>> branch-name ← Marker: End of their version +``` + +### Common Conflict Patterns + +**Simple conflict:** +``` +<<<<<<< HEAD +print("Hello") +======= +print("Hi") +>>>>>>> feature +``` +Decision: Which greeting do you want? + +**Both are needed:** +``` +<<<<<<< HEAD +timeout: 5000 +======= +debug: true +>>>>>>> feature +``` +Decision: Keep both (add comma)! + +**Deletion conflict:** +``` +<<<<<<< HEAD +# Function deleted on your branch +======= +def old_function(): + pass +>>>>>>> feature +``` +Decision: Delete or keep the function? + +## Common Mistakes to Avoid + +❌ **Forgetting to remove conflict markers** +```json +<<<<<<< HEAD ← Don't leave these in! + "timeout": 5000, + "debug": true +>>>>>>> add-debug ← Don't leave these in! +``` +This breaks your code! Always remove ALL markers. + +❌ **Committing without staging** +```bash +git commit # Error! You didn't add the file +``` +Always `git add` the resolved file first! + +❌ **Keeping only one side when both are needed** +If you delete one setting, you lose that work! + +❌ **Breaking syntax** +```json +"timeout": 5000 ← Missing comma! +"debug": true +``` +Always verify your file is valid after resolving! + +❌ **Not testing the result** +Always check that your resolved code works! + +## Aborting a Merge + +Changed your mind? You can abort the merge anytime before committing: + +```bash +git merge --abort +``` + +This returns your repository to the state before you started the merge. No harm done! + +## Key Commands + +```bash +# Discover changes before merging +git diff branch1 branch2 + +# Attempt a merge +git merge + +# Check which files have conflicts +git status + +# Abort the merge and start over +git merge --abort + +# After resolving conflicts: +git add +git commit + +# View conflicts in a different style +git diff --ours # Your changes +git diff --theirs # Their changes +git diff --base # Original version +``` + +## Pro Tips + +💡 **Use `git diff` first** +Always compare branches before merging: +```bash +git diff main..feature-branch +``` + +💡 **Prevent conflicts** +- Pull changes frequently +- Communicate with your team about who's working on what +- Keep branches short-lived and merge often + +💡 **Make conflicts easier** +- Work on different files when possible +- Make small, focused commits +- If editing the same file, coordinate with teammates + +💡 **When stuck** +- Read the conflict markers carefully +- Look at `git log` to understand what each side changed +- Use `git diff` to see the changes +- Ask a teammate to review your resolution +- Use a merge tool: `git mergetool` + +## Merge Tools + +Git supports visual merge tools that make resolving conflicts easier: + +```bash +# Configure a merge tool (one-time setup) +git config --global merge.tool vscode # or meld, kdiff3, etc. + +# Use the merge tool during a conflict +git mergetool +``` + +This opens a visual interface showing both versions side-by-side. + +## Real-World Scenario + +This exercise simulates a common real-world situation: + +**Scenario:** Two developers working on the same file +- Alice adds a timeout configuration +- Bob adds debug mode configuration +- Both push their changes +- When Bob tries to merge, he gets a conflict +- Bob resolves it by keeping both changes +- Everyone's work is preserved! + +This happens all the time in team development. Conflicts are normal! + +## What You've Learned + +After completing this module, you understand: + +- ✅ Merge conflicts happen when the same lines are changed differently +- ✅ `git diff` helps you discover changes before merging +- ✅ Conflict markers show both versions +- ✅ You decide what the final code should look like +- ✅ Remove all markers before committing +- ✅ Test your resolution to ensure it works +- ✅ Conflicts are normal and easy to resolve with practice + +## Next Steps + +Ready to continue? The next module covers **cherry-picking** - selectively applying specific commits from one branch to another. + +To start over: +```bash +.\reset.ps1 +.\setup.ps1 +``` + +**Need help?** Review the steps above, or run `git status` to see what Git suggests! diff --git a/01-essentials/04-merge-conflict/challenge b/01-essentials/04-merge-conflict/challenge new file mode 160000 index 0000000..676b08e --- /dev/null +++ b/01-essentials/04-merge-conflict/challenge @@ -0,0 +1 @@ +Subproject commit 676b08e650cacba9c8fabee80697ca968bc4f3ea diff --git a/01-essentials/04-merge-conflict/reset.ps1 b/01-essentials/04-merge-conflict/reset.ps1 new file mode 100644 index 0000000..1013842 --- /dev/null +++ b/01-essentials/04-merge-conflict/reset.ps1 @@ -0,0 +1,23 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Resets the Module 04 challenge environment. + +.DESCRIPTION + This script removes the challenge directory, allowing you to start fresh. + Run setup.ps1 again after resetting to recreate the environment. +#> + +Write-Host "`n=== Resetting Module 04 Challenge ===" -ForegroundColor Cyan + +if (Test-Path "challenge") { + Write-Host "Removing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" + Write-Host "`n[SUCCESS] Challenge environment reset complete!" -ForegroundColor Green + Write-Host "`nRun .\setup.ps1 to create a fresh challenge environment." -ForegroundColor Cyan + Write-Host "" +} else { + Write-Host "`n[INFO] No challenge directory found. Nothing to reset." -ForegroundColor Yellow + Write-Host "Run .\setup.ps1 to create the challenge environment." -ForegroundColor Cyan + Write-Host "" +} diff --git a/01-essentials/04-merge-conflict/setup.ps1 b/01-essentials/04-merge-conflict/setup.ps1 new file mode 100644 index 0000000..86fa42d --- /dev/null +++ b/01-essentials/04-merge-conflict/setup.ps1 @@ -0,0 +1,125 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Sets up the Module 04 challenge environment for merge conflicts. + +.DESCRIPTION + This script creates a challenge directory with a Git repository containing + two feature branches that have conflicting changes to the same file. + Students will learn to identify, understand, and resolve merge conflicts. +#> + +Write-Host "`n=== Setting up Module 04: Merge Conflicts ===" -ForegroundColor Cyan + +# Remove existing challenge directory if it exists +if (Test-Path "challenge") { + Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow + Remove-Item -Recurse -Force "challenge" +} + +# Create fresh challenge directory +Write-Host "Creating challenge directory..." -ForegroundColor Green +New-Item -ItemType Directory -Path "challenge" | Out-Null +Set-Location "challenge" + +# Initialize Git repository +Write-Host "Initializing Git repository..." -ForegroundColor Green +git init | Out-Null + +# Configure git for this repository +git config user.name "Workshop Student" +git config user.email "student@example.com" + +# ============================================================================ +# Create base project +# ============================================================================ +Write-Host "Creating base project..." -ForegroundColor Cyan + +# Initial commit with config file +$configContent = @" +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000 + } +} +"@ +Set-Content -Path "config.json" -Value $configContent +git add . +git commit -m "Initial commit with config" | Out-Null + +# Add README +$readmeContent = @" +# My Application + +A simple application for learning merge conflicts. + +## Configuration + +Edit `config.json` to configure the application. +"@ +Set-Content -Path "README.md" -Value $readmeContent +git add . +git commit -m "Add README" | Out-Null + +# ============================================================================ +# Branch 1: add-timeout (adds timeout setting) +# ============================================================================ +Write-Host "Creating add-timeout branch..." -ForegroundColor Cyan +git switch -c add-timeout | Out-Null + +$timeoutConfig = @" +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, + "timeout": 5000 + } +} +"@ +Set-Content -Path "config.json" -Value $timeoutConfig +git add . +git commit -m "Add timeout configuration" | Out-Null + +# ============================================================================ +# Branch 2: add-debug (adds debug setting - CONFLICTS with timeout!) +# ============================================================================ +Write-Host "Creating add-debug branch..." -ForegroundColor Cyan +git switch main | Out-Null +git switch -c add-debug | Out-Null + +$debugConfig = @" +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "port": 3000, + "debug": true + } +} +"@ +Set-Content -Path "config.json" -Value $debugConfig +git add . +git commit -m "Add debug mode configuration" | Out-Null + +# Switch back to main +git switch main | Out-Null + +# Return to module directory +Set-Location .. + +Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green +Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan +Write-Host "`nThe repository contains:" -ForegroundColor Yellow +Write-Host " - main branch: base configuration" -ForegroundColor White +Write-Host " - add-timeout branch: adds timeout setting" -ForegroundColor White +Write-Host " - add-debug branch: adds debug setting" -ForegroundColor White +Write-Host "`nBoth branches modify the same part of config.json!" -ForegroundColor Red +Write-Host "This will cause a merge conflict when you try to merge both." -ForegroundColor Red +Write-Host "`nNext steps:" -ForegroundColor Cyan +Write-Host " 1. Read the README.md for detailed instructions" -ForegroundColor White +Write-Host " 2. cd challenge" -ForegroundColor White +Write-Host " 3. Follow the guide to discover and resolve the conflict" -ForegroundColor White +Write-Host "" diff --git a/01-essentials/04-merge-conflict/verify.ps1 b/01-essentials/04-merge-conflict/verify.ps1 new file mode 100644 index 0000000..8a04114 --- /dev/null +++ b/01-essentials/04-merge-conflict/verify.ps1 @@ -0,0 +1,208 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Verifies the Module 04 challenge solution. + +.DESCRIPTION + This script checks that you've successfully resolved the merge conflict by: + - Merging both branches into main + - Resolving the conflict in config.json + - Keeping both timeout and debug settings + - Ensuring valid JSON syntax +#> + +$script:allChecksPassed = $true + +# ============================================================================ +# Helper Functions +# ============================================================================ + +function Write-Pass { + param([string]$Message) + Write-Host "[PASS] $Message" -ForegroundColor Green +} + +function Write-Fail { + param([string]$Message) + Write-Host "[FAIL] $Message" -ForegroundColor Red + $script:allChecksPassed = $false +} + +function Write-Hint { + param([string]$Message) + Write-Host "[HINT] $Message" -ForegroundColor Yellow +} + +function Write-Info { + param([string]$Message) + Write-Host "[INFO] $Message" -ForegroundColor Cyan +} + +# ============================================================================ +# Check challenge directory exists +# ============================================================================ + +if (-not (Test-Path "challenge")) { + Write-Host "[ERROR] Challenge directory not found." -ForegroundColor Red + Write-Host "Run .\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow + exit 1 +} + +Push-Location "challenge" + +if (-not (Test-Path ".git")) { + Write-Host "[ERROR] Not a git repository." -ForegroundColor Red + Write-Host "Run ..\setup.ps1 first to create the challenge environment." -ForegroundColor Yellow + Pop-Location + exit 1 +} + +Write-Host "`n=== Verifying Module 04: Merge Conflicts ===" -ForegroundColor Cyan + +# ============================================================================ +# Check current branch +# ============================================================================ +$currentBranch = git branch --show-current 2>$null +if ($currentBranch -eq "main") { + Write-Pass "Currently on main branch" +} else { + Write-Fail "Should be on main branch (currently on: $currentBranch)" + Write-Hint "Switch to main with: git switch main" +} + +# ============================================================================ +# Check that merge is not in progress +# ============================================================================ +if (Test-Path ".git/MERGE_HEAD") { + Write-Fail "Merge is still in progress (conflicts not resolved)" + Write-Hint "Resolve conflicts in config.json, then: git add config.json && git commit" + Pop-Location + exit 1 +} else { + Write-Pass "No merge in progress (conflicts resolved)" +} + +# ============================================================================ +# Check if config.json exists +# ============================================================================ +if (-not (Test-Path "config.json")) { + Write-Fail "File 'config.json' not found" + Write-Hint "The config.json file should exist" + Pop-Location + exit 1 +} + +# ============================================================================ +# Verify config.json is valid JSON +# ============================================================================ +try { + $configContent = Get-Content "config.json" -Raw + $config = $configContent | ConvertFrom-Json -ErrorAction Stop + Write-Pass "File 'config.json' is valid JSON" +} catch { + Write-Fail "File 'config.json' is not valid JSON" + Write-Hint "Make sure you removed all conflict markers (<<<<<<<, =======, >>>>>>>)" + Write-Hint "Check for missing commas or brackets" + Pop-Location + exit 1 +} + +# ============================================================================ +# Check for conflict markers +# ============================================================================ +if ($configContent -match '<<<<<<<|=======|>>>>>>>') { + Write-Fail "Conflict markers still present in config.json" + Write-Hint "Remove all conflict markers (<<<<<<<, =======, >>>>>>>)" + Pop-Location + exit 1 +} else { + Write-Pass "No conflict markers in config.json" +} + +# ============================================================================ +# Verify both settings are present (timeout and debug) +# ============================================================================ +if ($config.app.timeout -eq 5000) { + Write-Pass "Timeout setting preserved (5000)" +} else { + Write-Fail "Timeout setting missing or incorrect" + Write-Hint "Keep the timeout: 5000 setting from add-timeout branch" +} + +if ($config.app.debug -eq $true) { + Write-Pass "Debug setting preserved (true)" +} else { + Write-Fail "Debug setting missing or incorrect" + Write-Hint "Keep the debug: true setting from add-debug branch" +} + +# ============================================================================ +# Verify both branches were merged +# ============================================================================ +$addTimeoutMerged = git log --oneline --grep="add-timeout" 2>$null | Select-String "Merge" +$addDebugMerged = git log --oneline --grep="add-debug" 2>$null | Select-String "Merge" + +if ($addTimeoutMerged) { + Write-Pass "add-timeout branch has been merged" +} else { + # Check if it was a fast-forward merge (commits exist but no merge commit) + $timeoutCommit = git log --oneline --grep="Add timeout configuration" 2>$null + if ($timeoutCommit) { + Write-Pass "add-timeout branch changes are in main" + } else { + Write-Fail "add-timeout branch not merged" + Write-Hint "Merge with: git merge add-timeout" + } +} + +if ($addDebugMerged) { + Write-Pass "add-debug branch has been merged" +} else { + Write-Fail "add-debug branch not merged" + Write-Hint "Merge with: git merge add-debug" +} + +# ============================================================================ +# Check commit count (should have both merges) +# ============================================================================ +$totalCommits = [int](git rev-list --count HEAD 2>$null) +if ($totalCommits -ge 5) { + Write-Pass "Repository has $totalCommits commits (all merges complete)" +} else { + Write-Info "Repository has $totalCommits commits" + Write-Hint "Make sure both branches are merged" +} + +Pop-Location + +# ============================================================================ +# Final summary +# ============================================================================ +Write-Host "" +if ($script:allChecksPassed) { + Write-Host "=========================================" -ForegroundColor Green + Write-Host " CONGRATULATIONS! CHALLENGE PASSED!" -ForegroundColor Green + Write-Host "=========================================" -ForegroundColor Green + Write-Host "`nYou've successfully:" -ForegroundColor Cyan + Write-Host " ✓ Discovered changes using git diff" -ForegroundColor White + Write-Host " ✓ Merged the first branch" -ForegroundColor White + Write-Host " ✓ Encountered a merge conflict" -ForegroundColor White + Write-Host " ✓ Resolved the conflict by keeping both changes" -ForegroundColor White + Write-Host " ✓ Completed the merge" -ForegroundColor White + Write-Host "`nYou're now ready to handle merge conflicts in real projects!" -ForegroundColor Green + Write-Host "" + exit 0 +} else { + Write-Host "[SUMMARY] Some checks failed. Review the hints above." -ForegroundColor Red + Write-Host "" + Write-Host "Quick guide:" -ForegroundColor Cyan + Write-Host " 1. Make sure you're on main: git switch main" -ForegroundColor White + Write-Host " 2. Merge first branch: git merge add-timeout" -ForegroundColor White + Write-Host " 3. Merge second branch: git merge add-debug" -ForegroundColor White + Write-Host " 4. Resolve conflict: edit config.json, remove markers, keep both settings" -ForegroundColor White + Write-Host " 5. Stage resolved file: git add config.json" -ForegroundColor White + Write-Host " 6. Complete merge: git commit" -ForegroundColor White + Write-Host " 7. Run this verify script again" -ForegroundColor White + Write-Host "" + exit 1 +} diff --git a/01-essentials/04-cherry-pick/README.md b/01-essentials/05-cherry-pick/README.md similarity index 100% rename from 01-essentials/04-cherry-pick/README.md rename to 01-essentials/05-cherry-pick/README.md diff --git a/01-essentials/04-cherry-pick/reset.ps1 b/01-essentials/05-cherry-pick/reset.ps1 similarity index 100% rename from 01-essentials/04-cherry-pick/reset.ps1 rename to 01-essentials/05-cherry-pick/reset.ps1 diff --git a/01-essentials/04-cherry-pick/setup.ps1 b/01-essentials/05-cherry-pick/setup.ps1 similarity index 100% rename from 01-essentials/04-cherry-pick/setup.ps1 rename to 01-essentials/05-cherry-pick/setup.ps1 diff --git a/01-essentials/04-cherry-pick/verify.ps1 b/01-essentials/05-cherry-pick/verify.ps1 similarity index 100% rename from 01-essentials/04-cherry-pick/verify.ps1 rename to 01-essentials/05-cherry-pick/verify.ps1 diff --git a/01-essentials/05-revert/README.md b/01-essentials/06-revert/README.md similarity index 100% rename from 01-essentials/05-revert/README.md rename to 01-essentials/06-revert/README.md diff --git a/01-essentials/05-revert/reset.ps1 b/01-essentials/06-revert/reset.ps1 similarity index 100% rename from 01-essentials/05-revert/reset.ps1 rename to 01-essentials/06-revert/reset.ps1 diff --git a/01-essentials/05-revert/setup.ps1 b/01-essentials/06-revert/setup.ps1 similarity index 100% rename from 01-essentials/05-revert/setup.ps1 rename to 01-essentials/06-revert/setup.ps1 diff --git a/01-essentials/05-revert/verify.ps1 b/01-essentials/06-revert/verify.ps1 similarity index 100% rename from 01-essentials/05-revert/verify.ps1 rename to 01-essentials/06-revert/verify.ps1 diff --git a/01-essentials/06-reset/README.md b/01-essentials/07-reset/README.md similarity index 100% rename from 01-essentials/06-reset/README.md rename to 01-essentials/07-reset/README.md diff --git a/01-essentials/06-reset/reset.ps1 b/01-essentials/07-reset/reset.ps1 similarity index 100% rename from 01-essentials/06-reset/reset.ps1 rename to 01-essentials/07-reset/reset.ps1 diff --git a/01-essentials/06-reset/setup.ps1 b/01-essentials/07-reset/setup.ps1 similarity index 100% rename from 01-essentials/06-reset/setup.ps1 rename to 01-essentials/07-reset/setup.ps1 diff --git a/01-essentials/06-reset/verify.ps1 b/01-essentials/07-reset/verify.ps1 similarity index 100% rename from 01-essentials/06-reset/verify.ps1 rename to 01-essentials/07-reset/verify.ps1 diff --git a/01-essentials/07-stash/README.md b/01-essentials/08-stash/README.md similarity index 100% rename from 01-essentials/07-stash/README.md rename to 01-essentials/08-stash/README.md diff --git a/01-essentials/07-stash/reset.ps1 b/01-essentials/08-stash/reset.ps1 similarity index 100% rename from 01-essentials/07-stash/reset.ps1 rename to 01-essentials/08-stash/reset.ps1 diff --git a/01-essentials/07-stash/setup.ps1 b/01-essentials/08-stash/setup.ps1 similarity index 100% rename from 01-essentials/07-stash/setup.ps1 rename to 01-essentials/08-stash/setup.ps1 diff --git a/01-essentials/07-stash/verify.ps1 b/01-essentials/08-stash/verify.ps1 similarity index 100% rename from 01-essentials/07-stash/verify.ps1 rename to 01-essentials/08-stash/verify.ps1 diff --git a/01-essentials/08-multiplayer/01_FACILITATOR.md b/01-essentials/09-multiplayer/01_FACILITATOR.md similarity index 100% rename from 01-essentials/08-multiplayer/01_FACILITATOR.md rename to 01-essentials/09-multiplayer/01_FACILITATOR.md diff --git a/01-essentials/08-multiplayer/02_README.md b/01-essentials/09-multiplayer/02_README.md similarity index 100% rename from 01-essentials/08-multiplayer/02_README.md rename to 01-essentials/09-multiplayer/02_README.md diff --git a/01-essentials/08-multiplayer/03_TASKS.md b/01-essentials/09-multiplayer/03_TASKS.md similarity index 100% rename from 01-essentials/08-multiplayer/03_TASKS.md rename to 01-essentials/09-multiplayer/03_TASKS.md From bf07cb1868550bf3a5b8ab4515c70cb76783fbe5 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 13:14:38 +0100 Subject: [PATCH 39/61] refactor: check for main branch --- .../03-branching-and-merging/setup.ps1 | 41 +++++++++++++++---- .../03-branching-and-merging/verify.ps1 | 38 +++++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/01-essentials/03-branching-and-merging/setup.ps1 b/01-essentials/03-branching-and-merging/setup.ps1 index a9ca504..22b45fc 100755 --- a/01-essentials/03-branching-and-merging/setup.ps1 +++ b/01-essentials/03-branching-and-merging/setup.ps1 @@ -31,6 +31,19 @@ git init | Out-Null git config user.name "Workshop Student" git config user.email "student@example.com" +# Detect the default branch name (could be main, master, etc.) +# First commit creates the branch, so we detect it after that +$mainBranch = git branch --show-current +if (-not $mainBranch) { + # Fallback: Get default branch name from git config + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { + # Ultimate fallback: use "main" + $mainBranch = "main" + } +} +Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow + # ============================================================================ # Create a realistic project history with multiple merged branches # ============================================================================ @@ -96,8 +109,8 @@ Set-Content -Path "login.py" -Value $loginContent git add . git commit -m "Add password validation" | Out-Null -# Switch back to main and make more commits -git switch main | Out-Null +# Switch back to main branch +git switch $mainBranch | Out-Null $appContent = @" # app.py - Application entry point @@ -163,8 +176,8 @@ Set-Content -Path "api.py" -Value $apiContent git add . git commit -m "Add delete endpoint to API" | Out-Null -# Switch back to main and add documentation -git switch main | Out-Null +# Switch back to main branch and add documentation +git switch $mainBranch | Out-Null $readmeContent = @" # My Application @@ -241,8 +254,22 @@ Set-Content -Path "database.py" -Value $dbContent git add . git commit -m "Add disconnect method" | Out-Null -# Switch to main and merge -git switch main | Out-Null +# Switch to main branch and add another commit (to create divergent history) +git switch $mainBranch | Out-Null + +$configContent = @" +{ + "app": { + "port": 3000, + "debug": false + } +} +"@ +Set-Content -Path "config.json" -Value $configContent +git add . +git commit -m "Add configuration file" | Out-Null + +# Merge feature-database (will be three-way merge since main diverged) Write-Host "Merging feature-database into main..." -ForegroundColor Green git merge feature-database --no-edit | Out-Null @@ -278,7 +305,7 @@ Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan Write-Host "`nThe repository contains a realistic project history:" -ForegroundColor Yellow Write-Host " - Multiple feature branches (login, api, database)" -ForegroundColor White -Write-Host " - All branches have been merged into main" -ForegroundColor White +Write-Host " - All branches have been merged into $mainBranch" -ForegroundColor White Write-Host " - View the history: git log --oneline --graph --all" -ForegroundColor White Write-Host "`nNext steps:" -ForegroundColor Cyan Write-Host " 1. Read the README.md for detailed instructions" -ForegroundColor White diff --git a/01-essentials/03-branching-and-merging/verify.ps1 b/01-essentials/03-branching-and-merging/verify.ps1 index 246f8fe..a6beeb0 100755 --- a/01-essentials/03-branching-and-merging/verify.ps1 +++ b/01-essentials/03-branching-and-merging/verify.ps1 @@ -59,9 +59,33 @@ if (-not (Test-Path ".git")) { Write-Host "`n=== Verifying Module 03: Branching and Merging ===" -ForegroundColor Cyan # ============================================================================ -# Count initial setup commits (should be 13 commits from setup) +# Detect the main branch name (could be main, master, etc.) # ============================================================================ -$initialCommitCount = 13 +# Try to get the default branch from remote origin first +$mainBranch = git symbolic-ref refs/remotes/origin/HEAD 2>$null | Split-Path -Leaf +if (-not $mainBranch) { + # Fallback: try to detect from local branches + $allBranches = git branch --list 2>$null | ForEach-Object { $_.Trim('* ') } + if ($allBranches -contains "main") { + $mainBranch = "main" + } elseif ($allBranches -contains "master") { + $mainBranch = "master" + } else { + # Get the default branch from git config + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { + # Ultimate fallback: use the first branch + $mainBranch = $allBranches | Select-Object -First 1 + if (-not $mainBranch) { $mainBranch = "main" } + } + } +} +Write-Host "Detected main branch: $mainBranch" -ForegroundColor Cyan + +# ============================================================================ +# Count initial setup commits (should be 15 commits from setup) +# ============================================================================ +$initialCommitCount = 15 # ============================================================================ # Check for new commits beyond setup @@ -83,7 +107,7 @@ if ($totalCommits -gt $initialCommitCount) { # Check for branches (excluding the example branches) # ============================================================================ $allBranches = git branch --list 2>$null | ForEach-Object { $_.Trim('* ') } -$exampleBranches = @('main', 'feature-login', 'feature-api', 'feature-database') +$exampleBranches = @($mainBranch, 'feature-login', 'feature-api', 'feature-database') $studentBranches = $allBranches | Where-Object { $_ -notin $exampleBranches } if ($studentBranches.Count -gt 0) { @@ -115,11 +139,11 @@ if ($totalMerges -gt $setupMerges) { # Check current branch # ============================================================================ $currentBranch = git branch --show-current 2>$null -if ($currentBranch -eq "main") { - Write-Pass "Currently on main branch" +if ($currentBranch -eq $mainBranch) { + Write-Pass "Currently on $mainBranch branch" } else { Write-Info "Currently on '$currentBranch' branch" - Write-Hint "Typically you merge feature branches INTO main" + Write-Hint "Typically you merge feature branches INTO $mainBranch" } Pop-Location @@ -145,7 +169,7 @@ if ($script:allChecksPassed) { Write-Host "Quick guide:" -ForegroundColor Cyan Write-Host " 1. Create a branch: git switch -c my-feature" -ForegroundColor White Write-Host " 2. Make changes and commit them" -ForegroundColor White - Write-Host " 3. Switch to main: git switch main" -ForegroundColor White + Write-Host " 3. Switch to $mainBranch : git switch $mainBranch" -ForegroundColor White Write-Host " 4. Merge your branch: git merge my-feature" -ForegroundColor White Write-Host " 5. Run this verify script again" -ForegroundColor White Write-Host "" From ced65740a3c478f1ff429438e3deda5defc51556 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 13:16:34 +0100 Subject: [PATCH 40/61] fix: naming of main branch write host --- 01-essentials/03-branching-and-merging/setup.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/01-essentials/03-branching-and-merging/setup.ps1 b/01-essentials/03-branching-and-merging/setup.ps1 index 22b45fc..feec71b 100755 --- a/01-essentials/03-branching-and-merging/setup.ps1 +++ b/01-essentials/03-branching-and-merging/setup.ps1 @@ -129,8 +129,8 @@ Set-Content -Path "app.py" -Value $appContent git add . git commit -m "Add app.py entry point" | Out-Null -# Merge feature-login into main -Write-Host "Merging feature-login into main..." -ForegroundColor Green +# Merge feature-login into $mainBranch +Write-Host "Merging feature-login into $mainBranch..." -ForegroundColor Green git merge feature-login --no-edit | Out-Null # ============================================================================ @@ -197,8 +197,8 @@ Set-Content -Path "README.md" -Value $readmeContent git add . git commit -m "Update README with setup instructions" | Out-Null -# Merge feature-api into main -Write-Host "Merging feature-api into main..." -ForegroundColor Green +# Merge feature-api into $mainBranch +Write-Host "Merging feature-api into $mainBranch..." -ForegroundColor Green git merge feature-api --no-edit | Out-Null # ============================================================================ @@ -270,7 +270,7 @@ git add . git commit -m "Add configuration file" | Out-Null # Merge feature-database (will be three-way merge since main diverged) -Write-Host "Merging feature-database into main..." -ForegroundColor Green +Write-Host "Merging feature-database into $mainBranch..." -ForegroundColor Green git merge feature-database --no-edit | Out-Null # Final update on main From c3d9d3337cad895bcc101b7730e5f85ff18656f0 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 13:47:30 +0100 Subject: [PATCH 41/61] refactor: add better instructions for branching and merging --- .../03-branching-and-merging/README.md | 94 +++++++++---------- 1 file changed, 42 insertions(+), 52 deletions(-) diff --git a/01-essentials/03-branching-and-merging/README.md b/01-essentials/03-branching-and-merging/README.md index f40ef70..18c8f97 100644 --- a/01-essentials/03-branching-and-merging/README.md +++ b/01-essentials/03-branching-and-merging/README.md @@ -14,7 +14,7 @@ By the end of this module, you will: Create the challenge environment: -```bash +```pwsh .\setup.ps1 ``` @@ -39,7 +39,7 @@ This creates a repository with a realistic project history showing multiple merg First, explore the existing history to see what merging looks like: -```bash +```pwsh cd challenge git log --oneline --graph --all ``` @@ -55,7 +55,7 @@ You'll see a visual graph showing: - Find the merge commits (they have two parent lines converging) **Explore the branches:** -```bash +```pwsh # See all branches git branch --all @@ -66,7 +66,7 @@ git ls-tree --name-only main ``` **View specific merges:** -```bash +```pwsh # See all merge commits git log --merges --oneline @@ -78,7 +78,7 @@ git show Now practice creating your own branch: -```bash +```pwsh # Create and switch to a new branch git switch -c my-feature @@ -92,50 +92,45 @@ The `*` shows which branch you're currently on. Add some changes to your branch: -```bash -# Create a new file or modify an existing one -echo "# My Feature" > my-feature.md - +Create or edit a file then add and commit +```pwsh +# Create a new file or modify an existing one. # Stage and commit +git status # see the files that are untracked or changed git add . git commit -m "Add my feature" +# Create or edit another file # Make another commit -echo "More details" >> my-feature.md +git status # see the files that are untracked or changed git add . git commit -m "Expand feature documentation" + +git log --oneline --graph --all # see how your branch is moving forward from main ``` **Important:** Changes on your branch don't affect main! -```bash -# Switch to main -git switch main - -# Notice your my-feature.md doesn't exist here -ls - -# Switch back to your branch -git switch my-feature - -# Now it exists again! -ls -``` +1. Change back to the `main` branch `git switch main` +2. Check the changes you committed before. You'll notice that they're gone! +3. Now edit a file or create a new file and add it and commit it on your `main` branch (hint: `git add .`, `git commit -m`) + - This way we create diverging branches. The `main` branch has changes as well as your new `my-feature` branch. + - Run `git log --oneline --graph --all` to see how the tree is looking +4. Switch back to your branch `git switch my-feature` +5. The changes from the `main` branch are now gone. Check the changes you committed before. You'll notive they're back! ### Part 4: Merge Your Branch Bring your work into main: -```bash -# Switch to the branch you want to merge INTO -git switch main - -# Merge your feature branch -git merge my-feature - -# View the result -git log --oneline --graph --all -``` +1. Go back to the main branch `git switch main` +2. Run a `git log --oneline --graph --all` to see that the `HEAD` is on your `main` branch +3. It might be wise to first ensure that we're using Visual Studio Code to handle merge messages. If you haven't already set `git config --global core.editor "code --wait"` this sets Visual Studio Code to be the default editor for anything Git related. +4. Let's merge the recently created branch `git merge my-feature` +5. Visual Studio Code should open with a commit message. In order to solidify the commit simply close the window. That tells Git that the commit message has been written and the change should be committed. +6. Now run `git log --oneline --graph --all` and see your changes merge into the `main` branch! +7. Now let's clean up a bit, run `git branch -d my-feature` to remove the recently merged branch. + - If you hadn't merged the branch first this command would fail as Git will warn you that you have changes not merged into the `main` branch You should see your feature branch merged into main! @@ -143,7 +138,7 @@ You should see your feature branch merged into main! Create additional branches to practice: -```bash +```pwsh # Create another feature git switch -c another-feature @@ -156,7 +151,7 @@ git merge another-feature ``` **Verify your work:** -```bash +```pwsh # From the module directory (not inside challenge/) .\verify.ps1 ``` @@ -182,7 +177,7 @@ my-feature: D---E `HEAD` points to your current branch. It's Git's way of saying "you are here." -```bash +```pwsh # HEAD points to main git switch main @@ -226,7 +221,7 @@ If main hasn't changed, Git just moves the pointer forward. No merge commit need ### Branching -```bash +```pwsh # List all branches (* shows current branch) git branch @@ -251,7 +246,7 @@ git branch -D feature-name ### Merging -```bash +```pwsh # Merge a branch into your current branch git merge branch-name @@ -264,7 +259,7 @@ git merge --no-ff branch-name ### Viewing History -```bash +```pwsh # Visual branch graph git log --oneline --graph --all @@ -285,7 +280,7 @@ git branch --no-merged main ### Creating a Feature -```bash +```pwsh # Start from main git switch main @@ -305,7 +300,7 @@ git commit -m "Improve awesome feature" ### Merging a Feature -```bash +```pwsh # Switch to main git switch main @@ -318,7 +313,7 @@ git branch -d feature-awesome ### Keeping Main Updated While Working -```bash +```pwsh # You're on feature-awesome git switch feature-awesome @@ -337,7 +332,7 @@ git pull origin main ### "I'm on the wrong branch!" -```bash +```pwsh # Switch to the correct branch git switch correct-branch @@ -349,7 +344,7 @@ git branch Don't panic! You can move commits to another branch: -```bash +```pwsh # Create the correct branch from current state git branch correct-branch @@ -363,7 +358,7 @@ git switch correct-branch ### "The merge created unexpected results!" -```bash +```pwsh # Undo the merge git merge --abort @@ -373,7 +368,7 @@ git reset --hard HEAD~1 ### "I want to see what changed in a merge!" -```bash +```pwsh # Show the merge commit git show @@ -384,15 +379,10 @@ git diff main..feature-branch ## Tips for Success 💡 **Branch often** - Branches are cheap! Create one for each feature or experiment. - 💡 **Commit before switching** - Always commit (or stash) changes before switching branches. - 💡 **Keep branches focused** - One feature per branch makes merging easier. - 💡 **Delete merged branches** - Clean up with `git branch -d branch-name` after merging. - 💡 **Use descriptive names** - `feature-login` is better than `stuff` or `branch1`. - 💡 **Visualize often** - Run `git log --oneline --graph --all` to understand your history. ## What You've Learned @@ -412,7 +402,7 @@ After completing this module, you understand: Ready to continue? The next module covers **merge conflicts** - what happens when Git can't automatically merge changes. To start over: -```bash +```pwsh .\reset.ps1 .\setup.ps1 ``` From 0183a06134bb4df67966706ac3731c8b7a632e80 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 13:55:55 +0100 Subject: [PATCH 42/61] feat: set git defaultBranch to main --- install.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.ps1 b/install.ps1 index df689b2..be28425 100644 --- a/install.ps1 +++ b/install.ps1 @@ -635,6 +635,9 @@ if ($userChoice -ne "CloneOnly") { -CheckCommand "git" ` -MinVersion "2.23" ` -AdditionalArgs "-e" + + Write-Host "Setting the init.defaultBranch to be \"main\"" + git config --global init.defaultBranch main # Verify Git version specifically if ($results.Git) { From f0522e14bcd2f89c886881f333416715284be667 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 14:07:25 +0100 Subject: [PATCH 43/61] feat: setup merge and diff for git --- install.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index be28425..289f010 100644 --- a/install.ps1 +++ b/install.ps1 @@ -636,8 +636,19 @@ if ($userChoice -ne "CloneOnly") { -MinVersion "2.23" ` -AdditionalArgs "-e" - Write-Host "Setting the init.defaultBranch to be \"main\"" + Write-Host " Setting the init.defaultBranch to be 'main'" git config --global init.defaultBranch main + + Write-Host " Setting the default editor to code, to handle merge messages" + git config --global core.editor "code --wait" + + Write-Host " Setting vscode at the default code editor for merge conflicts" + git config --global merge.tool vscode + git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED' + + Write-Host " Setting vscode as the default code editor for diffs" + git config --global diff.tool vscode + git config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE' # Verify Git version specifically if ($results.Git) { From 8a82253fc27cc35c6bd73da372fbd15d93c2cac0 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 14:22:11 +0100 Subject: [PATCH 44/61] fix: add better explanation of merges --- .../03-branching-and-merging/README.md | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/01-essentials/03-branching-and-merging/README.md b/01-essentials/03-branching-and-merging/README.md index 18c8f97..633ace7 100644 --- a/01-essentials/03-branching-and-merging/README.md +++ b/01-essentials/03-branching-and-merging/README.md @@ -78,36 +78,15 @@ git show Now practice creating your own branch: -```pwsh -# Create and switch to a new branch -git switch -c my-feature - -# Verify you're on the new branch -git branch -``` - -The `*` shows which branch you're currently on. +1. Create a new branch with the name `my-feature` using `git switch -c my-feature` +2. Check which branch you're on with `git branch`. The `*` shows which branch you're currently on. ### Part 3: Make Commits on Your Branch -Add some changes to your branch: - -Create or edit a file then add and commit -```pwsh -# Create a new file or modify an existing one. -# Stage and commit -git status # see the files that are untracked or changed -git add . -git commit -m "Add my feature" - -# Create or edit another file -# Make another commit -git status # see the files that are untracked or changed -git add . -git commit -m "Expand feature documentation" - -git log --oneline --graph --all # see how your branch is moving forward from main -``` +1. Create a new file called `DOCS.md` and add some content. What doesn't matter, just something. +2. Add and commit the `DOCS.md` file (use the commands from `01-basics` module). +3. Add some more content to the `DOCS.md` file, add and commit the changes. +4. Now check how the tree is looking with `git log --oneline --graph --all` **Important:** Changes on your branch don't affect main! @@ -117,7 +96,7 @@ git log --oneline --graph --all # see how your branch is moving forward from mai - This way we create diverging branches. The `main` branch has changes as well as your new `my-feature` branch. - Run `git log --oneline --graph --all` to see how the tree is looking 4. Switch back to your branch `git switch my-feature` -5. The changes from the `main` branch are now gone. Check the changes you committed before. You'll notive they're back! +5. The changes from the `main` branch are now gone. Check the changes you committed before. You'll notice they're back! ### Part 4: Merge Your Branch From 40341d21a71d38e42512111335b1690d8ea8cea2 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 14:30:08 +0100 Subject: [PATCH 45/61] fix: add suggestion for a new file for branching and merging --- 01-essentials/03-branching-and-merging/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/01-essentials/03-branching-and-merging/README.md b/01-essentials/03-branching-and-merging/README.md index 633ace7..d50aa3f 100644 --- a/01-essentials/03-branching-and-merging/README.md +++ b/01-essentials/03-branching-and-merging/README.md @@ -92,7 +92,7 @@ Now practice creating your own branch: 1. Change back to the `main` branch `git switch main` 2. Check the changes you committed before. You'll notice that they're gone! -3. Now edit a file or create a new file and add it and commit it on your `main` branch (hint: `git add .`, `git commit -m`) +3. Now edit a file or create a new file (perhaps GUIDE.md, content of the file doesn't matter) and add it and commit it on your `main` branch (hint: `git add .`, `git commit -m`) - This way we create diverging branches. The `main` branch has changes as well as your new `my-feature` branch. - Run `git log --oneline --graph --all` to see how the tree is looking 4. Switch back to your branch `git switch my-feature` From 9fbdd941fa94cc46923d0e3dbe86fd93645cf615 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 14:31:00 +0100 Subject: [PATCH 46/61] feat: check for mainbranch --- 01-essentials/04-merge-conflict/setup.ps1 | 21 ++++++++-- 01-essentials/04-merge-conflict/verify.ps1 | 34 +++++++++++++--- 01-essentials/05-cherry-pick/setup.ps1 | 18 +++++++-- 01-essentials/05-cherry-pick/verify.ps1 | 45 ++++++++++++++-------- 01-essentials/06-revert/setup.ps1 | 15 +++++++- 01-essentials/07-reset/setup.ps1 | 15 +++++++- 01-essentials/08-stash/setup.ps1 | 18 +++++++-- 01-essentials/08-stash/verify.ps1 | 27 ++++++++++--- 8 files changed, 152 insertions(+), 41 deletions(-) diff --git a/01-essentials/04-merge-conflict/setup.ps1 b/01-essentials/04-merge-conflict/setup.ps1 index 86fa42d..5775cbd 100644 --- a/01-essentials/04-merge-conflict/setup.ps1 +++ b/01-essentials/04-merge-conflict/setup.ps1 @@ -30,6 +30,19 @@ git init | Out-Null git config user.name "Workshop Student" git config user.email "student@example.com" +# Detect the default branch name (could be main, master, etc.) +# First commit creates the branch, so we detect it after that +$mainBranch = git branch --show-current +if (-not $mainBranch) { + # Fallback: Get default branch name from git config + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { + # Ultimate fallback: use "main" + $mainBranch = "main" + } +} +Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow + # ============================================================================ # Create base project # ============================================================================ @@ -87,7 +100,7 @@ git commit -m "Add timeout configuration" | Out-Null # Branch 2: add-debug (adds debug setting - CONFLICTS with timeout!) # ============================================================================ Write-Host "Creating add-debug branch..." -ForegroundColor Cyan -git switch main | Out-Null +git switch $mainBranch | Out-Null git switch -c add-debug | Out-Null $debugConfig = @" @@ -104,8 +117,8 @@ Set-Content -Path "config.json" -Value $debugConfig git add . git commit -m "Add debug mode configuration" | Out-Null -# Switch back to main -git switch main | Out-Null +# Switch back to main branch +git switch $mainBranch | Out-Null # Return to module directory Set-Location .. @@ -113,7 +126,7 @@ Set-Location .. Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan Write-Host "`nThe repository contains:" -ForegroundColor Yellow -Write-Host " - main branch: base configuration" -ForegroundColor White +Write-Host " - $mainBranch branch: base configuration" -ForegroundColor White Write-Host " - add-timeout branch: adds timeout setting" -ForegroundColor White Write-Host " - add-debug branch: adds debug setting" -ForegroundColor White Write-Host "`nBoth branches modify the same part of config.json!" -ForegroundColor Red diff --git a/01-essentials/04-merge-conflict/verify.ps1 b/01-essentials/04-merge-conflict/verify.ps1 index 8a04114..739442e 100644 --- a/01-essentials/04-merge-conflict/verify.ps1 +++ b/01-essentials/04-merge-conflict/verify.ps1 @@ -59,15 +59,39 @@ if (-not (Test-Path ".git")) { Write-Host "`n=== Verifying Module 04: Merge Conflicts ===" -ForegroundColor Cyan +# ============================================================================ +# Detect the main branch name (could be main, master, etc.) +# ============================================================================ +# Try to get the default branch from remote origin first +$mainBranch = git symbolic-ref refs/remotes/origin/HEAD 2>$null | Split-Path -Leaf +if (-not $mainBranch) { + # Fallback: try to detect from local branches + $allBranches = git branch --list 2>$null | ForEach-Object { $_.Trim('* ') } + if ($allBranches -contains "main") { + $mainBranch = "main" + } elseif ($allBranches -contains "master") { + $mainBranch = "master" + } else { + # Get the default branch from git config + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { + # Ultimate fallback: use the first branch + $mainBranch = $allBranches | Select-Object -First 1 + if (-not $mainBranch) { $mainBranch = "main" } + } + } +} +Write-Host "Detected main branch: $mainBranch" -ForegroundColor Cyan + # ============================================================================ # Check current branch # ============================================================================ $currentBranch = git branch --show-current 2>$null -if ($currentBranch -eq "main") { - Write-Pass "Currently on main branch" +if ($currentBranch -eq $mainBranch) { + Write-Pass "Currently on $mainBranch branch" } else { - Write-Fail "Should be on main branch (currently on: $currentBranch)" - Write-Hint "Switch to main with: git switch main" + Write-Fail "Should be on $mainBranch branch (currently on: $currentBranch)" + Write-Hint "Switch to $mainBranch with: git switch $mainBranch" } # ============================================================================ @@ -196,7 +220,7 @@ if ($script:allChecksPassed) { Write-Host "[SUMMARY] Some checks failed. Review the hints above." -ForegroundColor Red Write-Host "" Write-Host "Quick guide:" -ForegroundColor Cyan - Write-Host " 1. Make sure you're on main: git switch main" -ForegroundColor White + Write-Host " 1. Make sure you're on $mainBranch : git switch $mainBranch" -ForegroundColor White Write-Host " 2. Merge first branch: git merge add-timeout" -ForegroundColor White Write-Host " 3. Merge second branch: git merge add-debug" -ForegroundColor White Write-Host " 4. Resolve conflict: edit config.json, remove markers, keep both settings" -ForegroundColor White diff --git a/01-essentials/05-cherry-pick/setup.ps1 b/01-essentials/05-cherry-pick/setup.ps1 index 879936f..5dfa6df 100644 --- a/01-essentials/05-cherry-pick/setup.ps1 +++ b/01-essentials/05-cherry-pick/setup.ps1 @@ -26,7 +26,8 @@ git init | Out-Null git config user.name "Workshop User" | Out-Null git config user.email "user@workshop.local" | Out-Null -# Create initial commits on main branch +# Detect the default branch name (could be main, master, etc.) +# First commit creates the branch, so we detect it after the first commit below $app = @" class App: def __init__(self): @@ -40,6 +41,14 @@ Set-Content -Path "app.py" -Value $app git add app.py git commit -m "Initial app implementation" | Out-Null +# Detect the main branch name after first commit +$mainBranch = git branch --show-current +if (-not $mainBranch) { + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { $mainBranch = "main" } +} +Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow + $readme = @" # Application @@ -164,12 +173,13 @@ Write-Host "========================================" -ForegroundColor Green Write-Host "`nYou are on the 'development' branch with multiple commits:" -ForegroundColor Cyan Write-Host "- Experimental features (not ready for production)" -ForegroundColor Yellow Write-Host "- Critical bug fixes (needed in production NOW)" -ForegroundColor Green +Write-Host "`nDetected main branch: $mainBranch" -ForegroundColor Cyan Write-Host "`nYour task:" -ForegroundColor Yellow Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White Write-Host "2. View the development branch commits: git log --oneline" -ForegroundColor White Write-Host "3. Identify which commits are bug fixes (look for 'Fix' in messages)" -ForegroundColor White -Write-Host "4. Switch to main branch: git checkout main" -ForegroundColor White -Write-Host "5. Cherry-pick ONLY the bug fix commits to main" -ForegroundColor White -Write-Host "6. Do NOT bring the experimental features to main" -ForegroundColor White +Write-Host "4. Switch to $mainBranch branch: git checkout $mainBranch" -ForegroundColor White +Write-Host "5. Cherry-pick ONLY the bug fix commits to $mainBranch" -ForegroundColor White +Write-Host "6. Do NOT bring the experimental features to $mainBranch" -ForegroundColor White Write-Host "`nHint: Look for commits mentioning 'security' and 'performance' fixes" -ForegroundColor Cyan Write-Host "Run '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/01-essentials/05-cherry-pick/verify.ps1 b/01-essentials/05-cherry-pick/verify.ps1 index 2512222..7449dbc 100644 --- a/01-essentials/05-cherry-pick/verify.ps1 +++ b/01-essentials/05-cherry-pick/verify.ps1 @@ -32,12 +32,27 @@ if (-not (Test-Path ".git")) { exit 1 } +# Detect the main branch name +$allBranches = git branch --list 2>$null | ForEach-Object { $_.Trim('* ') } +if ($allBranches -contains "main") { + $mainBranch = "main" +} elseif ($allBranches -contains "master") { + $mainBranch = "master" +} else { + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { + $mainBranch = $allBranches | Select-Object -First 1 + if (-not $mainBranch) { $mainBranch = "main" } + } +} +Write-Host "Detected main branch: $mainBranch" -ForegroundColor Cyan + # Check current branch $currentBranch = git branch --show-current 2>$null -if ($currentBranch -ne "main") { - Write-Host "[FAIL] You should be on the 'main' branch." -ForegroundColor Red +if ($currentBranch -ne $mainBranch) { + Write-Host "[FAIL] You should be on the '$mainBranch' branch." -ForegroundColor Red Write-Host "Current branch: $currentBranch" -ForegroundColor Yellow - Write-Host "Hint: Use 'git checkout main' to switch to main branch" -ForegroundColor Yellow + Write-Host "Hint: Use 'git checkout $mainBranch' to switch to $mainBranch branch" -ForegroundColor Yellow Set-Location .. exit 1 } @@ -54,15 +69,15 @@ if (Test-Path ".git/CHERRY_PICK_HEAD") { } # Check commit count on main (should be 4: 2 initial + 2 cherry-picked) -$mainCommitCount = (git rev-list --count main 2>$null) +$mainCommitCount = (git rev-list --count $mainBranch 2>$null) if ($mainCommitCount -ne 4) { - Write-Host "[FAIL] Expected 4 commits on main branch, found $mainCommitCount" -ForegroundColor Red + Write-Host "[FAIL] Expected 4 commits on $mainBranch branch, found $mainCommitCount" -ForegroundColor Red if ($mainCommitCount -lt 4) { - Write-Host "Hint: You should cherry-pick 2 bug fix commits to main" -ForegroundColor Yellow + Write-Host "Hint: You should cherry-pick 2 bug fix commits to $mainBranch" -ForegroundColor Yellow } else { Write-Host "Hint: You should cherry-pick ONLY the 2 bug fix commits, not all commits" -ForegroundColor Yellow } - Write-Host "`nExpected commits on main:" -ForegroundColor Yellow + Write-Host "`nExpected commits on $mainBranch:" -ForegroundColor Yellow Write-Host " 1. Initial app implementation" -ForegroundColor White Write-Host " 2. Add README" -ForegroundColor White Write-Host " 3. Fix security vulnerability in input validation (cherry-picked)" -ForegroundColor White @@ -72,9 +87,9 @@ if ($mainCommitCount -ne 4) { } # Check for merge commits (should be none - cherry-pick doesn't create merge commits) -$mergeCommits = git log --merges --oneline main 2>$null +$mergeCommits = git log --merges --oneline $mainBranch 2>$null if ($mergeCommits) { - Write-Host "[FAIL] Found merge commits on main. You should use cherry-pick, not merge." -ForegroundColor Red + Write-Host "[FAIL] Found merge commits on $mainBranch. You should use cherry-pick, not merge." -ForegroundColor Red Write-Host "Hint: Use 'git cherry-pick ' instead of 'git merge'" -ForegroundColor Yellow Set-Location .. exit 1 @@ -82,7 +97,7 @@ if ($mergeCommits) { # Check that security.py exists (from the security fix commit) if (-not (Test-Path "security.py")) { - Write-Host "[FAIL] security.py not found on main branch." -ForegroundColor Red + Write-Host "[FAIL] security.py not found on $mainBranch branch." -ForegroundColor Red Write-Host "Hint: You need to cherry-pick the 'Fix security vulnerability' commit" -ForegroundColor Yellow Set-Location .. exit 1 @@ -151,7 +166,7 @@ if ($appContent -match "enable_experimental_features") { } # Check commit messages to verify cherry-picks -$commits = git log --pretty=format:"%s" main 2>$null +$commits = git log --pretty=format:"%s" $mainBranch 2>$null $commitArray = $commits -split "`n" $hasSecurityFix = $false @@ -167,14 +182,14 @@ foreach ($commit in $commitArray) { } if (-not $hasSecurityFix) { - Write-Host "[FAIL] Security fix commit not found on main branch." -ForegroundColor Red + Write-Host "[FAIL] Security fix commit not found on $mainBranch branch." -ForegroundColor Red Write-Host "Hint: Cherry-pick the 'Fix security vulnerability' commit from development" -ForegroundColor Yellow Set-Location .. exit 1 } if (-not $hasPerformanceFix) { - Write-Host "[FAIL] Performance fix commit not found on main branch." -ForegroundColor Red + Write-Host "[FAIL] Performance fix commit not found on $mainBranch branch." -ForegroundColor Red Write-Host "Hint: Cherry-pick the 'Fix performance issue' commit from development" -ForegroundColor Yellow Set-Location .. exit 1 @@ -194,8 +209,8 @@ Write-Host "`n========================================" -ForegroundColor Green Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green Write-Host "========================================" -ForegroundColor Green Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Cherry-picked the security vulnerability fix to main" -ForegroundColor White -Write-Host "- Cherry-picked the performance issue fix to main" -ForegroundColor White +Write-Host "- Cherry-picked the security vulnerability fix to $mainBranch" -ForegroundColor White +Write-Host "- Cherry-picked the performance issue fix to $mainBranch" -ForegroundColor White Write-Host "- Left experimental features on development branch only" -ForegroundColor White Write-Host "- Kept development branch intact with all commits" -ForegroundColor White Write-Host "`nPerfect use of cherry-pick!" -ForegroundColor Green diff --git a/01-essentials/06-revert/setup.ps1 b/01-essentials/06-revert/setup.ps1 index f3e3911..b481baa 100644 --- a/01-essentials/06-revert/setup.ps1 +++ b/01-essentials/06-revert/setup.ps1 @@ -32,6 +32,9 @@ git init | Out-Null git config user.name "Workshop Student" git config user.email "student@example.com" +# Detect the default branch name after first commit (created below) +# Will be detected after the initial commit in SCENARIO 1 + # ============================================================================ # SCENARIO 1: Regular Revert (Basic) # ============================================================================ @@ -53,6 +56,14 @@ Set-Content -Path "calculator.py" -Value $calcContent git add . git commit -m "Initial calculator implementation" | Out-Null +# Detect the main branch name after first commit +$mainBranch = git branch --show-current +if (-not $mainBranch) { + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { $mainBranch = "main" } +} +Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow + # Create regular-revert branch git switch -c regular-revert | Out-Null @@ -136,7 +147,7 @@ Write-Host "[CREATED] regular-revert branch with bad divide commit" -ForegroundC Write-Host "`nScenario 2: Creating merge-revert scenario..." -ForegroundColor Cyan # Switch back to main -git switch main | Out-Null +git switch $mainBranch | Out-Null # Create merge-revert branch git switch -c merge-revert | Out-Null @@ -222,7 +233,7 @@ Write-Host "[CREATED] merge-revert branch with merge commit to revert" -Foregrou Write-Host "`nScenario 3: Creating multi-revert branch..." -ForegroundColor Cyan # Switch back to main -git switch main | Out-Null +git switch $mainBranch | Out-Null # Create multi-revert branch git switch -c multi-revert | Out-Null diff --git a/01-essentials/07-reset/setup.ps1 b/01-essentials/07-reset/setup.ps1 index 52ebf47..ccdaf38 100644 --- a/01-essentials/07-reset/setup.ps1 +++ b/01-essentials/07-reset/setup.ps1 @@ -33,6 +33,9 @@ git init | Out-Null git config user.name "Workshop Student" git config user.email "student@example.com" +# Detect the default branch name after first commit (created below) +# Will be detected after the initial commit + # ============================================================================ # Create initial commit (shared by all scenarios) # ============================================================================ @@ -45,6 +48,14 @@ Set-Content -Path "README.md" -Value $readmeContent git add . git commit -m "Initial commit" | Out-Null +# Detect the main branch name after first commit +$mainBranch = git branch --show-current +if (-not $mainBranch) { + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { $mainBranch = "main" } +} +Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow + # ============================================================================ # SCENARIO 1: Soft Reset (--soft) # ============================================================================ @@ -155,7 +166,7 @@ Write-Host "[CREATED] soft-reset branch with commit to reset --soft" -Foreground Write-Host "`nScenario 2: Creating mixed-reset branch..." -ForegroundColor Cyan # Switch back to initial commit and create mixed-reset branch -git switch main | Out-Null +git switch $mainBranch | Out-Null git switch -c mixed-reset | Out-Null # Build up scenario 2 commits @@ -256,7 +267,7 @@ Write-Host "[CREATED] mixed-reset branch with commits to reset --mixed" -Foregro Write-Host "`nScenario 3: Creating hard-reset branch..." -ForegroundColor Cyan # Switch back to main and create hard-reset branch -git switch main | Out-Null +git switch $mainBranch | Out-Null git switch -c hard-reset | Out-Null # Reset to basic state diff --git a/01-essentials/08-stash/setup.ps1 b/01-essentials/08-stash/setup.ps1 index 80481b3..c4f09d1 100644 --- a/01-essentials/08-stash/setup.ps1 +++ b/01-essentials/08-stash/setup.ps1 @@ -25,6 +25,9 @@ git init | Out-Null git config user.name "Workshop User" | Out-Null git config user.email "user@workshop.local" | Out-Null +# Detect the default branch name after first commit (created below) +# Will be detected after the initial commit + # Create initial application on main $app = @" class Application: @@ -45,6 +48,14 @@ Set-Content -Path "app.py" -Value $app git add app.py git commit -m "Initial application" | Out-Null +# Detect the main branch name after first commit +$mainBranch = git branch --show-current +if (-not $mainBranch) { + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { $mainBranch = "main" } +} +Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow + $readme = @" # MyApp @@ -78,7 +89,7 @@ git add login.py git commit -m "Start login service implementation" | Out-Null # Add a critical bug to main branch (simulating a bug that was introduced) -git checkout main | Out-Null +git checkout $mainBranch | Out-Null $appWithBug = @" class Application: @@ -140,13 +151,14 @@ Write-Host "========================================" -ForegroundColor Green Write-Host "`nSituation:" -ForegroundColor Cyan Write-Host "You're working on the login feature (feature-login branch)" -ForegroundColor White Write-Host "You have uncommitted changes - the feature is NOT complete yet" -ForegroundColor Yellow -Write-Host "`nUrgent: A critical security bug was found in production (main branch)!" -ForegroundColor Red +Write-Host "`nUrgent: A critical security bug was found in production ($mainBranch branch)!" -ForegroundColor Red Write-Host "You need to fix it immediately, but your current work isn't ready to commit." -ForegroundColor Red +Write-Host "`nDetected main branch: $mainBranch" -ForegroundColor Cyan Write-Host "`nYour task:" -ForegroundColor Yellow Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White Write-Host "2. Check your status: git status (see uncommitted changes)" -ForegroundColor White Write-Host "3. Stash your work: git stash save 'WIP: login feature'" -ForegroundColor White -Write-Host "4. Switch to main: git checkout main" -ForegroundColor White +Write-Host "4. Switch to $mainBranch: git checkout $mainBranch" -ForegroundColor White Write-Host "5. Fix the security bug in app.py (remove the comment and fix the auth)" -ForegroundColor White Write-Host "6. Commit the fix: git add app.py && git commit -m 'Fix critical security bug'" -ForegroundColor White Write-Host "7. Switch back: git checkout feature-login" -ForegroundColor White diff --git a/01-essentials/08-stash/verify.ps1 b/01-essentials/08-stash/verify.ps1 index 8c517d9..312ab2e 100644 --- a/01-essentials/08-stash/verify.ps1 +++ b/01-essentials/08-stash/verify.ps1 @@ -32,6 +32,21 @@ if (-not (Test-Path ".git")) { exit 1 } +# Detect the main branch name +$allBranches = git branch --list 2>$null | ForEach-Object { $_.Trim('* ') } +if ($allBranches -contains "main") { + $mainBranch = "main" +} elseif ($allBranches -contains "master") { + $mainBranch = "master" +} else { + $mainBranch = git config --get init.defaultBranch + if (-not $mainBranch) { + $mainBranch = $allBranches | Select-Object -First 1 + if (-not $mainBranch) { $mainBranch = "main" } + } +} +Write-Host "Detected main branch: $mainBranch" -ForegroundColor Cyan + # Check current branch $currentBranch = git branch --show-current 2>$null if ($currentBranch -ne "feature-login") { @@ -53,14 +68,14 @@ if ($status) { } # Verify main branch has the security fix -Write-Host "`nChecking main branch for bug fix..." -ForegroundColor Cyan -git checkout main 2>$null | Out-Null +Write-Host "`nChecking $mainBranch branch for bug fix..." -ForegroundColor Cyan +git checkout $mainBranch 2>$null | Out-Null # Check for bug fix commit -$mainCommits = git log --pretty=format:"%s" main 2>$null +$mainCommits = git log --pretty=format:"%s" $mainBranch 2>$null if ($mainCommits -notmatch "security bug|Fix.*bug|security fix") { - Write-Host "[FAIL] No security bug fix commit found on main branch." -ForegroundColor Red - Write-Host "Hint: After stashing, switch to main and commit a bug fix" -ForegroundColor Yellow + Write-Host "[FAIL] No security bug fix commit found on $mainBranch branch." -ForegroundColor Red + Write-Host "Hint: After stashing, switch to $mainBranch and commit a bug fix" -ForegroundColor Yellow git checkout feature-login 2>$null | Out-Null Set-Location .. exit 1 @@ -68,7 +83,7 @@ if ($mainCommits -notmatch "security bug|Fix.*bug|security fix") { # Check that app.py has been fixed if (-not (Test-Path "app.py")) { - Write-Host "[FAIL] app.py not found on main branch." -ForegroundColor Red + Write-Host "[FAIL] app.py not found on $mainBranch branch." -ForegroundColor Red git checkout feature-login 2>$null | Out-Null Set-Location .. exit 1 From 988ce3bc92c8764ea71abcdfc091621a69686100 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 14:31:30 +0100 Subject: [PATCH 47/61] fix: remove subproject "challenge" folders --- 01-essentials/03-branching-and-merging/challenge | 1 - 01-essentials/04-merge-conflict/challenge | 1 - 2 files changed, 2 deletions(-) delete mode 160000 01-essentials/03-branching-and-merging/challenge delete mode 160000 01-essentials/04-merge-conflict/challenge diff --git a/01-essentials/03-branching-and-merging/challenge b/01-essentials/03-branching-and-merging/challenge deleted file mode 160000 index 8ae52cc..0000000 --- a/01-essentials/03-branching-and-merging/challenge +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8ae52cc25c2a5417b1d6b5316949394da02f6914 diff --git a/01-essentials/04-merge-conflict/challenge b/01-essentials/04-merge-conflict/challenge deleted file mode 160000 index 676b08e..0000000 --- a/01-essentials/04-merge-conflict/challenge +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 676b08e650cacba9c8fabee80697ca968bc4f3ea From 1c20a06c217f994bce632374d8121c8240256eec Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 15:20:32 +0100 Subject: [PATCH 48/61] fix: cherry-pick conflict --- 01-essentials/05-cherry-pick/setup.ps1 | 39 +++++++++---------------- 01-essentials/05-cherry-pick/verify.ps1 | 27 +++++++++++------ 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/01-essentials/05-cherry-pick/setup.ps1 b/01-essentials/05-cherry-pick/setup.ps1 index 5dfa6df..d988419 100644 --- a/01-essentials/05-cherry-pick/setup.ps1 +++ b/01-essentials/05-cherry-pick/setup.ps1 @@ -128,40 +128,29 @@ git add app.py git commit -m "Add beta features framework" | Out-Null # Commit 4: Performance bug fix (SHOULD be cherry-picked) -$appWithPerformance = @" -class App: +# This commit adds caching to the existing file to fix performance +# It should apply cleanly to main since it doesn't depend on experimental features +$performanceCode = @" + +class DataCache: def __init__(self): - self.version = '1.0.0' - self.experimental_mode = False - self.beta_features = [] self.cache = {} - def start(self): - print('App started') - if self.experimental_mode: - self.enable_experimental_features() - - def enable_experimental_features(self): - print('Experimental features enabled') - - def add_beta_feature(self, feature): - self.beta_features.append(feature) - - def get_data(self, key): + def get(self, key): # Use cache to improve performance if key in self.cache: return self.cache[key] - data = self.fetch_data(key) - self.cache[key] = data - return data + return None - def fetch_data(self, key): - # Simulate data fetching - return {'key': key, 'value': 'data'} + def set(self, key, value): + self.cache[key] = value + + def clear(self): + self.cache.clear() "@ -Set-Content -Path "app.py" -Value $appWithPerformance -git add app.py +Set-Content -Path "cache.py" -Value $performanceCode +git add cache.py git commit -m "Fix performance issue with data caching" | Out-Null # Return to module directory diff --git a/01-essentials/05-cherry-pick/verify.ps1 b/01-essentials/05-cherry-pick/verify.ps1 index 7449dbc..8cf2774 100644 --- a/01-essentials/05-cherry-pick/verify.ps1 +++ b/01-essentials/05-cherry-pick/verify.ps1 @@ -77,7 +77,7 @@ if ($mainCommitCount -ne 4) { } else { Write-Host "Hint: You should cherry-pick ONLY the 2 bug fix commits, not all commits" -ForegroundColor Yellow } - Write-Host "`nExpected commits on $mainBranch:" -ForegroundColor Yellow + Write-Host "`nExpected commits on ${mainBranch}:" -ForegroundColor Yellow Write-Host " 1. Initial app implementation" -ForegroundColor White Write-Host " 2. Add README" -ForegroundColor White Write-Host " 3. Fix security vulnerability in input validation (cherry-picked)" -ForegroundColor White @@ -125,23 +125,32 @@ if (-not (Test-Path "app.py")) { exit 1 } -# Check that app.py has the performance fix (cache) but NOT experimental features -$appContent = Get-Content "app.py" -Raw - -# Should have cache (from performance fix) -if ($appContent -notmatch "cache") { - Write-Host "[FAIL] app.py is missing the performance fix (cache)." -ForegroundColor Red +# Check that cache.py exists (from performance fix) +if (-not (Test-Path "cache.py")) { + Write-Host "[FAIL] cache.py not found on $mainBranch branch." -ForegroundColor Red Write-Host "Hint: You need to cherry-pick the 'Fix performance issue' commit" -ForegroundColor Yellow Set-Location .. exit 1 } -if ($appContent -notmatch "get_data") { - Write-Host "[FAIL] app.py is missing the get_data method from performance fix." -ForegroundColor Red +# Check that cache.py has the DataCache class +$cacheContent = Get-Content "cache.py" -Raw + +if ($cacheContent -notmatch "DataCache") { + Write-Host "[FAIL] cache.py is missing the DataCache class." -ForegroundColor Red Set-Location .. exit 1 } +if ($cacheContent -notmatch "def get\(") { + Write-Host "[FAIL] cache.py is missing the get method." -ForegroundColor Red + Set-Location .. + exit 1 +} + +# Check that app.py does NOT have experimental features +$appContent = Get-Content "app.py" -Raw + # Should NOT have experimental features if ($appContent -match "experimental_mode") { Write-Host "[FAIL] app.py contains experimental features (experimental_mode)." -ForegroundColor Red From a34b7d155ff3f51a2d41ffddfb75ccd0c6321e74 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 15:22:45 +0100 Subject: [PATCH 49/61] refactor: move reset to advanced --- {01-essentials/07-reset => 02-advanced/01-reset}/README.md | 0 {01-essentials/07-reset => 02-advanced/01-reset}/reset.ps1 | 0 {01-essentials/07-reset => 02-advanced/01-reset}/setup.ps1 | 0 {01-essentials/07-reset => 02-advanced/01-reset}/verify.ps1 | 0 02-advanced/{01-rebasing => 02-rebasing}/README.md | 0 02-advanced/{01-rebasing => 02-rebasing}/reset.ps1 | 0 02-advanced/{01-rebasing => 02-rebasing}/setup.ps1 | 0 02-advanced/{01-rebasing => 02-rebasing}/verify.ps1 | 0 .../{02-interactive-rebase => 03-interactive-rebase}/README.md | 0 .../{02-interactive-rebase => 03-interactive-rebase}/reset.ps1 | 0 .../{02-interactive-rebase => 03-interactive-rebase}/setup.ps1 | 0 .../{02-interactive-rebase => 03-interactive-rebase}/verify.ps1 | 0 02-advanced/{03-worktrees => 04-worktrees}/README.md | 0 02-advanced/{03-worktrees => 04-worktrees}/reset.ps1 | 0 02-advanced/{03-worktrees => 04-worktrees}/setup.ps1 | 0 02-advanced/{03-worktrees => 04-worktrees}/verify.ps1 | 0 02-advanced/{04-bisect => 05-bisect}/README.md | 0 02-advanced/{04-bisect => 05-bisect}/reset.ps1 | 0 02-advanced/{04-bisect => 05-bisect}/setup.ps1 | 0 02-advanced/{04-bisect => 05-bisect}/verify.ps1 | 0 02-advanced/{05-blame => 06-blame}/README.md | 0 02-advanced/{05-blame => 06-blame}/reset.ps1 | 0 02-advanced/{05-blame => 06-blame}/setup.ps1 | 0 02-advanced/{05-blame => 06-blame}/verify.ps1 | 0 .../{06-merge-strategies => 07-merge-strategies}/README.md | 0 .../{06-merge-strategies => 07-merge-strategies}/reset.ps1 | 0 .../{06-merge-strategies => 07-merge-strategies}/setup.ps1 | 0 .../{06-merge-strategies => 07-merge-strategies}/verify.ps1 | 0 28 files changed, 0 insertions(+), 0 deletions(-) rename {01-essentials/07-reset => 02-advanced/01-reset}/README.md (100%) rename {01-essentials/07-reset => 02-advanced/01-reset}/reset.ps1 (100%) rename {01-essentials/07-reset => 02-advanced/01-reset}/setup.ps1 (100%) rename {01-essentials/07-reset => 02-advanced/01-reset}/verify.ps1 (100%) rename 02-advanced/{01-rebasing => 02-rebasing}/README.md (100%) rename 02-advanced/{01-rebasing => 02-rebasing}/reset.ps1 (100%) rename 02-advanced/{01-rebasing => 02-rebasing}/setup.ps1 (100%) rename 02-advanced/{01-rebasing => 02-rebasing}/verify.ps1 (100%) rename 02-advanced/{02-interactive-rebase => 03-interactive-rebase}/README.md (100%) rename 02-advanced/{02-interactive-rebase => 03-interactive-rebase}/reset.ps1 (100%) rename 02-advanced/{02-interactive-rebase => 03-interactive-rebase}/setup.ps1 (100%) rename 02-advanced/{02-interactive-rebase => 03-interactive-rebase}/verify.ps1 (100%) rename 02-advanced/{03-worktrees => 04-worktrees}/README.md (100%) rename 02-advanced/{03-worktrees => 04-worktrees}/reset.ps1 (100%) rename 02-advanced/{03-worktrees => 04-worktrees}/setup.ps1 (100%) rename 02-advanced/{03-worktrees => 04-worktrees}/verify.ps1 (100%) rename 02-advanced/{04-bisect => 05-bisect}/README.md (100%) rename 02-advanced/{04-bisect => 05-bisect}/reset.ps1 (100%) rename 02-advanced/{04-bisect => 05-bisect}/setup.ps1 (100%) rename 02-advanced/{04-bisect => 05-bisect}/verify.ps1 (100%) rename 02-advanced/{05-blame => 06-blame}/README.md (100%) rename 02-advanced/{05-blame => 06-blame}/reset.ps1 (100%) rename 02-advanced/{05-blame => 06-blame}/setup.ps1 (100%) rename 02-advanced/{05-blame => 06-blame}/verify.ps1 (100%) rename 02-advanced/{06-merge-strategies => 07-merge-strategies}/README.md (100%) rename 02-advanced/{06-merge-strategies => 07-merge-strategies}/reset.ps1 (100%) rename 02-advanced/{06-merge-strategies => 07-merge-strategies}/setup.ps1 (100%) rename 02-advanced/{06-merge-strategies => 07-merge-strategies}/verify.ps1 (100%) diff --git a/01-essentials/07-reset/README.md b/02-advanced/01-reset/README.md similarity index 100% rename from 01-essentials/07-reset/README.md rename to 02-advanced/01-reset/README.md diff --git a/01-essentials/07-reset/reset.ps1 b/02-advanced/01-reset/reset.ps1 similarity index 100% rename from 01-essentials/07-reset/reset.ps1 rename to 02-advanced/01-reset/reset.ps1 diff --git a/01-essentials/07-reset/setup.ps1 b/02-advanced/01-reset/setup.ps1 similarity index 100% rename from 01-essentials/07-reset/setup.ps1 rename to 02-advanced/01-reset/setup.ps1 diff --git a/01-essentials/07-reset/verify.ps1 b/02-advanced/01-reset/verify.ps1 similarity index 100% rename from 01-essentials/07-reset/verify.ps1 rename to 02-advanced/01-reset/verify.ps1 diff --git a/02-advanced/01-rebasing/README.md b/02-advanced/02-rebasing/README.md similarity index 100% rename from 02-advanced/01-rebasing/README.md rename to 02-advanced/02-rebasing/README.md diff --git a/02-advanced/01-rebasing/reset.ps1 b/02-advanced/02-rebasing/reset.ps1 similarity index 100% rename from 02-advanced/01-rebasing/reset.ps1 rename to 02-advanced/02-rebasing/reset.ps1 diff --git a/02-advanced/01-rebasing/setup.ps1 b/02-advanced/02-rebasing/setup.ps1 similarity index 100% rename from 02-advanced/01-rebasing/setup.ps1 rename to 02-advanced/02-rebasing/setup.ps1 diff --git a/02-advanced/01-rebasing/verify.ps1 b/02-advanced/02-rebasing/verify.ps1 similarity index 100% rename from 02-advanced/01-rebasing/verify.ps1 rename to 02-advanced/02-rebasing/verify.ps1 diff --git a/02-advanced/02-interactive-rebase/README.md b/02-advanced/03-interactive-rebase/README.md similarity index 100% rename from 02-advanced/02-interactive-rebase/README.md rename to 02-advanced/03-interactive-rebase/README.md diff --git a/02-advanced/02-interactive-rebase/reset.ps1 b/02-advanced/03-interactive-rebase/reset.ps1 similarity index 100% rename from 02-advanced/02-interactive-rebase/reset.ps1 rename to 02-advanced/03-interactive-rebase/reset.ps1 diff --git a/02-advanced/02-interactive-rebase/setup.ps1 b/02-advanced/03-interactive-rebase/setup.ps1 similarity index 100% rename from 02-advanced/02-interactive-rebase/setup.ps1 rename to 02-advanced/03-interactive-rebase/setup.ps1 diff --git a/02-advanced/02-interactive-rebase/verify.ps1 b/02-advanced/03-interactive-rebase/verify.ps1 similarity index 100% rename from 02-advanced/02-interactive-rebase/verify.ps1 rename to 02-advanced/03-interactive-rebase/verify.ps1 diff --git a/02-advanced/03-worktrees/README.md b/02-advanced/04-worktrees/README.md similarity index 100% rename from 02-advanced/03-worktrees/README.md rename to 02-advanced/04-worktrees/README.md diff --git a/02-advanced/03-worktrees/reset.ps1 b/02-advanced/04-worktrees/reset.ps1 similarity index 100% rename from 02-advanced/03-worktrees/reset.ps1 rename to 02-advanced/04-worktrees/reset.ps1 diff --git a/02-advanced/03-worktrees/setup.ps1 b/02-advanced/04-worktrees/setup.ps1 similarity index 100% rename from 02-advanced/03-worktrees/setup.ps1 rename to 02-advanced/04-worktrees/setup.ps1 diff --git a/02-advanced/03-worktrees/verify.ps1 b/02-advanced/04-worktrees/verify.ps1 similarity index 100% rename from 02-advanced/03-worktrees/verify.ps1 rename to 02-advanced/04-worktrees/verify.ps1 diff --git a/02-advanced/04-bisect/README.md b/02-advanced/05-bisect/README.md similarity index 100% rename from 02-advanced/04-bisect/README.md rename to 02-advanced/05-bisect/README.md diff --git a/02-advanced/04-bisect/reset.ps1 b/02-advanced/05-bisect/reset.ps1 similarity index 100% rename from 02-advanced/04-bisect/reset.ps1 rename to 02-advanced/05-bisect/reset.ps1 diff --git a/02-advanced/04-bisect/setup.ps1 b/02-advanced/05-bisect/setup.ps1 similarity index 100% rename from 02-advanced/04-bisect/setup.ps1 rename to 02-advanced/05-bisect/setup.ps1 diff --git a/02-advanced/04-bisect/verify.ps1 b/02-advanced/05-bisect/verify.ps1 similarity index 100% rename from 02-advanced/04-bisect/verify.ps1 rename to 02-advanced/05-bisect/verify.ps1 diff --git a/02-advanced/05-blame/README.md b/02-advanced/06-blame/README.md similarity index 100% rename from 02-advanced/05-blame/README.md rename to 02-advanced/06-blame/README.md diff --git a/02-advanced/05-blame/reset.ps1 b/02-advanced/06-blame/reset.ps1 similarity index 100% rename from 02-advanced/05-blame/reset.ps1 rename to 02-advanced/06-blame/reset.ps1 diff --git a/02-advanced/05-blame/setup.ps1 b/02-advanced/06-blame/setup.ps1 similarity index 100% rename from 02-advanced/05-blame/setup.ps1 rename to 02-advanced/06-blame/setup.ps1 diff --git a/02-advanced/05-blame/verify.ps1 b/02-advanced/06-blame/verify.ps1 similarity index 100% rename from 02-advanced/05-blame/verify.ps1 rename to 02-advanced/06-blame/verify.ps1 diff --git a/02-advanced/06-merge-strategies/README.md b/02-advanced/07-merge-strategies/README.md similarity index 100% rename from 02-advanced/06-merge-strategies/README.md rename to 02-advanced/07-merge-strategies/README.md diff --git a/02-advanced/06-merge-strategies/reset.ps1 b/02-advanced/07-merge-strategies/reset.ps1 similarity index 100% rename from 02-advanced/06-merge-strategies/reset.ps1 rename to 02-advanced/07-merge-strategies/reset.ps1 diff --git a/02-advanced/06-merge-strategies/setup.ps1 b/02-advanced/07-merge-strategies/setup.ps1 similarity index 100% rename from 02-advanced/06-merge-strategies/setup.ps1 rename to 02-advanced/07-merge-strategies/setup.ps1 diff --git a/02-advanced/06-merge-strategies/verify.ps1 b/02-advanced/07-merge-strategies/verify.ps1 similarity index 100% rename from 02-advanced/06-merge-strategies/verify.ps1 rename to 02-advanced/07-merge-strategies/verify.ps1 From 3130874981309dfe4228ae66c54e55b158619b1e Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 15:41:21 +0100 Subject: [PATCH 50/61] fix: cleanup cherry-picking tasks and tips --- 01-essentials/05-cherry-pick/README.md | 437 ++++++++++++++++++------- 1 file changed, 320 insertions(+), 117 deletions(-) diff --git a/01-essentials/05-cherry-pick/README.md b/01-essentials/05-cherry-pick/README.md index e963683..f732e10 100644 --- a/01-essentials/05-cherry-pick/README.md +++ b/01-essentials/05-cherry-pick/README.md @@ -1,4 +1,4 @@ -# Module 09: Cherry-Pick +# Module 05: Cherry-Pick ## Learning Objectives @@ -6,155 +6,358 @@ By the end of this module, you will: - Understand what cherry-picking is and how it works - Know when to use cherry-pick vs merge or rebase - Apply specific commits from one branch to another -- Handle cherry-pick conflicts if they occur - Understand common use cases for cherry-picking +- Learn how cherry-pick creates new commits with different hashes -## Challenge Description +## Setup -You have a `development` branch with several commits. Some of these commits are bug fixes that need to be applied to the `main` branch immediately, but other commits are experimental features that shouldn't be merged yet. +Create the challenge environment: -Your task is to: -1. Review the commits on the development branch -2. Identify which commits are bug fixes -3. Cherry-pick only the bug fix commits to the main branch -4. Verify that main has the bug fixes but not the experimental features - -## Key Concepts - -### What is Cherry-Pick? - -Cherry-pick allows you to apply a specific commit from one branch to another. Instead of merging an entire branch, you can selectively choose individual commits. - -``` - A---B---C---D development - / - E---F main +```pwsh +.\setup.ps1 ``` -After cherry-picking commit C: -``` - A---B---C---D development - / - E---F---C' main +This creates a repository with a `development` branch containing both bug fixes and experimental features. + +## Overview + +**Cherry-pick** allows you to copy specific commits from one branch to another. Unlike merging (which brings ALL commits), cherry-pick lets you be surgical about exactly which changes you want to apply. + +Think of it like picking cherries from a tree - you select only the ripe ones you want, leaving the rest behind. + +### Why Use Cherry-Pick? + +- **Selective deployment** - Apply critical bug fixes without merging unfinished features +- **Hotfixes** - Quickly move a fix from development to production +- **Backporting** - Apply fixes to older release branches +- **Wrong branch** - Move commits you accidentally made on the wrong branch +- **Duplicate commits** - Apply the same fix across multiple branches + +## Your Task + +### The Scenario + +You're working on a project where: +- The `main` branch is stable and in production +- The `development` branch has new features being tested +- Development has critical bug fixes that need to go to production NOW +- But development also has experimental features that aren't ready yet + +You need to cherry-pick ONLY the bug fixes to main, leaving the experimental features behind. + +### Part 1: Explore the Development Branch + +First, see what commits are on the development branch: + +```pwsh +cd challenge + +# View all commits on development branch +git log --oneline development + +# View the full commit graph +git log --oneline --graph --all ``` -Note that C' is a new commit with the same changes as C but a different commit hash. +**Study the commits:** +- Look for commits with "Fix" in the message (these are bug fixes) +- Look for commits with "experimental" or "beta" (these should stay on development) +- Note the commit hashes (the 7-character codes like `abc1234`) -### Cherry-Pick vs Merge vs Rebase - -- **Merge**: Brings all commits from another branch and creates a merge commit -- **Rebase**: Replays all commits from your branch on top of another branch -- **Cherry-Pick**: Applies one or more specific commits to your current branch - -### When to Use Cherry-Pick - -Cherry-pick is useful when you: -- Need a bug fix from a feature branch but can't merge the whole branch yet -- Want to apply a specific commit to a release branch -- Need to backport a fix to an older version -- Made a commit on the wrong branch and need to move it -- Want to duplicate a commit across multiple branches - -### Cherry-Pick Creates New Commits - -Important: Cherry-picked commits are new commits with different hashes. The original commit remains on the source branch, and a copy is created on the target branch with the same changes but a different commit ID. - -## Useful Commands - -```bash -# View commits on another branch -git log --oneline - -# View a specific commit's details +**Inspect specific commits:** +```pwsh +# See what files a commit changed git show +# Example: +# git show abc1234 +``` + +You should see: +- 2 commits that fix bugs (security and performance) +- 2 commits that add experimental features + +### Part 2: Switch to Main Branch + +Before cherry-picking, you need to be on the target branch (main): + +```pwsh +# Switch to main branch +git switch main + +# Verify you're on main +git branch +``` + +The `*` should be next to `main`. + +**Check what's currently on main:** +```pwsh +# See main's commits +git log --oneline + +# See what files exist +ls +``` + +Main should only have the initial app and README - no bug fixes yet, no experimental features. + +### Part 3: Cherry-Pick the Bug Fixes + +Now copy the bug fix commits from development to main: + +1. Find the security fix commit hash by looking at your earlier `git log --oneline --graph --all` + - Look for a commit message like "Fix security vulnerability in input validation" + - Note its hash (first 7 characters) + +2. Cherry-pick the security fix: + ```pwsh + git cherry-pick + + # Example if the hash is abc1234: + # git cherry-pick abc1234 + ``` +3. Verify it worked: Check that security.py, with `ls` or check your file explorer in VSCode, now exists and check that the commit has been added to the main branch with `git log --oneline --graph --all` +4. Find the performance fix commit hash + - Look for "Fix performance issue with data caching" + - Note its hash + +5. Cherry-pick the performance fix: + ```pwsh + git cherry-pick + ``` + +6. Verify both fixes are now on main: + ```pwsh + # You should see both security.py and cache.py + ls + + # View the graph showing both branches + git log --oneline --graph --all + ``` + +### Part 4: Verify Your Solution + +Check that you completed the challenge correctly: + +```pwsh +# From inside the module directory +.\verify.ps1 +``` + +The verification checks: +- ✅ You're on the main branch +- ✅ Security fix is applied to main +- ✅ Performance fix is applied to main +- ✅ Experimental features are NOT on main +- ✅ Development branch still has all commits + +## Understanding Cherry-Pick + +### What Actually Happens? + +When you cherry-pick a commit, Git: +1. Looks at what changed in that specific commit +2. Applies those same changes to your current branch +3. Creates a NEW commit with those changes + +``` +Before cherry-pick: + +development: A---B---C---D + / +main: E---F + +After: git switch main && git cherry-pick C + +development: A---B---C---D + / +main: E---F---C' +``` + +Notice: +- `C'` is a NEW commit (different hash than original `C`) +- Original `C` still exists on development +- Main now has the changes from C, but not B or D + +### Cherry-Pick vs Merge + +**Merge brings everything:** +```pwsh +git switch main +git merge development +# Result: A, B, C, and D all come to main +``` + +**Cherry-pick is selective:** +```pwsh +git switch main +git cherry-pick C +# Result: Only C comes to main (as C') +``` + +### Important: New Commits, New Hashes + +Cherry-picked commits are COPIES, not moves: +- Original commit stays on source branch +- New commit created on target branch +- Different commit hash (because different parent) +- Same changes, same message, different identity + +## Key Commands + +### Viewing Commits + +```pwsh +# See commits on another branch +git log branch-name --oneline + +# See what a specific commit changed +git show + +# See commit graph +git log --oneline --graph --all + +# See only commit message (not changes) +git log --oneline +``` + +### Cherry-Picking + +```pwsh # Cherry-pick a single commit git cherry-pick -# Cherry-pick multiple commits -git cherry-pick +# Cherry-pick multiple commits (in order) +git cherry-pick # Cherry-pick a range of commits git cherry-pick .. -# If conflicts occur during cherry-pick: -# 1. Resolve conflicts in files -# 2. Stage the resolved files -git add -# 3. Continue the cherry-pick -git cherry-pick --continue - # Abort a cherry-pick if something goes wrong git cherry-pick --abort - -# View commit history graph -git log --oneline --graph --all ``` -## Verification +### After Cherry-Pick -Run the verification script to check your solution: +```pwsh +# Verify the commit was added +git log --oneline -```bash -.\verify.ps1 +# See what files changed +git show HEAD + +# Compare branches +git log main..development --oneline ``` -The verification will check that: -- You're on the main branch -- The security bug fix commit has been applied to main -- The performance bug fix commit has been applied to main -- The experimental features are NOT on main -- The commits were cherry-picked (not merged) - -## Challenge Steps - -1. Navigate to the challenge directory -2. You're currently on the development branch -3. View the commits: `git log --oneline` -4. You'll see several commits - identify the bug fixes -5. Switch to main branch: `git switch main` -6. Cherry-pick the bug fix commits (you'll need their commit hashes) -7. Verify the result with `git log --oneline` -8. Run the verification script - -## Tips - -- Use `git log development --oneline` to see commits on the development branch -- Use `git show ` to view details of a specific commit -- You can cherry-pick by commit hash - you only need the first 7 characters -- Cherry-pick commits in chronological order (oldest first) to avoid conflicts -- If you make a mistake, use `.\reset.ps1` to start over -- The commit message will be preserved when cherry-picking - -## Common Cherry-Pick Scenarios +## Common Workflows ### Hotfix to Production -You have a critical bug fix on a development branch that needs to go to production immediately: -```bash -git switch production -git cherry-pick -``` -### Wrong Branch -You accidentally committed on the wrong branch: -```bash -# On wrong branch, note the commit hash +Critical bug found in production: + +```pwsh +# You're on feature-new-ui branch +# You just committed a critical security fix + git log --oneline -# Switch to correct branch -git switch correct-branch -git cherry-pick -# Go back and remove from wrong branch -git switch wrong-branch -git reset --hard HEAD~1 +# Note the hash of your fix commit + +# Switch to production branch +git switch production + +# Apply just that fix +git cherry-pick + +# Deploy to production +# Your fix is live, but new UI stays in development ``` -### Backporting -You need to apply a fix to an older release branch: -```bash +### Backporting to Old Versions + +```pwsh +# You fixed a bug on main +git switch main +git log --oneline +# Note the fix commit hash + +# Apply to older release branch +git switch release-2.5 +git cherry-pick + +# Apply to even older release git switch release-2.0 -git cherry-pick +git cherry-pick + +# Same fix now on three branches! ``` -## What You'll Learn +## Troubleshooting -Cherry-pick is a surgical tool in your Git toolbox. While merge and rebase work with entire branches, cherry-pick lets you be selective about which changes to apply. This is invaluable for managing hotfixes, maintaining multiple release branches, and handling situations where you need specific changes without bringing along everything else. Understanding when to use cherry-pick versus other Git operations is a mark of Git expertise. +### "I can't remember the commit hash!" + +```pwsh +# See commits on the source branch +git log development --oneline + +# Search for specific text in commit messages +git log development --oneline --grep="security" + +# See recent commits with more detail +git log development --oneline -n 10 +``` + +### "I cherry-picked in the wrong order!" + +Order matters! If commit B depends on commit A, cherry-pick A first: + +```pwsh +# Wrong order might cause issues +git cherry-pick B # Might fail if it needs changes from A + +# Correct order +git cherry-pick A +git cherry-pick B +``` + +### "How do I see what will change before cherry-picking?" + +```pwsh +# See what changes are in a commit +git show + +# Compare your current branch with a commit +git diff HEAD +``` + +## Tips for Success + +💡 **Copy the commit hashes** - Write them down before switching branches +💡 **Cherry-pick oldest first** - Apply commits in chronological order +💡 **Check your branch** - Always verify you're on the target branch first with `git branch` +💡 **Verify after each pick** - Run `git log --oneline` to confirm it worked +💡 **Use the graph** - `git log --oneline --graph --all` shows the full picture +💡 **Original stays put** - Cherry-pick copies, doesn't move commits + +## What You've Learned + +After completing this module, you understand: + +- ✅ Cherry-pick copies specific commits between branches +- ✅ `git cherry-pick ` applies a commit to current branch +- ✅ Cherry-picked commits get new hashes but same changes +- ✅ Use cherry-pick for selective deployment of changes +- ✅ Cherry-pick is different from merge (selective vs all) +- ✅ Original commit stays on source branch + +## Next Steps + +Ready to continue? Cherry-pick is a powerful tool for selective change management. Next modules will cover more advanced Git operations. + +To start over: +```pwsh +.\reset.ps1 +``` + +**Need help?** Run `git status` to see what Git suggests, or `git log --oneline --graph --all` to see the full picture! From 939bd397f1265a8b654facdc978fd80532a2667f Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 15:42:05 +0100 Subject: [PATCH 51/61] refactor: move 08 down a step --- 01-essentials/{08-stash => 07-stash}/README.md | 0 01-essentials/{08-stash => 07-stash}/reset.ps1 | 0 01-essentials/{08-stash => 07-stash}/setup.ps1 | 0 01-essentials/{08-stash => 07-stash}/verify.ps1 | 0 .../{09-multiplayer => 08-multiplayer}/01_FACILITATOR.md | 0 01-essentials/{09-multiplayer => 08-multiplayer}/02_README.md | 0 01-essentials/{09-multiplayer => 08-multiplayer}/03_TASKS.md | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename 01-essentials/{08-stash => 07-stash}/README.md (100%) rename 01-essentials/{08-stash => 07-stash}/reset.ps1 (100%) rename 01-essentials/{08-stash => 07-stash}/setup.ps1 (100%) rename 01-essentials/{08-stash => 07-stash}/verify.ps1 (100%) rename 01-essentials/{09-multiplayer => 08-multiplayer}/01_FACILITATOR.md (100%) rename 01-essentials/{09-multiplayer => 08-multiplayer}/02_README.md (100%) rename 01-essentials/{09-multiplayer => 08-multiplayer}/03_TASKS.md (100%) diff --git a/01-essentials/08-stash/README.md b/01-essentials/07-stash/README.md similarity index 100% rename from 01-essentials/08-stash/README.md rename to 01-essentials/07-stash/README.md diff --git a/01-essentials/08-stash/reset.ps1 b/01-essentials/07-stash/reset.ps1 similarity index 100% rename from 01-essentials/08-stash/reset.ps1 rename to 01-essentials/07-stash/reset.ps1 diff --git a/01-essentials/08-stash/setup.ps1 b/01-essentials/07-stash/setup.ps1 similarity index 100% rename from 01-essentials/08-stash/setup.ps1 rename to 01-essentials/07-stash/setup.ps1 diff --git a/01-essentials/08-stash/verify.ps1 b/01-essentials/07-stash/verify.ps1 similarity index 100% rename from 01-essentials/08-stash/verify.ps1 rename to 01-essentials/07-stash/verify.ps1 diff --git a/01-essentials/09-multiplayer/01_FACILITATOR.md b/01-essentials/08-multiplayer/01_FACILITATOR.md similarity index 100% rename from 01-essentials/09-multiplayer/01_FACILITATOR.md rename to 01-essentials/08-multiplayer/01_FACILITATOR.md diff --git a/01-essentials/09-multiplayer/02_README.md b/01-essentials/08-multiplayer/02_README.md similarity index 100% rename from 01-essentials/09-multiplayer/02_README.md rename to 01-essentials/08-multiplayer/02_README.md diff --git a/01-essentials/09-multiplayer/03_TASKS.md b/01-essentials/08-multiplayer/03_TASKS.md similarity index 100% rename from 01-essentials/09-multiplayer/03_TASKS.md rename to 01-essentials/08-multiplayer/03_TASKS.md From aa24c50b45f53bfb97d9d8039ba4a6ff0d77211b Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 15:50:54 +0100 Subject: [PATCH 52/61] fix: revert conflict --- 01-essentials/06-revert/README.md | 151 +++++++++++++++++------------ 01-essentials/06-revert/setup.ps1 | 145 +++++---------------------- 01-essentials/06-revert/verify.ps1 | 52 +++++----- 3 files changed, 141 insertions(+), 207 deletions(-) diff --git a/01-essentials/06-revert/README.md b/01-essentials/06-revert/README.md index 5d91d19..967acb8 100644 --- a/01-essentials/06-revert/README.md +++ b/01-essentials/06-revert/README.md @@ -35,8 +35,8 @@ Before starting this module, you should be comfortable with: Run the setup script to create the challenge environment: -```powershell -./setup.ps1 +```pwsh +.\setup.ps1 ``` This creates a `challenge/` directory with three branches demonstrating different revert scenarios: @@ -52,44 +52,57 @@ You're working on a calculator application. A developer added a `divide` functio ### Your Task -1. Navigate to the challenge directory: - ```bash +1. **Navigate to the challenge directory:** + ```pwsh cd challenge ``` -2. You should be on the `regular-revert` branch. View the commit history: - ```bash +2. **Check which branch you're on** (you should be on `regular-revert`): + ```pwsh + git branch + ``` + The `*` should be next to `regular-revert`. + +3. **View the commit history:** + ```pwsh git log --oneline ``` -3. Find the commit with the broken divide function (message: "Add broken divide function - needs to be reverted!") +4. **Find the commit with message:** "Add broken divide function - needs to be reverted!" + - Note the commit hash (the 7-character code at the start, like `a1b2c3d`) + - Write it down or copy it -4. Revert that specific commit: - ```bash +5. **Revert that specific commit** (replace `` with the actual hash): + ```pwsh git revert ``` -5. Git will open your editor for the revert commit message. The default message is fine—save and close. +6. **Visual Studio Code will open** with the revert commit message: + - The default message is fine (it says "Revert 'Add broken divide function...'") + - Close the editor window to accept the commit message + - Git will create the revert commit ### What to Observe -After reverting, check: +After reverting, check your work: -```bash -# View the new revert commit +```pwsh +# View the new revert commit in history git log --oneline -# Check that divide function is gone -cat calculator.py | grep "def divide" # Should return nothing +# Check that divide.py file is gone (reverted) +ls +# You should see calculator.py but NOT divide.py -# Check that modulo function still exists (it came after the bad commit) -cat calculator.py | grep "def modulo" # Should find it +# Check that modulo function still exists in calculator.py (it came after the bad commit) +cat calculator.py +# You should see def modulo # Check that multiply function still exists (it came before the bad commit) -cat calculator.py | grep "def multiply" # Should find it +# (You already see it when you cat the file above) ``` -**Key insight:** Revert creates a new commit that undoes the changes from the target commit, but leaves all other commits intact. +**Key insight:** Revert creates a NEW commit that undoes the changes from the target commit, but leaves all other commits intact. ### Understanding the Timeline @@ -141,35 +154,43 @@ When reverting a merge, you must specify which parent to keep using the `-m` fla ### Your Task -1. Switch to the merge-revert branch: - ```bash +1. **Switch to the merge-revert branch:** + ```pwsh git switch merge-revert ``` -2. View the commit history and find the merge commit: - ```bash +2. **View the commit history with the graph:** + ```pwsh git log --oneline --graph ``` - Look for: "Merge feature-auth branch" + Look for the merge commit message: "Merge feature-auth branch" + - Note the commit hash + - Write it down or copy it -3. Revert the merge commit using `-m 1`: - ```bash +3. **Revert the merge commit using `-m 1`** (replace `` with actual hash): + ```pwsh git revert -m 1 ``` - **Explanation:** - - `-m 1` tells Git to keep parent 1 (main branch) + **What `-m 1` means:** + - `-m 1` tells Git to keep parent 1 (the main branch side) - This undoes all changes from the feature-auth branch - Creates a new "revert merge" commit -4. Save the default commit message and check the result: - ```bash - # Verify auth.py is gone - ls auth.py # Should not exist +4. **Visual Studio Code will open** with the revert commit message: + - Close the editor window to accept it + - Git will create the revert commit + +5. **Check the result:** + ```pwsh + # View files - auth.py should be gone + ls + # You should see calculator.py but NOT auth.py # Verify calculator.py no longer imports auth - cat calculator.py | grep "from auth" # Should return nothing + cat calculator.py + # Should NOT see "from auth import" anywhere ``` ### What Happens Without -m? @@ -242,44 +263,48 @@ Two separate commits added broken mathematical functions (`square_root` and `log ### Your Task -1. Switch to the multi-revert branch: - ```bash +1. **Switch to the multi-revert branch:** + ```pwsh git switch multi-revert ``` -2. View the commit history: - ```bash +2. **View the commit history:** + ```pwsh git log --oneline ``` - Find the two commits: + Find the two bad commits: - "Add broken square_root - REVERT THIS!" - "Add broken logarithm - REVERT THIS TOO!" + + Note both commit hashes (write them down) -3. Revert both commits in one command: - ```bash +3. **Revert both commits in one command** (replace with actual hashes): + ```pwsh git revert ``` - **Important:** List commits from **oldest to newest** for cleanest history. + **Important:** List commits from **oldest to newest** for cleanest history (square_root first, then logarithm). - Alternatively, revert them one at a time: - ```bash + **Alternatively**, revert them one at a time: + ```pwsh git revert git revert ``` -4. Git will prompt for a commit message for each revert. Accept the defaults. +4. **Visual Studio Code will open TWICE** (once for each revert): + - Close the editor each time to accept the default commit message + - Git will create two revert commits -5. Verify the result: - ```bash - # Check that both bad functions are gone - cat calculator.py | grep "def square_root" # Should return nothing - cat calculator.py | grep "def logarithm" # Should return nothing +5. **Verify the result:** + ```pwsh + # View files - sqrt.py and logarithm.py should be gone + ls + # You should see calculator.py but NOT sqrt.py or logarithm.py - # Check that good functions remain - cat calculator.py | grep "def power" # Should find it - cat calculator.py | grep "def absolute" # Should find it + # Check that good functions remain in calculator.py + cat calculator.py + # You should see def power and def absolute ``` ### Multi-Revert Strategies @@ -311,11 +336,17 @@ This is useful when reverting multiple commits and you want one combined revert ## Verification -Verify your solutions by running the verification script: +After completing all three challenges, verify your solutions: -```bash -cd .. # Return to module directory -./verify.ps1 +```pwsh +cd .. # Return to module directory (if you're in challenge/) +.\verify.ps1 +``` + +Or from inside the challenge directory: + +```pwsh +..\verify.ps1 ``` The script checks that: @@ -329,7 +360,7 @@ The script checks that: ### Basic Revert -```bash +```pwsh # Revert a specific commit git revert @@ -342,7 +373,7 @@ git revert HEAD~1 ### Merge Commit Revert -```bash +```pwsh # Revert a merge commit (keep parent 1) git revert -m 1 @@ -352,7 +383,7 @@ git revert -m 2 ### Multiple Commits -```bash +```pwsh # Revert multiple specific commits git revert @@ -365,7 +396,7 @@ git revert HEAD~3..HEAD ### Revert Options -```bash +```pwsh # Revert but don't commit automatically git revert --no-commit diff --git a/01-essentials/06-revert/setup.ps1 b/01-essentials/06-revert/setup.ps1 index b481baa..cfe303b 100644 --- a/01-essentials/06-revert/setup.ps1 +++ b/01-essentials/06-revert/setup.ps1 @@ -67,75 +67,37 @@ Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow # Create regular-revert branch git switch -c regular-revert | Out-Null -# Good commit: Add multiply -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b +# Good commit: Add multiply using append +$multiplyFunc = @" def multiply(a, b): """Multiply two numbers.""" return a * b "@ -Set-Content -Path "calculator.py" -Value $calcContent +Add-Content -Path "calculator.py" -Value $multiplyFunc git add . git commit -m "Add multiply function" | Out-Null -# BAD commit: Add broken divide function -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b - -def multiply(a, b): - """Multiply two numbers.""" - return a * b +# BAD commit: Add broken divide function using separate file +$divideContent = @" +# divide.py - Division functionality def divide(a, b): """Divide a by b - BROKEN: doesn't handle division by zero!""" return a / b # This will crash if b is 0! "@ -Set-Content -Path "calculator.py" -Value $calcContent +Set-Content -Path "divide.py" -Value $divideContent git add . git commit -m "Add broken divide function - needs to be reverted!" | Out-Null -# Good commit: Add modulo (after bad commit) -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b - -def multiply(a, b): - """Multiply two numbers.""" - return a * b - -def divide(a, b): - """Divide a by b - BROKEN: doesn't handle division by zero!""" - return a / b # This will crash if b is 0! +# Good commit: Add modulo (after bad commit) using append +$moduloFunc = @" def modulo(a, b): """Return remainder of a divided by b.""" return a % b "@ -Set-Content -Path "calculator.py" -Value $calcContent +Add-Content -Path "calculator.py" -Value $moduloFunc git add . git commit -m "Add modulo function" | Out-Null @@ -254,109 +216,50 @@ Set-Content -Path "calculator.py" -Value $calcContent git add . git commit -m "Reset to basic calculator" | Out-Null -# Good commit: Add power function -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b +# Good commit: Add power function using append +$powerFunc = @" def power(a, b): """Raise a to the power of b.""" return a ** b "@ -Set-Content -Path "calculator.py" -Value $calcContent +Add-Content -Path "calculator.py" -Value $powerFunc git add . git commit -m "Add power function" | Out-Null -# BAD commit 1: Add broken square_root -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b - -def power(a, b): - """Raise a to the power of b.""" - return a ** b +# BAD commit 1: Add broken square_root in separate file +$sqrtContent = @" +# sqrt.py - Square root functionality def square_root(a): """BROKEN: Returns wrong result for negative numbers!""" return a ** 0.5 # This returns NaN for negative numbers! "@ -Set-Content -Path "calculator.py" -Value $calcContent +Set-Content -Path "sqrt.py" -Value $sqrtContent git add . git commit -m "Add broken square_root - REVERT THIS!" | Out-Null -# BAD commit 2: Add broken logarithm -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b - -def power(a, b): - """Raise a to the power of b.""" - return a ** b - -def square_root(a): - """BROKEN: Returns wrong result for negative numbers!""" - return a ** 0.5 # This returns NaN for negative numbers! +# BAD commit 2: Add broken logarithm in separate file +$logContent = @" +# logarithm.py - Logarithm functionality def logarithm(a): """BROKEN: Doesn't handle zero or negative numbers!""" import math return math.log(a) # This crashes for a <= 0! "@ -Set-Content -Path "calculator.py" -Value $calcContent +Set-Content -Path "logarithm.py" -Value $logContent git add . git commit -m "Add broken logarithm - REVERT THIS TOO!" | Out-Null -# Good commit: Add absolute value (after bad commits) -$calcContent = @" -# calculator.py - Simple calculator - -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b - -def power(a, b): - """Raise a to the power of b.""" - return a ** b - -def square_root(a): - """BROKEN: Returns wrong result for negative numbers!""" - return a ** 0.5 # This returns NaN for negative numbers! - -def logarithm(a): - """BROKEN: Doesn't handle zero or negative numbers!""" - import math - return math.log(a) # This crashes for a <= 0! +# Good commit: Add absolute value (after bad commits) using append +$absoluteFunc = @" def absolute(a): """Return absolute value of a.""" return abs(a) "@ -Set-Content -Path "calculator.py" -Value $calcContent +Add-Content -Path "calculator.py" -Value $absoluteFunc git add . git commit -m "Add absolute value function" | Out-Null diff --git a/01-essentials/06-revert/verify.ps1 b/01-essentials/06-revert/verify.ps1 index 0f32470..1fc89f5 100644 --- a/01-essentials/06-revert/verify.ps1 +++ b/01-essentials/06-revert/verify.ps1 @@ -51,19 +51,19 @@ if ($LASTEXITCODE -ne 0) { $allChecksPassed = $false } - # Check that calculator.py exists + # Check that divide.py is removed (was reverted) + if (-not (Test-Path "divide.py")) { + Write-Host "[PASS] Broken divide.py successfully reverted (file removed)" -ForegroundColor Green + } else { + Write-Host "[FAIL] divide.py still exists (should be reverted)" -ForegroundColor Red + Write-Host "[HINT] The bad commit should be reverted, removing divide.py" -ForegroundColor Yellow + $allChecksPassed = $false + } + + # Check that calculator.py exists and has correct content if (Test-Path "calculator.py") { $calcContent = Get-Content "calculator.py" -Raw - # Check that divide function is NOT in the code (was reverted) - if ($calcContent -notmatch "def divide") { - Write-Host "[PASS] Broken divide function successfully reverted" -ForegroundColor Green - } else { - Write-Host "[FAIL] divide function still exists (should be reverted)" -ForegroundColor Red - Write-Host "[HINT] The bad commit should be reverted, removing the divide function" -ForegroundColor Yellow - $allChecksPassed = $false - } - # Check that modulo function still exists (should be preserved) if ($calcContent -match "def modulo") { Write-Host "[PASS] modulo function preserved (good commit after bad one)" -ForegroundColor Green @@ -160,26 +160,26 @@ if ($LASTEXITCODE -ne 0) { $allChecksPassed = $false } + # Check that sqrt.py is removed (reverted) + if (-not (Test-Path "sqrt.py")) { + Write-Host "[PASS] Broken sqrt.py successfully reverted (file removed)" -ForegroundColor Green + } else { + Write-Host "[FAIL] sqrt.py still exists (should be reverted)" -ForegroundColor Red + $allChecksPassed = $false + } + + # Check that logarithm.py is removed (reverted) + if (-not (Test-Path "logarithm.py")) { + Write-Host "[PASS] Broken logarithm.py successfully reverted (file removed)" -ForegroundColor Green + } else { + Write-Host "[FAIL] logarithm.py still exists (should be reverted)" -ForegroundColor Red + $allChecksPassed = $false + } + # Check calculator.py content if (Test-Path "calculator.py") { $calcContent = Get-Content "calculator.py" -Raw - # Check that square_root is NOT in code (reverted) - if ($calcContent -notmatch "def square_root") { - Write-Host "[PASS] Broken square_root function reverted" -ForegroundColor Green - } else { - Write-Host "[FAIL] square_root function still exists (should be reverted)" -ForegroundColor Red - $allChecksPassed = $false - } - - # Check that logarithm is NOT in code (reverted) - if ($calcContent -notmatch "def logarithm") { - Write-Host "[PASS] Broken logarithm function reverted" -ForegroundColor Green - } else { - Write-Host "[FAIL] logarithm function still exists (should be reverted)" -ForegroundColor Red - $allChecksPassed = $false - } - # Check that power function still exists (good commit before bad ones) if ($calcContent -match "def power") { Write-Host "[PASS] power function preserved" -ForegroundColor Green From 575e083f332719f1895c1cfe969d229bf81307c4 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 16:11:24 +0100 Subject: [PATCH 53/61] refactor: rewrite the merge-revert section It is an advanced and difficult revert to accomplish and should probably be done through a reset instead, which means that we're modifying history which is dangerous and so should be handled by someone who understands these dangers. --- 01-essentials/06-revert/README.md | 270 +++++++++++------------------ 01-essentials/06-revert/reset.ps1 | 4 +- 01-essentials/06-revert/setup.ps1 | 121 +++++-------- 01-essentials/06-revert/verify.ps1 | 73 ++++---- 4 files changed, 186 insertions(+), 282 deletions(-) diff --git a/01-essentials/06-revert/README.md b/01-essentials/06-revert/README.md index 967acb8..e6c4a8a 100644 --- a/01-essentials/06-revert/README.md +++ b/01-essentials/06-revert/README.md @@ -1,8 +1,8 @@ -# Module 05: Git Revert - Safe Undoing +# Module 06: Git Revert - Safe Undoing ## About This Module -Welcome to Module 05, where you'll learn the **safe, team-friendly way to undo changes** in Git. Unlike destructive commands that erase history, `git revert` creates new commits that undo previous changes while preserving the complete project history. +Welcome to Module 06, where you'll learn the **safe, team-friendly way to undo changes** in Git. Unlike destructive commands that erase history, `git revert` creates new commits that undo previous changes while preserving the complete project history. **Why revert is important:** - ✅ Safe for shared/pushed commits @@ -17,19 +17,18 @@ Welcome to Module 05, where you'll learn the **safe, team-friendly way to undo c By completing this module, you will: -1. Revert regular commits safely while preserving surrounding changes -2. Revert merge commits using the `-m` flag -3. Understand merge commit parent numbering -4. Handle the re-merge problem that occurs after reverting merges -5. Revert multiple commits at once -6. Know when to use revert vs. other undo strategies +1. Revert commits safely while preserving surrounding changes +2. Revert old commits in the middle of history +3. Understand how revert preserves commits before and after the target +4. Revert multiple commits at once +5. Know when to use revert vs. other undo strategies ## Prerequisites Before starting this module, you should be comfortable with: - Creating commits (`git commit`) - Viewing commit history (`git log`) -- Understanding branches and merging (Module 03) +- Understanding branches (Module 03) ## Setup @@ -41,7 +40,7 @@ Run the setup script to create the challenge environment: This creates a `challenge/` directory with three branches demonstrating different revert scenarios: - `regular-revert` - Basic commit reversion -- `merge-revert` - Merge commit reversion +- `middle-revert` - Reverting a commit in the middle of history - `multi-revert` - Multiple commit reversion ## Challenge 1: Reverting a Regular Commit @@ -120,140 +119,108 @@ main.py (initial) → multiply (good) → divide (BAD) → modulo (good) → rev The revert commit adds a new point in history that undoes the divide changes. -## Challenge 2: Reverting a Merge Commit +## Challenge 2: Reverting a Commit in the Middle ### Scenario -Your team merged a `feature-auth` branch that added authentication functionality. After deployment, you discovered the authentication system has critical security issues. You need to revert the entire merge while the security team redesigns the feature. +You're working on improving your calculator application. Several commits were made in sequence: +1. Added input validation (good) +2. Added output formatter (BAD - has bugs!) +3. Added configuration module (good - but came after the bad commit) -**This is different from reverting a regular commit!** Merge commits have **two parents**, so you must tell Git which parent to keep. +The formatter has critical bugs and needs to be removed, but you want to keep both the validation module (added before) and the configuration module (added after). -### Understanding Merge Commit Parents +**This demonstrates an important Git principle:** Revert works on ANY commit in history, not just recent ones! -When you merge a feature branch into main: +### Understanding History Preservation + +Here's what the commit history looks like: ``` - feature-auth (parent 2) - ↓ - C---D - / \ -A---B-----M ← Merge commit (has TWO parents) - ↑ -parent 1 (main) +A (initial) → B (validation) → C (formatter BAD) → D (config) + ↑ + We want to remove THIS ``` -The merge commit `M` has: -- **Parent 1**: The branch you merged INTO (main) -- **Parent 2**: The branch you merged FROM (feature-auth) +When you revert commit C: -When reverting a merge, you must specify which parent to keep using the `-m` flag: -- `-m 1` means "keep parent 1" (main) - **Most common** -- `-m 2` means "keep parent 2" (feature-auth) - Rare +``` +A (initial) → B (validation) → C (formatter BAD) → D (config) → E (revert C) + ↑ + Removes formatter, keeps validation & config +``` -**In practice:** You almost always use `-m 1` to keep the main branch and undo the feature branch changes. +**Key insight:** The revert creates a NEW commit (E) that undoes commit C, but leaves B and D completely intact! ### Your Task -1. **Switch to the merge-revert branch:** +1. **Switch to the middle-revert branch:** ```pwsh - git switch merge-revert + git switch middle-revert ``` -2. **View the commit history with the graph:** +2. **View the commit history:** ```pwsh - git log --oneline --graph + git log --oneline ``` - Look for the merge commit message: "Merge feature-auth branch" - - Note the commit hash - - Write it down or copy it + You should see three commits after the initial: + - "Add input validation module" + - "Add broken formatter - needs to be reverted!" + - "Add configuration module" -3. **Revert the merge commit using `-m 1`** (replace `` with actual hash): +3. **Find the broken formatter commit:** + - Look for the message: "Add broken formatter - needs to be reverted!" + - Note the commit hash (the 7-character code) + - Write it down + +4. **Revert that middle commit** (replace `` with actual hash): ```pwsh - git revert -m 1 + git revert ``` - **What `-m 1` means:** - - `-m 1` tells Git to keep parent 1 (the main branch side) - - This undoes all changes from the feature-auth branch - - Creates a new "revert merge" commit - -4. **Visual Studio Code will open** with the revert commit message: +5. **Visual Studio Code will open** with the revert commit message: + - The default message is fine - Close the editor window to accept it - Git will create the revert commit -5. **Check the result:** +6. **Check the result:** ```pwsh - # View files - auth.py should be gone + # View files - formatter.py should be gone ls - # You should see calculator.py but NOT auth.py + # You should see validation.py and config.py but NOT formatter.py - # Verify calculator.py no longer imports auth - cat calculator.py - # Should NOT see "from auth import" anywhere + # View the history + git log --oneline + # You should see the new revert commit at the top ``` -### What Happens Without -m? +### What to Observe -If you try to revert a merge commit without the `-m` flag: +After reverting, notice: -```bash -git revert -# Error: commit is a merge but no -m option was given +```pwsh +# Check which files exist +ls + +# You should see: +# - calculator.py (from initial commit) +# - validation.py (from commit BEFORE bad one) ✅ +# - config.py (from commit AFTER bad one) ✅ +# - formatter.py is GONE (reverted) ❌ ``` -Git doesn't know which parent you want to keep, so it refuses to proceed. +**Important:** Git successfully removed the bad formatter while keeping everything else! -### The Re-Merge Problem +### Why This Matters -**Important gotcha:** After reverting a merge, you **cannot simply re-merge** the same branch! +This scenario demonstrates revert's power in real-world situations: +- You discover a bug in code committed days or weeks ago +- Many commits have been made since then +- You can't just delete the old commit (that would break history) +- Revert lets you surgically remove just the bad commit -Here's why: - -``` -Initial merge: -A---B---M (merged feature-auth) - ↑ - All changes from feature-auth are now in main - -After revert: -A---B---M---R (reverted merge) - ↑ - Changes removed, but Git remembers they were merged - -Attempting to re-merge: -A---B---M---R---M2 (try to merge feature-auth again) - ↑ - Git thinks: "I already merged these commits, - nothing new to add!" (Empty merge) -``` - -**Solutions if you need to re-merge:** - -1. **Revert the revert** (recommended): - ```bash - git revert - ``` - This brings back all the feature-auth changes. - -2. **Cherry-pick new commits** from the feature branch: - ```bash - git cherry-pick - ``` - -3. **Merge with --no-ff** and resolve conflicts manually (advanced). - -### When to Revert Merges - -Revert merge commits when: -- ✅ Feature causes production issues -- ✅ Need to temporarily remove a feature -- ✅ Discovered critical bugs after merging -- ✅ Security issues require immediate rollback - -Don't revert merges when: -- ❌ You just need to fix a small bug (fix it with a new commit instead) -- ❌ You plan to re-merge the same branch soon (use reset if local, or revert-the-revert later) +**Revert is your "undo button" for shared history!** ## Challenge 3: Reverting Multiple Commits @@ -311,7 +278,7 @@ Two separate commits added broken mathematical functions (`square_root` and `log **Reverting a range of commits:** -```bash +```pwsh # Revert commits from A to B (inclusive) git revert A^..B @@ -321,7 +288,7 @@ git revert HEAD~3..HEAD **Reverting without auto-commit:** -```bash +```pwsh # Stage revert changes without committing git revert --no-commit @@ -353,8 +320,7 @@ The script checks that: - ✅ Revert commits were created (not destructive deletion) - ✅ Bad code is removed - ✅ Good code before and after is preserved -- ✅ Merge commits still exist in history -- ✅ Proper use of `-m` flag for merge reverts +- ✅ Revert works on commits in the middle of history ## Command Reference @@ -371,14 +337,17 @@ git revert HEAD git revert HEAD~1 ``` -### Merge Commit Revert +### Reverting Old Commits ```pwsh -# Revert a merge commit (keep parent 1) -git revert -m 1 +# Revert a specific commit from any point in history +git revert -# Revert a merge commit (keep parent 2) - rare -git revert -m 2 +# Revert a commit from 5 commits ago +git revert HEAD~5 + +# View what a commit changed before reverting +git show ``` ### Multiple Commits @@ -450,7 +419,7 @@ Consider alternatives when: Sometimes reverting causes conflicts if subsequent changes touched the same code: -```bash +```pwsh # Start revert git revert @@ -463,47 +432,24 @@ git revert 1. Open conflicted files and fix conflicts (look for `<<<<<<<` markers) 2. Stage resolved files: - ```bash + ```pwsh git add ``` 3. Continue the revert: - ```bash + ```pwsh git revert --continue ``` Or abort if you change your mind: -```bash +```pwsh git revert --abort ``` ## Common Mistakes -### 1. Forgetting -m for Merge Commits +### 1. Using Reset on Pushed Commits -```bash -# ❌ Wrong - will fail -git revert - -# ✅ Correct -git revert -m 1 -``` - -### 2. Trying to Re-Merge After Revert - -```bash -# After reverting a merge: -git revert -m 1 - -# ❌ This won't work as expected -git merge feature-branch # Empty merge! - -# ✅ Do this instead -git revert # Revert the revert -``` - -### 3. Using Reset on Pushed Commits - -```bash +```pwsh # ❌ NEVER do this with pushed commits git reset --hard HEAD~3 @@ -511,11 +457,11 @@ git reset --hard HEAD~3 git revert HEAD~3..HEAD ``` -### 4. Reverting Commits in Wrong Order +### 2. Reverting Commits in Wrong Order When reverting multiple related commits, revert from newest to oldest: -```bash +```pwsh # If you have: A → B → C (and C depends on B) # ✅ Correct order @@ -530,7 +476,7 @@ git revert C ## Best Practices 1. **Write clear revert messages:** - ```bash + ```pwsh git revert -m "Revert authentication - security issue #1234" ``` @@ -561,15 +507,6 @@ git revert C ## Troubleshooting -### "Commit is a merge but no -m option was given" - -**Problem:** Trying to revert a merge commit without `-m`. - -**Solution:** -```bash -git revert -m 1 -``` - ### "Empty Revert / No Changes" **Problem:** Revert doesn't seem to do anything. @@ -580,7 +517,7 @@ git revert -m 1 - Wrong commit hash **Solution:** -```bash +```pwsh # Check what the commit actually changed git show @@ -601,24 +538,22 @@ git log --grep="Revert" Or consider fixing forward with a new commit instead of reverting. -### "Can't Re-Merge After Reverting Merge" +### "Reverting Old Commit Breaks Something" -**Problem:** After reverting a merge, re-merging the branch brings no changes. +**Problem:** After reverting an old commit, something else stops working. -**Solution:** Revert the revert commit: -```bash -# Find the revert commit -git log --oneline +**Why:** The old commit might have been a dependency for later commits. -# Revert the revert (brings changes back) -git revert -``` +**Solution:** +1. Check what changed: `git diff HEAD~1 HEAD` +2. Either fix the issue with a new commit, or +3. Revert the revert if needed: `git revert ` ## Advanced: Revert Internals Understanding what revert does under the hood: -```bash +```pwsh # Revert creates a new commit with inverse changes git revert @@ -645,9 +580,8 @@ You've learned: - ✅ `git revert` creates new commits that undo previous changes - ✅ Revert is safe for shared/pushed commits -- ✅ Merge commits require `-m 1` or `-m 2` flag -- ✅ Parent 1 = branch merged into, Parent 2 = branch merged from -- ✅ Can't simply re-merge after reverting a merge +- ✅ Revert works on any commit in history, even old ones +- ✅ Commits before and after the reverted commit are preserved - ✅ Multiple commits can be reverted in one command - ✅ Revert preserves complete history for audit trails diff --git a/01-essentials/06-revert/reset.ps1 b/01-essentials/06-revert/reset.ps1 index d40ab84..3c026ac 100644 --- a/01-essentials/06-revert/reset.ps1 +++ b/01-essentials/06-revert/reset.ps1 @@ -1,14 +1,14 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Resets the Module 05 challenge environment to start fresh. + Resets the Module 06 challenge environment to start fresh. .DESCRIPTION This script removes the challenge directory and re-runs setup.ps1 to create a fresh challenge environment. #> -Write-Host "`n=== Resetting Module 05: Git Revert Challenge ===" -ForegroundColor Cyan +Write-Host "`n=== Resetting Module 06: Git Revert Challenge ===" -ForegroundColor Cyan # Check if challenge directory exists if (Test-Path "challenge") { diff --git a/01-essentials/06-revert/setup.ps1 b/01-essentials/06-revert/setup.ps1 index cfe303b..12c6d6e 100644 --- a/01-essentials/06-revert/setup.ps1 +++ b/01-essentials/06-revert/setup.ps1 @@ -1,17 +1,17 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Sets up the Module 05 challenge environment for learning git revert. + Sets up the Module 06 challenge environment for learning git revert. .DESCRIPTION This script creates a challenge directory with three branches demonstrating different revert scenarios: - - regular-revert: Basic revert of a single bad commit - - merge-revert: Reverting a merge commit with -m flag + - regular-revert: Basic revert of a single bad commit at the end + - middle-revert: Reverting a bad commit in the middle of history - multi-revert: Reverting multiple commits at once #> -Write-Host "`n=== Setting up Module 05: Git Revert Challenge ===" -ForegroundColor Cyan +Write-Host "`n=== Setting up Module 06: Git Revert Challenge ===" -ForegroundColor Cyan # Remove existing challenge directory if it exists if (Test-Path "challenge") { @@ -104,90 +104,63 @@ git commit -m "Add modulo function" | Out-Null Write-Host "[CREATED] regular-revert branch with bad divide commit" -ForegroundColor Green # ============================================================================ -# SCENARIO 2: Merge Revert (Merge Commit with -m flag) +# SCENARIO 2: Revert in Middle of History # ============================================================================ -Write-Host "`nScenario 2: Creating merge-revert scenario..." -ForegroundColor Cyan +Write-Host "`nScenario 2: Creating middle-revert scenario..." -ForegroundColor Cyan # Switch back to main git switch $mainBranch | Out-Null -# Create merge-revert branch -git switch -c merge-revert | Out-Null +# Create middle-revert branch +git switch -c middle-revert | Out-Null -# Create a feature branch to merge -git switch -c feature-auth | Out-Null +# Good commit: Add validation module +$validationContent = @" +# validation.py - Input validation -# Add auth functionality -$authContent = @" -# auth.py - Authentication module - -def login(username, password): - \"\"\"Login user.\"\"\" - print(f"Logging in {username}...") - return True - -def logout(username): - \"\"\"Logout user.\"\"\" - print(f"Logging out {username}...") - return True -"@ -Set-Content -Path "auth.py" -Value $authContent -git add . -git commit -m "Add authentication module" | Out-Null - -# Add password validation -$authContent = @" -# auth.py - Authentication module - -def validate_password(password): - \"\"\"Validate password strength.\"\"\" - return len(password) >= 8 - -def login(username, password): - \"\"\"Login user.\"\"\" - if not validate_password(password): - print("Password too weak!") +def validate_number(value): + """Check if value is a valid number.""" + try: + float(value) + return True + except ValueError: return False - print(f"Logging in {username}...") - return True - -def logout(username): - \"\"\"Logout user.\"\"\" - print(f"Logging out {username}...") - return True "@ -Set-Content -Path "auth.py" -Value $authContent +Set-Content -Path "validation.py" -Value $validationContent git add . -git commit -m "Add password validation" | Out-Null +git commit -m "Add input validation module" | Out-Null -# Integrate auth into calculator (part of the feature branch) -$calcContent = @" -# calculator.py - Simple calculator -from auth import login +# BAD commit: Add broken formatter +$formatterContent = @" +# formatter.py - Output formatting -def add(a, b): - """Add two numbers.""" - return a + b - -def subtract(a, b): - """Subtract b from a.""" - return a - b - -def secure_divide(a, b, username): - """Secure divide - requires authentication.""" - if login(username, "password123"): - return a / b - return None +def format_result(result): + """BROKEN: Doesn't handle None or errors properly!""" + return f"Result: {result.upper()}" # This crashes if result is not a string! "@ -Set-Content -Path "calculator.py" -Value $calcContent +Set-Content -Path "formatter.py" -Value $formatterContent git add . -git commit -m "Integrate auth into calculator" | Out-Null +git commit -m "Add broken formatter - needs to be reverted!" | Out-Null -# Switch back to merge-revert and merge feature-auth -git switch merge-revert | Out-Null -git merge feature-auth --no-ff -m "Merge feature-auth branch" | Out-Null +# Good commit: Add configuration (depends on validation, not formatter) +$configContent = @" +# config.py - Application configuration -Write-Host "[CREATED] merge-revert branch with merge commit to revert" -ForegroundColor Green +from validation import validate_number + +DEFAULT_PRECISION = 2 + +def set_precision(value): + """Set calculation precision.""" + if validate_number(value): + return int(value) + return DEFAULT_PRECISION +"@ +Set-Content -Path "config.py" -Value $configContent +git add . +git commit -m "Add configuration module" | Out-Null + +Write-Host "[CREATED] middle-revert branch with bad commit in the middle" -ForegroundColor Green # ============================================================================ # SCENARIO 3: Multi Revert (Multiple Bad Commits) @@ -275,8 +248,8 @@ Set-Location .. Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green Write-Host "`nThree revert scenarios have been created:" -ForegroundColor Cyan -Write-Host " 1. regular-revert - Revert a single bad commit (basic)" -ForegroundColor White -Write-Host " 2. merge-revert - Revert a merge commit with -m flag" -ForegroundColor White +Write-Host " 1. regular-revert - Revert a bad commit at the end" -ForegroundColor White +Write-Host " 2. middle-revert - Revert a bad commit in the middle of history" -ForegroundColor White Write-Host " 3. multi-revert - Revert multiple bad commits" -ForegroundColor White Write-Host "`nYou are currently on the 'regular-revert' branch." -ForegroundColor Cyan Write-Host "`nNext steps:" -ForegroundColor Cyan diff --git a/01-essentials/06-revert/verify.ps1 b/01-essentials/06-revert/verify.ps1 index 1fc89f5..4013851 100644 --- a/01-essentials/06-revert/verify.ps1 +++ b/01-essentials/06-revert/verify.ps1 @@ -1,16 +1,16 @@ #!/usr/bin/env pwsh <# .SYNOPSIS - Verifies the Module 05 challenge solutions. + Verifies the Module 06 challenge solutions. .DESCRIPTION Checks that all three revert scenarios have been completed correctly: - regular-revert: Single commit reverted - - merge-revert: Merge commit reverted with -m flag + - middle-revert: Commit in middle of history reverted - multi-revert: Multiple commits reverted #> -Write-Host "`n=== Verifying Module 05: Git Revert Solutions ===" -ForegroundColor Cyan +Write-Host "`n=== Verifying Module 06: Git Revert Solutions ===" -ForegroundColor Cyan $allChecksPassed = $true $originalDir = Get-Location @@ -87,53 +87,50 @@ if ($LASTEXITCODE -ne 0) { } # ============================================================================ -# SCENARIO 2: Merge Revert Verification +# SCENARIO 2: Middle Revert Verification # ============================================================================ -Write-Host "`n=== Scenario 2: Merge Revert ===" -ForegroundColor Cyan +Write-Host "`n=== Scenario 2: Middle Revert ===" -ForegroundColor Cyan -git switch merge-revert 2>&1 | Out-Null +git switch middle-revert 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { - Write-Host "[FAIL] merge-revert branch not found" -ForegroundColor Red + Write-Host "[FAIL] middle-revert branch not found" -ForegroundColor Red $allChecksPassed = $false } else { - # Check that a revert commit for the merge exists - $revertMerge = git log --oneline --grep="Revert.*Merge" 2>$null - if ($revertMerge) { - Write-Host "[PASS] Merge revert commit found" -ForegroundColor Green + # Check that a revert commit exists + $revertCommit = git log --oneline --grep="Revert" 2>$null + if ($revertCommit) { + Write-Host "[PASS] Revert commit found" -ForegroundColor Green } else { - Write-Host "[FAIL] No merge revert commit found" -ForegroundColor Red - Write-Host "[HINT] Use: git revert -m 1 " -ForegroundColor Yellow + Write-Host "[FAIL] No revert commit found" -ForegroundColor Red + Write-Host "[HINT] Use: git revert " -ForegroundColor Yellow $allChecksPassed = $false } - # Check that the original merge commit still exists (revert doesn't erase it) - $mergeCommit = git log --merges --oneline --grep="Merge feature-auth" 2>$null - if ($mergeCommit) { - Write-Host "[PASS] Original merge commit still in history (not erased)" -ForegroundColor Green + # Check that formatter.py is removed (reverted) + if (-not (Test-Path "formatter.py")) { + Write-Host "[PASS] Broken formatter.py successfully reverted (file removed)" -ForegroundColor Green } else { - Write-Host "[INFO] Original merge commit not found (this is OK if you used a different approach)" -ForegroundColor Yellow + Write-Host "[FAIL] formatter.py still exists (should be reverted)" -ForegroundColor Red + Write-Host "[HINT] The bad commit should be reverted, removing formatter.py" -ForegroundColor Yellow + $allChecksPassed = $false } - # Check that auth.py no longer exists or its effects are reverted - if (-not (Test-Path "auth.py")) { - Write-Host "[PASS] auth.py removed (merge reverted successfully)" -ForegroundColor Green + # Check that validation.py still exists (good commit before bad one) + if (Test-Path "validation.py") { + Write-Host "[PASS] validation.py preserved (good commit before bad one)" -ForegroundColor Green } else { - Write-Host "[INFO] auth.py still exists (check if merge was fully reverted)" -ForegroundColor Yellow + Write-Host "[FAIL] validation.py missing (should still exist)" -ForegroundColor Red + $allChecksPassed = $false } - # Check that calculator.py exists - if (Test-Path "calculator.py") { - $calcContent = Get-Content "calculator.py" -Raw - - # After reverting the merge, calculator shouldn't import auth - if ($calcContent -notmatch "from auth import") { - Write-Host "[PASS] Auth integration reverted from calculator.py" -ForegroundColor Green - } else { - Write-Host "[FAIL] calculator.py still imports auth (merge not fully reverted)" -ForegroundColor Red - Write-Host "[HINT] Reverting the merge should remove the auth integration" -ForegroundColor Yellow - $allChecksPassed = $false - } + # Check that config.py still exists (good commit after bad one) + if (Test-Path "config.py") { + Write-Host "[PASS] config.py preserved (good commit after bad one)" -ForegroundColor Green + } else { + Write-Host "[FAIL] config.py missing (should still exist)" -ForegroundColor Red + Write-Host "[HINT] Only revert the bad commit, not the good ones after it" -ForegroundColor Yellow + $allChecksPassed = $false } } @@ -211,11 +208,11 @@ if ($allChecksPassed) { Write-Host "=========================================" -ForegroundColor Green Write-Host "`nYou've mastered git revert!" -ForegroundColor Cyan Write-Host "You now understand:" -ForegroundColor Cyan - Write-Host " ✓ Reverting regular commits safely" -ForegroundColor White - Write-Host " ✓ Reverting merge commits with -m flag" -ForegroundColor White + Write-Host " ✓ Reverting commits safely without erasing history" -ForegroundColor White + Write-Host " ✓ Reverting old commits in the middle of history" -ForegroundColor White Write-Host " ✓ Reverting multiple commits at once" -ForegroundColor White - Write-Host " ✓ Preserving history while undoing changes" -ForegroundColor White - Write-Host "`nReady for Module 06: Git Reset!" -ForegroundColor Green + Write-Host " ✓ Preserving commits before and after the reverted one" -ForegroundColor White + Write-Host "`nReady for Module 07: Git Reset!" -ForegroundColor Green Write-Host "" exit 0 } else { From 0474a6de0e31230e09a44d8bdfa6805871299132 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 16:23:32 +0100 Subject: [PATCH 54/61] fix: remove revert-middle challenge --- 01-essentials/06-revert/README.md | 125 +++-------------------------- 01-essentials/06-revert/setup.ps1 | 75 ++--------------- 01-essentials/06-revert/verify.ps1 | 58 +------------ 3 files changed, 20 insertions(+), 238 deletions(-) diff --git a/01-essentials/06-revert/README.md b/01-essentials/06-revert/README.md index e6c4a8a..57bd780 100644 --- a/01-essentials/06-revert/README.md +++ b/01-essentials/06-revert/README.md @@ -18,10 +18,9 @@ Welcome to Module 06, where you'll learn the **safe, team-friendly way to undo c By completing this module, you will: 1. Revert commits safely while preserving surrounding changes -2. Revert old commits in the middle of history -3. Understand how revert preserves commits before and after the target -4. Revert multiple commits at once -5. Know when to use revert vs. other undo strategies +2. Understand how revert creates new commits instead of erasing history +3. Revert multiple commits at once +4. Know when to use revert vs. other undo strategies ## Prerequisites @@ -38,9 +37,8 @@ Run the setup script to create the challenge environment: .\setup.ps1 ``` -This creates a `challenge/` directory with three branches demonstrating different revert scenarios: +This creates a `challenge/` directory with two branches demonstrating different revert scenarios: - `regular-revert` - Basic commit reversion -- `middle-revert` - Reverting a commit in the middle of history - `multi-revert` - Multiple commit reversion ## Challenge 1: Reverting a Regular Commit @@ -119,110 +117,7 @@ main.py (initial) → multiply (good) → divide (BAD) → modulo (good) → rev The revert commit adds a new point in history that undoes the divide changes. -## Challenge 2: Reverting a Commit in the Middle - -### Scenario - -You're working on improving your calculator application. Several commits were made in sequence: -1. Added input validation (good) -2. Added output formatter (BAD - has bugs!) -3. Added configuration module (good - but came after the bad commit) - -The formatter has critical bugs and needs to be removed, but you want to keep both the validation module (added before) and the configuration module (added after). - -**This demonstrates an important Git principle:** Revert works on ANY commit in history, not just recent ones! - -### Understanding History Preservation - -Here's what the commit history looks like: - -``` -A (initial) → B (validation) → C (formatter BAD) → D (config) - ↑ - We want to remove THIS -``` - -When you revert commit C: - -``` -A (initial) → B (validation) → C (formatter BAD) → D (config) → E (revert C) - ↑ - Removes formatter, keeps validation & config -``` - -**Key insight:** The revert creates a NEW commit (E) that undoes commit C, but leaves B and D completely intact! - -### Your Task - -1. **Switch to the middle-revert branch:** - ```pwsh - git switch middle-revert - ``` - -2. **View the commit history:** - ```pwsh - git log --oneline - ``` - - You should see three commits after the initial: - - "Add input validation module" - - "Add broken formatter - needs to be reverted!" - - "Add configuration module" - -3. **Find the broken formatter commit:** - - Look for the message: "Add broken formatter - needs to be reverted!" - - Note the commit hash (the 7-character code) - - Write it down - -4. **Revert that middle commit** (replace `` with actual hash): - ```pwsh - git revert - ``` - -5. **Visual Studio Code will open** with the revert commit message: - - The default message is fine - - Close the editor window to accept it - - Git will create the revert commit - -6. **Check the result:** - ```pwsh - # View files - formatter.py should be gone - ls - # You should see validation.py and config.py but NOT formatter.py - - # View the history - git log --oneline - # You should see the new revert commit at the top - ``` - -### What to Observe - -After reverting, notice: - -```pwsh -# Check which files exist -ls - -# You should see: -# - calculator.py (from initial commit) -# - validation.py (from commit BEFORE bad one) ✅ -# - config.py (from commit AFTER bad one) ✅ -# - formatter.py is GONE (reverted) ❌ -``` - -**Important:** Git successfully removed the bad formatter while keeping everything else! - -### Why This Matters - -This scenario demonstrates revert's power in real-world situations: -- You discover a bug in code committed days or weeks ago -- Many commits have been made since then -- You can't just delete the old commit (that would break history) -- Revert lets you surgically remove just the bad commit - -**Revert is your "undo button" for shared history!** - -## Challenge 3: Reverting Multiple Commits +## Challenge 2: Reverting Multiple Commits ### Scenario @@ -303,7 +198,7 @@ This is useful when reverting multiple commits and you want one combined revert ## Verification -After completing all three challenges, verify your solutions: +After completing both challenges, verify your solutions: ```pwsh cd .. # Return to module directory (if you're in challenge/) @@ -320,7 +215,6 @@ The script checks that: - ✅ Revert commits were created (not destructive deletion) - ✅ Bad code is removed - ✅ Good code before and after is preserved -- ✅ Revert works on commits in the middle of history ## Command Reference @@ -580,7 +474,6 @@ You've learned: - ✅ `git revert` creates new commits that undo previous changes - ✅ Revert is safe for shared/pushed commits -- ✅ Revert works on any commit in history, even old ones - ✅ Commits before and after the reverted commit are preserved - ✅ Multiple commits can be reverted in one command - ✅ Revert preserves complete history for audit trails @@ -589,10 +482,10 @@ You've learned: ## Next Steps -1. Complete all three challenge scenarios -2. Run `./verify.ps1` to check your solutions +1. Complete both challenge scenarios +2. Run `.\verify.ps1` to check your solutions 3. Experiment with reverting different commits -4. Move on to Module 06: Git Reset (dangerous but powerful!) +4. Move on to Module 07: Git Reset (dangerous but powerful!) --- diff --git a/01-essentials/06-revert/setup.ps1 b/01-essentials/06-revert/setup.ps1 index 12c6d6e..e772cd8 100644 --- a/01-essentials/06-revert/setup.ps1 +++ b/01-essentials/06-revert/setup.ps1 @@ -4,10 +4,9 @@ Sets up the Module 06 challenge environment for learning git revert. .DESCRIPTION - This script creates a challenge directory with three branches demonstrating + This script creates a challenge directory with two branches demonstrating different revert scenarios: - - regular-revert: Basic revert of a single bad commit at the end - - middle-revert: Reverting a bad commit in the middle of history + - regular-revert: Basic revert of a single bad commit - multi-revert: Reverting multiple commits at once #> @@ -104,68 +103,9 @@ git commit -m "Add modulo function" | Out-Null Write-Host "[CREATED] regular-revert branch with bad divide commit" -ForegroundColor Green # ============================================================================ -# SCENARIO 2: Revert in Middle of History +# SCENARIO 2: Multi Revert (Multiple Bad Commits) # ============================================================================ -Write-Host "`nScenario 2: Creating middle-revert scenario..." -ForegroundColor Cyan - -# Switch back to main -git switch $mainBranch | Out-Null - -# Create middle-revert branch -git switch -c middle-revert | Out-Null - -# Good commit: Add validation module -$validationContent = @" -# validation.py - Input validation - -def validate_number(value): - """Check if value is a valid number.""" - try: - float(value) - return True - except ValueError: - return False -"@ -Set-Content -Path "validation.py" -Value $validationContent -git add . -git commit -m "Add input validation module" | Out-Null - -# BAD commit: Add broken formatter -$formatterContent = @" -# formatter.py - Output formatting - -def format_result(result): - """BROKEN: Doesn't handle None or errors properly!""" - return f"Result: {result.upper()}" # This crashes if result is not a string! -"@ -Set-Content -Path "formatter.py" -Value $formatterContent -git add . -git commit -m "Add broken formatter - needs to be reverted!" | Out-Null - -# Good commit: Add configuration (depends on validation, not formatter) -$configContent = @" -# config.py - Application configuration - -from validation import validate_number - -DEFAULT_PRECISION = 2 - -def set_precision(value): - """Set calculation precision.""" - if validate_number(value): - return int(value) - return DEFAULT_PRECISION -"@ -Set-Content -Path "config.py" -Value $configContent -git add . -git commit -m "Add configuration module" | Out-Null - -Write-Host "[CREATED] middle-revert branch with bad commit in the middle" -ForegroundColor Green - -# ============================================================================ -# SCENARIO 3: Multi Revert (Multiple Bad Commits) -# ============================================================================ -Write-Host "`nScenario 3: Creating multi-revert branch..." -ForegroundColor Cyan +Write-Host "`nScenario 2: Creating multi-revert branch..." -ForegroundColor Cyan # Switch back to main git switch $mainBranch | Out-Null @@ -247,10 +187,9 @@ git switch regular-revert | Out-Null Set-Location .. Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green -Write-Host "`nThree revert scenarios have been created:" -ForegroundColor Cyan -Write-Host " 1. regular-revert - Revert a bad commit at the end" -ForegroundColor White -Write-Host " 2. middle-revert - Revert a bad commit in the middle of history" -ForegroundColor White -Write-Host " 3. multi-revert - Revert multiple bad commits" -ForegroundColor White +Write-Host "`nTwo revert scenarios have been created:" -ForegroundColor Cyan +Write-Host " 1. regular-revert - Revert a single bad commit" -ForegroundColor White +Write-Host " 2. multi-revert - Revert multiple bad commits" -ForegroundColor White Write-Host "`nYou are currently on the 'regular-revert' branch." -ForegroundColor Cyan Write-Host "`nNext steps:" -ForegroundColor Cyan Write-Host " 1. cd challenge" -ForegroundColor White diff --git a/01-essentials/06-revert/verify.ps1 b/01-essentials/06-revert/verify.ps1 index 4013851..8ac105a 100644 --- a/01-essentials/06-revert/verify.ps1 +++ b/01-essentials/06-revert/verify.ps1 @@ -4,9 +4,8 @@ Verifies the Module 06 challenge solutions. .DESCRIPTION - Checks that all three revert scenarios have been completed correctly: + Checks that both revert scenarios have been completed correctly: - regular-revert: Single commit reverted - - middle-revert: Commit in middle of history reverted - multi-revert: Multiple commits reverted #> @@ -87,57 +86,9 @@ if ($LASTEXITCODE -ne 0) { } # ============================================================================ -# SCENARIO 2: Middle Revert Verification +# SCENARIO 2: Multi Revert Verification # ============================================================================ -Write-Host "`n=== Scenario 2: Middle Revert ===" -ForegroundColor Cyan - -git switch middle-revert 2>&1 | Out-Null - -if ($LASTEXITCODE -ne 0) { - Write-Host "[FAIL] middle-revert branch not found" -ForegroundColor Red - $allChecksPassed = $false -} else { - # Check that a revert commit exists - $revertCommit = git log --oneline --grep="Revert" 2>$null - if ($revertCommit) { - Write-Host "[PASS] Revert commit found" -ForegroundColor Green - } else { - Write-Host "[FAIL] No revert commit found" -ForegroundColor Red - Write-Host "[HINT] Use: git revert " -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check that formatter.py is removed (reverted) - if (-not (Test-Path "formatter.py")) { - Write-Host "[PASS] Broken formatter.py successfully reverted (file removed)" -ForegroundColor Green - } else { - Write-Host "[FAIL] formatter.py still exists (should be reverted)" -ForegroundColor Red - Write-Host "[HINT] The bad commit should be reverted, removing formatter.py" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check that validation.py still exists (good commit before bad one) - if (Test-Path "validation.py") { - Write-Host "[PASS] validation.py preserved (good commit before bad one)" -ForegroundColor Green - } else { - Write-Host "[FAIL] validation.py missing (should still exist)" -ForegroundColor Red - $allChecksPassed = $false - } - - # Check that config.py still exists (good commit after bad one) - if (Test-Path "config.py") { - Write-Host "[PASS] config.py preserved (good commit after bad one)" -ForegroundColor Green - } else { - Write-Host "[FAIL] config.py missing (should still exist)" -ForegroundColor Red - Write-Host "[HINT] Only revert the bad commit, not the good ones after it" -ForegroundColor Yellow - $allChecksPassed = $false - } -} - -# ============================================================================ -# SCENARIO 3: Multi Revert Verification -# ============================================================================ -Write-Host "`n=== Scenario 3: Multi Revert ===" -ForegroundColor Cyan +Write-Host "`n=== Scenario 2: Multi Revert ===" -ForegroundColor Cyan git switch multi-revert 2>&1 | Out-Null @@ -209,9 +160,8 @@ if ($allChecksPassed) { Write-Host "`nYou've mastered git revert!" -ForegroundColor Cyan Write-Host "You now understand:" -ForegroundColor Cyan Write-Host " ✓ Reverting commits safely without erasing history" -ForegroundColor White - Write-Host " ✓ Reverting old commits in the middle of history" -ForegroundColor White Write-Host " ✓ Reverting multiple commits at once" -ForegroundColor White - Write-Host " ✓ Preserving commits before and after the reverted one" -ForegroundColor White + Write-Host " ✓ Preserving all other commits while undoing specific changes" -ForegroundColor White Write-Host "`nReady for Module 07: Git Reset!" -ForegroundColor Green Write-Host "" exit 0 From cdd695b2505479cd9a15da2c7337a2e7003bfeac Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 16:29:06 +0100 Subject: [PATCH 55/61] fix: cleanup revert module --- 01-essentials/06-revert/README.md | 104 ++---------------------------- 1 file changed, 4 insertions(+), 100 deletions(-) diff --git a/01-essentials/06-revert/README.md b/01-essentials/06-revert/README.md index 57bd780..38a12d0 100644 --- a/01-essentials/06-revert/README.md +++ b/01-essentials/06-revert/README.md @@ -293,10 +293,10 @@ Use `git revert` when: Consider alternatives when: -- ❌ **Commits are still local** - Use `git reset` instead (Module 06) +- ❌ **Commits are still local** - Use `git reset` instead (advanced module) - ❌ **Just want to edit a commit** - Use `git commit --amend` -- ❌ **Haven't pushed yet** - Reset is cleaner for local cleanup -- ❌ **Need to combine commits** - Use interactive rebase +- ❌ **Haven't pushed yet** - Reset is cleaner for local cleanup, but more dangerous, stick to revert if in doubt +- ❌ **Need to combine commits** - Use interactive rebase IF nothing has been pushed to cloud - ❌ **Reverting creates complex conflicts** - Might need manual fix forward ## Revert vs. Reset vs. Rebase @@ -344,7 +344,7 @@ git revert --abort ### 1. Using Reset on Pushed Commits ```pwsh -# ❌ NEVER do this with pushed commits +# ❌ NEVER do this with pushed commits. Or at least try your best to avoid it. git reset --hard HEAD~3 # ✅ Do this instead @@ -398,99 +398,3 @@ git revert C - Revert the minimum necessary - Don't bundle multiple unrelated reverts - One problem = one revert commit - -## Troubleshooting - -### "Empty Revert / No Changes" - -**Problem:** Revert doesn't seem to do anything. - -**Possible causes:** -- Commit was already reverted -- Subsequent commits already undid the changes -- Wrong commit hash - -**Solution:** -```pwsh -# Check what the commit actually changed -git show - -# Check if already reverted -git log --grep="Revert" -``` - -### "Conflicts During Revert" - -**Problem:** Revert causes merge conflicts. - -**Why:** Subsequent commits modified the same code. - -**Solution:** -1. Manually resolve conflicts in affected files -2. `git add ` -3. `git revert --continue` - -Or consider fixing forward with a new commit instead of reverting. - -### "Reverting Old Commit Breaks Something" - -**Problem:** After reverting an old commit, something else stops working. - -**Why:** The old commit might have been a dependency for later commits. - -**Solution:** -1. Check what changed: `git diff HEAD~1 HEAD` -2. Either fix the issue with a new commit, or -3. Revert the revert if needed: `git revert ` - -## Advanced: Revert Internals - -Understanding what revert does under the hood: - -```pwsh -# Revert creates a new commit with inverse changes -git revert - -# This is equivalent to: -git diff ^.. > changes.patch -patch -R < changes.patch # Apply in reverse -git add . -git commit -m "Revert ''" -``` - -**Key insight:** Revert computes the diff of the target commit, inverts it, and applies it as a new commit. - -## Going Further - -Now that you understand revert, you're ready for: - -- **Module 06: Git Reset** - Learn the dangerous but powerful local history rewriting -- **Module 07: Git Stash** - Temporarily set aside uncommitted changes -- **Module 08: Multiplayer Git** - Collaborate with advanced workflows - -## Summary - -You've learned: - -- ✅ `git revert` creates new commits that undo previous changes -- ✅ Revert is safe for shared/pushed commits -- ✅ Commits before and after the reverted commit are preserved -- ✅ Multiple commits can be reverted in one command -- ✅ Revert preserves complete history for audit trails - -**The Golden Rule of Revert:** Use revert for any commit that might be shared with others. - -## Next Steps - -1. Complete both challenge scenarios -2. Run `.\verify.ps1` to check your solutions -3. Experiment with reverting different commits -4. Move on to Module 07: Git Reset (dangerous but powerful!) - ---- - -**Need Help?** -- Review the command reference above -- Check the troubleshooting section -- Re-run `./setup.ps1` to start fresh -- Practice reverting in different orders to understand the behavior From b0d2d43c8b59b3751c0391c7d95d1585911ee5f9 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 16:37:57 +0100 Subject: [PATCH 56/61] fix: stash issues --- 01-essentials/07-stash/README.md | 356 +++++++++++++++++++++++------- 01-essentials/07-stash/setup.ps1 | 2 +- 01-essentials/07-stash/verify.ps1 | 12 +- 3 files changed, 283 insertions(+), 87 deletions(-) diff --git a/01-essentials/07-stash/README.md b/01-essentials/07-stash/README.md index 47b6ead..c0505b4 100644 --- a/01-essentials/07-stash/README.md +++ b/01-essentials/07-stash/README.md @@ -1,4 +1,4 @@ -# Module 11: Stash +# Module 07: Git Stash - Temporary Storage ## Learning Objectives @@ -64,12 +64,112 @@ stash@{2} <- Oldest stash You can have multiple stashes and apply any of them. -## Useful Commands +## Setup -```bash -# Stash current changes +Run the setup script to create the challenge environment: + +```pwsh +.\setup.ps1 +``` + +This creates a `challenge/` directory where you're working on a login feature with uncommitted changes, and a critical bug needs fixing on main. + +## Your Task + +### The Scenario + +You're working on a login feature on the `feature-login` branch. Your work is incomplete (has TODOs), so it's not ready to commit. + +Suddenly, your teammate reports a **critical security bug** in production! You need to: +1. Temporarily save your incomplete work +2. Switch to the main branch +3. Fix the urgent bug +4. Return to your feature and continue working + +### Step-by-Step Instructions + +1. **Navigate to the challenge directory:** + ```pwsh + cd challenge + ``` + +2. **Check your current status:** + ```pwsh + git status + ``` + You should see modified `login.py` (uncommitted changes) + +3. **Stash your work with a message:** + ```pwsh + git stash save "WIP: login feature" + ``` + +4. **Verify working directory is clean:** + ```pwsh + git status + ``` + Should say "nothing to commit, working tree clean" + +5. **Switch to main branch:** + ```pwsh + git switch main + ``` + +6. **Open app.py and find the bug:** + ```pwsh + cat app.py + ``` + Look for the comment "# BUG: This allows unauthenticated access!" + +7. **Fix the bug** by editing app.py: + - Remove the buggy comment line + - You can leave the implementation as-is or improve it + - The important thing is removing the comment that says "allows unauthenticated access" + +8. **Commit the fix:** + ```pwsh + git add app.py + git commit -m "Fix critical security bug" + ``` + +9. **Switch back to your feature branch:** + ```pwsh + git switch feature-login + ``` + +10. **Restore your stashed work:** + ```pwsh + git stash pop + ``` + This applies the stash and removes it from the stash stack + +11. **Complete the TODOs in login.py:** + - Open login.py in your editor + - Complete the login method (verify password and return session) + - Add a logout method + - Remove all TODO comments + +12. **Commit your completed feature:** + ```pwsh + git add login.py + git commit -m "Complete login feature" + ``` + +13. **Verify your solution:** + ```pwsh + ..\verify.ps1 + ``` + +## Key Stash Commands + +### Basic Operations + +```pwsh +# Stash current changes with a message +git stash save "description" + +# Stash without a message (not recommended) git stash -git stash save "description" # With a descriptive message # Stash including untracked files git stash -u @@ -77,7 +177,17 @@ git stash -u # List all stashes git stash list -# Apply most recent stash and remove it from stack +# Show what's in the most recent stash +git stash show + +# Show full diff of stash +git stash show -p +``` + +### Applying Stashes + +```pwsh +# Apply most recent stash and remove it (RECOMMENDED) git stash pop # Apply most recent stash but keep it in stack @@ -86,114 +196,192 @@ git stash apply # Apply a specific stash git stash apply stash@{1} -# Show what's in a stash -git stash show -git stash show -p # Show full diff +# Apply a specific stash by number +git stash apply 1 +``` -# Drop (delete) a stash -git stash drop stash@{0} +### Managing Stashes + +```pwsh +# Drop (delete) the most recent stash +git stash drop + +# Drop a specific stash +git stash drop stash@{1} # Clear all stashes git stash clear -# Create a branch from a stash +# Create a new branch from a stash git stash branch new-branch-name ``` +## Understanding Stash vs Pop vs Apply + +### Stash Pop (Recommended) +```pwsh +git stash pop +``` +- Applies the stash to your working directory +- **Removes** the stash from the stack +- Use this most of the time + +### Stash Apply (Keep Stash) +```pwsh +git stash apply +``` +- Applies the stash to your working directory +- **Keeps** the stash in the stack +- Useful if you want to apply the same changes to multiple branches + +### When to Use Which + +**Use `pop` when:** +- You're done with the stash and won't need it again (99% of the time) +- You want to keep your stash list clean + +**Use `apply` when:** +- You want to test the same changes on different branches +- You're not sure if you want to keep the stash yet + ## Verification Run the verification script to check your solution: -```bash +```pwsh +..\verify.ps1 +``` + +Or from the module directory: + +```pwsh .\verify.ps1 ``` The verification will check that: -- The bug fix commit exists on main -- Your feature is completed on the feature branch -- Changes were properly stashed and restored -- No uncommitted changes remain +- ✅ The bug fix commit exists on main +- ✅ Your feature is completed on the feature-login branch +- ✅ All TODOs are removed from login.py +- ✅ No uncommitted changes remain -## Challenge Steps +## Troubleshooting -1. Navigate to the challenge directory -2. You're on feature-login with uncommitted changes -3. Check status: `git status` (you'll see modified files) -4. Stash your changes: `git stash save "WIP: login feature"` -5. Verify working directory is clean: `git status` -6. Switch to main: `git switch main` -7. View the bug in app.js and fix it (remove the incorrect line) -8. Commit the fix: `git add app.js && git commit -m "Fix critical security bug"` -9. Switch back to feature: `git switch feature-login` -10. Restore your work: `git stash pop` -11. Complete the feature (the TODOs in login.js) -12. Commit your completed feature -13. Run verification +### "Cannot switch branches - you have uncommitted changes" -## Tips +**Problem:** Git won't let you switch branches with uncommitted changes. -- Always use `git stash save "message"` to describe what you're stashing -- Use `git stash list` to see all your stashes -- `git stash pop` applies and removes the stash (use this most often) -- `git stash apply` keeps the stash (useful if you want to apply it to multiple branches) -- Stashes are local - they don't get pushed to remote repositories -- You can stash even if you have changes to different files -- Stash before pulling to avoid merge conflicts -- Use `git stash show -p` to preview what's in a stash before applying +**Solution:** +```pwsh +# Stash your changes first +git stash save "work in progress" -## Common Stash Scenarios +# Now you can switch +git switch other-branch -### Scenario 1: Quick Branch Switch -```bash -# Working on feature, need to switch to main -git stash -git switch main -# Do work on main -git switch feature +# When you come back, restore your work +git switch original-branch git stash pop ``` -### Scenario 2: Pull with Local Changes -```bash -# You have local changes but need to pull -git stash -git pull -git stash pop -# Resolve any conflicts +### "I don't remember what's in my stash" + +**Problem:** You stashed something but forgot what it was. + +**Solution:** +```pwsh +# List all stashes +git stash list + +# Show summary of what changed +git stash show stash@{0} + +# Show full diff +git stash show -p stash@{0} ``` -### Scenario 3: Experimental Changes -```bash -# Try something experimental -git stash # Save current work -# Make experimental changes -# Decide you don't like it -git restore . # Discard experiment -git stash pop # Restore original work -``` +### "Stash conflicts when I apply" -### Scenario 4: Apply to Multiple Branches -```bash -# Same fix needed on multiple branches -git stash -git switch branch1 -git stash apply -git commit -am "Apply fix" -git switch branch2 -git stash apply -git commit -am "Apply fix" -git stash drop # Clean up when done -``` +**Problem:** Applying a stash causes merge conflicts. -## Stash Conflicts - -If applying a stash causes conflicts: -1. Git will mark the conflicts in your files -2. Resolve conflicts manually (like merge conflicts) +**Solution:** +1. Git marks conflicts in your files with `<<<<<<<` markers +2. Open the files and resolve conflicts manually 3. Stage the resolved files: `git add ` -4. The stash is automatically dropped after successful pop +4. If you used `pop`, the stash is automatically dropped 5. If you used `apply`, manually drop it: `git stash drop` -## What You'll Learn +### "I accidentally cleared my stash" -Git stash is an essential tool for managing context switches in your daily workflow. It lets you maintain a clean working directory while preserving incomplete work, making it easy to handle interruptions, urgent fixes, and quick branch switches. Mastering stash makes you more efficient and helps avoid the temptation to make "WIP" commits just to switch branches. Think of stash as your temporary workspace that follows you around. +**Problem:** You deleted a stash you still needed. + +**Unfortunately:** Stashes are hard to recover once deleted. Lessons learned: +- Use `git stash pop` instead of `git stash drop` when you're unsure +- Use `git stash show -p` to preview before dropping +- Consider committing work instead of stashing for important changes + +## Tips for Success + +💡 **Always add a message** - `git stash save "your message"` helps you remember what you stashed +💡 **Use pop, not apply** - Pop removes the stash automatically, keeping your stash list clean +💡 **Stash before pulling** - Avoid merge conflicts when pulling updates +💡 **Preview before applying** - Use `git stash show -p` to see what's in a stash +💡 **Stashes are local** - They don't get pushed to remote repositories +💡 **Clean working directory** - Always verify with `git status` after stashing + +## Common Use Cases + +### Quick Branch Switch +```pwsh +# You're working on feature-A +git stash save "feature A progress" +git switch hotfix-branch +# Fix the issue, commit +git switch feature-A +git stash pop +``` + +### Pull with Local Changes +```pwsh +# You have uncommitted changes +git stash save "local changes" +git pull +git stash pop +# Resolve conflicts if any +``` + +### Test Clean State +```pwsh +# Stash changes to test on clean code +git stash save "testing clean state" +# Run tests +git stash pop # Restore your changes +``` + +## What You've Learned + +After completing this module, you understand: + +- ✅ Stash temporarily saves uncommitted changes +- ✅ Stash lets you switch contexts without committing +- ✅ `git stash pop` applies and removes the stash +- ✅ `git stash apply` applies but keeps the stash +- ✅ Stashes are local and not pushed to remote +- ✅ Stash is essential for handling interruptions and urgent fixes + +**Key Takeaway:** Stash is your "temporary clipboard" for incomplete work. It helps you stay productive when you need to context-switch without making messy "WIP" commits. + +## Next Steps + +Ready to continue? You've now mastered the essential Git commands for daily development: +- Committing and history +- Branching and merging +- Cherry-picking specific changes +- Safely reverting commits +- Temporarily stashing work + +Move on to **Module 08: Multiplayer Git** to practice collaborating with others! + +To start over: +```pwsh +.\reset.ps1 +``` diff --git a/01-essentials/07-stash/setup.ps1 b/01-essentials/07-stash/setup.ps1 index c4f09d1..4359767 100644 --- a/01-essentials/07-stash/setup.ps1 +++ b/01-essentials/07-stash/setup.ps1 @@ -158,7 +158,7 @@ Write-Host "`nYour task:" -ForegroundColor Yellow Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White Write-Host "2. Check your status: git status (see uncommitted changes)" -ForegroundColor White Write-Host "3. Stash your work: git stash save 'WIP: login feature'" -ForegroundColor White -Write-Host "4. Switch to $mainBranch: git checkout $mainBranch" -ForegroundColor White +Write-Host "4. Switch to ${mainBranch}: git checkout $mainBranch" -ForegroundColor White Write-Host "5. Fix the security bug in app.py (remove the comment and fix the auth)" -ForegroundColor White Write-Host "6. Commit the fix: git add app.py && git commit -m 'Fix critical security bug'" -ForegroundColor White Write-Host "7. Switch back: git checkout feature-login" -ForegroundColor White diff --git a/01-essentials/07-stash/verify.ps1 b/01-essentials/07-stash/verify.ps1 index 312ab2e..9e723f1 100644 --- a/01-essentials/07-stash/verify.ps1 +++ b/01-essentials/07-stash/verify.ps1 @@ -73,7 +73,15 @@ git checkout $mainBranch 2>$null | Out-Null # Check for bug fix commit $mainCommits = git log --pretty=format:"%s" $mainBranch 2>$null -if ($mainCommits -notmatch "security bug|Fix.*bug|security fix") { +$hasSecurityFix = $false +foreach ($commit in $mainCommits) { + if ($commit -match "security|Fix.*bug") { + $hasSecurityFix = $true + break + } +} + +if (-not $hasSecurityFix) { Write-Host "[FAIL] No security bug fix commit found on $mainBranch branch." -ForegroundColor Red Write-Host "Hint: After stashing, switch to $mainBranch and commit a bug fix" -ForegroundColor Yellow git checkout feature-login 2>$null | Out-Null @@ -128,7 +136,7 @@ if (-not (Test-Path "login.py")) { $loginContent = Get-Content "login.py" -Raw # Check that login method exists and is implemented -if ($loginContent -notmatch "login\(username, password\)") { +if ($loginContent -notmatch "def login") { Write-Host "[FAIL] login.py should have a login method." -ForegroundColor Red Set-Location .. exit 1 From 2a5eb137f621006c42dcc17bbe3039122eddc615 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 16:42:45 +0100 Subject: [PATCH 57/61] fix: stash challenge --- 01-essentials/07-stash/setup.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/01-essentials/07-stash/setup.ps1 b/01-essentials/07-stash/setup.ps1 index 4359767..1703a44 100644 --- a/01-essentials/07-stash/setup.ps1 +++ b/01-essentials/07-stash/setup.ps1 @@ -158,10 +158,10 @@ Write-Host "`nYour task:" -ForegroundColor Yellow Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White Write-Host "2. Check your status: git status (see uncommitted changes)" -ForegroundColor White Write-Host "3. Stash your work: git stash save 'WIP: login feature'" -ForegroundColor White -Write-Host "4. Switch to ${mainBranch}: git checkout $mainBranch" -ForegroundColor White +Write-Host "4. Switch to ${mainBranch}: git switch $mainBranch" -ForegroundColor White Write-Host "5. Fix the security bug in app.py (remove the comment and fix the auth)" -ForegroundColor White Write-Host "6. Commit the fix: git add app.py && git commit -m 'Fix critical security bug'" -ForegroundColor White -Write-Host "7. Switch back: git checkout feature-login" -ForegroundColor White +Write-Host "7. Switch back: git switch feature-login" -ForegroundColor White Write-Host "8. Restore your work: git stash pop" -ForegroundColor White Write-Host "9. Complete the TODOs in login.py" -ForegroundColor White Write-Host "10. Commit your completed feature" -ForegroundColor White From fcaf97f60be0b54d83d07c3f079862e08907984a Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 17:03:51 +0100 Subject: [PATCH 58/61] refactor: simplify the multiplayer part --- .../08-multiplayer/01_FACILITATOR.md | 17 +- .../02_AZURE-DEVOPS-SSH-SETUP.md | 264 ++++++++ .../{02_README.md => 03_README.md} | 0 .../{03_TASKS.md => 04_TASKS.md} | 0 .../08-multiplayer/images/01_settings.png | Bin 0 -> 34123 bytes .../08-multiplayer/images/02_ssh_option.png | Bin 0 -> 92945 bytes .../08-multiplayer/images/03_add_new_key.png | Bin 0 -> 76451 bytes .../images/04_copy_paste_key.png | Bin 0 -> 173853 bytes AZURE-DEVOPS-SSH-SETUP.md | 628 ------------------ 9 files changed, 265 insertions(+), 644 deletions(-) create mode 100644 01-essentials/08-multiplayer/02_AZURE-DEVOPS-SSH-SETUP.md rename 01-essentials/08-multiplayer/{02_README.md => 03_README.md} (100%) rename 01-essentials/08-multiplayer/{03_TASKS.md => 04_TASKS.md} (100%) create mode 100644 01-essentials/08-multiplayer/images/01_settings.png create mode 100644 01-essentials/08-multiplayer/images/02_ssh_option.png create mode 100644 01-essentials/08-multiplayer/images/03_add_new_key.png create mode 100644 01-essentials/08-multiplayer/images/04_copy_paste_key.png delete mode 100644 AZURE-DEVOPS-SSH-SETUP.md diff --git a/01-essentials/08-multiplayer/01_FACILITATOR.md b/01-essentials/08-multiplayer/01_FACILITATOR.md index 5f76172..21233b8 100644 --- a/01-essentials/08-multiplayer/01_FACILITATOR.md +++ b/01-essentials/08-multiplayer/01_FACILITATOR.md @@ -232,23 +232,8 @@ To reuse the repository: ## Tips -- **Keep groups small** (4-8 people) for more interaction +- **Keep groups small** (2 people per repository) for more interaction - **Encourage communication** - the exercise works best when people talk - **Let conflicts happen** - they're the best learning opportunity - **Walk the room** - help students who get stuck - **Point students to 03_TASKS.md** - Simple explanations of clone, push, pull, and fetch for beginners - ---- - -## Troubleshooting - -### SSH Issues -- Verify SSH key added to Azure DevOps (User Settings → SSH Public Keys) -- Test: `ssh -T git@ssh.dev.azure.com` - -### Permission Issues -- Check user is added to project -- Verify Contribute permission on repository - -### Service Issues -- Check status: https://status.dev.azure.com diff --git a/01-essentials/08-multiplayer/02_AZURE-DEVOPS-SSH-SETUP.md b/01-essentials/08-multiplayer/02_AZURE-DEVOPS-SSH-SETUP.md new file mode 100644 index 0000000..4373939 --- /dev/null +++ b/01-essentials/08-multiplayer/02_AZURE-DEVOPS-SSH-SETUP.md @@ -0,0 +1,264 @@ +# Azure DevOps SSH Setup - Best Practices Guide + +This guide provides comprehensive instructions for setting up SSH authentication with Azure DevOps. SSH is the recommended authentication method for secure Git operations. + +## Why SSH is Best Practice + +SSH (Secure Shell) keys provide a secure way to authenticate with Azure DevOps without exposing passwords or tokens. Here's why SSH is the security best practice: + +**Security Benefits:** +- **No Password Exposure**: Your credentials never travel over the network +- **Strong Encryption**: Uses RSA cryptographic algorithms +- **No Credential Prompts**: Seamless authentication after initial setup +- **Revocable**: Individual keys can be removed without changing passwords +- **Auditable**: Track which key was used for each operation + +--- + +## Prerequisites + +Before starting, ensure you have: + +- **Git 2.23 or higher** installed + ```powershell + git --version + ``` + +- **Azure DevOps account** with access to your organization/project + - If you don't have one, create a free account at [dev.azure.com](https://dev.azure.com) + +- **PowerShell 7+ or Bash terminal** for running commands + ```powershell + pwsh --version + ``` + +--- + +## Step 1: Generate SSH Key Pair + +SSH authentication uses a key pair: a private key (stays on your computer) and a public key (uploaded to Azure DevOps). + +### Generate RSA Key + +Open your terminal and run: + +```powershell +ssh-keygen -t rsa +``` + +**Note about RSA:** Azure DevOps currently only supports RSA SSH keys. While newer algorithms like Ed25519 offer better security and performance, they are not yet supported by Azure DevOps. See the note at the end of this guide for more information. + +### Save Location + +When prompted for the file location, press `Enter` to accept the default: + +``` +Enter file in which to save the key (/Users/yourname/.ssh/id_rsa): +``` + +**Default locations:** +- **Windows**: `C:\Users\YourName\.ssh\id_rsa` and `C:\Users\YourName\.ssh\id_rsa.pub` + +### Passphrase (Optional but Recommended) + +You'll be prompted to enter a passphrase, just press `Enter` no password is needed: + +``` +Enter passphrase (empty for no passphrase): +Enter same passphrase again: +``` + +### Verify Key Generation + +Check that your keys were created: + +**Linux/Mac:** +**Windows PowerShell:** +```powershell +dir $HOME\.ssh\ +``` + +You should see two files: +- `id_rsa` - Private key (NEVER share this) +- `id_rsa.pub` - Public key (safe to share for upload to Azure DevOps) + +--- + +## Step 2: Add SSH Public Key to Azure DevOps + +Now you'll upload your public key to Azure DevOps. + +### Navigate to SSH Public Keys Settings + +1. Sign in to Azure DevOps at [https://dev.azure.com](https://dev.azure.com) +2. Click your **profile icon** in the top-right corner +3. Select **User settings** from the dropdown menu +4. Click **SSH Public Keys** + +![Azure DevOps - User Settings Menu](./images/02_ssh_option.png) +*Navigate to your user settings by clicking the profile icon in the top-right corner* + +### Add New SSH Key + +5. Click the **+ New Key** button + +![Azure DevOps - Add SSH Public Key Dialog](./images/03_add_new_key.png) +*Click '+ New Key' to begin adding your SSH public key* + +### Copy Your Public Key + +Open your terminal and display your public key: + +**Linux/Mac:** +```bash +cat ~/.ssh/id_rsa.pub +``` + +**Windows PowerShell:** +```powershell +type $HOME\.ssh\id_rsa.pub +``` + +**Windows Command Prompt:** +```cmd +type %USERPROFILE%\.ssh\id_rsa.pub +``` + +The output will look like this: +``` +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2YbXnrSK5TTflZSwUv9KUedvI4p3JJ4dHgwp/SeJGqMNWnOMDbzQQzYT7E39w9Q8ItrdWsK4vRLGY2B1rQ+BpS6nn4KhTanMXLTaUFDlg6I1Yn5S3cTTe8dMAoa14j3CZfoSoRRgK8E+ktNb0o0nBMuZJlLkgEtPIz28fwU1vcHoSK7jFp5KL0pjf37RYZeHkbpI7hdCG2qHtdrC35gzdirYPJOekErF5VFRrLZaIRSSsX0V4XzwY2k1hxM037o/h6qcTLWfi5ugbyrdscL8BmhdGNH4Giwqd1k3MwSyiswRuAuclYv27oKnFVBRT+n649px4g3Vqa8dh014wM2HDjMGENIkHx0hcV9BWdfBfTSCJengmosGW+wQfmaNUo4WpAbwZD73ALNsoLg5Yl1tB6ZZ5mHwLRY3LG2BbQZMZRCELUyvbh8ZsRksNN/2zcS44RIQdObV8/4hcLse30+NQ7GRaMnJeAMRz4Rpzbb02y3w0wNQFp/evj1nN4WTz6l8= your@email.com +``` + +**Copy the entire output** (from `ssh-rsa` to your email address). + + +### Paste and Name Your Key + +![Azure DevOps - Add SSH Public Key Dialog](./images/04_copy_paste_key.png) + +6. In the Azure DevOps dialog: + - **Name**: Give your key a descriptive name (e.g., "Workshop Laptop 2026", "Home Desktop", "Work MacBook") + - **Public Key Data**: Paste the entire public key you just copied +7. Click **Save** + +**Naming tip**: Use names that help you identify which machine uses each key. This makes it easier to revoke keys later if needed. + +--- + +## Step 3: Using SSH with Git + +Now that SSH is configured, you can use it for all Git operations. + +### Clone a Repository with SSH + +To clone a repository using SSH: + +```bash +git clone git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} +``` + +**Example** (replace placeholders with your actual values): +```bash +git clone git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project +``` + +**How to find your SSH URL:** +1. Navigate to your repository in Azure DevOps +2. Click **Clone** in the top-right +3. Select **SSH** from the dropdown +4. Copy the SSH URL + +![Azure DevOps - Get SSH Clone URL](./images/azure-devops-clone-ssh.png) +*Select SSH from the clone dialog to get your repository's SSH URL* + +### Convert Existing HTTPS Repository to SSH + +If you already cloned a repository using HTTPS, you can switch it to SSH: + +```bash +cd /path/to/your/repository +git remote set-url origin git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} +``` + +**Verify the change:** +```bash +git remote -v +``` + +You should see SSH URLs: +``` +origin git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project (fetch) +origin git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project (push) +``` + +### Daily Git Operations + +All standard Git commands now work seamlessly with SSH: + +```bash +# Pull latest changes +git pull + +# Push your commits +git push + +# Fetch from remote +git fetch + +# Push a new branch +git push -u origin feature-branch +``` + +**No more credential prompts!** SSH authentication happens automatically. + +--- + +## Additional Resources + +- **Azure DevOps SSH Documentation**: [https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate) +- **SSH Key Best Practices**: [https://security.stackexchange.com/questions/tagged/ssh-keys](https://security.stackexchange.com/questions/tagged/ssh-keys) +- **Git with SSH**: [https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key) + +--- + +## Quick Reference + +### Common Commands + +```bash +# Generate RSA key +ssh-keygen -t + +# Display public key (Linux/Mac) +cat ~/.ssh/id_rsa.pub + +# Display public key (Windows) +type $HOME\.ssh\id_rsa.pub + +# Test SSH connection +ssh -T git@ssh.dev.azure.com + +# Clone with SSH +git clone git@ssh.dev.azure.com:v3/{org}/{project}/{repo} + +# Convert HTTPS to SSH +git remote set-url origin git@ssh.dev.azure.com:v3/{org}/{project}/{repo} + +# Check remote URL +git remote -v +``` + +### SSH URL Format + +``` +git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} +``` + +**Example:** +``` +git@ssh.dev.azure.com:v3/mycompany/git-workshop/great-print-project +``` + +--- + +**You're all set!** SSH authentication with RSA keys is now configured for secure, passwordless Git operations with Azure DevOps. diff --git a/01-essentials/08-multiplayer/02_README.md b/01-essentials/08-multiplayer/03_README.md similarity index 100% rename from 01-essentials/08-multiplayer/02_README.md rename to 01-essentials/08-multiplayer/03_README.md diff --git a/01-essentials/08-multiplayer/03_TASKS.md b/01-essentials/08-multiplayer/04_TASKS.md similarity index 100% rename from 01-essentials/08-multiplayer/03_TASKS.md rename to 01-essentials/08-multiplayer/04_TASKS.md diff --git a/01-essentials/08-multiplayer/images/01_settings.png b/01-essentials/08-multiplayer/images/01_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..7feb35949bca282fc826f9756a4dca015b752972 GIT binary patch literal 34123 zcmZ^~1z23mvNntccLKpR5ZoOG2=49{Ah^4`dkDc9+}&M+ySux)`#(8npR@P9|NUlq zJ*%g?tGiZp_o}Y9YQp4Y#gO1};laSbkR-%~6~VwDH^9Ka(_kS$C3D<5O<-V%l4e3e z@)ANq#Paqw#%7jAU|`~5@oF&YN`qMGT8bo4u)+e82QsL!=mL_kbiOyFp_1Ydf#`-` z<*RZc;LCM{E3-?8!wFEU^z}3_TP!qa3%?v2bd|Tlfjcg$wY#+2oV}j&o4j%DuQVBf zWj0*rP2nqJ_-ad}(^4#Cu+dW`i?}1B`hpV(Kv}j6(C-S11VNfY?sqJ=;7#ByslepT z@jt(pn8AtIgM*1<_LzNbBF5ed0Nap(D56CH%f`IgQ@YNTY{fMoz(GXaqW(N4ze({~ zR%vtC3t-2j9ZCc#pt^r;gac;kD3l)0FyPDWm_%p$6SfyU^p*H-a1=-z=t|$iJ^?c( zp>|X=?L9I0kZQb?Ns*Dk6spp6gTp02o49vT@Sd^zu%3{b^C_ttm;F{+Lp+k%M7^PZ zF|PZUI>8po&)QJ&&^mAdyQ<1@y|Cou@E@-6{N`jo9P~%QFt(Vk($sbNiO60mwoY#76;IK z5@Tn5A(oHlk;RhOGF0I$ryRYGQHbh2&E-;feQWIaLa9EODu_=x9z*acH3J5_K$3{z z=blJ!(N@oJ%|n(#vFk+}F}|%J*f(%!OS5mx0}xwt z`a@lOV&3u*?g^0ra~SZGS$sc#1i$0y_LFwy2Te9djC9l`SQNNj(FXS1-{^mxzt+CpG{qwe}Y7m?7vklIRZOGH>7g@|-%#mASRXE&=-LlWuC*;oH7Qc_c5qP56zR@ixd?wVki+NUE z>9l0>y8JE#%(B>f2CAqko+)q79Nvh`CQ_y(6nK7Wip!fe+KL?E#e(r>5~fWT-r)4# zJ=r<=;CuC)CG z;M9@leC;dYETL7taV(%531V=-t(>AbX0U1_n-`e#nkVB2Eb9 z+ls72BO*tI6EpZhPX2LFfIJi;Ic!8s@<+TlS2c8z=p9K&Y}^2JE>cqzuK;)K`~W4U z(5wQ%3~b$3v24p<0l`^z2T-h#K!2=Ego!*;9L=z^ug4Q{2O>AzOVRBD9*n+4`qCqq z4xJ01up@eYRyj%skM@OEDVCy_^x|%MwTIUk>MV$*g|gUUoq?&@!V<eb^%0f-}IoM5_uBCOwQhkyy(WN6+{rMon5qc1lV>sYBxQ1Mp*Zfa1G} zJt|i+EeZBdsX?B>{sFZCxxoyo6ilgb%FjPZbEOMtb1ZXM73meYzho3?Dq9qDutlec z0LAEX<`l=}&lT4Ti;9wq+zLfX!e;l1;n}OVFd?Gqo?aE#uPjI)( zM>eLcDIZ5;5~PQ7=<-yiWhc+30j7+s_-wAYU!%BiKjJ#!s5Il$TpPMmq%FppRumOujVwLN=KUY z?l3td4JSP(b0lR1;~;G#VjyYpmO3xQ>!05SA3q+3-GtxX9q7zI=Z;UI%#6?JW$9J; zjDvAhvV^iHnO9D3L<-K^0ylNLp}QkO&WYYyNX$)5J{fX$+m`4@=*PrXDOASJm;xuM za_9~7t1|2oF3qDKK_t&6S8zk3Wyd!-|ABi8At5-f+MhwBpdY1g$hv_5On)fP9n z8<&}k*3eg$SG8LR)m+vk8z`AMj3$^4*N@h(e)CvFu5+jxt}Ur-`5v>>vKVp7vCt4j z9C`7H1V@2uD?gwavY%ulY7o7%cAC3jl+c_S*uK| zpe4wo_}TqF)sny&(*y3g6{f3u-7s1#u3t=!W1M&VHQmRJ%hkx^^v0dN&ppy~q@F3J zb+q+yO{;a;OC3-INcCFslJR*FKNO%QVPkSKIl}fP z@A$bM*C(ZwF-NxnKL@W1`x;mh1SGYYl$@M)Jim!Q9A|E2Q8jWjelkulb`ERWvcD`T z{ZT5Tab81Km{+ZzwhrYG{NcTm{l-y8Ow-M{ z9o9x*YB+1^M?{SaMy=Bg0VZ8((M??rC3_0{M&r+A6GgU0o6C=3*Lc{U=#pACCgr;L zXEDt%2g9xDZ!{at(AvdY3!N?xdXKSuXt1!}HMf)%HLr}qz71CJ$2>Ki?62h=N@*oF z(JeQoHELJC^ue!xRH3O;#VYlzH9skCo-Lj2Em<#5uHE}uBz2fyH&Yj0cWSh0gmg?h zN2hn6$6NE_9^})T4%*Au6+Rt;jhjQF9Z_|uk?ir7PqiAYk8=qdbWZWA@p)QdTGtJL z#e~Y!6^0YN;s!iRhQ&9hkndt1D$=uu=J!WOz+j%l*-Wc>%XRx+ttMF{nsK2lu3;&o;*S2*(@J4ukFfuMCg_3QZi|uTtZhEpnYdjFh<->k( zQsj{4&_C{xVbi?fws}!;qIb;DtW#=XXT^1sdTbM-xvF_g-=t^Wcw*O7WhK2_MboY( ztBcg=eusO$)O(CJzii#J-sr@#Yy3>NS!UAUVBPV=_kKU_vbXkmg|5B8t>UJA^L2>d z@_k|YY|i^eurnYbqzdJn7oFGrQvK~gs^6comNHLHCS8@^!Fz57XU6UE@nN5UA(sK* z^6|CkcK<1nodM--_g+ovzVR$tty-gnj|Y5bMZOS>L8;+F>YL$KC0a43k&(57sg2`l_4^d40M1ri z!vPEo>+_!rTtboj0t^gd*-Tm8QC&uw+t9{}UjMs|ff2o{mF=H?z<6D`L0Ky!M}1;f zD@$t!ZdX3if3@HS<^L2jkP`o^iK7J{sk)3jv5<|u5ivVGBRwN20G^nbnAiThF}I?y z=wIZZUwovdj*hn63=A$VF7z%e^fvY;3`|^HTnvoN49v`QpcZrvZq|#;x&$nd9zfr*}x z;r}9YG&BBxko~FoH`%|Q>)*ri{^^Wc#lgs4$i~Xb$l4L`x5Rn>HPZj6{F~2z6Uv*p z8d<6fn}H}DK+go=;$r@b>VMb#pOhMZQ?fGs56XYk{Dbn(BXBDkIoMb_{Sl#xwV5LT zMD~9b|8FYwzsUehtgH-7f06yO{D0GE{6A>^S^mFif`0v66z>f&A_k)26fJq1oD!YQ80HJ;HRpx?PmiqVr5)^0F zKLns)Nd&|vzDZqtgNuOH*`e$tg+oR_fYMv>he7BG!&As+!63S?e^*v>adN7=uZ>Jw zTO27-yO;1{bIfS=TKD?8j26ds#*q4~-xCuUdjCl@1g&-8gB_nYTA)YOX8kn}PVZo_ z+!)(pAFJAYX*WHz({TXv6+^~?%_U=s!Hb@8I3 zQa9QdphYA1Kx{%Z&jl`ssTJ>DVSMxOPo-99tv*GFs;y(If@y>3QMuR{ulwcbjK z_0jz|t-sY|sU?k`Ve+)rFB{z(;8^n@AM2@$hT@}kC4q4+(kqw48Z>%AVnME=PF1pa z`!*hi9C-1hHF(@kK-OQYD=o$iQtT${*+37Qa=$e8DI2l2<66z3-S<$8D1)lnsDy7rO&)7iqeMtSC#D_JudwmKh7b{(`Y30)6=m%+1h*019i4E6Oc zu~javr=*?BIkO@IK51B%HgKgj=WkWp&Wcp;IHSd$Bzdu6LA+p?(z+C1jwyM1 zTt89Z_I>W%IA?osS^XqUfhg>qGLVSXlsv2>*)2&>Lj5{2FkCb+Ix4jvTWEfP;n*<{ z%&7AsxKMyLMHHM7#`A1l zA_b80SmHvxymSrtD9db1;ytT8G$~az*9JdfZ$d`J91To{6U9{0%cPVS7gbnXs1RZC z%_iyJqREo&int~Rs1a{QVo2jLY{N{`4zWvq6-3zdpJjV4?SYQc zU2b0=wXjl$Bpa&RQa66pYPI;noS{OdwBR%@8Mv224)%GU=pK4QvA_XymR=KG(bqpU za@b{x=G_?iyUyvF5D{^RO&nmvSlXtNqlo)L{hf&6p;m&-F+Z3x>QHBxvihs$u)xAm z)&ELJ$wllJ^8GUSnJ}B~tLDka2k80cjWa7d`RRQ5qSxL zXoFib&_(~}0Ayrf{7+pSxm-$_9MvBEhd=)zK2{kBr;jN=y=#FL9K}Bc zmjaap(`L4&ru*@q%;kI*KylA$b4ugi-rcF3@uSGbR-^$8Lm7(bU!K4ssAgp6Q z{U0e4+e2BhFW;Rm`@9<}2nq`BxEslVxQmJxh+doGn@mY89Aq=s!l_Gkw+feKG!+3i ztX`s~t4FW7wFTB?jx{%k*5rtKjFehzBvLyL9WQhi3>@~RgUvkF&1)l>S=VXSX?l;M zKl5~=f#m#x_G2xDcv{cn70KMuC)YI^DuvZgS7L9os9w-4_zo@$?(9W#xY>Ss2wva9 z3DQZ0W5SH>kiS6G*6so*1gy|#xl6TauT!Uu`qr053im_6fi!dakV1yrVPK=nwdQFZ zhKRjDYQGLNEv7$f>o0Nk>@+5J;WjsYGyBJLv2efK2KDA=NZroP1%G4^@FQFUT5Xk~ zE<(V3oOQ$K7;WW6H?4)*;OxYk$@GJ~G@dnO@W-I^gGDthn7@&laAt}&P+hr6B{-=xI7lT@Eno-FkH;G)zCY&-U3mpLA&`3pc*=~h z%2*jmPkt8lbQUeMwI1%7t}Y`YDu2%ELA`wIKKKE_hiGc~_3x!wMe!qNTKiWy4~xn? z1mTr<#}ZCp1oI8Hk!k&$CO>t}i@d%SC;e@-=zBjiK+m72{HrGp&H8xZ`)RUb7h zdsStMdQ~l^H(=ImYew_2p&FO69?|Pk9Ls)`P1nxA5gqj`6I@134=Faqn zqR2P38J#pCDO`Chu3A!^Q&&@2W}nq;m=DwBoscriqP2+hH@3}Yq?X{J&?rDm5|n>_ zr9)DhE{h>x>t{OHtxm|7pvykyycdRVVi69Ah*T^Labi5R|fyRaku2asI63^ z-|$Oc9oz4cOV59)V<~G^O|XzMG%lzqFRLnRrBhWS%3zMkOas%hXSuwN_%Ld1YFT*o zzCTvdB7-xqETxE<^l1Wzl`p^j$g1$V6!6To_Y{q1NQeIW{YeZi2Lb1AY;6Il>MvO^ zL|3mCKAMpz#>DFB6lLWU)#MOnE0kcZ9ll)z2F}>9d+pm$dIk{?vYIMSQ?)Mvg+NJ8P@&!2(sxrW!-A|Gog#Tvz zLJSOW5<7?-d#6yen;?{LSQW*jp>EXHn2C&|v!n4l@!i$J3o&gllqcj^laS*KTKa=h z_$BL?2}8uR?%4mna(($BLZLfoyR!m2=o&`oe$xxtxo*a81ViSl%+f~zxHVi%WF2#I zVrxF%m3)Q#-TRBN%GVf;HB%slSlam?FXvlQUrf>lW3mgakwyCnOOGve{4JUtgu+@g zYo6~*`4>5cy+?t+OpC;Jc*I0S+-^RoboMZ|CCN}}h8jA9Eql8XPtkHbrnuhDB{wn0C?4S0RA2t#RqeD~P7kP_eikrk*o=g+{_6b*We|*Xq(o zCW$=%Xq|%8$N6ev)2R79=eiKMZ}Q8PDfVg5bGkuDmE92nRfLnz-7%nOb$c?lXhrU=lqu#< z`V8`)mQ2@KEVI+`y$)=4i-?G*S_HrrBd}VeQ-Tz--v!)vBO|`c9i_ayyqdE{z876w zPpQJ|hBfyVq5kM8X@=$p1Gk}lpwC+<8d-Vcmc6-SvP9=s1fbO2F3p_L2g?I9M(>v* zY(0Cs@$|Mx(eAxbAFr25v2~w!V1b4>+j)X|e*ST$>m>ZvL$;%i@XwLRaYnp4?~g^9 zRVh|s1k2>Lr(i%UYHr0ghZrzbU84qK0U+M;9=Y=g{Jz`m*=asm9_~)>Wur1)F)h27 zv84SyfYYp~sPx4GA;op})!rgUwKK4TKH9pmRS*A4Tb!ga;Z&u&5!s*3Ygj9I>0;a*kuSVyEZB_Z~6MpW$}R4}(I87yv`$nOTJ@E+LWZIHw^Lf@qbPy4>z% zEond0{`!1>y4qqtJM^gMrJ|sKc8V~l40}U%i+Yf#?S9yHzeYLk-1zD=^l<62#x-ni z7xZ)ZgCJITY05fv8HOT37afLD-S|hdBcd=CdYT^#L3`zO6i&zU*K6~Pvqz8~?AlJ@ zqvj&KKiv0h!!4ktfNb~SEt)`2@G&gU%ZxTT{&Kl#A-9r9lAh7oggK@0bH`C3PN$P` z=;iaYavAs&;eL>?RGqLkh2U>sA&p#^{mOM(0M{oDI?awu${jkK z2V7=T8V{gVtVig%@%Hm4dJ`PNpKU(j3fFq~gf}=i$T6Pg#{l=)soiS1-s7yjKKr7k`#3(CBNqQ5mac%iSD!$;!<-N2M1CLW^2T?+5`nVb zPw2JNPvjGb*yj9nGfbb#ZnxC~85HEOKL%G%X5+lB+6d(i?>R`dWOI~gp0D<^$;*HJ z0ZSTE_Y^A2U{z5`qYA*rSEcgY6Mb6jaxzcu`p~VqRnsL#Jw0Iz& z!NUbO>f{y34=f(sekMoN6v(@BwUpnYG;7{_6@zaIXCeL}h znwHjbju=YewMBV*apS`1_}x%RMY`+O!8doMMOvNe?@u=tD)DywJ@dJzOLg6>>{d(U zGRD`&RTJ%A9xpA{tD2d<;3KrR7fX_SVz=vA-NBoa*&-1MpNtl(jjs27>OAJl{rw^E z_xARj&!&v7cBsh2qJ_c6AC&sn?fMByRLdi$Jb$LaSM#pO%X@k_yhEZ07>%XBUhHRh zQH(l_Dap~PS8lAjtOuA7WDMWB^*z)qX&P7M@$kJC)0^Ygl3cfjD_gQ4o`ANJEYO8J)to!w%T>M3{?&j} z9jD}h=$AaIt=8lgPyDplJCNwhAnjq{Bv5jJlqP4sdEc2po=E|=-jqAGzd^S-NU3s! zKWAIJ;y4|J;PZ^s6~WN)VsEaQ(i|a9l;JovnBn#IxNOtmFddnk(;wM|0{?*jx)Udd zMv8MdX)u!^%V5nGk~p-Ek+aE#2=~L6$4|@Vky6-AS2vx(3E6>8wc&EM6IQzI;fN=ZjLKT!<6YvXSOX#8yS0@YG=0U74RO#yJ$) zegNYU^)}si5DP;`+9{jCx5qxOyNQ8;f!i_s?Er^hC3m+v3;S;LppmCn1r$jH%ys|6 z@Fp#&)Irs!{c(dXdr0ai^5qftXOHw%fATy=E$MQZp#3}W_3?NLNFOXh)$0SLjfiSN2;~K6TxodhM4_$GS-B5`RwV zg6O_U0mCmr*n0l2BRCwJ6E*sfH~t>7)f%&icz~Do=eN9B^Y`bY5{CAh?2Lz#)fN{$ zk5M)j;}PM;0lxdS7LR*+Pb54$RXABBjpMUV$ljSFBZ!~Q7uvgP2HU-!BdYJ0_x`-Z zjs6mD8+ZJTqIi1va-7G!(%7MD4Iwh$MhDOJh~$pD^>@Jgi$gWH&1~`PwjA%}^f>=E z3^cwZOaBo>Qr9P$^b{6sJqWRv@SRbg-($M!+O*{-zhSEOSGb+dB8Nbl(KQ{u$T+IS z!^hJ8!JyhBdl={32YF)7)@J^MN50O6hrEu7g|bP@rm>TxX&2i-$+NES+oRggAvUNTD5Z&WE!eSO@| zxZZ2C8_Y|5X>%xWyjyV?kLo)(iTiV2O(6ttHCy-YC-9-wY;_;iJUn{SZZdxjj+#O- z&m72OhM=~EW%-l?hs*v2SN$WkNPVXZyJ>Lo?&lT1C8cp2~M-l}_s4ZACB; z&wb|M@#q^#wl5L?_}!15-Ujiw&hZdk_=QX#VQVj*;G4dHo7*CR%gWoe96!}@_ZRFG zHW92uM#6+#q~)lz#81B;{8cTAm=jAMk*qRvr$eGeSR=GH!ub#>>fT{>44981u)K8Y zJXj(zH0$NgW^k5P+04Y<;^?weM9mZ1D@$nrigjLiSdky-#(K>{Sly}wzc9ex$eT{G zqk`}7^y3zRB;wu&d^UfYmP0r?I{JW!e=wX0xP7qv0U(?hT;Y-nlj{pl?6vfEz3YCc zz;^=jxFlc*QcJTYCnx8K0)6*k0=H_dmQ~$`EN~9{#&{jj=bjA(i`4>le*H>I!*`?s z_GX60h}C}^A5(zr0Lb2+(zJWIp30~sCndQKQEguaC@2@WivQWtv81-!G8{T?yDIX9 z>X1jrpQC~rTn54VO#OpnQ-ofhyN-Z^N)$O6VnlAjcff7ixl*30G5gyi6>L365^wp= z&9a6jtgY^tw}F$zO5W{AmR*kPr*W~PtT#DEb+D(gWt(lK?ajp+h9vHfhfqJXVS)Sm z`|ro?&)yGj&nYP>PvfdhVT#qN2V^08feTLMOr|8%^e}b$Zl;4ndU@kLBZ7vp=xB!; z`1%JIUfb8&C4>0R%(SDkALD(jiWgcT`W|3q2ps(tM}_7izz_I79Q;(&*S?#Fcd)B7 zmr`d~;wJvlRzv)A&HFfVfc6)(~st-pG_H@bz zoA|RYk4~diJ$~Y-7hCUlVzH^mWROyZ#~bt`-qg&D@}otFes?e^XkxFFHmIjz@rUhR zRX^csvr~z^c2lGq;Ax>_I&IkMy{HNLaWBK${VaTU*a#2nUyv9n)EKwaOx?Uphx6di z&$>LqP_VoQoNQMQ+r$$JoST6dy07OJrlEn6x~Vh zqP9L~UUZ-U@IeuaQy*j|P!q&WANufOwSSY_6m0LwDMk|r;{e`P?wT+t((X0_P*y#! z`E45CxP{}g3DPB~X=qdkT+5UfT?x-hYog_M5Qu(cuBzijPj{$XK&%q-Ayc~C){ok{ z;?-zUxEui|>P4*H6rv^x6Cj{%s*YGNrk=uFDh-a!9VB#m6J|p-=1rY&0{RG9M=(CO z18D&z3bI7JqNQkoQz{+YyB@F`LMg43Lho#X{EZLi!6p)&vS1!TTMcSdCn2c*Y);LE zFH~QBQl+y`f-Q*3O)DIH17F`-&b?nYVRkhdvopNzxZ=4(1f6Ba$YKD+SI*rUZ*R}< zc+B4m6@lHK^sb@_IL?X+_Cit4#}el>ytf7^jhxPSsowf(hF(#45Aw$pXke2yG~@t} zGr1Dcp#(;8J-OtBIAQMU5x-IALJQTez7Ub&EM{{IZBJ==fDb@F2LURQ8lB-_zxfw#p zD7xt%Y6>G+rioY0m|mhjOc5QUmelK&h&1w(1&M-QYC-I7>#weSLMY8@w55n~ooF8I z4)NYd3u?>kRzt%y5Gg?>l|Jup^g7MiryxUOvO#YBe$B&XzP#hrCFs2}!{hAn*Nk$~ zrWNv|HCp{zi`=9`|6#Y1f*@noqR0%DQ^LKc9DO=5{4^N+r?mRsiJ|7}x$v#9_eT>b-38l#*}^{f&^|%Z-<_?tfOv9q`)gYFC#VMnj;Cv<*9_JXk3!wP~46OV4&bfwYsmOB1WU}YT98!Ze2e;CCwMQ z(l2$&OML`oB^2edkjoy+UO&sgtsg`a`B39?*t9=lRg0Euw!yOnd_elm0rpVIxjn+N zAK2(INq8?(y1KLQh>DiWd0hlEJC<&V<^lJy;-#tgfT~gSy9L+NsY|vJKD7=Fg*xUm z!&djK0i9lhy2Xqp^+?_^Hl@pnnfR#m*<#1teV*bR_P|rmSlLe=$L_{>kA~SjB6JMe z>RQ&G%=u9|G4ZxA4CAhfhI-@;D@h=qfLqaIuHebB%}B+!Td?c9g# zI2ki6+EMdC@CwV5CyXseJ+nfwd1^tOo!lskIaT_>Sbk|@Gf~gGZ|Id{-5mj!kBZA} zZzMItb<1wh`(T3P%LWi))64z_A>mNE<;C9Yk|ukB51MXi|)*+vD@l$B(;- za;d>0wO_~_=-#}JPhH4KynEx-wrJD_b>Fb@>c-NzgxsI{T_6y=wJ*5Q#Yjcm&8esI z$d+{Lzx#pa=mW@}ud4d85+`0p5U|N9{0WNOitbc&r{pmRTgP#+)(qAcbO19^W>@TX zZpyAvbw5|Z33MrRU`Ob!Ix7o_pVLl`wQDx3{Hpy4SK!2%+$i7dafFdS7bUG(i(JbDqq# zweTiidw%y7q!5$HyuY~Xxf6_Bx{RM^d9Yi3sTf5>0~P@J_t6E1v{lI>}G;E0~lSPUTiX@YZThiUu6kR6)?24g zNXM)Bc%NUZD}=7ON8xm)*sfnPmszKxg>kK!=Xe;PFvLz<_bb(@$X(E;WTm-FSvOm5 zRQ%Y-pI#$GByX#1-hAo<7vzK*17_QKmfQHT4;8pO?8Q7zCYxNGFN|MBJNovcb^D`n zg@l6g9&B(j`>B0vz{^v~0eSQk0xki~W$c^ha6-o~YOL-$>*>AXl5+3Myu_V{udSmJ zT`QF4ShBvVC4$N-n^dan?7w) zW3PdX{^BZg3-e;$xmWxzyu`^)M>87#ma-}8#Gz9^fy>MCdF%BP-t`|NO{`Mb&8^9v z_KdtZ7j2#S3ga6@Q&UrWzTGgtO;^jk$9em6?|z2Ik~k6q0=zO2O?m>!>_9awIm{6f z$C~S|ikcu$=(|+KdHcI=(b%Po)A?GP*C-TnDTXZPzwq0n7uiG7m3 z?n%z~a?$<9AAi>I?)^~dFBcnW9g}Ru1<&upQ)*KNr?IW7Y1xO$pW8!8T9$8l`*k_5 zhB0cS%Mb;3UBD9oaKap%?q8Eo96m&6L6D+_N=u=2m=d%oB!`9AH{(rK{Qf3sI7RMs znzT?%7D-cI&Pq`BJ6gn`Tr?T*gtQej2&4ZoKkTIt%h-dR4CI4B0od~MjvDqwb{R-k zj2W`V&T!2gd~+FRXusG>yEtJdWdn)$yf>7h<8H~Ka)M=@Drni|fVfOQv4g0`)|Kbl zb8vcKcsOoI-Q;?1*+vfNPV!Z<5=u-HNR9hLMTaD z^RKCGTAgNPHkj{>czo`x35-D3Y%)iT4;LyViS{9Gu_lyx_k4tGA9?0x38;ev zLiYOh$y9?9DzqRfxQ1|q`UF?Hx6|1-Tsc=S(e(VGB5XUmZz*z6+yVO1fP;3nn@pJT z@PsaOfxa`dX?hP3W*YnbV)cGa%-!ZhG>~C6_fGv|c%y?b$P7hxN{@|wykQw%yBbe@ zeOL-0;1uR5MFlc#=ik5I`(dU7K4no1w%W`p91%+s(pH6DzycPr17+SQx4r!@tYKF%s zdsdXVr@4N=Ba16&Jf#fR7QDT@Y$St@bRBSs=yxD%jb4j%cTg$n$(vr0Oy_c+w6S+y4|7h8icWs z5>nvezTU8m39MmBpZE~H*z89W4BXg)1o-{t^#1xKlZ#uB#Ghd?BMn_Lw0o`1gJk0Q_P7H5 zKplINi^FX+zO_fmjBIFbL$~fM0Y^6&enr8r&`!v+iv^j@YWi2{OVe$j6ys*3J|)59 zQzGaVf$U(3(Cm!Xy7whCMN9E|H@_N5ig*G;vsUV4qN>TbJR;kA-Tz0__ zCb=#JqVT_foxvPjf&4o}ocUCA=n=_tT`;@MJJ$&8htH#od&LSZRt zT=g~rL+veo708>kAXTWpC zrSyYnH@2Ru;B4_*NVT?zE~e1LD&51S*n+N4_id<|0=s_v4DbV>wR_YKkJfvVR04Z$2vd^g*OHRSRISx5i&(pf zh(tXRQbM@g?700v(MNtbli2{M3)UFEkOC0WnKVh6&S;M5_=q&G7jq7^Yl|#gFD#ei z^UwwJbLVuaEcMr=yb~SAnj^+lid6h?0$28Z0yo0_steIhV>O&=V*Ab1upEF5hF`HQ z&00iPhk8~>UoP3czS^pj*%@G8ivKY%#AICbB60-^IYx}=nIj>a6cxB&V1X~x zZwlX?b&&Iqz>KK^2q2gwkaUVTxQ@c(*noj<@@f6pjo4>uzhBBzxg-hrSme)aO};8) z`HTm?oI+|^qgJuju{RieL}`#*Is&pNo)bZVO+nVWYIho zLXAqrXRko30E8vIf=MF*}6NQ~vgIC4*rTVDKhX;f(+CnI5`R~oC_bP!hv|qPcf2X$5!GP4eRXj*lN}qHtPa2 zW;jF8-8cJ_o`T zY>Le~pQf;(@ZDr;hrw8ohk6>Wr9OBDiJs54aCq<3e6d_^zTHjK{b}xewgP9z0Cx|v z?H^+48mt$y&A((^seGZKX*r$KBEDfN@A}r0_~Riqim3{;{o8*N`jM$s|6BKMtOoFe zv3EXiIZ1{#tUTV2ft+&0bGyej?K<_AIQUa7E~h`+iACBSzAFolVU@y*IKb+OZ+x5I$E}7c*ki-m-;f-Po|pC?Pf9zgEHOoYA`gWKOiIx-s^I!_X`g`A7L4X z&DxHWo)wBvLpZt1y2}MPmvx&d>+z5b34&vxO^aTu=P8l+xIdjpO)!x5z%gOTyr(S` zFLbMvH!^q6f-cX(CqzKba+M@2uW_B|XHIIgDlFV4xw^1}v{nw>bN24wBdvVqvv4;C z69uuEW1+)1fhPzj3v4np$?(y3UToS=Ll`YNlGF@f5dGcQ@uOgOFPWt>1iyMm(n28b zS{?3bQ;evXy{xl55^->zolkm z_>8Z*Q3#+IT*%IX2$Lk&MeNlLAy0E;}3*+Enj?cH}9ZT(TOp*FUnpTH(R@&5R zW!i3UZu>euuSg~`>5+Ilw}hM55XIPfRib3oBJD?#T(AVPpY60JQPWx;%(0Pkv}{_e zULKUj0ncgGQ`M9io?nk}h@f=&sd8&J>A;kFYrj3CJW_w9E-gi+sDmvEGL7u|;gx4R z{#NYY5x4Awb|V@!t5{{|wc2GcE|QV0J5RE>MLJsB0^KHPIBRC9`mj_+kqQc~AGi`` zvTDMQf`E2sEvz1A1f8VBCS1zXpwKC{DB0LP4jVk(97ZUmvfKD?)7^-tHLyuS!n%8ZqpFXwz^^`4t?^KWmwqM&QX@oHe zMcQ;BxUa^ah!n#==}CW@Mqc8@7s^sfU&d{;efpM4A-3^;a!H5+xbcxW8*&u{IU;02 z1cW>aes*j6E`Xe6%m-n(hT3ku!jw;`BMo6Xjzn+1w7TZ+_0w(Ld`$CaTu%FtVa}nyp2P0ru|(! zscDfqTrPXgkAAeO?CE-bjj|w(#iXfYMc{gIk38q8Q~D+gu&t4&&8B%5N`|xgE7UI& z?aws#{+8G7E^Aov?-uoB@-9G*^;&b=Uvs+W0|CTwci-+T$_1CoXG_!!^*_%x`N(s6 z^qkngI;-qTz%48^fMYR$obGK`q8`;GX9(3LL6@Tf*fC91*W&}PFd7d{%KUZlqX$_f zkgTB4=CGxqQLXxQx2W%MF^zK;(p%6 zT>-daoZ)IdF>=%+4Sxyl`kba9-LFzw55JCI!q;GADho0Y^ET|6Vw<*u;F)}~uP1Jo z+Z$q${OCiS?dG0XopD{?ql#3ua*hH0omaGNa{hIo1@Kqehyd$x=}fGsFTE$PY#G}X z>^J7|LT@#d8CR{Uz@aTKbMzo6T31?lA@#}=srouscJ2$~OY!CQo=llB^~tsbEyA!%G8B&PPR1eH6PwqZbCesb?L#~F6weHkX-lqMAjAdcDdOs zkLS)k%xAF!;FEKk8|Z6?{}YUMYzcJ?3PyvC)A>8zPv8Lxb+AnZZPZIqQfS5uL$OwZ zaB`W)y!+0F$M#j;U_lwJD>VJ$+`l3riIC#(*z)@^-d0r1$;WDrzhe(gX`xh=TYg!q z<<#4BN7z4#1-rw@xV~~wrPKTteZ3>;orlrCM+XLnMt0p~Pd6m$Qcbi8=>H?mx+kc! zLWSNh8crKnq}pwl9M5~OXtp#)NF#hHukaD>w7Ej|e*HIUHQ!K8xF9%2_l*JL4^}8> z_Yrvq;=kE|75F9wJY^7R90(ia9{-pO-W(QCK+s(2;=K&b73` z%AFq=HoM>a)k?~bkeX>J?!OU`0+?{EN?Qr&yKh**nETB=VN^@l%bx&)Y`n`$s2-bd z@=`*<&mXcgZD%Z>o0ttC2ngCedMXQZ*~2}BPH^M8etor6)^mWrc|!m0@i%Xw#NfhB zr3xUdhXWR+enu!;FFDJj@&9Y@tD~ywx_;pR(sBR+MH&HVP(u134bt68OLsRCf^;cx z=uYVdk?xX)1A=r(H{6Yn==1)6@A$?Y-#dnX95;Kfz1Ey-=9=@jW8VlzaSY}X#};}N zSCUWB&QiXpD=B6g9C{-+SWX(JPEkIem%d4WwWb;qkn~MtSg-YznY5o*@n7Zk z_0N;i;>!|jEbfSPBC>>vYs-sY$)yIq3;95mkjU%bLus0-8QVLb&{c=xY^tnURHSR@WUm00?9-dzs#`>k(CkI={xy&TQjOvKQ*W7!i zipG^5c7=|fkxkQt#+|jijV65lT?n6t9-^AhH)$7)$t2iQ^Mf8QU2+)%Hac4x&Pv+Z z@9WG1SYg6(?EZ#09AcOTo7cFrEE|dZrw#AWq)pk%5#9K#ldOSAknKRJ8rR_54Jg0p ztC$$(sFNCuM=e_SF+3>+x1j18crM-UWO&g<7|WG+opZAEw#Olygzq-)Fj;`OBXUf- z*y;gf?T?0YUv)P7s_NlfZkmkfJk}rj1;#zIX2489Y{RT!mBCUU+mz9=Dt4n2eN^0< zjUukktcq)n7jrn#9#9g(-WX*Xm@|DZ>(MmLJ5f-byLRncL;uf}gpxIB&i%0yvOdL` zEoFWXLBSTYl4e%NK9er{mq0?4%qKt;F02tK{u6{Q_2;MchaQmon1FMCMs~L4gx&YR z`AMQrpT4fn?L6R7DLQ`e30XP|)_B%LC{jwY?`p>GNHX?gZL^L>;{b=fR@8z%gF~@I zdKmTI#R)ofLD7gFJ*y_xggW|0p_>^Sk7-3`ANUYcKEC(!(;>qL3^mwaI$<~br$a@- z$bWX$9@*<-@*&FDdyR>LH6>Y1R=xr)x_a1SXDz2~RfyjATRaM1RyVxlKw!pu#_Iry z30yIWuMhF?0r4I8aRCh(cbe%P1VY8J9CtLh81=-YxaXU8!uLU&uzYlACi}V${CR!-aQv|)rc1l0$+TB& zX%2cB!%5jWyJ8ski4tvnK#jEX+B%Q#2o1p{lQS8Q(J+4mOPY5P-Co@el`1Hz-!zZq zV6AmZkQseRWB(itis74*xzhCcqdAB9gL5Rl)h4+1%w!NB^+zt0yOH2fzxlyEIbF|# zO}wKv^^aDxPl$0n+%d1KA8@_FJ~&>;n1*7W@suHgN?@T|&Rd~>$uCZ(1e%X=3(-TZ zB$z+OPu%uXIH-7uJ63BYUdkhc<+`P2_v9~&QNq9U5YE&QZl<>7)~H~&hXoxJ*(U`$ zS)~t2uEk8SqRo{_{k@yaM;vD@mjKqwPL@jL3ngKhx>hv5PK&D5pg>^g?*2o^ag_MhP7Tl;RKPyhEVK?pv> z-P2;48=FgFx|;e|pTF(9#l~75yqIlX2*`wBh2a+c%cFS%UVHl>*~HMq7^Od4N3Zls z?afu$>^`UV3g#03`6nc#I?@-s|7tJ5jD(b>)-nij@+;2>GM@%Jv(A`zG9S%fug0r;bfVu@m~ejvlEeGi^5-eT_WC9qOI3 z(w~j3%Qwovv6$GI_vZmNJCI7K96Tb2=^ph_U@9{clcfSdTga`p`SZ`G;}sd*`p)C$ z-~s5r_~?zeDB%s_OFrb{_`59tk;IMXLP!T&{-^{t8qj#(KnINANhKlK+K2x2I+vm+3sCQV1;BX{nv0XIpsH`w zWSh{?mT`2nYjo7%{H;sc!r;ESbWBcl9Nza;A*h$Hh*~LOcKzOaDe8)s^!gXN2ug(M7Ca<wj4vr>lOrrvMAzUn{No4(hhNIVKCm#MT+4SmwA`5-@|};KyOi}X_$6#>h}^B z#jzTy-Htggye745zk9iR5@Tm=0^QVmF;q9gZyz`oJb_@5mR6UgC*|dJR0u}!42-Cf1j^^P1ZO?u+!eCP&lq@^dC1uX1JP$)Q&&Q$ z#CTi9>waV%ny;b=>DDy0*$G;(q{b`}eyA&#HJPfn&`ZQvY8k1^;^~dIqAhsJ+0CK0 zqlFD=Zy62eTofzP>@LHPxak;>R@P0BMowQHTlc$Lc zp0_DAIBSP5ap%x&(_r;AU3{3OX<|NI3~yE7>m%XH$rvvtFC}xRd6|l}o5*#s6&9r~ zQsgj;u=%5SfMja1N^wRqaNeB3bFFc1pH}(%Iy%pzBfZ_V%NXKit5>_QS`lA!yS<jhqXUiS-Geg@2FHoaB26QnX@C7*Fq(b4m;*b@k#DjnQ?2PI;Q>1lzx8z2bTzE@!Lyw2!y`s$xkqE$tGd7IQ*U(0J%? z!9J$P4IMASF>o!LYBa8h?>Q53cwAje;%tC`Pw~>5bq%|vOXiOT1L1mCG|V8v{0nYG@Dy*iYB^TJ*>(aVf9mOCE98qfH6 z@Z4TSH_Kb4N;%dKk*d0G|Io2(Y_0O3A?Y3I?)F5!a=LlnOD({q>_p;0ruxSx1-yI+ zLtqJM(Sysh;naoRdqkY# z?QoZWt+i0SIk4Q?nL=c@#f@Q&8{5I-d}v?tYLm|8DobUI+Dy;}I%0Ug$i4H-Mdw0E zB>)i(Gv|Y)%V-UUE!M-*&lUGGdf913w-4gq)4S5=GXw&ykCC!z)|oF0TVx~M8b<<_ z&v9P|NnJfEL%uUXG(KY44w1M&&pvgQO(9$*U3s`oZCh;}>dU1lS~0wE)|;r@P>*EA0zZ->mYtGDw+-5^KNpV1rOC8z3!30<`BoYn@vBySiI~$hNb864v`#K zz8MzP0dpTmq$22mH)97g`Bsl7lGON8B#5DLr-Z!$B(^R3y za>KtfZkjAXt5H2BrF&YWQq%b;!`IV#n}EMjWP?#&X#^-4DQv`4s5Rp+f1>P(C{BZA5GI4uQ#- z3hb0%7sthVs{t94D5&(5^6yV~HFBitHR%KEh^OYiOLDf_Sa==TFqILF%zSqhJNmjU zSUZ?SvhbtnsFGN2^#ucMasBb~Hr27U#rjs)PO)Vc;bln6VA2J_VRfciX$c)&4u)gf zYU<^7haOigX2m{x$yUVNK}2t~XjMf0KEpdS)58S@L==gu1;NO_zO*1ZtMMnI?xzWmT+^sL5g!>XVS?y%oqS%Lo46*u}&ZRII)s#kUC zX(9X+xL)EOBcg|d&2|ke?2^$0Ww*X$fNwM9>dd)apWdh*-03&(lux@?1&4~`WE7rAlKABn0g1Jt?jC!vRN*fNOKI!RVW83vxxgEcW{iuFnuJV$LI4W{j|$oO6ERnRjS;!`35;bI^x0~$()zMp@t(6ni-E( zVrZGCV%p&0st%2cO1{iuvR7Lq#J# z1Kuq^Jv4>JK%>2l5n}ev1+g6%$4+lJtF;vb(-N0zWp&DCe~4SZm+q0&?s@oVywqIj zs9&_CDvZVTkU?OuE8{78nlD|xJ!U9laEx~8Y$~9TIdf-Px__teAh4cprMqi7GK1>eYQWYP51a17AhvNb5 zGQ9ZQN(rYSA*e8>&|j(lZCyBwG{XPK{0J6X*6h#C%KMd}C{|p7=kfF*PyC>P#L2q{ zfiG}geB}zwDzH6xjW+4dMm5Gx!Esn?{gJ-6C!bHpmcez~aROU)_H2p3USax&gE+}- zODv6N6VNuxVauf?cYx;u>B^dH&e5FybfYzjH?^EnM}{Rp!`18?(HVv2=T;LnS|GCr zfL}lT&vPt95y&Tp;qsPxbgbUmZnp}V1Um;#^`I1Wy6mOsvg+E!QL^f*lu)sP?XSs<@Y_o!MZ+wYV>Exwct5=?*n&Ti6 zK-4x#R@=|G>bN#D@Vz$B@21{RY+0~iL+g?D4nHM98SLW%T1I@o8gMogP$LYcuBW)m zCZPm-(`}~bON-R556GT;4W}Oq-;BKOM7S!Qp&A#aLt^CsD+jzGvp4XvvFJ2seO`C? za!15i{`t+I{&VOVI7{+o;B9SMzBkJW4^35Tc>Z;(a_4;#+p~6MW92{rCIN1bS7uvy zbYm&so+Aj*Jtl(PEb|Xa8cIJ*-MMrg^xAstpH7N+`NgofCH!JDeB02Qc4C`3KlBhKx z4R%~Nt+L>ICb5*&%NLj@sD;}_DwQ3I*?#`4Y+Wj5yRRmStBVWs%yk{0aYHyxWvf|? zB^HUb4AG4>mcM?zJglp7Z(x{&?rF^rUEL>^IUAdmj=IZ@yE`h$9`@IsX9cLWIxfsV zB=IRk0OwWV$rRR~04l^=VT87q3EepePn{t4NZ~Ac{{FzVQL}AKRnAS-Im=!+cXC`NsE^{3)K>nc zF|XlxIvrotJrg2%gz}+Q4<<9D2&#UnwYC3BE$9J!np<^ov10$afg5dzdA-Y#Gnb{VrJn?-T9#5g=XIkUv)H3>#s6SN9;ik%R?I~@R%gQyGobFuY@7<<^G zus3*5Em=+@(MDSj4kE9pPgN?+GXcf&cs+Ef;t$4=p5W`Pt4%%D58rAr!e~&zl#HT#im)bBenAi-}>(L|C9zS0I}wd zUdz8B(BM$ppukA8R`4``$_IY^U;;f@W5Q?ndt48k1wm#}0YCqLXjtKJASp%AI571b zmCf{5+UMdwayjzkazMsP>D({}Kxae!*?fPDFB75)|DF8K-|>e4>8v&C4`}L+(_N?w z&FGgC!(Gf2Uq>gF4UENVq#VWWsI?#n$)(NXsu)lBm?djTZsI_)sxAoX4};5eIZCTk zF3|ZFw`J)_uzTX`zcW{N&b}b=*ZIB8Q29(^1JS~N9+UvrQN#h|Kt@6MN4jo*K(=mU zr!aQ0KN|Sg2;RVMK%hv6ztbbXPU3)^8-Ts^ALlRs9rl)7|E2B!P9k{oI||wSI*l~> zfoRXIt)LL4Q$m247O9JtBYP{&w_uD$&lCFr#3THWtUVoaB-f=zrz9>w?p~onpgCDt z@(G;}xRGyN2p=%&g7)-lmM_r?Iyx|Cc5Wf9mkrx&05FtEG2#MG4!2b?XUc>$&_>dQ zvan5PH63a`61=XEO8@%vFBpI()Gwq<7f%NN5pOmIQ0TdIBB=4r6{`&gFcr)=xVcr# z<8f=f+dyVPB2~WmtzxPhOm6%?^}+0|KEUKd{x?G8Ei~MNwd7Z7|Hi(AfNUrMbH#Z` z^zpA50#Zf1fRc85_6GAmRY&NpmLIZv{U4g@Cy*rH>3+xlJGA=80sz%057umi27Ue* zqTBq_muNVoIvU*nOOqv}gwGhB%vUbxcyTZ}$VE<`X4o57N*YfIgaK@hpz{t-_#}bb z1+a;No*Th<4Z0#+2i!-qLuCh{nu9VJ_xyCMe$Kz2GXILs0hE{W1I@|ozS*Bsf3{)? zL};W@mna&?%zN$50X>-)yZL8H+v7zuKe`panBH7prMO?(9MoFt0KjFCD}YuLDdcXy zbZ!Ky36fw=!LHLs?joiiK753iOJ%)fWo5kib99oyQY-y#2+FNu7=epz1=^N8f?#uw z$33X1?|mB;tWd}f+vECI*v)JFlmm~66Z%CCUIOH!5CSy*GomT<+oik{UH~9G(7X8u zIvlS;xwd9&%Y~5TEmf3NBsOLT+lrf35-KX#9`fGI+Nl(bg&RimBB|Ws6lKM##&m9h z+>SQb_YQ&2R~%c+G+*q@d<^aN0T@Fe1-zn+cj{LW0hP;9KOrIi#7Z8X}R{-|xMm+t3cYirwg zZ}~RZ#-;&wmrdr@uY+PjDj%RWnx26H0^ZP^K%T7xT4GjRlzLbO?#U~#tVD_=LucjhTwKewSQo!}Z34 zmTdBm^h=qeI-XEdNgTQQ=@1eZxS5%mDR->pdj#I6YHzCUCJVPcZ zWVz?*AG2k< z+<7m(JDMiP*r4m4+-iYpkp|GwujTiW1wn8*#bbbrQKLeRqiw0PuF}lw=E?~`)3>}p zXRZUYpIsaXT%S@d=K;_$$*vpkemtmt8q*qh-wQz6(J?&VqZGSO%1?wdJJtOnam;aR zvJ`-6P^)VG`87nN(lM*K4VvIN=kPhO#Y@Li-85)5DNm940ZmB|moMaIWwYdDYiemL zsTtVWi?XRrT!@QaNJ;;=D3Css7h@HvG0r;~LN4{HHDNJ1**ebT_W3>dg!Z!%OXtVi zo2D5kZ%BC!tmB#P--ccqaWpK0tHJg$k1@$li3V9d=4fs3u@f`pyzg_)T@E_Rzbhj6K14Xw8eN1)>~eE!%DCTKGf zSL8nWrieY|`)r?WWp}<=zw`Lpw+hR-dh!i~*c>Pab}v2OlIRn|4~i%#w2UpLV|sa< zH#d@=hUMK2PP;H*2avHLVci=c>|xdKAj_o|&?Tl>av(Et&9&=N$3Tuh1G=B9cPEo$ zKl#JT?AqR9J>sq?EbP14SAPfnGRDXkN48)L^y~%RC+$!aiyzRg;AR|HL=VQgJ`p$z zc0Jj#`}&?nJkT&Js|&-gcXz7XxZbh$vlvmY%WBHaNC@-Sg&ZXGvU0mR`)%Ly7w@L_ z4SVHttydzr)-$8ugagnr3A$caco;m(4umYJUS{=eo%&z)mwT1r#aUKDfU-g+z)M~7 zh5FqlZ@fdiRfaZv2j1UTUom|&jKfy&L{qtuf)FuEMtOY|Oko8ygTJ=k?*ajFW~7oUDk#9y{TjpL{rryXZuceD(9;!AgTm6dA0-Mt^g>h`IKV6uc88xeN3q z9B)(LzF5ghdjE9E1rDB3L11&Dr1RARe;_Hb&8l|2L#wHh@Xw{#h}SWQt}g1e_5?rD zW~$8S<17*;t}k@o&g9*8i>(IJ7q=nYyDkPWF`4GkYw^$lO=&|8=v0C&qL^EYt%aEE zS=UtWQ+J*(hKl`NM^h+O;NsO2k7L`{-PxguO2I!zW%)m`+EIVyii&POg62<@W^{g) zTV4ABpfARMtDXH0V0(DY9Zj^*CVzaV9Y@5&)NR~&|6QtM_X2bO02-FIQPH zQ+p7wO_DhRICi$GJs=8$OujVl2g3)FPV5y&Wm!_9P%`dbYy)Q0LmvcW$tFi$GLQ3h znx0}hIy%oAkMCXb8FHT{Sg}CbF5~4N%wiOD^o(hhAM^w0ed})r2?5vX6elQ-hPp|$ zefS@zfsnz6!-DwDC>;SaHAX+(TU`{zwpRsAZ%%f!^Ald*$DZ}oqN0;pb1)C5Q`Te8 z*(h9vi2tNOs{Vp-HKDtBf{;%TB`e7eSaua?6xMgb_@1D5k?xcGv=5%hhxrnzgyB_0 zwZQn?Voi1Og&8n?HGdv#mNbjAdtQqiB$)1*fxV%gb4Ms9@<^2zZ+mYmZA)nYFzq|o zT9Y6|w0(3i-Zs~B-FgBSjeF|)Y(Z~kouOVWN_nZ5s{4;J?)gl01?KYle+DnVi7J&|vwreo3_3=H4?l9rNWIu}LP^I@@i>l{#-jae3_O5Zfgp?7GJk=_7KLhCPyBin>%#igq_khqv z^eguOAh5S7xE1$)=xE4NLqB9Nvnk2wN|@Z7TA=Uk?QJ9^z~fPPzxv~cGrOEhc4pAj z{9b+;a+#Fja&y<&Q{tX*(w=e!^+$`ltJB|$oa_yCBZBuMDaH8Q&?}9u&K-=(-yVKs z-Dd;C{Bp<|B7*mp!uemqI^-@g`2d?212~{!s0{W09OnI$fy160QJBb*iq~t&>JjHP zELSh$;93spA^Rp|!7OokQ!+;U?BBgsM3NvHL) z-t7LwlY4qrKe|8Px)@@&Ug^8RwA&4q%7x<%k{+U3=qdFJr>lBG_(B0XV5Pij%YD%3 zc6{dedMe z(!wBxwMmOF>6`FX9{}Lt&Cdn$u6ur~=>UJeYsM^Gq)`EsIlQGzIHTLGt?-FJ?Q1wQ zLZMbJmrms1=c^VpaFv&4=n~p$%6sMJ`+0S>SuEPfXK@z|rS5@90;H*-tN^dw^BFU& z+}S3N#0N;pk;(8ml?|x5=2)^^b-o$et|XIH7MeK&i&2UTQAwL+RA}M^ehf^Xb(<|4 zb3Tin29Ky&v1C38c^pqcp_?5{ljV<2K=?s)7A477R(tB5A+U}YIIr>JUwmC1x^?7I zsl6-4xuGBN=eETP6ld52^wm+0Hz#Ddbf+H8*2`MGeUs&mKXRF0P|{(1!vwC3*RsufFz55pb`X(P$8F}#pmdm`ekU}uhXrUpS`|-Yo^*OUH(jZd zn1K`AP*L#!$+-{!oJ4~TY*$@-xt$z87IWx?pS78*c#mrt_EzF+7CCK9I^@Yyaghy# zr@X2Bf~4NTlJIR=SeJMfS=n9?K~r?u+FaBMUj;JnC@>ueO%RO$)#o#%pC6jC4-QVr z_VswkMET}|=HeWe8$UgL^4upV*Xlt?VM+L4@|62QfAzeLn0-SN%#eZLIR`g>rx-WT zoXZoO6N-91{NZ#S$E+8F1gepd-K6Ic4a=5J+-X`(KCJ_wLlTQ;cxuy^hXRG#r6#E% zsi7dzp_!oF5kvr99{;p;yr`d<>VkL0Vq%;euu>^)P-cNk>$RWXTlXUSvG`9H01u?{ zo99v}XS@5_M@tgQ`qJ4qvvxUgiTapr*;HCGf#m#gt|xKz`k9w&!!6g>2l|k@CZk2% zZ~^|#=eL%8+n+%6Sr|Uh2gR)K?e%)W4~9q2RQn@p8uP;l|w*bolhkO-yMHnc+d`_~0ReSF51_oyb_CzqI%v zF9oF92n%=_k6Sp$R$-;A>y9UDzgU1fQzbpO(^O*yKhxUU zx_XjAYF9X+Q^U~|Z0vk}##dT>+JiBJ9lx~m@?_S{3L{QzBy?Znd;GL=*9q&0F;P5F9c{Dg)>Ul_UeVoYu&4d~(fX+q-mc<6B7f_Bm{N6WB~U z=z{{JE;jpL0(Naihn!%Z^&I{gO_cb~E(cwS;k^6SHlB_RYCNe;@&cYrk6AaD0PMRf zrzJ^$o0D_^V!3&*<8k}*Z-ct?Pj^DMgKES1&L@Y4B35{i??~Inr~ni zqxC=ceGq1fF9WXps!=SfeQcBA`c#s8wTG^Yci4?Q9Zi;N)aPS~Q{GeJ) zLra}cGx&;GOW2V%=X`#8+@~&MV!DNe5B;V_vDB=a7yZ&IyJuN5vN`&QNMp3>jVwM0 zd0p<6j%pq7P)8JMi~#|QacEd*yHe6af)v;Q=3|a;*WO?!@99$gi$jD(${f~szG>W1 z&-C3;0E5W*d>+0I>!h6_UOK_XU9#zEG(K+c>FWBVp~=U8Lx}Kw zk6j2I&{SfTjW-wj4^E6jwOK0!Uc5Lu8+at6JcHNV*mp(n(Hr_k{@|fg3jZO$83GZ+ z8`6u2co-*N58zfIq8N}{A7x8F=CsrrtIi*v*@$_F1zhbDgFsTk@v~0a8_s6{|6!hzp7OqP60c2JJ71B<4#!GN zXL{g`^G4ozr?uuRMm!~iOdFmQN4nZkL&h5-h2$5)Kp?VZ=7hi;|G8->4+L_}QBi5W9pKl+s0ZMoXof^lT(Tio z_yYB6*eo8f?9Z=#GQCXD*U*dWfxfXLTtQ2rkliCm3TjKUKsjkJ&v2lUFV*A+;3+I!BxVZB?#q0X4t1IYZvf|Vc zaIFn-QOy1_HH>sZV*Xhn4XZ;x6~LSe#Kq1l0$umvrp@|i|8VFvB45a}%_o0WNe3)} zE_OubA1VMdVgMM*&{G)wiz>H^%0od}%Tto%`NhB6m6w6RL*-A)Wq?if4<#}w$Wn6@ zXt)2NGw@*7OMsi{ecTdvl!^%U_D+_}s)+tWZgha$|DDKxDe}K)kF2TJ+8W{$k4gNN zHAn|YUtd2~a=EQLIwQ6xic0Eb`mvRd!8E$R2Ztu@YsALb>0*)JB4iQ{adJ8t$&ykn zRs({neU!~5ZSAF{J=s0hZU04?8Ad?!Xa;R7{1#guNAy>1hp|hM!!#tJ{gK0u`KAwF zL9B2adheum`pMA2b}|eG(}090!6t*Vb3%Fe%-(x@!*GlU$bUHJgCK`?c5}GVo^Ihj3YGL92u* zJ#b?vJI4gtNw)Ft8RoB{g*4LT$I_Il-rhcbov{C87Wc>?ZsFWiHq zH4JAE4$;@$NZHc!X8Mf@p7@s$dhWyQzwR z=Gk8BxNEyx$+M!xV-KM}tt1F{X-$_??WL9RGC-7#dXBp{6@lzf7wx#Ri=b;YA?RzwstUw+cC$|% za-RHLdGr2*Ax-0L$v<`l(FhMbWK3oIXN$BVWp@ncRB<=-8tx=}>#S^;JpMhrH^dPz zQ;t*$uSk)cJPz-3uQjJw@iK%~YUGUR#@Bg0$IZTpSVtR4iNJ`p# znO@Ak>Xx=DC3IO&C49{|%j9uVsdgWk_LxszcOa4tFnZ|eNv`XGmc?k9DI zl6sNm`ir{3?M-f{(f9Pa0-gqgE^ax;Q+Lb&oDn%6r2B63=j($7>$CbV$)17`?*ti; ztx*CsaY%USfx{dD^tzH~9RnVR0s4*7<4z%fO+vmjAh`DNa5{SSC?H8KB-zFj-0dmA z*OPw@gXiPN6c7B+I5mMZHr;wi0v+R14m{lcB;oFZ)E!ZotANl`g{Sdy73-Y0w zd?_QQ^kTYo)Kj3I_0S)&P=E2M*0C|OZAEV)DT(hL{cmwzb2z{)i zjn(7oi6Tu^snsJ6^C{bt@uKDufl9%Dv{916!p)||EB+d&#Odv4e;_fcR-QB&oJH_4I3KWOpP#lW8yA-FmySuyFx14kCy}kGOf4lGd zY&OXxnaoTwnfyZK<;3A&v0(uK0GyJk!$5IQ1A-E(=;jg8;}uz1&s0`YF|lZHR~U5pk1>Y*c=OoJ>w>W5}D& zo>4o508Bu2@5&epVCE#8kw8D-$K{kvYZnXMiyZPocsn?fP7~-x*TXgrH7cogSTp4_ zK6syMvY17lnaLQU(sYf*DL|97dtUICxpTjkn3{{3+>On4BcmY^!DOo5(7zDZ9j}hJ zi4a>GA`wytB4A%tIi?qyk`ngaErH*H#MQ*>UPZd$2HW23)}?0 z{Pa(oH%CTM5GnSdh)P880~iz{0;M4Kfjr!7rE3&m*7&l#W$?>g2KDPbJq? zlTI4)G97QD+7AM!^Pw_~^f?52gF_J?6lP#OY1Z$05gdOtiz3TC`$b9c(RmSKWK$B# zCveN5OKuve@RU=GTtzEH_MZIYRCsx9?4YDjA50a*r5KCG!%WSD!YGg;ppV@Z?Je5u z8LGL@Rw#BmZzIID69njkKv5#Zw(?Wj-#za)BdhA!Fe2}nEK2TVRa#ZxV;!}K%}e%$V~6wQqN!tfRR9Fu9& zSEMIc7T`GGFT3z|_5gCr-R&>q#t#%rj3~i5DI_vj2vHAV4y}#f)4Hx32hZcL-H00d zXn&seCzK4tMQ8+=Ihd2crMRA;Y9fUwh!c7K!VW%j{#mGouRqEg`A^|yvYAGE;QYW& zLH9P++wDx$1{|KYR8@mmMV@T2p6!AkUnu2J|?bS8=7M`D2&W>Z|=l<{W7Fb_JE52FZ8hR8a{*PY|-V@SWt zr(BP=w$Rh3a>C)nv*l-gz&YRQ(vs&kH--nUpZDo`g55p0Y4^8GbXLG90XpTE2P$|q zeCYEL!QjuCd<>Agoshd*AD8_w&cXZ%QANITdiQtt5PgQF6qVI@NN&QGt!wCPYYqTLq7$m*+Jt+e>neiZ{-D}8>P14d~g0t z^$Ixa>p(IC7yiooE3T6yg5(QgK5`gzUf@Zfa3J%!#vx%V338}R4(kLSU$|rs&F05V zVs_T3AEN2vw7Ij2 zWAbN;YlTHcDMjvuq9vg-yTu7bCo0?(w*n`*&GpUcTxz_cI@#^Y-ocMBH%o`MW-LF@ zMxqmChH`21RHo!6PN(?H7+7#w-LQosIkC~Oow3zebSFPKNpl7B#pnvq=MiM-<+z$F zvXrqj{qP!D_<{B#nAO(e){L=M%v{uB$AWfZvB+8QD1}XONs3!utFTSLv*h;EiTK9| zgVA*+@Q8;9>j>ruc)XmbI{42N(gh->4RR%N4dwOa;Wl4SSyo_Ia+^iOuQT`bb8He}dTdrNTd%@*48UH= z9Kx1tQ8}?5AvkB3zMq6^NSC3(7uS__0_!{k}M5`g0p}NF9nl&vAEiz5o+TsQelQPqh z8oJ8zs&-4^nv1#=LnU*^kwmkh`jPq-ea{8>I>)-9+LFqaFVTxF3*jg1^9_-N5$Bjh zSPGn*`2o#f{X}coeH;EHQ;f@1<5jB{I}+hBgCuGZJnWaLaV1JoQ)~s?xwYa!QB=4y z9EWu^={22AT@Q<9i~Cs7*sS%DpnZ3BR5-Wh`qfqkyOj z*a_GRSZ2BCd+EEczYx}BMP&WRI@OL@i`<8Q})1%UZ!b&FiGPf5324 zIbV5M8NA`LQGMQ5wLd>zRnoT_SsC$NWLo^Igzk3nfTzN&f>KVD2uf~l4s}juPefP> zmLpvv689$;)8iJ7w?<=o*G=n9w2Kj#>%jQH!@#>xkWgxrrT$1MPa5- zoJ8)P4rBM77nXglKO6H=^OlqKc?}i=KcK*o@5=CSh4O{+9|%wpu`)WF9%A^AcEqm5 z^+{`G&eE>K&cf1i=SvFT}@U%f%7?otx48`o;J4r^F>;sOe;2zn+Kmie1-K%@gGARHp?*Qa$FsT zs^O@q9~Lty7_rGP;xqk~7S;62v1C_a&v@*qY`nxisNjbKZ#daTuGeV9#Lr*%$HO~}&<)w*inTS%-t zS*AbMD{jECqF;D*4*nwUsUkCTU~zYNnEs7BX(o#`?8m6irxv<1`1uw2Bl#9%1jo;V zx|M2GcMt8;-t4!*C$@Z@eDa>-m+Ozh5yHLB5-Zour;o#rx8Ht#tMASZPQ_`%M!>V@ zsm>j&E!r$RoiOe%>_^8v;?=cl-3z=HnH!9Vi~d2uy2i8?3pac|_NwXVG|U-&AESvs6Xht|q4o z*XVJJeYV(pgfzEg)3es-%)DdrM7vRD+Tdu@@yPpjH|Dy#O14bfUf^DF-M;bio8Rhf ze(H4A=UT8cAThWK;fx2F$Kyi%^d znaKRSFFZFN4x|^5>lG|YK zZ-uwD-q6n!rHjJ1SkI0{L2uU+I_GD* z8|f5|t=~&_NEYm^PuX*lU=!a1Qc4Ab;rf{#{w)AL!;Xe6ck&rgO8ldv6E8kMtURHxt%ETk8yy2312G>gAt51;!xs}SMG>)o$btWO ziOrmx?6~OZU0q%2T$$-?9ZcyNIXOA$8JOsqm}r4DXdT^coDAG(Z5&DdZscF>h!{H> zIhflyncLbBzPD>&XzT36OHBOU(Z4@`kJH%A{69U}IR3*HkU{$Q5_(2D2KxVt%*ov3 z|3mg(@;BLEeEr=W&wFEBDvrhu!nW4d#x_oT|Cw=~zk2$w!vBo(Z$f!PL{V0q~Ot+;%!aI9?P0Du5MQbbVM4dgf-N*}fNuHP|9nj~`7vqCD=PdSg03N*1J zEWt!rKwe%jFCZ+-NwoRC=7VBS*mcfzzCfYSJ}oH`(csEUho@u1g7(l&%d+>FTS~e+ z^VM#R!QIJmDtocRNJ~%(pNCSHbcc$r#axj|8?Qt`yuC`J9UL*?sHXts#^J-QH!ruZ z0ik(8t-ea$ZyQ#5JB;eU&C;i0=_cE?NX3afFi8j$sGn_gn<~!Fc9Rec9nQx$S9!;s zuas>bCJ7emvfQgJLX#efwYD@$A(W4-)XG5`7)sOfEQgJ?)5UFxD(D{S*JcCqDc~bv zvev0ST22L&@~W)HI6kTH@t|R~3i*Ifq7YEA_rfBc9(SkI-#SdDGHVYSjQ8lMZHmpF zX??KTFUTSVwk19ZS4=jL%VZXW4%APFaQcf0@WuJ(C#Ud%Tw^5MncC!&Af%J7!^E~f zf~oFX+}J88oRS80QJ+RLY6YEv1tz{c8eQ{Y7Wy`Szo07mo$@*{_@FEM8wZX3m1*bq ziN+TRGLrz-!DJsCs=YYInh3bB4{*`}jn1hn`lTt0iQAVAiE}ZMV$W>5duY^Ue7%&) zyO8C|-P7@y7Oy1T=ka>q3^l|wzSdUxqDtc}#4?p^&u5zr`;+4#ekv4~p4kz*5~GAu ze3Mw)!^65-t-uA)g1r&(W~ zTn`btqc-zhWKMPo7^5Pb;T#~hG$)dYKWSLGJEN6h(`mSq)qhkk`c{-U{XK_K*m$Z+ zvhTY&7XzA93IjqULBIj!4y9I{)RnPBSH4Rr>R_3q1BCZam&-*eiN$P(iH^JjD}@Ct z(S+*T>ybF-pE5ZQGM2)j*JN6U-@u5Bcp$no#oMR9-sjC>itAfU8w%i$eD{or-|F`T zhsE%HUYJ)x6iV`$n(s?1(uVaZc} znSL-HU2PcuTQE6NYjT7N(e{&dp0BO*j!27?5WMmbGyexvHrs_~4qq@Su36H^9Vy1C zhd6Ae5<_I>7j$m}-I=gbtNKvS?5aF3Fcn-(O_m!I4{`=P>5ksCvgzm|<&05Q=M23P zujW)VN!FhJ$DWQNv1uS{N+N%87_bE8!(VN-N8WI0aO&1jYNc z`%dz69H|>It!4EIvwZ0sF&;>n$tv>4Nxli14oN?7iGM$xIN%>4i`lW^$Jv53@beXe zgTxDojc)Aox(tzfYy7@6hxe1r@3@&LNZQcfcY3k&Sr%<$T?6p&W{5$M3IF~0cS#CV zd>CrW8*4*zxw1&6(ngERqE}#*!86B<7?fdG-=B2iHkt0F()Ct%o5%?ba_pJ@@y9Uy zz+pdtc&p2)sxLyQ%))ug*e77e9Z!C2{#7PgJqAa|I7#%H>#wUWM|DH`?@mFGDWAa| zY>OsGgAZzBr^2t15}OWi*jlJ$bne9mk-h_9{__ZI5CP_raQ3=J30P=S(JW16*DwQh zKZCWMr%78#fszXR>p^4zx5+CRJ)JqK&dZ{oz9x0;y?&M7k!;3Dy_@%=SW6Yxp8eI!qX#flI*IukRu|1NE( z$eJsnBS2blfLyt|Um*Eo8UQfrh>Z#4#LeRZ4FcUC|BxUE@;NU^o38T?X>*?cspsu+ z%zufe6ltItgxv{ye|DT9`oBuNJ_}U94av;hx9i4%-8da5zeeibB@Mxb!gJ{mA^0v8~I@bQ1O}VPg&_V-9n$ovj+FglMrC zIJvyPsrvG&xiV@c#g=g=pe3f|wa4PvEJ$SS6f)5j6{{BEnpr3i@FV*#En{zAAt*99$?sU?q8(d-FPSi-byy)6_1rDQ_h% zv4&U2nUdL$qKH)iq*UANIS68rlF=3Z$ZPI&r9}1R=L|6>1^>HP5e2}zy1QFkytB;X z$Q#wYA71k6G8QWO%C6tDhv^G>h}mIPM9I0j~&imPG5bb!o6C%NB&qZfxO{=8}EVvK+UN+(1Z zyc^LAM(3|!e1AGoM|^5LEFAoki)`^?6@;X)_J+pOrSOFg?dWs2CpvZsuoYvc(m$1F zT@*Bdq;4!hfi>c))x2Dpi%ol_Mf)1>f-{_c|82-+v`A+m3TjFlVqY-XD(nwE=wt@x zBHfiKMp|GP6-QaxhhE868Z$f7rDhpl+3H{X(=al~&t;0zFowjR++N6=g$woQy`1h%5}Wr6Mw?e|jxXK-F2W}Gad(nbD;?UM$y%hSB6-zI-9iD`s@ zJ=>14bn2NPrq@egjdC6&nYcgJl6!wZSHb8Kc}l4 zabJ<8-?Rm(DMwEob2=cle8#y99nNX4u?qc9-dey7ryu*JphV`;pGf>;prp;S zJQ2xL|6Ho=z6vyKHdSrh4lA^+>Z5l*MIHXN&7|!55>I(Wm)afVe z6b{(^*^*e+;v>dnG#;exH3onJo1xLjsvc23A(*`SG~`x8%xE3|dX?A_rr}J_?W9c&Mj=+-Nr2 z)mF8NJ+s^)8LyQ6=_1E53eVXhC3YsJ&Glct?Jv8jnZCLw(h#_3B}#@iKNrhsi(#jB z4s)rKBB|71TwF$Hq*KH{7k61FuTY_tUJW>ql5i#|cC)Bjx$|0CmFX(6 z)Jv2-j;p37#9WMZ6dKN)CIzDusS0i+%n@(no{=33UFbGgan8d81_!$yP9KD3d|vf= zI*Ex9TdN)(9%i+CRd4rtn5EVFiU;Ty2d+Rl;QHxfsU2Bz>0GI7lFYvapMNcLmR{+! zxU{p|8;JL6i|j9N9HN1(tJA7d0j$KNLvT+I(I-LIO6=f!6yq zpmW*%Djf9_li50;l@xXMVWT@ZxZ_6EE@E_+^3-YuR&**~?&WUHHzt|M_&58S&r;!p zHRgT%U|h%j+FPJs*`a@&UiFv3Bwde(Up6cHxzu%CO6Yv)XHBN(>%XWIEAFqSKGW&7 zm!d*$jI1^FyUW=yVvH9Ug|arwkO(uQroa#%9csNl>s?i{>PwiD4o&gE3%| zO5KOss88H^mvyg;p;fw0tUtyUmmK+ioRVy3Nz8A>stQxsa5l2R`+eQ~a(n|n4B6M6 zMKNCP{WKUyf$<5s#qA=eHymB3)lGqO+vDaC4(n30KMD_r&FcQRX6$DgXD-c+ZnOHu z=Ezy&R{fHVIL6cM$qSGb*qbduA)&5V`q$tpTv<7vmz%lP{zzKj`o|$eNK6cB`+;BY zvbRjD!D6l)?xbC_@isljGFAjYKtjyHY-d#|Y1&iC$TlW&z6>gIEH^tJg`o|7#^uk5js4mU zi$~^tN@ey;rUWfac(En-QbX)vhRx&*X|%_b_TC763C( z!SLn0gyDCIa*^Q>m$);lYQqJJJ~+PT^995Q^e(Qodh4SA9Rjy@y|n~t)%6%K1hsoY zf8j4V`xPDaf>y835TAKs_AChWb73s!%G<zFK}UCpDwlBjj>wh5TtNB6I6?CC!br z986@rVZ89hn1L+`{!k=us=i}M7wd)d**I$S#OgpSaqR#>z#J@bRi~G@?(V`C(nT*x zSb~(*;_GFxO67pcepPcA|W=$ejrR@e=x zr9n3XUw4hVYa91EHO-XG-5_|N470Y! zn!%L}u~l?K11F0T9ObQ%kg%iq{<;h#QXCQhMlA7+=2RrI(mc>05!CHzW z(CSzkvsh)+lDF2xYX9jBxTg(`j<%oNE?L7g#zlJuH1cM}e2U|b&xk-Hu(mm!w>P?| zz;&M~R*3@>q!Q^0L#JIVR zrsxFw<|gu?-U^p9c}`p*0CIAt+rrW$Wny9ikK3gS!Gn={ljz+8bqf=CQ`0?IX+fY- zE(NK(LGs?Xqys06peo$|gtu%9RpJU%>u)aP5d`#O>05U5mF$(H4tt|`yqRve{1WSS z1D5U|E*PWve3m&RR(;>VE_;`~y?JIp(fxed-55`?jrt>>I{NX2IY7WMgtkma)7KHu#4aHA39u?gnwWO>jaw6(u{w*O4C*vKnHOw1EL0f*UyyusJ}<>zK`E`M|X(j)O7p2Fin1SgKY%- zAixGPFqjUx(+Z4cl(`^1hg&^%U}xL#c_?{fsQEE(LK@a=7Qv~1UD23-w7$xhH32Cz z8BGgHDlUBL*@~7;T*)=x~vclEUUNldm#_MbWDe?@S3rp}^&Gl31un`<8)1 z-i~3bY4c<;uiumUwx9dS*LsP3h(YjO_wk9&*8^g8o;E*>0e#&Ci-2>$ua8_|r&bG~ zkX*>X%{$|2L0efwK&(W?D>D?vgeFO77@Z@D+Ect19hX>a`}JiK@%#N{NkuBFwBqk9 zSSlf85V*ZDghn!%}#AcsgHCxj4*F;+;r&()9 zy>mRm97}JheX>L%-AhuQgDO0s|2w$f0vShA;rXVC)BLI45Q1v4cCzS%W-q>BB`O_x zG;7Jfo0tMxo2iGJy+-dtsb0C<&9eXsZ&vWui z{yBo@V(medy**;LG5m$iM!jx@Pn&9)W^ZqAaImF*%iZb9QNDTP!4UnOI147UWnEJ$ z(2=Zj#IUHdTq~bRFSDB|RBEsRX0q8Vo6VXhP^nxz`y9_R%1o}4ohQ<25&892qpW%A zVO}Ti3Cy+J9ozVbJ95BFJ{Bs|x85V?TbFA$pgaoB7+Q9a8v>+pT_9kQuC9!yQE(Vu~4Dt4CL8uKrD-+S)Y&X+o3ktyc2~YH&h%A}Ed228O-k%;Ds% zm7;iY-9b~4Cp%h^HU)9HSqxXCP1zc8PJ3r*bh&D?MBa~mmaXXxEmJxP5cG*C=0nVQ z*U@}>w}=Nav#ht?4~K6lliTYn()f)Y(XLO?ahJlx(+MUI<=~c??E$n5WgzIhe+L&i`|4{oKo=+~59AZf}d2nDp#`{ErK0zEg zP!fIv@6%Fy$CNz)pTCYrOpd z13pdk&F_FpWWT?}@08bLUAze2W*GyH9yUIuT~fR-y($E*LDQ}2=h;gBNPAw?_xcYr zq#}pa4qf{DM^1K-pgdS|Vcn_|k@tq2h?W^NUA7L59y`*TWTm*{k_OG(Q!6h4o@R zns+olucrFmCgRnmFeE~sEJiS^-ricS7q=&WTXByx(6IA-mE zdZiV0!@d9EdNf-WXb_1K|4VsV$F)w)BhBdda$;lwVm``&d%Mk;F_{O%ihy zUP#3fOR;iLvc=5QEuNxy;Y~{OK2@Gk$y~51`>pjv{?504b7gGrzHDz!^Ep=g(2Kv$ z!~Hpm2h%0pn>eYswjxCe zgHF4%mWxR`k;Xvvi+hN4#y~B$#xT=(!Zxze{kmx2&d8 z34m2Dl>e289iWF7QF4a?>{P3HT2V}yCpFg4MXM_QT5xpe<>~Hp3Tl=!!sQhKtiX z3Nl@FwI~KVkTc+vhk3SAQ2!{ARy|>|%Ba6+D!+rvENg+KvKqPc3gQC>Q6tT!%7eZV z!Z7hxfNx}orkiPrs@-wotQmHqm1?mv)-RREJ728B1a|8x$iW?IRVp-e^KBB4&)151 zQaa5}l3%*=`*DOt^aRJXwa1Zu34~~N4N>03Nd6{h`-vnKA{n!i z#uy%WchCdfwXqWE@bA#No*X0iQBctkyqRy$EVD9uuu|SDKhZ zeEB(()2ME`KLz?M9DgIu{6H?$YdN+r3eWPb{2FK_hVVldh=Uo}`rlLu4z^{bJct#8 zSLU1K-liD}6>24)u41k>*1o3d7}i8l&aBFe-McnS?5Y;kaz0M$9EGtTWrc-ASkCiY z&0kigRVHI6?i)Dnj7aTO?mbSzJ77zV>2z&uxn6fVa%CfeiJZw zr!Hgee2EzedxUH0RN1muZLCU8p?xs#&vic+V#(M#nCaV2fbNmOn9_wtt%7VhzjGI0 z_A!ZhD0*2ExEYFIA6cQ(>ORjKMR(_cOWXAv%4iH*=|=>}CY96yz_S)R_lgUe4Y z;+96MRiAv|2W?`#RNsr7&t@G_o{HG1jkfvBw{R7z({dS4uT{Pb3}L)=h0}Z38rd?h z^lUgEpB`*Osn&8SI$zCCXeIqD$ZgbY5KojG{}tNmCL&NIlSAOm<9uEca6DYMbTk~v zREmhlu%7Vd{g7by*>PSn1o`$HbON;soqY1Up9dlVVDso`e=zg|&x%!+MD*MG z?FRyeG}NkDXJL{(9@I%)aox}ht0Ir|ASBzhc@f5{gO0Br@*WAGC*ORl@D7sllya)GU>1lCv|PILaD2 zoz~F=iYD#W*Dlt!X^HrW?wwpQP)6UO$XT4Falkq4xhXUYfPOL^<++(w>i!1boww0F z7+2+kyXwOrFq9ZXAg;mT)y)o{L7U7S?7Z6U?Rnqr)-Bd?_WFd>0I}%vFq0%67e_nu zEtTWa)^-_F)KKhsnRDxy_4NQ4;bu=%8Ft`uJ|r!5EFJII3}73gQNTg?v)3-mWbORsFRs$I6@N?{FJ-E2^ZR@zm-t*76-c z-tzxmz9L1HlWBgM{LLCFiT4x6(dt99CyB{O%y%mrgOP%r<@p9INDt+~%ZUW4wN_`I z@+j2K*WHxu(kWB8Q3koEhOA~mcj~}&+-P3==EnKlZl*ctp9Oi5*sPcBbay_FgA{Jr z-&Vers8!_893m>%PY=ZCoVM+~2aT`6jxTSH=04!8B=SdC!e*)VhM|d?^{PECE$cOl zjbP`z#4~@ASl1vX(SiLPg`rzW9TvZqK&95FE5vF!i<&~KS&JB|=kp?Jo=$Sr{&c3K zO2ZIoEiM{SkP{kub0~QPoH zT@~CV@x=LA#_`BRIZa{K&G9|qJI=j2T&$OBZDwFy9){@K0$smWb3^fHwVLMozRaj~ zZI>z3_EnqbL!$Ys0`q!l(I1F}FI_)vhB4t(fP1kTS4 zg!RPLhxhc{Kvwajj?h>4p6<@N5s)388c~Hmb`@UsRc5POlVLot`&-Ly)M_-?w9V=H zuILA0&-?=Hjif)#zrC!vPYoHM>aSV4)pvrReJK1=H&^~rW4p$;RBzKezAZyQ8e5~y zD2RlDMDBJQgkv6@7X&`f#Zu+2=)`GmpnljsvWUY;6)4Ky2Ggm{70CR$lHkb<=WY+D zNy=tTKT=VmtZp7=8<#cJ?JVnF(3~9}uGC0*YyjFN7Mv8`@2#_r;IWJfkGlkEP{UFh zD#_j4+5#o{ah{T!_XdS|j} z99})prO~axFxPt=>3PE7?AD8TjcxdxJJ|FC-3@~QzuQ|IaF1HBn7Vs7$LH-I3*Se(&LVQiy`BaBD9VcZ0{XaV38Dd6NA#x&~kWvWsIYlXkCL zwpd3L?R`3o3e0aKAXL-G^;6RRFibUw}`ZcJrqf4wps{-F=Tagr=fQ!X*mL|p# zSOfE}%dSE(kILuEpzzzPrK$p5bwZ>4R-dIXWcRpu2NF_@8%!vH24>hu8YAz@x<832 z6EG%(=m&0hvo4nd;-*_#4DLIW+aD#D!GqOKN3Hf!>wKgtBazABquoj<+Uh8xEHaE< z3M=Z2+0GCI_L~bMpeqiNZ4jH^Kq!e$hs$Xn@aD$jHjq8eZoL>-YoF8qwd+>^ROxAn zHPL=T1g|-#9L|1aJKWW#c?&xs&xd+c+Qex=H2m_m1mKy$984kp_itFs*F-kwI}gnL z31ceJgxK3BVY}> zyB;53T{DmIJdbx*x{v(s#r7IRA%BCi*Mx+__2b<}BvUH|LEBM>dAwoQOSRVY*WuU0 z<7xDMYC+Uj2-%pdv{_j+S1NdI^a_g4Rd>u~J<%m4rR^9?<79p=YZ%uLf<@a=WN~|# zBs(^7z1)`MRMMUN;=D+rX8AFdq<<`i`h7mv;XtE~NnuNL=G{lBDvhspJ43UhMB||Q z<_Z;Zflj*?9<<4qs!TA@12(H^vjPJOBWU4CtGc|IW}m_3D(|PT3%L=|noJLx@qUi{`552MMS2!S7Mz*WysbEdaGbPl7| z_%`zD%+&q#MxxHE>_GA+JK8=-{nQ^8Ur|0O^6jAZV9>K>Os_`!`qRSE26TNRzZLl2$pf){#e>3K>%Ab)ZS=97@B`_3{N& zF_=bq4S)l^)CQQ4bP*&+>_l8Pk(y>Tq;iK<)o!Shv*ji*?yZlWTanTEUV$strMDp} zya`kWds}l=atf&>qM(L_W-1(rOc&<>{_oNJuh4NjBeAiFMp+JdQqT#Jh+pO>p#`TH zSy2cXwT{=j949P=zjfji40+$pROk_e=VtM4BNc$S3Xq;-+>fy9ovqYhxHgzbj_CJy zSK+tZ7O|ms9jyWyEkt{-nzV$Oki|gbpOgN}_ zD@_9jJ&_`7sRJ~q9PyaRfsvCW(mpW~F-{aAtw_jv?`PwKaK9W+tHY))Ev}W(QeikZRg0WyZrIM z{qVJflU@*P#oa_uhh*HwQxGmSfCL2DE=B;LTh!AfA7MJM{!9Jm>1HRaFe^>2?^W4` zq@<)Fg2@vViPhX_I(Jp2qHegYUPhiSF`;d3ffa%2_?ayv5VJC75hSiTKO@CUI~BVH zYglP^GY_jAZz#5m+0i?>`z?e@CW+RX&H^<`#9Eqj$YWb^n7~oF9ELhkC+rPP{kPg*WGn3eX>Z}qL)@mdGRFwKzEdEM3ZHIg znY8&!I!@Y9p1^J&0uyP;ic|94Qe{I7eiYrg3**irDjSUY8=MNWb9*wM_#Ml5Sti&n z@JjY?5a2wKz}6BxE;^^2ey_?%Td#AKdw%_v3XyplYJ(Egw!qmLlbnxb@WqRC zcXpxA3>zCR3%GwEcHVDy{Uon#=}nfBs%YxcXc~rNEH%(Ib_crZk>w}ZM`?vXu=nfb zz_0#x+x`Y^w#uz19U(zYf^iKa0l`4JP}%!_kj9HwBbg|1TA ztxup0w3mlk0Bog(X94?<5%@WRzYf8_9ha`<@Gj1wg$eRLG2zn#;eYl&t-k+>u;K*u z%@-?e#qD_8mJd&u`ck36!#eSB`p!r$1pUMbmQu|T)IH{+)q`W=R{eDf@=-Y%K{R3`-C zM-c#A+81;;qmFo#I(@NfYVq>9YKe(k+_~x3x9JyjUA%%bq`4W~+-W5`LG@ zd@0K!mCqIm+Iza`okk@uoVIX%dkjEEf)WKbCJwqe8F&!TbhTUX^XI(+iVN_91DNS- zIq6VePy%lZS&IP!=fwZ&3}H4`IkD7Wsnz0Q-7W|}&MZ>r zCF(~X<0ZNn>SL!E8y7ccJ1=px3}b;x=D$uRFO*w`R#t+Hr%I%#!=tq%cP^B=hL zJBC0H{Qs_NmZjz2Mf0bKk3h6q1AOS%&J@br<#gc#?!Y%flBNzPonQ04$BJq4ypyfK!g51 zB8qgbcvTq}xTDf#lLI!Y4nuhcj0k_6PJJJ8tC&E$#H8XKq!IT1YSwSI_f8l=9?F$M z`UPg*Iqg&f?_rHIX*PAA61b!JT>n0<5SVcm`W}ktVjvXgJ^?V|G`0x@PikaXMS@FY zRU)5vcZnYjw`$|dWUFd26XLjbi%TTsyKVq66qq)&5Fas^s;caDO+5{A06+h5VEH^YF0@?%-7w27AZczZbEf5alDL76RvEbiy3e==9ddp z%5!9pp`rYpJeAW$xfq&m@H63O9H99?JAlfqd?YM|#zx=%!ol;d(&x}1+HC1jSy}U?KwZx8O(dx=II* zE6?FmegKCKj7LHz>qv~iCulBwQVa{S`T^-Nkl_KJ1}^l&Z=vniF~iRZyYu(8ejO5| zW~gW(8wV~}RRw!L+qs=n6B+swXe(pj^uezFyY^y$=;oZH(PQtstXGBr<;ek)yn+@V z{M*S6KI5HBLJ)dHd0n0ab0=e${h-l+N=fW}K{rKCQeX+9-Fadb>Xz8_^Uz=L{3IAk z`t0A*CFziOs5MhUmFN=!2(!#X_COrFI)(m`kA7p_e%YMC-?(CdU*J|cdU)xKpBjU| z$7K2AGZp**11dr#;rBNap`&F`MCNyG>Er};$C!M^P~X$cW`T2hg#(HnoCrjV9dhYG z4BGuJk??015wHWG`wKmQ@i{Lyyqb9_Nw29`k!N~}WnB1$-fxS-|2x_DHyffAqK|J^ zsz}woNVpt80vzy5x*~VAiB_Kf$KG2Ljqku?Aw{%NNH;8mAAV_zElr+-PAl*ty zN_V$_ba!`mNXK2r_xF4MH~Y_DTtmm+K+>I zbA`1WxLlWJ0h${!i7e1M%ES`JN|A8&Z#WnsOUwq-TSsI(%i|b6a$1aOl$+cRA+uBa zG$IxJmOgK^)3tcdbDQC#!!tx!1qApQ9eQ@11g6L2kN8JGF&DWqYRQZUIU^D>fT6>( zK-18eBd2aX4`1<&=03n`eUfDXc`~{g>=(SD#=R0O)v-nyA1?aG9nmKXUi&lbVDxDS z(+&7MALUPCF7^SL$r~0X()%DZw@KC-3YCgQ8SIAVTRs|)jgkaZfnQZZX|)ZT&Sc~O zM1q9$U+8(BR66-=go}r#>Jw~-M`r*BOQ;kx@{s=A263|D&#GO8c#nwG@DZ;v5P!}g z1q*La&x;Ng9uHwX?X<^h9Z;Y|AanFxw)GCv!jq`pD4CQOV4z85aR22ifgyxJp*WO$ zH*N>teIQdjQ`l}MS`+$FRujqqa`W)ve5YNw{LW?jj z{GZeh%*on@rDXmQvms1$Joz>1aR*n<`wL4|@(}`*^GKcnA}T~b9_YYL6SHrDSGtIS zKo2epLc=p~f_KY0Sv=<*4h4Y15tjSRu4{S&<-teFY9t$#)=8__fw z_22;zlA(hqj}QEk7V~(-pJRIjRKMIP^+N+w#UoCpaLDw&KqW=c1J24J_brJ(c)vSI zG6km82g>E1)b~{ulgCDGmVZp+olNWQDW!2vGA`myJVi!rVl%*gcBU|-Qu>tkY2;Ih zILwwoym(iPiSx%|^TR?2w(s)D$eQ1@tS`9+3nbkQ;4^FvAmeG#~s5xwZ z?Y)0vmhOhbObAR5EfFltSu@NK?(Fu{2k6dUzb5nF7YMPoAa!(fh)GJ`YNOB#!7RQ& zPfSp?_N$^7c$1{C#R)>p4yadWFCtz9~V;=UA27I^Ae0?@*vrAOnIJ;OYqym#3& z@=5Q2Fc!k=OwTowK36sRv9IXUp<%sks-2DF8Df-(V51b(#|J#K*%(&OPA$Tg`yyAs^$ z$xV$`$@cf>v>2BvQrUChTiVmh?ZTv%aX)E!#0d_LGNz!+T<5@|LhpyQHRmkEN7-ck zWN!&(SvtY$kLrF3jqzQ)KU@=gvoF%HuDi})U>g83FSoAPpp|OHPFAhvq|(X^e*tdi zQS<(Sg)SktrR8iET}ZK36M@+zZ{?@`lP-V9)Pjk_r5ZGh-*CnsZl=sk&CR`Q{-xQYjN}9j2Zz-kGJ*lC6 z%poD(yEqrJVkL^@_fD zx+jG91Kdle4L+y~)z%B2NwcGUVwhBm3l&UuMoD2L87zQKLB9ewgn!`82!IGGIxQ@d zR_$ z)y`1v`h#j290r|6hn?j%Upmt-?B@o4KM-21vMS2UK~`BT=X9>rn-i6Hszi6Fr$;yg zto~I1OFk{N_-xx@=>sP_yOO&0q0|>Cb-yTs#+jL!yu7?#cVi8+>=CinfB4NHUa((>SfNB1n}2BA_ieoVjAurEtwsC$gp;u~e)xoh zs##KBR0?ny0$7h5jt;&3$M+^n*!i8`=PT!_ws`hnQol{Pu~}+va#|J&3knJ{k(LHT z#M`Z9Va8(=p-U?EQUNMxQmk~JL&N3$aA3K|5qS=Mt)+WY>f?**;-9oL@bcE_EY$zd zsnL0*pWgC&@)$q~l%}Iu(#Hxmu3ISFk`Wh&3$hN%faw!_)TCP>(R@(AtlcbN za{i;q?ebX?0p|y%#1^QXZ%N!oOUd3O&EgQgP&??nd-)hn7GZVKQ~2t99t*ZImH zTYVmwTe4yuviP6JX55smb-BH|EG)k%oT>OQ5u3q12x?pkN}Au{7OB-e%j~OnEU?cF z?{(*0sQnm&&u-EkCdU0D#TF#D}^UPPdO%?dU1>bdo zx|AFT^Q;DkI_I{Z;v0Q&Sl2BAuF^4J5RNw zxjVK+Kg(bcCoqWSY%LZkb0Mb(%syNAiBKDK}GEEBlBa|sq%s;zT9XcqhCaI zrUJI8uZQ#1YTK-c+33yVd`>891U_8Qlsj6}$9k@Dtkm`O^GSN9%vOQgA4XNY_w|?C zCo}V|CX>7&^eR7&e{(Obwn+q?@Y5n5pYDyDZy?u$tcV=M1vtdC?P{4%sXyE;`Vlda zu_{$PzVVY_qJYxl=6nY*^-^BBUC4#3x4|%20f;`)UYr1HNVCRrrrwGHhO*kmOI5^O zpwU>b;q~dUC_X+uy4vkl+CsgfXol%fDy3v|sNbEBD-C0pXdtS(qZvl@TSw>9j^MAH zAAXMd?;%hOgyQP?hl_4=U{2`N5LVv~HEOJoMJbO#sCB;$ZHHR688XA>h=%o|3w0yuS$Y^Yhag`i<$s z_MK{TFv@HFSl_+jtSzb29%FU}Y!E;QowQMlm|VtWH{6sv>9D&V{LxbUuU-JXabbj9 zKr`3AFPL=khskoe`Fh_&!`yV0F>mwuaNmh95S@J2v#-82n07S%oswiXgixglm4vNh z|5boi6e1=9C$P_=2Vd+P|5$5WXdaW+44gOnGasnUmQC$nYo1h2lr zaV<9;0wi^cd%)Nn%YAdJzr!}YT_zr)*OSN61y+<6`5gD9CM}`xjSqZ%NODQ@U7C8- z>2Tg5m*q5Bfc;R@r6-qZxY?FZ@vm@O1kno5`eL&mJ^Z6BKMlbGmn z3&!mJaJ`Dd5bt%naOncmwuccWVYr!j^tHNB99VhplWaA8Qn=s1Ga-YnLC~`vRwccP zTI;G>oX^|Sh%l*I9%2E-tgMF5xK9qe9<5&3-gO|d?-9WeBeT{;J?FCgQ93C8@_D(M z9sQ^y^4kL`-FkMIDD|IthkC>zyC8)eJ7+%0 zh-lsjkM4yBL^UDw*!}bQ#D}TEV*R+zeVXJ^^f!*X)6mTFvo#+WG`BK5TvkG1Nyzsv zw>@{zUc1{&^6LZ#bwN!ZdVIQk$8;{>es%o$`b<0*s53fD90nCk#B3_N(#6Zv!ITat z^Asd8|J%91gt4C}y~P*F!Zo3R{(o%aPuO2y@ZCl#tvSkw9%1NOM=*6HA@{LCi zR9lTA7oYmN5NF`N@OD0!&tnw1nXj`?RFM=)91VyI$~f;bmc707db$JD1h2)qRMbpU z!$`|UiPfA~_=JsVgD4qx;%q4=hMyS6b3#JRpQVc!59F7NbJ0YQu1-oAUCgl1W7vk>Tek>m8xHA3#%GFxV?Yz2cER(nkUN)rk zr`de@Y%qgur?rjxM3@M()JK zwG-ycRTv?6wbdN%^^ea7Z{z^^C?nXiOM=tA{husg`T`Jl79scFeV|G zL5x;x7ZsJwhkJOF!9;8G;u#BBze(Ke3#(p>i2|f2#L$Bnx=+U@j_rK7+3%=X_89Lp zmo@F~u0CpdkT3ZkdOmEJKHvV~cJ%6!NsXmNv@2X{NlLbTozK$Y`L^0rP~JIkkI2x; zLyp}IcIJHD;a@YECC0{{KfuSto?sF0_Z1jSmnd5=4JL(!ghY2re0d%om#!v@Zq*H| zCIBrs{ag4>Z%%`e2B&S-*!9IT44-LK(jVh1LEBmH{ACfV$bCx1cWvn`&@r#5Jqof} zgz2kv5@H;8gf3RHclT(JMp!&@=s_|5cN0m7#FAm4qiz=u_%|p`J-uOWx{C%pNhS`M zG`qS8C6Sqr(Ie&(Zgqp4S_ubr}3EPV7FJvs#-5;;RLOy?nlf`Rjv^uvDQ9)o(kRBnJO8+^n zm3Vu&2_ zbZB`NVYjW6B1TQ$HvuTZE4>CPetO*9!mtvtlPvqMKdp8I!x-k)PA+T?d>g^LV*>S) zVX(<+VvQnMUf1ehE4rv|l;mIg!U`tJ;2};mr%a&pSA>M4+e!c;PKD$p$%P}WVn$X{ zE7(AhPeMsa9!uT=fUj8C3hv--P7yB-&mHO-V>TS>Ut=rs9tgrG+lbL&>6Pz9i}YXv}F@ zRRIF`G|$hEUs$_0M*WfSg3kRAu{yC&otHc*6K17rhf|0w6`u#m(8PnPVsyo|bb7u{ ztk0v8o#6ydAwb2CkccbUC5n7RAieJ*@tU__=C)bHzILu=3*d?EJdUIsJ${a#Gd;p`=g-xl1icjRcUMI<0Ygb=_=5~@)4IJLn=V_fH|IdL6)Fk8 zjC7WvVQ3K1bzQfdcc8EwRLr)p0AE6&1Rh?(NErnLVyP8rC4Oz^yj!195V-5kV~*+c zQF0ko%?=6gB_I}gt+c@~Bf*-tNpNLg_0`g{$T92IX_bpfW1OQ`%obQ3p=6DZ`yUP` z9RI+OhuNDfjw2K^I*g6QrezMfI9g)Ht4p?(X$R$z4&1N?fu!9TooR+%8RuWlaR)T# zM;hy1mYf)Ng;%l5iMN-z_251FP9QB)aIA+D&wV4zkf=6afI}1aBDfHJV-fEFn5_oh zZ&56U(|v4-BxB{0GvB|yuQa_(1r<@T^xFCD3&iXAH-KIg!iC4AaWxVf5otYHNoRTK zxHknDmhj=CLjt#2Wf`NNl&;EYXlVGeq5P%E)BvLUFN`r#C>`C~5RR+-`tX(%9i5o# zQ)U3lNv85)YnyM(>pO)c%F*c<%!bKOj(u5ncr;nps5K2|wTx%&ij}4A*AczHRxHmt zu;hc1VN~r{X+M?L=u$gNuHm-d_k}^8+R=iBV>k|9x7{=>KqQ~$zcOKe?sY; zTfFwVE?Mdzn0^RyY=lV7<_0&168Z};VG-{ZY} zyspuyi=xc>Cc*7Q5;l4^lf&h$O-o4ql#)_eO?G*LabJ9hXry|JXH$8%r-luMIo)P# zK+x;jo2>~J{SBw|9^fDIEyq1jbQMzaq%}UwMz%|acb~6^NC_;JFf+PUOvaa}34Yi_ zCeNAJF>+-uwQEl1ao9$`!Ti?Jn=t`HK{=?%Q?a*{!=zfixW`G+X$iI8@=y>94z^d*J7@4&C}?JmWctveT*gc8>q`<%5;vl zjjxblNA++dyue4nO8LcXL%1yi_UGv7cH$p*rUeqP&nOAu;SHS{eSm1Be}9&7p$F3& z{3GHZNL`q>UAWfdo55Ht+S4iI`a+yP^f}OBhsPKEya(Um|L*4m+tw?F#=r~y&*zvC zxKgPbJ(GXw^uPaXeF*}Kg`Zu$|9LtX#Ic2m+`7`{S(%keI)R^`D!xRzLus zcp6QRZuzh0)gS=q7KxMr0(JfS<82By>EFq)f4?m#@4^66fh(v2$rt_SGlU7Q{*@d1 zV}<^Ddb6m#uU&94(bv(HGzX=wC`G0PLy?<2NOHvM@z+m1X@#pf~{}NTXUe-+a59 z=5MH$_h#R|xGs(T$2z*pfUAO?1A{;?L}H*!^F{tn+E6VMK=#kCu&Vz^DI|Zy01?!u z421v8Z%;u4HH`4XKSf;?T)ljz!u8)>t%P8TA%aT4J^9Z>bO}1Ric@I$&*5oN0+IZ; zxA8A&*C7X28PsNI{~>0z5UCoxsr1I+kFogYHii=D%-!h~aIX`fsOx;R7AdhcO|v!Lwe)|4gDc(3W@CCW$iH z(KmgUeJ*;#qHp^U`Ru0ogsBWek^e!*k!nQ)OLGg&*YMwc$;hA~`eu%z-Tgnq2I}GX z33wY%??EP@9RIVvt+?Q7bvH}ee_aDf6(KN#r0Es1|F}abL@j{&kv}lOzyIeoWEJ7J z_oM%D2TQQx*8guB?(j6dp5Pzh0xM8yv-AKu;>0tkGSSmpEG;&HVCR<9>+Y~(aR%eh zmjLZWVIp6hG95dE4YIz=sGxtDjk-24H<1|TV>RUM$M4IHr2B9i6Eal_y21#TinaMG zm^^RqYc7}Em}zLb7}EkFKLq4hka!}>d6!2uaC*;1@|+`n%ua+Cy&y@(CsHObfmTN# z3$DNI4^_hhMmz+&jCpxx6cj%<9W~$DRn3B=fLtQWLmzVsG8$S5fW3>pa=xek4W9OD zPMh57B{?g`5dG;Hjt*x>2X>t+duAkRphXNw%+9FXogMBF#`W>th;ags1UZuC`%2{ug0+8Dim`#WEoxx~Dut*--;Ezf)s$?eXgni*E zOuS$gQeE!KknotgubZVo7OUgm`KKAfPoF+nTNkr+&Fz3(&`O&%8mLV}9yj=0)WvOLjytpm>mF~y&&bKV7rD;eb`}=g)bQ`ZvJC3X%H%{ z#^5Hea|XV}PV(-O3b@#|2cS4FIpQ%oE)j13m4}MnV9}#!|7RA;h0CKr%-y&aU3ViV@79THNX_vETjQc`3&rPLP$Nk1NQ^ z&L-yf?B53sq&t57qKHH_8Sa#_S*Qm&Ai69YW78$6H{ui&6dAxrl?rP1OT4$)_HE^H zw3wO{6C--J~pm(G}x3AxTA z+q&wln?5ZxIG1SVGR+U`>!$KMA9hEzGsH75fs0;w**Q5cXlSMiH1RBir2myQqAocH z?XapqZfhgOIfVL~WCY2}%z-|Od4}**aHvG*!=h;9tfqsxy6z|bjOGj@4bEwxdY6x$?H#U=LSL2F z1FzHmbKXF}d;u>(3p~;%yABHYAb}Z4>3zL9k|}wAF;{ZwRCiRto0cl*L10Z~({g4V z^D8WlF<-?e_fwo3-;(|EavN)=WXfwV9rfKcj?EE&pKwW}kt!K0WZq-Y$Lwp;vIvwS zA%Mic)O}=s&x-O0;=Ui_`AO_1K@|Ek--1_Im?g@mk)HDAynX4vk(`_6Sr5+nIQ=d~ zW`{~b!$C069d+~-=j0F56UGM$e5}niwG~fae zwDo-D$^1-3H?wN9fD@rd>7Fl$5))hWR`_iqrkmVqT7I;N>o{1|1-w_=a_RRbL?m(? z%u4p5l3VFaW5$@|&FObOngw;Mvw&n!WjzKbPmC_a(}}dj?0&gWBNai+jVr4N$OvEB zR*%-+VED9RHJsK6)iC>l+l93>?98b?o_=(_8txA!_*(b1X&U^3fK( zbhBDOcezxML^^tWhI)IxbK4nBEx70*{iDqWGTc(p)~k1;;jp29IR^q#P#=hN50|`n zO}bcLSzlcrT)r-4#MJ&m^6FPVUqo2g?S&>R^rH@ATZbSp0Vo2VtFSrKl3@e~!bL=u z`8rKE_FDsZI<(yEHr9`~HWeYm1O0q~LVJ^=sOeXYkBe^Qj!bsH!ie2gR>iYev+qF( zX|P;``x3yDZH|H0$wjfl%;#9-y;Q@iGG!57X8)Ed)uA99VAx?!!>Ys^M|p0c*TOXdCJ8OVYi z=pp^h1LETeshRpp!e>-j&Q-q&&UC9eM^mL=8gYYeU2s)eb;&i52C<<|DGI8|5?dO#o#?8r#HO?h0!{Wzc zyh{UqL_aW!+0X=rCki=T=`1n>etGYiz<^9Qn61|U5efZEzxgOoDKxr#M0Nwc*Gzm% z-ZyaBEuR_0qm5~qr-Y7z(m-%uuq)c(6z?98!HI~mQq-GeY2 zyuj+5>A0Waxh2{S1h^_sPED2G2|l`)t>Mu9>G0%90|1DYG#J3sp+eVyoh-V?dg58E zYU8>B20;wUh*6K3FT@b`UEsE^N0A>S~SPN%Og{kJLSZF6gz2jGPW{MhP^ zV-Qa!+V8(fr9-GUBb9%8w#j^Tj@6<6cn)|ur+T@6wN(`m@n&KoY3r7NsDf5Sn4lj# zpU$@<1aGcRx=5Md*N=}!yvEhnRAJN&f9Zy92it%_zF3#C3H0h@W|t_s+QV9eQwL10 z8R$g#<|~IV4_@K;-F`)#Br54I0(C|baV_yO9}MLb7JYXUlN@-*U;X#ftN_m<4&(xr zm1p;8vd#yyuG_=k2_`-`G;gIQZkw&Qf7WI3YKG@GR{FE2Fd*I_f&|we?%Z4S=_>mz zP||KP-#D{!h{{4#Hu8dtUPLSQ2|U`*HG|o6e2P&djsC&GfVHmbmg*kV?b5;AH)%Q< zE+T;dv;5aZK9xQ;EO{0{+Sj4>w%6?w_$%d67KR~;f|6jBq4%`pb3=Gl3>$R zv1TCg&Jie77WBA<=}jETTrSqGlEKLidzjR;>D7cm+-uaBumHFsXu7sW9bT#>-Zwk1 z1>X>vMp~({_x?ERWS@PA%%Q%_T&cl)XD7|`CU1k{ZQX~Wx2DmY5AhA%?| zhucDq#R6~nKd&V7(RmORinD^5f=65?7hsgrP%*q&ibU7_WmBR1uD#chCxfz&2p-4# zdGVHubznuZfbp`+f>2DB4~dzn-whalt+&K1(PL-<{e6!s?xm!liZB;3T#N$xZrzQ@*goD^)r4$ATV{`LwbQv(gq~BvjKg z^pi?_r0X}0s0Tz&9(H`hkH*s7b|j0hj?msStta|MzZMv2KfXF4NyG?B_3kvLeD< zRH$Hok;Niz|Nhjs()V9FPLDyedbueXP?VZIZXJ(CD7CEa<2C`qP}l3)7&vzc%(@q; z1cx+-L(ge2m;{ZUhUV1)NclSkJD zAY(OO%zgk^!%_3uVV)Ar2rMQNF4LDUL5Zv$8sDm|j-=`hPh%r;Jnv4*jb(`sTB+-B z7*vI+;R{G!e_X9IO_P?A^K%TRw}{)#qe&xRHK_HS5EWzOdjV&}GF@AH1rsQ+J<$e$ z4Cpe__y_MX?R58LgpTI{fX{}{L$8R1N!tip*rNEdaqasi#1T-S1t8GeVT$vjKiypq zOmy^uFxqz2bhcsWUXa+O+@Y487swhlVF>#|Pz1rL9b<9=DTcDZEN8HYKSx z7yU^j3k@w^k2?2943GTq>DPo3ohH|N4aU>cQ-cIz{z{vzefP^l z?TEP=@h8#2P#9z|Vq}Q0-NN!P9Qh)!K~NY{xIW~f0cjOqBgIQ0fiGMzW1x+CY8!Zk+xU)M^QZfyD5xk0nf$?$u zcm;^?Q#iN~F0_pju&1nM%Iib}!_|Dzo;|}v8C4nd8;`_imyne7x;YOH>u7Cl9UaAF zb9b(e;?Qk&pT@ogwHxyC^4Ff_)8v$XO_t#2R+hye+yr_+t*HDpXkIQ8PyfDF;X5fg zc?9Vz&*SB0wgTO~<4`gG5S+H-iz3bE5e83T-(_Ndl7o-=toPxACLo$RdwS}mrsntH zfg>bbPB3#!a-5ImPk#S4p1^4su6-fD*ob<8%2j722q3uTO;FaS3$B7_asG6 Do)j*lTI6dlPqCw*_obXp_0EV@@}DRonGo;p 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

=Bp!IQY^iyZa5Niw*%}MyRYQHBC`f* zL(7fmV@Jc;b-QFkb(bKs)4~6-AEn>?%81ufRXVhD&^@z- z@MQ{(vQoE+&G&C-BVq4+i=&|eU<3nHmeaDU!O&G>dPc`;XsDbsmN+yw;d&Vhm#IQI z=JN%+>nLZIx3oOF7FltF@`I0b5y{%d$%$gZSkJaBg(-PP`t9@Q`j47^vr(0h06SeK zSQT*BeM0g^J>};(pRN9V-l)oLw%t$eA^Ovd>vrJ}o<2`My_2S1MLw}pe)a3Y4HLlQ zBWZ$YNXORaH51e#{^p)R$fCtywThz zt58Tu*c#k>t4onG_}m;}nB>=bJGC{33+u$Kxfy1NK5Xvtut~T9>P@IqiiraMT*tTs zoc`Z%Dw!9|kL!!sih3PM6q1FGy6?>w77i9k{lM;DUm|y zYswyIjpp)v(&I+ZR$Ul?osx=DMFGP8k}8>gHrza3Uu4D?B?4p5e=#rspG<+R?fz}5 z|5LQIXw*vh=h?Rvi4t`1XXn0Xy=JQe)PXOnZY1BgzDk*Pw5NX8nH+@u z3$dh@L_L=FJXodE2*6Sv;%Ar{V1XaO~^W;J(%IpUY5jl-i7>aZvLt zgo0&arR=5|<$zyf-nJp9>Vhs%@;LlECSbyHq15&rJ*uQq=4sX0#ki-kYiJbpu!s?O z*d-@Xv^4Y0_X8jcxJT)Kg@2vOS$Cb1_VnPoD0{L$zhWy*A;a0}ve#(=cHJ4$B*_Nf zRb;yGd3u)np*2Lodbc)iNKD%M(G!~ID^6td?2|oLrDkmQAz1oqX*V)ysN+%R&X;y} zYi}#DLRfo3ts{KdwkB3G`lN1NoMzOTnN55l+wcV7T#`t0m^&pNELOMm*&vSM|YoZ!x7;(@x zb-*nLE)R_q3B9E*?~mXUGw@C7@rC z8~K|eNc?+L^Z+)o2WU_2rP@^fdMej;;6r@&$fwCh0gOi}1Q5EC$;tUtBmqfF;XjvQ z&(YJqKuM~s`fd$iSIA3JbO(N&b!U6A{xT_#}SIrY01A`+$ zDMU~GHg|O3x%IIZgU^iTKT<+H>gAs<#kQ6AONhLIvzdAneIh{+5XQmSzYDzH^Z$0D z<^n6x0C{Ia91d$jRFE}bJOd9*V%o@0i;0GziuxRPljM2{w|XgTE1VpEQn zZRZR{E|LJi{Rfg@=rFCX%;+`_gEZi$9>%Mrz{uSlZlHe+yeaj@L^WMXXs(eQDM*~1 zEh-CWtrTU3{Z*bgi=+9osSy$=CLbN%CgqM7ByPS!{u3sbKE}_`9@+wrk=B8KvZR;s989iaH8q z^C$cG;<>*^cqW8ISM%6d&$0vj{|rB99}t3!vb#G8{`Uvq*%d1F=%SO9=(EiJpEsGX z0YDBS#nbuUAAn~IKu0{dfEFM4JO9&#=T8Ft z2H^m}Tgc^%{GUGj=gq%Jt3Gx4Mwmwne XvzVhMP1;N%0D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 Bm2Ut5 literal 0 HcmV?d00001 diff --git a/01-essentials/08-multiplayer/images/04_copy_paste_key.png b/01-essentials/08-multiplayer/images/04_copy_paste_key.png new file mode 100644 index 0000000000000000000000000000000000000000..6b9b5919bf3f2ed38f66334b4f2586e0fe04e0e1 GIT binary patch literal 173853 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

3B?2QOn>Aui?A?AT6BqZdrH#Ft|2#Nej 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 z.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 R#xw9EB_=QWO+-K7{{U!pj(h+B literal 0 HcmV?d00001 diff --git a/AZURE-DEVOPS-SSH-SETUP.md b/AZURE-DEVOPS-SSH-SETUP.md deleted file mode 100644 index 81f0326..0000000 --- a/AZURE-DEVOPS-SSH-SETUP.md +++ /dev/null @@ -1,628 +0,0 @@ -# Azure DevOps SSH Setup - Best Practices Guide - -This guide provides comprehensive instructions for setting up SSH authentication with Azure DevOps. SSH is the recommended authentication method for secure Git operations. - -## Why SSH is Best Practice - -SSH (Secure Shell) keys provide a secure way to authenticate with Azure DevOps without exposing passwords or tokens. Here's why SSH is the security best practice: - -**Security Benefits:** -- **No Password Exposure**: Your credentials never travel over the network -- **Strong Encryption**: Uses RSA cryptographic algorithms -- **No Credential Prompts**: Seamless authentication after initial setup -- **Better for Automation**: Scripts and CI/CD pipelines benefit from passwordless authentication -- **Revocable**: Individual keys can be removed without changing passwords -- **Auditable**: Track which key was used for each operation - -**Comparison with HTTPS/PAT:** -- HTTPS with Personal Access Tokens (PAT) requires storing tokens, which can be accidentally committed to repositories -- SSH keys separate your authentication (private key stays on your machine) from the service -- SSH connections are faster after initial setup (no token validation on every request) - ---- - -## Prerequisites - -Before starting, ensure you have: - -- **Git 2.23 or higher** installed - ```powershell - git --version - ``` - -- **Azure DevOps account** with access to your organization/project - - If you don't have one, create a free account at [dev.azure.com](https://dev.azure.com) - -- **PowerShell 7+ or Bash terminal** for running commands - ```powershell - pwsh --version - ``` - ---- - -## Step 1: Generate SSH Key Pair - -SSH authentication uses a key pair: a private key (stays on your computer) and a public key (uploaded to Azure DevOps). - -### Generate RSA Key - -Open your terminal and run: - -```powershell -ssh-keygen -t rsa -b 4096 -C "your.email@example.com" -``` - -**Important notes:** -- Replace `your.email@example.com` with your actual email address -- The `-C` flag adds a comment to help identify the key later -- The `-b 4096` flag specifies a 4096-bit key size for enhanced security - -**Note about RSA:** Azure DevOps currently only supports RSA SSH keys. While newer algorithms like Ed25519 offer better security and performance, they are not yet supported by Azure DevOps. See the note at the end of this guide for more information. - -### Save Location - -When prompted for the file location, press `Enter` to accept the default: - -``` -Enter file in which to save the key (/Users/yourname/.ssh/id_rsa): -``` - -**Default locations:** -- **Linux/Mac**: `~/.ssh/id_rsa` -- **Windows**: `C:\Users\YourName\.ssh\id_rsa` - -### Passphrase (Optional but Recommended) - -You'll be prompted to enter a passphrase, just press `Enter` no password is needed: - -``` -Enter passphrase (empty for no passphrase): -Enter same passphrase again: -``` - -**Passphrase pros and cons:** -- **With passphrase**: Extra security layer - even if someone steals your private key, they can't use it without the passphrase -- **Without passphrase**: More convenient - no prompt when pushing/pulling (but less secure if your machine is compromised) - -**Recommendation**: Use a passphrase, especially on laptops or shared machines. - -### Verify Key Generation - -Check that your keys were created: - -**Linux/Mac:** -**Windows PowerShell:** -```powershell -dir $HOME\.ssh\ -``` - -You should see two files: -- `id_rsa` - Private key (NEVER share this) -- `id_rsa.pub` - Public key (safe to share) - ---- - -## Step 2: Add SSH Public Key to Azure DevOps - -Now you'll upload your public key to Azure DevOps. - -### Navigate to SSH Public Keys Settings - -1. Sign in to Azure DevOps at [https://dev.azure.com](https://dev.azure.com) -2. Click your **profile icon** in the top-right corner -3. Select **User settings** from the dropdown menu -4. Click **SSH Public Keys** - -![Azure DevOps - User Settings Menu](./images/azure-devops-user-settings.png) -*Navigate to your user settings by clicking the profile icon in the top-right corner* - -### Add New SSH Key - -5. Click the **+ New Key** button - -![Azure DevOps - Add SSH Public Key Dialog](./images/azure-devops-add-ssh-key.png) -*Click '+ New Key' to begin adding your SSH public key* - -### Copy Your Public Key - -Open your terminal and display your public key: - -**Linux/Mac:** -```bash -cat ~/.ssh/id_rsa.pub -``` - -**Windows PowerShell:** -```powershell -type $HOME\.ssh\id_rsa.pub -``` - -**Windows Command Prompt:** -```cmd -type %USERPROFILE%\.ssh\id_rsa.pub -``` - -The output will look like this: -``` -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC2YbXnrSK5TTflZSwUv9KUedvI4p3JJ4dHgwp/SeJGqMNWnOMDbzQQzYT7E39w9Q8ItrdWsK4vRLGY2B1rQ+BpS6nn4KhTanMXLTaUFDlg6I1Yn5S3cTTe8dMAoa14j3CZfoSoRRgK8E+ktNb0o0nBMuZJlLkgEtPIz28fwU1vcHoSK7jFp5KL0pjf37RYZeHkbpI7hdCG2qHtdrC35gzdirYPJOekErF5VFRrLZaIRSSsX0V4XzwY2k1hxM037o/h6qcTLWfi5ugbyrdscL8BmhdGNH4Giwqd1k3MwSyiswRuAuclYv27oKnFVBRT+n649px4g3Vqa8dh014wM2HDjMGENIkHx0hcV9BWdfBfTSCJengmosGW+wQfmaNUo4WpAbwZD73ALNsoLg5Yl1tB6ZZ5mHwLRY3LG2BbQZMZRCELUyvbh8ZsRksNN/2zcS44RIQdObV8/4hcLse30+NQ7GRaMnJeAMRz4Rpzbb02y3w0wNQFp/evj1nN4WTz6l8= your@email.com -``` - -**Copy the entire output** (from `ssh-rsa` to your email address). - -### Paste and Name Your Key - -6. In the Azure DevOps dialog: - - **Name**: Give your key a descriptive name (e.g., "Workshop Laptop 2026", "Home Desktop", "Work MacBook") - - **Public Key Data**: Paste the entire public key you just copied -7. Click **Save** - -![Azure DevOps - SSH Key Added Successfully](./images/azure-devops-ssh-key-success.png) -*Your SSH key has been successfully added and is ready to use* - -**Naming tip**: Use names that help you identify which machine uses each key. This makes it easier to revoke keys later if needed. - ---- - -## Step 3: Configure SSH (Optional but Recommended) - -Create or edit your SSH configuration file to specify which key to use with Azure DevOps. - -### Create/Edit SSH Config File - -**Linux/Mac:** -```bash -mkdir -p ~/.ssh -nano ~/.ssh/config -``` - -**Windows PowerShell:** -```powershell -if (!(Test-Path "$HOME\.ssh")) { New-Item -ItemType Directory -Path "$HOME\.ssh" } -notepad $HOME\.ssh\config -``` - -### Add Azure DevOps Host Configuration - -Add these lines to your `~/.ssh/config` file: - -``` -Host ssh.dev.azure.com - IdentityFile ~/.ssh/id_rsa - IdentitiesOnly yes -``` - -**For Windows users**, use backslashes in the path: -``` -Host ssh.dev.azure.com - IdentityFile C:\Users\YourName\.ssh\id_rsa - IdentitiesOnly yes -``` - -**What this does:** -- `Host ssh.dev.azure.com` - Applies these settings only to Azure DevOps -- `IdentityFile` - Specifies which private key to use (your RSA key) -- `IdentitiesOnly yes` - Prevents SSH from trying other keys - -### Save the Configuration - -Save and close the file: -- **Nano**: Press `Ctrl+X`, then `Y`, then `Enter` -- **Notepad**: Click File → Save, then close - ---- - -## Step 4: Test SSH Connection - -Verify that your SSH key is working correctly. - -### Test Command - -Run this command to test your connection: - -```bash -ssh -T git@ssh.dev.azure.com -``` - -### Expected Output - -**First-time connection** will show a host key verification prompt: - -``` -The authenticity of host 'ssh.dev.azure.com (20.42.134.1)' can't be established. -RSA key fingerprint is SHA256:ohD8VZEXGWo6Ez8GSEJQ9WpafgLFsOfLOtGGQCQo6Og. -Are you sure you want to continue connecting (yes/no)? -``` - -Type `yes` and press Enter to add Azure DevOps to your known hosts. - -**Successful authentication** will show: - -``` -remote: Shell access is not supported. -shell request failed on channel 0 -``` - -![Azure DevOps - Successful SSH Test](./images/azure-devops-ssh-test-success.png) -*Successful SSH test output showing authenticated connection* - -**This is normal!** Azure DevOps doesn't provide shell access, but this message confirms your SSH key authentication worked. - -### Troubleshooting Connection Issues - -If the connection fails, see the [Troubleshooting section](#troubleshooting) below. - ---- - -## Step 5: Using SSH with Git - -Now that SSH is configured, you can use it for all Git operations. - -### Clone a Repository with SSH - -To clone a repository using SSH: - -```bash -git clone git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} -``` - -**Example** (replace placeholders with your actual values): -```bash -git clone git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project -``` - -**How to find your SSH URL:** -1. Navigate to your repository in Azure DevOps -2. Click **Clone** in the top-right -3. Select **SSH** from the dropdown -4. Copy the SSH URL - -![Azure DevOps - Get SSH Clone URL](./images/azure-devops-clone-ssh.png) -*Select SSH from the clone dialog to get your repository's SSH URL* - -### Convert Existing HTTPS Repository to SSH - -If you already cloned a repository using HTTPS, you can switch it to SSH: - -```bash -cd /path/to/your/repository -git remote set-url origin git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} -``` - -**Verify the change:** -```bash -git remote -v -``` - -You should see SSH URLs: -``` -origin git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project (fetch) -origin git@ssh.dev.azure.com:v3/myorg/git-workshop/great-print-project (push) -``` - -### Daily Git Operations - -All standard Git commands now work seamlessly with SSH: - -```bash -# Pull latest changes -git pull - -# Push your commits -git push - -# Fetch from remote -git fetch - -# Push a new branch -git push -u origin feature-branch -``` - -**No more credential prompts!** SSH authentication happens automatically. - ---- - -## Troubleshooting - -### Permission Denied (publickey) - -**Error:** -``` -git@ssh.dev.azure.com: Permission denied (publickey). -fatal: Could not read from remote repository. -``` - -**Causes and solutions:** - -1. **SSH key not added to Azure DevOps** - - Go back to [Step 2](#step-2-add-ssh-public-key-to-azure-devops) and verify your public key is uploaded - - Check you copied the **entire** public key (from `ssh-rsa` to your email) - -2. **Wrong private key being used** - - Verify your SSH config file points to the correct key - - Test with: `ssh -vT git@ssh.dev.azure.com` (verbose output shows which keys are tried) - -3. **SSH agent not running** (if you used a passphrase) - - Start the SSH agent: - ```bash - eval "$(ssh-agent -s)" - ssh-add ~/.ssh/id_rsa - ``` - -### Connection Timeout - -**Error:** -``` -ssh: connect to host ssh.dev.azure.com port 22: Connection timed out -``` - -**Causes and solutions:** - -1. **Firewall blocking SSH port (22)** - - Check if your organization's firewall blocks port 22 - - Try using HTTPS as a fallback - -2. **Network restrictions** - - Try from a different network (mobile hotspot, home network) - - Contact your IT department about SSH access - -3. **Proxy configuration** - - If behind a corporate proxy, you may need to configure SSH to use it - - Add to `~/.ssh/config`: - ``` - Host ssh.dev.azure.com - ProxyCommand nc -X connect -x proxy.company.com:3128 %h %p - ``` - -### Host Key Verification Failed - -**Error:** -``` -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @ -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! -``` - -**Causes and solutions:** - -1. **Azure DevOps updated their host keys** (rare but happens) - - Check [Azure DevOps SSH key fingerprints](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate#verify-the-host-key-fingerprint) - - If fingerprint matches, remove old key and re-add: - ```bash - ssh-keygen -R ssh.dev.azure.com - ``` - -2. **Man-in-the-middle attack** (security risk!) - - If fingerprint doesn't match Microsoft's published keys, **DO NOT PROCEED** - - Contact your security team - -### SSH Key Not Working After Creation - -**Symptoms:** -- Created key successfully -- Added to Azure DevOps -- Still getting "Permission denied" - -**Solutions:** - -1. **Check file permissions** (Linux/Mac only) - ```bash - chmod 700 ~/.ssh - chmod 600 ~/.ssh/id_rsa - chmod 644 ~/.ssh/id_rsa.pub - ``` - -2. **Verify key format** - - Ensure you copied the **public key** (.pub file) to Azure DevOps, not the private key - - Public key starts with `ssh-rsa` - -3. **Test with verbose output** - ```bash - ssh -vvv git@ssh.dev.azure.com - ``` - - Look for lines like "Offering public key" to see which keys are tried - - Check for "Authentication succeeded" message - ---- - -## Security Best Practices - -Follow these security guidelines to keep your SSH keys safe: - -### Use Passphrase Protection - -**Always use a passphrase for your SSH keys**, especially on: -- Laptops (risk of theft) -- Shared machines -- Devices that leave your office/home - -**How to add a passphrase to an existing key:** -```bash -ssh-keygen -p -f ~/.ssh/id_rsa -``` - -### Never Share Your Private Key - -**Critical security rule:** -- **NEVER** share your private key (`~/.ssh/id_rsa`) -- **NEVER** commit private keys to Git repositories -- **NEVER** send private keys via email or chat - -**Only share:** -- Public key (`~/.ssh/id_rsa.pub`) - This is safe and intended to be shared - -### Use Different Keys for Different Purposes - -Consider creating separate SSH keys for: -- Work projects -- Personal projects -- Different organizations - -**Benefits:** -- Limit blast radius if one key is compromised -- Easier to revoke access to specific services -- Better audit trail - -**Example: Create a work-specific key:** -```bash -ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_work -C "work.email@company.com" -``` - -Then add to `~/.ssh/config`: -``` -Host ssh.dev.azure.com-work - HostName ssh.dev.azure.com - IdentityFile ~/.ssh/id_rsa_work -``` - -### Rotate Keys Periodically - -**Recommended schedule:** -- Personal projects: Annually -- Work projects: Every 6 months -- High-security projects: Every 3 months - -**How to rotate:** -1. Generate new SSH key pair -2. Add new public key to Azure DevOps -3. Test the new key works -4. Remove old public key from Azure DevOps -5. Delete old private key from your machine - -### Revoke Compromised Keys Immediately - -If your private key is exposed: -1. **Immediately** remove the public key from Azure DevOps - - User Settings → SSH Public Keys → Click the key → Delete -2. Generate a new key pair -3. Update all repositories to use the new key - -### Protect Your Private Key File - -Ensure correct file permissions: - -**Linux/Mac:** -```bash -chmod 600 ~/.ssh/id_rsa -``` - -**Windows:** -```powershell -icacls "$HOME\.ssh\id_rsa" /inheritance:r /grant:r "$($env:USERNAME):F" -``` - -### Use SSH Agent Forwarding Carefully - -SSH agent forwarding (`-A` flag) can be convenient but risky: -- Only use with trusted servers -- Prefer ProxyJump instead when possible - -### Enable Two-Factor Authentication (2FA) - -While SSH keys are secure, enable 2FA on your Azure DevOps account for additional security: -1. Azure DevOps → User Settings → Security → Two-factor authentication -2. Use an authenticator app (Microsoft Authenticator, Google Authenticator) - ---- - -## Additional Resources - -- **Azure DevOps SSH Documentation**: [https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate) -- **SSH Key Best Practices**: [https://security.stackexchange.com/questions/tagged/ssh-keys](https://security.stackexchange.com/questions/tagged/ssh-keys) -- **Git with SSH**: [https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key](https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key) - ---- - -## Quick Reference - -### Common Commands - -```bash -# Generate RSA key -ssh-keygen -t rsa -b 4096 -C "your.email@example.com" - -# Display public key (Linux/Mac) -cat ~/.ssh/id_rsa.pub - -# Display public key (Windows) -type $HOME\.ssh\id_rsa.pub - -# Test SSH connection -ssh -T git@ssh.dev.azure.com - -# Clone with SSH -git clone git@ssh.dev.azure.com:v3/{org}/{project}/{repo} - -# Convert HTTPS to SSH -git remote set-url origin git@ssh.dev.azure.com:v3/{org}/{project}/{repo} - -# Check remote URL -git remote -v -``` - -### SSH URL Format - -``` -git@ssh.dev.azure.com:v3/{organization}/{project}/{repository} -``` - -**Example:** -``` -git@ssh.dev.azure.com:v3/mycompany/git-workshop/great-print-project -``` - ---- - -## Important Note: RSA and Modern SSH Key Algorithms - -**Why This Guide Uses RSA:** - -This guide exclusively uses RSA keys because **Azure DevOps currently only supports RSA SSH keys**. As of January 2026, Azure DevOps does not support modern SSH key algorithms like Ed25519, ECDSA, or other newer formats. - -**About RSA Security:** - -RSA is an older cryptographic algorithm that has been the industry standard for decades. While RSA with 4096-bit keys (as used in this guide) is still considered secure for most use cases, it has some limitations compared to modern alternatives: - -**RSA Drawbacks:** -- **Larger key sizes**: RSA requires 4096 bits for strong security, resulting in larger keys -- **Slower performance**: Key generation and signature operations are slower than modern algorithms -- **Older cryptographic foundation**: Based on mathematical principles from the 1970s -- **More CPU-intensive**: Authentication operations require more computational resources - -**Modern Alternatives (Not Supported by Azure DevOps):** - -If Azure DevOps supported modern algorithms, we would recommend: - -**Ed25519:** -- **Faster**: Significantly faster key generation and authentication -- **Smaller keys**: 256-bit keys (much smaller than RSA 4096-bit) -- **Modern cryptography**: Based on elliptic curve cryptography (ECC) with strong security guarantees -- **Better performance**: Less CPU usage, faster operations -- **Widely supported**: GitHub, GitLab, Bitbucket, and most modern Git platforms support Ed25519 - -**ECDSA:** -- Also based on elliptic curve cryptography -- Faster than RSA but slightly slower than Ed25519 -- Supported by many platforms - -**Current State:** - -RSA with 4096-bit keys remains secure and is acceptable for Git authentication, despite being outdated compared to modern algorithms. The Azure DevOps team has not provided a timeline for supporting Ed25519 or other modern key types. - -**For Other Platforms:** - -If you're using GitHub, GitLab, Bitbucket, or other Git hosting services, we strongly recommend using Ed25519 instead of RSA: - -```bash -# For platforms that support Ed25519 (GitHub, GitLab, Bitbucket, etc.) -ssh-keygen -t ed25519 -C "your.email@example.com" -``` - -**References:** -- [Ed25519 Wikipedia](https://en.wikipedia.org/wiki/EdDSA#Ed25519) -- [SSH Key Algorithm Comparison](https://security.stackexchange.com/questions/5096/rsa-vs-dsa-for-ssh-authentication-keys) -- [Azure DevOps SSH Documentation](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate) - ---- - -**You're all set!** SSH authentication with RSA keys is now configured for secure, passwordless Git operations with Azure DevOps. From bd69774191bc62d4e75cf18ba1ffe0e4c5e83823 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 17:25:33 +0100 Subject: [PATCH 59/61] feat: drastically simplify the multiplayer module --- 01-essentials/08-multiplayer/03_README.md | 1398 ++------------------- 01-essentials/08-multiplayer/04_TASKS.md | 395 ------ 01-essentials/08-multiplayer/numbers.txt | 11 + 3 files changed, 142 insertions(+), 1662 deletions(-) delete mode 100644 01-essentials/08-multiplayer/04_TASKS.md create mode 100644 01-essentials/08-multiplayer/numbers.txt diff --git a/01-essentials/08-multiplayer/03_README.md b/01-essentials/08-multiplayer/03_README.md index ff8a010..62cad92 100644 --- a/01-essentials/08-multiplayer/03_README.md +++ b/01-essentials/08-multiplayer/03_README.md @@ -1,1303 +1,167 @@ -# Module 08: Multiplayer Git - The Number Challenge +# Multiplayer Git -## Learning Objectives +Work with others using branches and pull requests. -By the end of this module, you will: -- Clone and work with remote repositories on a cloud server -- Collaborate with teammates using a shared repository -- Experience push rejections when your local repository is out of sync -- Resolve merge conflicts in a real team environment -- Practice the fundamental push/pull workflow -- Apply all the Git skills you've learned in a collaborative setting +## Goal -## Welcome to Real Collaboration! +Learn to collaborate on a shared repository using: +- **Branches** - work independently without breaking main +- **Pull Requests** - review and merge changes safely -Congratulations on making it this far! You've learned Git basics: committing, branching, merging, and even resolving conflicts solo. But here's where it gets real - **working with actual teammates on a shared codebase**. +## The Workflow -This module is different from all the others. There's no `setup.ps1` script creating a simulated environment. Instead, you'll work with: -- A real Git server: **Azure DevOps** (your facilitator will provide the specific URL) -- Real teammates -- A shared repository where everyone works together -- Real merge conflicts when multiple people edit the same file -- Real push rejections when your local repository falls out of sync +``` +1. Create branch → 2. Make changes → 3. Push branch + ↓ +6. Delete branch ← 5. Merge PR ← 4. Create PR +``` -**This is exactly how professional developers collaborate every day on GitHub, GitLab, Bitbucket, Azure DevOps, and company Git servers.** - -**New to remote Git commands?** Check out [GIT-BASICS.md](./GIT-BASICS.md) for simple explanations of clone, push, pull, and fetch! - -Ready? Let's collaborate! +This is how professional teams work together on code. --- -## The Number Challenge +## Step 1: Clone the Repository -### What You'll Do - -Your team will work together to sort a jumbled list of numbers (0-20) into the correct order. The repository contains a file called `numbers.txt` with numbers in random order: +Get the repository URL from your facilitator, then: +```powershell +git clone +code ``` -17 -3 -12 -8 -19 -... -``` - -**Your goal:** Work as a team to rearrange the numbers so they appear in order from 0 to 20: - -``` -0 -1 -2 -3 -4 -... -20 -``` - -**The rules:** -- Each person moves **ONE number per commit** -- You **MUST pull before making changes** to get the latest version -- **Communicate with your team** - coordination is key! - -**The challenge:** You'll experience merge conflicts when two people edit the file at the same time, and push rejections when your local copy is out of sync with the server. - -### Why This Exercise? - -This exercise teaches collaboration in a safe, structured way: - -1. **Simple task:** Moving a number is easy. The hard part is Git, not the work itself. -2. **Clear success:** You can instantly see when all numbers are sorted. -3. **Guaranteed conflicts:** Multiple people editing the same file creates conflicts to practice resolving. -4. **Push rejections:** You'll experience what happens when your database goes out of sync with the remote. -5. **Team coordination:** Success requires communication and collaboration. -6. **Safe experimentation:** It's okay to make mistakes - you can always pull a fresh copy! - -### Repository Structure - -``` -number-challenge/ -├── numbers.txt # The file everyone edits - contains numbers 0-20 -└── README.md # Quick reference for the challenge -``` - -**Note:** The repository is already set up on the server. You'll clone it and start collaborating! --- -## Prerequisites +## Step 2: Create a Branch -Before starting, ensure you have: +Never work directly on `main`. Create your own branch: -### 1. Your Azure DevOps Account - -Your facilitator will provide: -- **Organization and Project URLs** for the workshop -- **Azure DevOps account credentials** (Microsoft Account or Azure AD) -- **SSH Key Setup** (Recommended - see below) - -**First-time setup:** Visit the Azure DevOps URL provided by your facilitator and sign in to verify your account works. - -### 2. Git Configuration - -Verify your Git identity is configured: - -```bash -git config --global user.name -git config --global user.email +```powershell +git switch -c ``` -If these are empty, set them now: +This creates a new branch and switches to it. -```bash -git config --global user.name "Your Name" -git config --global user.email "your.email@example.com" +--- + +## Step 3: Make Changes + +1. Open `numbers.txt` in VS Code +2. Move one number to its correct position +3. Save the file (`Ctrl+S`) + +--- + +## Step 4: Commit and Push + +```powershell +git add . +git commit -m "fix: move 7 to correct position" +git push ``` -**Why this matters:** Every commit you make will be tagged with this information. +Your branch is now on Azure DevOps. -### 3. Authentication Setup: SSH Keys (Recommended) +--- -**SSH is the best practice for secure Git authentication.** It provides secure, passwordless access to Azure DevOps without exposing credentials. +## Step 5: Create a Pull Request -#### Quick SSH Setup +[Detailed guide](https://learn.microsoft.com/en-us/azure/devops/repos/git/pull-requests?view=azure-devops&tabs=browser#create-a-pull-request) -**If you haven't set up SSH keys yet, follow these steps:** +1. Go to Azure DevOps in your browser +2. Navigate to **Repos** → **Pull Requests** 3. Click **New Pull Request** +4. Set: + - **Source branch:** `` + - **Target branch:** `main` +5. Add a title describing your change +6. Click **Create** -1. **Generate SSH key:** - ```bash - ssh-keygen -t rsa -b 4096 -C "your.email@example.com" - ``` - Press Enter to accept default location, optionally add a passphrase for extra security. +--- - **Note:** Azure DevOps requires RSA keys. See [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md) for details on why we use RSA. +## Step 6: Review and Merge -2. **Copy your public key:** +1. Review the changes shown in the PR (Person B) +2. If everything looks good, click **Complete** +3. Select **Complete merge** +4. Your changes are now in `main` - **Linux/Mac:** - ```bash - cat ~/.ssh/id_rsa.pub - ``` +--- - **Windows PowerShell:** +## Step 7: Update Your Local Main + +After merging, update your local copy: + +```powershell +git switch main +git pull +``` + +--- + +## Step 8: Repeat + +1. Create a new branch for your next change +2. Make changes, commit, push +3. Create another PR +4. Continue until all numbers are sorted + +--- + +## Step 9: Create a merge conflict + +1. Both people should create a branch with changes to `feature-1` and `feature-2`, you task is to change the position of number 5. Where you place it is up to you. +2. Now both people should push their respective branch `git push ` +3. Now merge `feature-1` branch first, going throught the Pull Request flow. +4. Then merge `feature-2` branch second, and notice you'll get a MERGE CONFLICT. +5. It is not the owner of `feature-2` branch to resolve the conflict. This is done by merge the `main` branch into `feature-2` locally and so the owner of `feature-2` has to do the following + ```pwsh + # First get the latest changes on main + git switch main + git pull + + # Then go back to the branch you can from + git switch feature-2 + + # Now we resolve the merge. We're merging the main branch INTO the feature-2 branch. + git merge main + # Resolve the merge conflict in numbers.txt + # Once resolved + git add numbers.txt + git commit + # VSCode will open up with a default message of "Merge main into feature-2" + # finish the commit. And push the changes + git push + ``` +6. Now the owner of `feature-2` can checkout the pull request on azure again and see that the merge conflict has been resolved and can therefore "Complete" the merge request, using the button in the top right corner with the name "Complete" + +## Quick Reference + +| Command | What It Does | +|---------|--------------| +| `git switch -c ` | Create and switch to new branch | +| `git push -u origin ` | Push branch to Azure DevOps | +| `git switch main` | Switch to main branch | +| `git pull` | Get latest changes from remote | + +--- + +## Common Issues + +### "My PR has conflicts" +1. Update your branch with latest main: ```powershell - type $HOME\.ssh\id_rsa.pub + git switch main + git pull + git switch + git merge main ``` +2. Resolve conflicts in VS Code +3. Commit and push again -3. **Add to Azure DevOps:** - - Sign in to Azure DevOps - - Click your profile icon (top-right) → **User settings** - - Select **SSH Public Keys** - - Click **+ New Key** - - Paste your public key and give it a name (e.g., "Workshop Laptop 2026") - - Click **Save** - - ![Azure DevOps - SSH Public Keys](./images/azure-devops-ssh-keys.png) - *Navigate to User Settings → SSH Public Keys to add your SSH key* - -4. **Test your SSH connection:** - ```bash - ssh -T git@ssh.dev.azure.com - ``` - - Expected output: `remote: Shell access is not supported.` - This is normal and means authentication worked! - -**For detailed SSH setup instructions including troubleshooting, see:** [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md) - -#### Alternative: HTTPS with Personal Access Token (PAT) - -If you cannot use SSH (firewall restrictions, etc.), you can use HTTPS with a Personal Access Token: - -1. Sign in to Azure DevOps -2. Click your profile icon → **Personal access tokens** -3. Click **+ New Token** -4. Give it a name, set expiration, and select **Code (Read & Write)** scope -5. Click **Create** and **copy the token** (you won't see it again!) -6. Use the token as your password when Git prompts for credentials - -**Note:** SSH is recommended for security and convenience. With SSH, you won't need to enter credentials for every push/pull. - ---- - -## Part 1: Getting Started (15 minutes) - -### Step 1: Get Ready - -Your facilitator will explain the exercise. Everyone on the team will work together on the same repository. - -**Important:** Everyone will work on the **same branch (`main`)**. This simulates real team development where multiple developers collaborate on a shared codebase. - -### Step 2: Understand the Exercise - -Your team will collaborate to sort the numbers in `numbers.txt` from 0 to 20. - -**The approach:** -- Everyone works on the same file (`numbers.txt`) -- Everyone works on the same branch (`main`) -- Each person moves one number per commit -- Communication is key to avoid too many conflicts! - -### Step 3: Clone the Repository - -Your facilitator will provide the exact repository URL. The format depends on your authentication method: - -**Using SSH (Recommended):** - -```bash -# Replace {organization}, {project}, and {repository} with values from your facilitator -git clone git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge -cd number-challenge -``` - -**Example:** -```bash -git clone git@ssh.dev.azure.com:v3/workshoporg/git-workshop/number-challenge -cd number-challenge -``` - -**Using HTTPS (with PAT):** - -```bash -# Replace {organization} and {project} with values from your facilitator -git clone https://dev.azure.com/{organization}/{project}/_git/number-challenge -cd number-challenge -``` - -**Note:** Use the exact URL provided by your facilitator to ensure you're cloning the correct repository. - -**Expected output:** - -``` -Cloning into 'number-challenge'... -remote: Enumerating objects: 10, done. -remote: Counting objects: 100% (10/10), done. -remote: Compressing objects: 100% (7/7), done. -remote: Total 10 (delta 2), reused 0 (delta 0), pack-reused 0 -Receiving objects: 100% (10/10), done. -Resolving deltas: 100% (2/2), done. -``` - -Success! You now have a local copy of the shared repository. - -### Step 4: Explore the Repository - -Let's see what we're working with: - -```bash -# List files -ls -la - -# View the numbers file -cat numbers.txt -``` - -**What you'll see in `numbers.txt`:** +### "I need to make more changes to my PR" +Just commit and push to the same branch - the PR updates automatically: +```powershell +git add . +git commit -m "fix: address review feedback" +git push ``` -17 -3 -12 -8 -19 -1 -14 -6 -11 -0 -20 -9 -4 -16 -2 -18 -7 -13 -5 -15 -10 -``` - -The numbers are all jumbled up! Your team's goal is to sort them from 0 to 20. - -### Step 5: Understanding the Workflow - -**For this exercise, everyone works on the `main` branch together.** - -Unlike typical Git workflows where you create feature branches, this exercise intentionally has everyone work on the same branch to experience: -- **Push rejections** when someone else pushed before you -- **Merge conflicts** when two people edit the same line -- **The pull-before-push cycle** that's fundamental to Git collaboration - -**There's no need to create a branch** - you'll work directly on `main` after cloning. - ---- - -## Part 2: Your First Contribution (20 minutes) - -Now you'll practice the basic collaborative workflow: make a change, commit, push, and help others pull your changes. - -### Step 1: Decide Who Goes First - -Pick someone to go first. They'll move one number to its correct position. - -### Step 2: First Person - Move ONE Number - -**First person:** Open `numbers.txt` in your text editor. - -Look at the file and find a number that's in the wrong position. Let's say you decide to move the `0` to the top. - -**Before:** -``` -17 -3 -12 -8 -19 -1 -14 -6 -11 -0 ← Let's move this to the top! -20 -... -``` - -**After editing:** -``` -0 ← Moved to the top! -17 -3 -12 -8 -19 -1 -14 -6 -11 -20 -... -``` - -**Key point:** Move ONLY ONE number per commit. This keeps things simple and helps everyone track changes. - -### Step 3: Commit Your Change - -```bash -git status -# You should see: modified: numbers.txt - -git add numbers.txt -git commit -m "Move 0 to its correct position" -``` - -**Expected output:** - -``` -[main abc1234] Move 0 to its correct position - 1 file changed, 1 insertion(+), 1 deletion(-) -``` - -### Step 4: Push to Remote - -This uploads your commit to the shared server: - -```bash -git push origin main -``` - -**Expected output:** - -``` -Enumerating objects: 5, done. -Counting objects: 100% (5/5), done. -Delta compression using up to 8 threads -Compressing objects: 100% (3/3), done. -Writing objects: 100% (3/3), 345 bytes | 345.00 KiB/s, done. -Total 3 (delta 1), reused 0 (delta 0), pack-reused 0 -remote: Analyzing objects... (100%) (3/3) (X ms) -remote: Storing packfile... done (X ms) -remote: Storing index... done (X ms) -To ssh.dev.azure.com:v3/{organization}/{project}/number-challenge - abc1234..def5678 main -> main -``` - -Success! Your change is now on the server for everyone to see. - -### Step 5: Others - Pull the Change - -**Everyone else:** Make sure you get the latest changes: - -```bash -# Pull the changes from the server -git pull origin main -``` - -**Expected output:** - -``` -From ssh.dev.azure.com:v3/{organization}/{project}/number-challenge - * branch main -> FETCH_HEAD -Updating 123abc..456def -Fast-forward - numbers.txt | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) -``` - -**Verify you have the update:** - -```bash -cat numbers.txt -# Should show 0 at the top -``` - -You now have the first person's change! - -### Step 6: Next Person's Turn - -**Next person:** Now it's your turn! Pick a different number and move it to its correct position. - -Follow the same cycle: -1. Pull first: `git pull origin main` (always get the latest!) -2. Edit `numbers.txt` - move ONE number -3. Commit: `git add numbers.txt && git commit -m "Move X to correct position"` -4. Push: `git push origin main` -5. Tell the team you pushed! - -### Step 7: Keep Taking Turns - -Continue this pattern: -- **Always pull before editing** -- Move one number per person -- Commit with a clear message -- Push to share with the team -- Communicate when you've pushed - -**As you work, the file gradually becomes more sorted:** - -``` -After a few rounds: -0 -1 -2 -3 -17 ← Still needs to be moved -12 ← Still needs to be moved -... -``` - -**Congratulations! You've completed your first collaborative Git workflow!** You've learned the core cycle: pull → work → commit → push. This is what professional developers do hundreds of times per day. - ---- - -## Part 3: Deliberate Conflict Exercise (30 minutes) - -Now for the **real** learning: merge conflicts! You'll deliberately create a conflict, then resolve it together. - -### The Scenario - -Merge conflicts happen when two people edit the same lines in the same file. Git can't automatically decide which version to keep, so it asks you to resolve it manually. - -**What you'll do:** -1. Two people will BOTH edit the same line in `numbers.txt` -2. Person A pushes first (succeeds) -3. Person B tries to push (gets rejected!) -4. Person B pulls (sees conflict markers) -5. You resolve the conflict together -6. Person B pushes the resolution - -This is a **deliberate practice** scenario. In real projects, conflicts happen by accident - now you'll know how to handle them! - -### Setup: Choose Two People - -Pick two people to create the conflict. Let's call them **Person A** and **Person B**. - -Everyone else can watch and learn - you'll create your own conflicts later! - -### Step 1: Both People Start Fresh - -Make sure you both have the latest code: - -```bash -git pull origin main - -# Check status - should be clean -git status -``` - -Both people should see: "Your branch is up to date with 'origin/main'" and "nothing to commit, working tree clean" - -### Step 2: Person A - Make Your Change First - -**Person A:** Open `numbers.txt` and move a specific number. Let's say you decide to move `17` down a few lines. - -**Before:** -``` -17 ← Let's move this -3 -12 -8 -``` - -**After (Person A's version):** -``` -3 -17 ← Moved here -12 -8 -``` - -**Commit and push IMMEDIATELY:** - -```bash -git add numbers.txt -git commit -m "Person A: Move 17 down" -git push origin main -``` - -Person A should see the push succeed. - -### Step 3: Person B - Make DIFFERENT Change (DON'T PULL YET!) - -**Person B:** This is critical - do NOT pull Person A's changes yet! - -Instead, edit the SAME lines with a DIFFERENT change. Move `17` to a different position: - -**Before:** -``` -17 ← Let's move this somewhere else -3 -12 -8 -``` - -**After (Person B's version):** -``` -3 -12 -17 ← Moved to a different position than Person A! -8 -``` - -**Commit (but don't push yet):** - -```bash -git add numbers.txt -git commit -m "Person B: Move 17 down" -``` - -### Step 4: Person B - Try to Push (This Will Fail!) - -```bash -git push origin main -``` - -**You'll see an error like this:** - -``` -To ssh.dev.azure.com:v3/{organization}/{project}/number-challenge - ! [rejected] main -> main (fetch first) -error: failed to push some refs to 'git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge' -hint: Updates were rejected because the remote contains work that you do -hint: not have locally. This is usually caused by another repository pushing -hint: to the same ref. You may want to first integrate the remote changes -hint: (e.g., 'git pull ...') before pushing again. -hint: See the 'Note about fast-forwards' in 'git push --help' for details. -``` - -**Don't panic!** This is completely normal and expected. Git is protecting you from overwriting Person A's work. - -**What happened:** Person A pushed commits that you don't have. Git requires you to pull first and integrate their changes before you can push yours. - -### Step 5: Person B - Pull and See the Conflict - -```bash -git pull origin main -``` - -**You'll see:** - -``` -From ssh.dev.azure.com:v3/{organization}/{project}/number-challenge - * branch main -> FETCH_HEAD -Auto-merging numbers.txt -CONFLICT (content): Merge conflict in numbers.txt -Automatic merge failed; fix conflicts and then commit the result. -``` - -**This is a merge conflict!** Git tried to merge Person A's changes with yours, but couldn't automatically combine them because you both edited the same lines. - -### Step 6: Check Git Status - -```bash -git status -``` - -**Output:** - -``` -On branch main -You have unmerged paths. - (fix conflicts and run "git commit") - (use "git merge --abort" to abort the merge) - -Unmerged paths: - (use "git add ..." to mark resolution) - both modified: numbers.txt - -no changes added to commit (use "git add" and/or "git commit -a") -``` - -Git is telling you: "The file `numbers.txt` has conflicts. Both of you modified it. Please resolve and commit." - -### Step 7: Open the Conflicted File - -```bash -cat numbers.txt -# Or open in your text editor: code numbers.txt, vim numbers.txt, nano numbers.txt -``` - -**Find the conflict markers:** - -``` -3 -<<<<<<< HEAD -12 -17 -======= -17 -12 ->>>>>>> abc1234567890abcdef1234567890abcdef12 -8 -``` - -### Understanding Conflict Markers - -``` -<<<<<<< HEAD # Start marker -12 # YOUR version (Person B's order) -17 -======= # Divider -17 # THEIR version (Person A's order from remote) -12 ->>>>>>> abc1234... # End marker (shows commit hash) -``` - -**The three sections:** -1. `<<<<<<< HEAD` to `=======`: Your current changes (what you committed locally) -2. `=======` to `>>>>>>>`: Their changes (what Person A pushed to the server) -3. You must choose one, combine them, or write something new - -### Step 8: Resolve the Conflict TOGETHER - -**Talk with the group!** Look at both versions and decide which order makes sense. - -**Person A's version:** -``` -3 -17 -12 -8 -``` - -**Person B's version:** -``` -3 -12 -17 -8 -``` - -**Decide together:** Which is closer to the correct sorted order (0-20)? Or is there a better way? - -**Edit the file to:** -1. Remove ALL conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) -2. Keep the agreed-upon order -3. Make sure the file is clean - -**Example resolved version:** - -``` -3 -12 -17 -8 -``` - -**Save the file!** - -### Step 9: Verify the Resolution - -```bash -cat numbers.txt -``` - -Make sure you don't see any conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). The file should just contain numbers. - -### Step 10: Mark as Resolved and Commit - -Tell Git you've resolved the conflict: - -```bash -git add numbers.txt -``` - -This stages the resolved file. - -Now commit the resolution: - -```bash -git commit -m "Resolve conflict - agreed on number order" -``` - -**Note:** Git may open an editor with a default merge commit message. You can keep it or customize it. - -**Expected output:** - -``` -[main def5678] Resolve conflict - agreed on number order -``` - -### Step 11: Person B - Push the Resolution - -```bash -git push origin main -``` - -This time it should succeed! The conflict is resolved and the agreed-upon order is on the server. - -### Step 12: Everyone - Pull the Resolved Version - -**Everyone else:** Get the resolved version: - -```bash -git pull origin main -``` - -Check the file - you should see the agreed-upon resolution. - -**You've successfully resolved your first merge conflict together!** In real projects, this is a daily occurrence. You now know exactly what to do when you see those conflict markers. - ---- - -## Part 4: Continue Sorting (Until Complete) - -Now that you understand the pull-push cycle and how to resolve conflicts, continue working as a team to sort all the numbers! - -### The Goal - -Keep working until `numbers.txt` contains all numbers sorted from 0 to 20: - -``` -0 -1 -2 -3 -4 -5 -... -18 -19 -20 -``` - -### The Workflow - -Continue the pattern you've learned: - -1. **Pull first:** Always start with `git pull origin main` -2. **Check the file:** Look at `numbers.txt` to see what still needs sorting -3. **Move one number:** Edit the file to move ONE number closer to its correct position -4. **Commit:** `git add numbers.txt && git commit -m "Move X to position Y"` -5. **Push:** `git push origin main` -6. **Communicate:** Tell the team you've pushed so they can pull -7. **Repeat!** - -### Tips for Success - -**Coordinate with your team:** -- Decide who goes next to avoid too many conflicts -- Call out what number you're working on -- Pull frequently to stay in sync - -**Handle conflicts calmly:** -- If you get a push rejection, don't panic - just pull first -- If you get a merge conflict, work through it together -- Remember: conflicts are normal and you know how to resolve them! - -**Check your progress:** -- View the file regularly: `cat numbers.txt` -- Count how many numbers are in the right position -- Celebrate as the file gets more sorted! - -### When You're Done - -When all numbers are sorted correctly, verify your success: - -```bash -git pull origin main -cat numbers.txt -``` - -**Expected final result:** -``` -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -``` - -**Congratulations!** Your team successfully collaborated using Git to complete the challenge! - ---- - -## Part 5: What You've Learned - -You've now experienced the fundamental Git collaboration workflow that professional developers use every day! - ---- - -## Commands Reference - -### Essential Git Commands for Collaboration - -**Cloning and Setup:** -```bash -git clone # Create local copy of remote repo -git config user.name "Your Name" # Set your name (one-time setup) -git config user.email "your@email.com" # Set your email (one-time setup) -``` - -**Branching:** -```bash -git switch -c # Create and switch to new branch -git switch # Switch to existing branch -git branch # List local branches (* = current) -git branch -a # List all branches (local + remote) -git branch -d # Delete branch (safe - prevents data loss) -``` - -**Making Changes:** -```bash -git status # See current state and changed files -git add # Stage specific file -git add . # Stage all changed files -git commit -m "message" # Commit staged changes with message -git commit # Commit and open editor for message -``` - -**Synchronizing with Remote:** -```bash -git pull origin # Fetch and merge from remote branch -git push origin # Push commits to remote branch -git push -u origin # Push and set upstream tracking -git fetch origin # Download changes without merging -git remote -v # Show configured remotes -``` - -**Conflict Resolution:** -```bash -git status # See which files have conflicts -# Edit files to remove <<<<<<, =======, >>>>>>> markers -git add # Mark file as resolved -git commit -m "Resolve conflict in ..." # Commit the resolution -git merge --abort # Abort merge and go back to before -``` - -**Merging and Integration:** -```bash -git merge # Merge branch into current branch -git merge main # Common: merge main into feature branch -git log --oneline --graph --all # Visualize branch history -``` - -**Viewing Changes:** -```bash -git diff # See unstaged changes -git diff --staged # See staged changes -git show # Show last commit -git log --oneline # See commit history (concise) -git log --oneline --graph # See branch structure visually -``` - ---- - -## Common Scenarios & Solutions - -### "My push was rejected!" - -**Error:** -``` -! [rejected] main -> main (fetch first) -error: failed to push some refs to 'git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge' -``` - -**What it means:** Someone else pushed commits to the branch since you last pulled. - -**Solution:** -```bash -# Pull their changes first -git pull origin main - -# If conflicts, resolve them (see Part 3) -# If no conflicts, you can now push -git push origin main -``` - ---- - -### "I have merge conflicts!" - -**What you see:** -``` -CONFLICT (content): Merge conflict in numbers.txt -Automatic merge failed; fix conflicts and then commit the result. -``` - -**Solution:** -1. Don't panic - this is normal! -2. Run `git status` to see which files have conflicts -3. Open `numbers.txt` in your editor -4. Find the conflict markers: `<<<<<<<`, `=======`, `>>>>>>>` -5. **Talk with your team** - decide which version makes sense -6. Remove ALL markers and keep the agreed order -7. Verify: `cat numbers.txt` (make sure no markers remain!) -8. Stage: `git add numbers.txt` -9. Commit: `git commit -m "Resolve conflict in numbers.txt"` -10. Push: `git push origin main` - ---- - -### "Someone else pushed, how do I get their changes?" - -**Solution:** -```bash -# Pull the latest changes -git pull origin main -``` - -If you have uncommitted changes, Git might ask you to commit or stash first: -```bash -# Option 1: Commit your changes first -git add numbers.txt -git commit -m "Move number X" -git pull origin main - -# Option 2: Stash your changes temporarily -git stash -git pull origin main -git stash pop # Restore your changes after pull -``` - ---- - -### "Two people edited the same numbers!" - -**This creates a conflict - which is exactly what we want to practice!** - -**Solution:** -Follow the complete conflict resolution workflow from Part 3. The key steps: -1. Person who tries to push second gets rejection -2. They pull (sees conflict) -3. Everyone looks at the conflict markers together -4. Decide which version to keep (or create new version) -5. Remove markers, verify the file, commit, push - ---- - -### "How do I see what changed?" - -**Before committing:** -```bash -git diff # See unstaged changes -git diff --staged # See staged changes (after git add) -``` - -**After committing:** -```bash -git show # Show last commit's changes -git log --oneline # See commit history (one line per commit) -git log --oneline --graph # See branch structure with commits -``` - ---- - -### "I want to start over on this file!" - -**Scenario:** You made a mess and want to restore the file to the last committed version. - -**Solution:** -```bash -# Discard all changes to the file (CAREFUL: can't undo this!) -git restore numbers.txt - -# Or restore to a specific commit -git restore --source=abc1234 numbers.txt -``` - -**If you want to keep your changes but try a different approach:** -```bash -# Save your work temporarily -git stash - -# Work is saved, file is back to clean state -# Later, restore your work: -git stash pop -``` - ---- - -### "I accidentally deleted all the numbers!" - -**Don't worry - Git has your back!** - -**Solution:** -```bash -# If you haven't committed the deletion: -git restore numbers.txt - -# If you already committed the deletion but haven't pushed: -git log --oneline # Find the commit before deletion -git reset --hard abc1234 # Replace abc1234 with the good commit hash - -# If you already pushed: -# Ask your facilitator for help, or let someone else pull and fix it! -``` - ---- - -## Troubleshooting - -### Authentication Issues - -**Problem:** "Authentication failed" or "Permission denied" when pushing or pulling - -**Solution (SSH - Recommended):** - -1. **Verify your SSH key is added to Azure DevOps:** - - Sign in to Azure DevOps - - User Settings (profile icon) → SSH Public Keys - - Confirm your key is listed - -2. **Test SSH connection:** - ```bash - ssh -T git@ssh.dev.azure.com - ``` - - Expected: `remote: Shell access is not supported.` (This is normal!) - - If you get "Permission denied (publickey)": - - Your SSH key is not added or Azure DevOps can't find it - - See [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md) for detailed troubleshooting - -3. **Check your remote URL uses SSH:** - ```bash - git remote -v - ``` - - Should show: `git@ssh.dev.azure.com:v3/...` (not `https://`) - - If it shows HTTPS, switch to SSH: - ```bash - git remote set-url origin git@ssh.dev.azure.com:v3/{organization}/{project}/number-challenge - ``` - -**Solution (HTTPS with PAT):** - -1. **Verify you're using a Personal Access Token** (not your account password) - - Azure DevOps → User Settings → Personal access tokens - - Create new token with **Code (Read & Write)** scope - - Use token as password when Git prompts for credentials - -2. **Check token permissions:** - - Token must have **Code (Read & Write)** scope - - Verify token hasn't expired - -3. **Update stored credentials** (if cached incorrectly): - - **Windows:** - ```powershell - git credential-manager erase https://dev.azure.com - ``` - - **Mac:** - ```bash - git credential-osxkeychain erase https://dev.azure.com - ``` - - **Linux:** - ```bash - git config --global --unset credential.helper - ``` - -**Recommendation:** Use SSH to avoid credential management issues. See [AZURE-DEVOPS-SSH-SETUP.md](../../AZURE-DEVOPS-SSH-SETUP.md). - ---- - -### Can't Pull or Push - "Unrelated Histories" - -**Problem:** -``` -fatal: refusing to merge unrelated histories -``` - -**What happened:** Your local branch and remote branch don't share a common ancestor (rare, but happens if branches were created independently). - -**Solution:** -```bash -git pull origin main --allow-unrelated-histories -``` - -Then resolve any conflicts if they appear. - ---- - -### Accidentally Deleted Numbers - -**Problem:** "I deleted numbers and committed it!" - -**Solution:** - -**If not pushed yet:** -```bash -# Find the commit before deletion -git log --oneline - -# Example: abc1234 was the last good commit -git reset --hard abc1234 -``` - -**If already pushed:** -```bash -# Find the commit with the correct numbers -git log --oneline - -# Restore the file from that commit -git checkout abc1234 -- numbers.txt - -# Commit the restoration -git add numbers.txt -git commit -m "Restore accidentally deleted numbers" -git push origin main -``` - -**Pro tip:** Use `git log --all --full-history -- numbers.txt` to see all commits that touched that file. - ---- - -### File Still Has Conflict Markers - -**Problem:** You thought you resolved the conflict, but when you look at the file: -``` -3 -<<<<<<< HEAD -12 -17 -======= -17 -12 ->>>>>>> abc1234 -8 -``` - -**What happened:** You forgot to remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`). - -**Solution:** -```bash -# Open the file -nano numbers.txt # or vim, code, etc. - -# Search for "<<<<<<<" and remove ALL markers -# Keep only the numbers you want - -# Verify it's clean -cat numbers.txt - -# If it looks good, commit the fix -git add numbers.txt -git commit -m "Remove remaining conflict markers" -``` - ---- - -## Success Criteria - -You've completed this module when you can check off ALL of these: - -**Basic Collaboration:** -- [ ] Cloned the repository from Azure DevOps using SSH -- [ ] Successfully pushed at least one commit to the shared repository -- [ ] Successfully pulled changes from other team members -- [ ] Contributed to sorting the numbers file - -**Conflict Resolution:** -- [ ] Experienced or witnessed a merge conflict -- [ ] Saw the conflict markers in the file (`<<<<<<<`, `=======`, `>>>>>>>`) -- [ ] Resolved a conflict (or helped someone resolve one) -- [ ] Successfully pushed the resolution - -**Push/Pull Cycle:** -- [ ] Experienced a push rejection when someone else pushed first -- [ ] Understood why the push was rejected -- [ ] Pulled changes before pushing again -- [ ] Successfully pushed after pulling - -**Final Result:** -- [ ] The `numbers.txt` file contains all numbers from 0 to 20 in sorted order -- [ ] No conflict markers remain in the file -- [ ] Everyone on the team contributed at least one commit - -**Bonus (if time permits):** -- [ ] Helped someone else resolve a conflict -- [ ] Created multiple merge conflicts and resolved them -- [ ] Experimented with `git stash` when needing to pull with local changes - ---- - -## What You've Learned - -**Collaborative Git Skills:** -- ✅ Cloning repositories from remote Git servers -- ✅ Working with teammates on a shared repository -- ✅ The push/pull cycle for synchronizing work -- ✅ Experiencing and resolving real merge conflicts -- ✅ Understanding push rejections and why they happen -- ✅ Communicating with teammates during collaborative work -- ✅ Using Git in a realistic team environment -- ✅ SSH authentication for secure Git operations - -**Real-World Applications:** - -**These skills are exactly what you'll use at work:** -- This workflow is identical across GitHub, GitLab, Bitbucket, Azure DevOps, and any Git server -- Professional teams do this hundreds of times per day -- Understanding merge conflicts is critical for team collaboration -- Push rejections happen constantly in real teams - you now know how to handle them -- SSH authentication is the industry standard for secure Git operations - -**You're now ready to:** -- Contribute to open source projects on GitHub -- Join a development team and collaborate effectively -- Handle merge conflicts without panic -- Understand the fundamental push/pull workflow -- Work on distributed teams across time zones - ---- - -## What's Next? - -### More Advanced Git Modules - -Continue your Git journey with advanced techniques: - -- **02-advanced/01-rebasing**: Learn to rebase instead of merge for cleaner history -- **02-advanced/02-interactive-rebase**: Clean up messy commits before submitting PRs -- **02-advanced/03-worktrees**: Work on multiple branches simultaneously -- **02-advanced/04-bisect**: Find bugs using binary search through commit history -- **02-advanced/05-blame**: Investigate who changed what and when -- **02-advanced/06-merge-strategies**: Master different merge strategies and when to use them - -### Practice More - -- Try contributing to a real open source project on GitHub -- Practice more complex workflows (multiple feature branches, rebasing, etc.) -- Help teammates at work or school with Git issues - ---- - -## Congratulations! - -**You've completed the Multiplayer Git module!** - -You started this workshop learning basic Git commands like `git init` and `git commit`. Now you're collaborating with teammates, resolving conflicts, and handling push rejections like a professional developer. - -**What makes you different from most beginners:** -- You've experienced REAL merge conflicts and resolved them -- You've worked on a REAL shared repository with teammates -- You've experienced REAL push rejections and learned how to handle them -- You've practiced the entire workflow professionals use daily - -**Most importantly:** You're no longer afraid of merge conflicts or push rejections. You know exactly what to do when you see those `<<<<<<<` markers or get a "rejected" error. - -**Keep practicing, keep collaborating, and welcome to the world of professional Git!** - ---- - -**Happy Collaborating!** diff --git a/01-essentials/08-multiplayer/04_TASKS.md b/01-essentials/08-multiplayer/04_TASKS.md deleted file mode 100644 index b682bdb..0000000 --- a/01-essentials/08-multiplayer/04_TASKS.md +++ /dev/null @@ -1,395 +0,0 @@ -# Multiplayer Git Tasks - -These tasks walk you through collaborating with Git in the cloud. You'll clone a shared repository, make changes, and sync with your teammates. - -## Prerequisites - -Before starting, make sure you have: -- [ ] An account on the team's Azure DevOps project -- [ ] SSH key configured (ask your facilitator if you need help) -- [ ] Git installed on your computer - ---- - -## Task 1: Clone the Repository - -Cloning creates a local copy of a remote repository on your computer. - -### Steps - -1. Get the SSH URL from Azure DevOps: - - Navigate to the repository - - Click **Clone** - - Select **SSH** - - Copy the URL - -2. Open PowerShell and run: - ```powershell - git clone - ``` - -3. Open the folder in VS Code: - ```powershell - code - ``` - -4. Open the VS Code terminal (`` Ctrl+` ``) and verify the clone worked: - ```powershell - git status - git log --oneline --graph --all - ``` - -### What Just Happened? - -``` -Azure DevOps Your Computer -┌─────────────┐ ┌─────────────┐ -│ Repository │ ───── clone ──> │ Repository │ -│ (original) │ │ (copy) │ -└─────────────┘ └─────────────┘ -``` - -You now have: -- A complete copy of all files -- The entire commit history -- A connection back to the original (called "origin") - ---- - -## Task 2: Make Changes and Push - -Pushing sends your local commits to the remote repository. - -### Steps - -1. In VS Code, create a new file: - - Click **File → New File** (or `Ctrl+N`) - - Add some content, for example: `Hello from ` - - Save as `hello-.txt` (use `Ctrl+S`) - -2. In the VS Code terminal, stage and commit your change: - ```powershell - git add . - git commit -m "feat: add greeting from " - ``` - -3. Push to the remote: - ```powershell - git push - ``` - -### What Just Happened? - -``` -Your Computer Azure DevOps -┌─────────────┐ ┌─────────────┐ -│ Commit A │ │ Commit A │ -│ Commit B │ ───── push ───> │ Commit B │ -│ Commit C │ (new!) │ Commit C │ -└─────────────┘ └─────────────┘ -``` - -Your new commit is now on the server. Others can see it and download it. - ---- - -## Task 3: Pull Changes from Others - -Pulling downloads new commits from the remote and merges them into your branch. - -### Steps - -1. Check if there are new changes: - ```powershell - git status - ``` - Look for "Your branch is behind..." - -2. Pull the changes: - ```powershell - git pull - ``` - -3. See what's new: - ```powershell - git log --oneline -10 - ``` - -### What Just Happened? - -``` -Azure DevOps Your Computer -┌─────────────┐ ┌─────────────┐ -│ Commit A │ │ Commit A │ -│ Commit B │ │ Commit B │ -│ Commit C │ ───── pull ───> │ Commit C │ -│ Commit D │ (new!) │ Commit D │ -└─────────────┘ └─────────────┘ -``` - -Your local repository now has all the commits from the remote. - ---- - -## Task 4: The Push-Pull Dance - -When working with others, you'll often need to pull before you can push. - -### The Scenario - -You made a commit, but someone else pushed while you were working: - -``` -Azure DevOps: A ── B ── C ── D (teammate's commit) -Your Computer: A ── B ── C ── E (your commit) -``` - -### Steps - -1. Try to push: - ```powershell - git push - ``` - This will fail with: "Updates were rejected because the remote contains work that you do not have locally" - -2. Pull first: - ```powershell - git pull - ``` - -3. Now push: - ```powershell - git push - ``` - -### What Happened? - -``` -Before pull: - Remote: A ── B ── C ── D - Local: A ── B ── C ── E - -After pull (Git merges automatically): - Local: A ── B ── C ── D ── M - \ / - E ───┘ - -After push: - Remote: A ── B ── C ── D ── M - \ / - E ───┘ -``` - ---- - -## Task 5: Understanding Fetch - -Fetch downloads changes but does **not** merge them. This lets you see what's new before deciding what to do. - -### Steps - -1. Fetch updates from the remote: - ```powershell - git fetch - ``` - -2. See what's different: - ```powershell - git log HEAD..origin/main --oneline - ``` - This shows commits on the remote that you don't have locally. - -3. When ready, merge: - ```powershell - git merge origin/main - ``` - -### Fetch vs Pull - -| Command | Downloads | Merges | Safe to run anytime? | -|---------|-----------|--------|----------------------| -| `git fetch` | Yes | No | Yes | -| `git pull` | Yes | Yes | Usually | - -**Think of it this way:** -- `fetch` = "Show me what's new" -- `pull` = "Give me what's new" (same as `fetch` + `merge`) - ---- - -## Task 6: Working with Branches - -Branches let you work on features without affecting the main code. - -### Steps - -1. Create and switch to a new branch: - ```powershell - git switch -c feature/-greeting - ``` - -2. In VS Code, create a new file: - - Click **File → New File** (or `Ctrl+N`) - - Add some content, for example: `A special greeting` - - Save as `special.txt` (use `Ctrl+S`) - -3. Stage and commit: - ```powershell - git add . - git commit -m "feat: add special greeting" - ``` - -4. Push your branch to the remote: - ```powershell - git push -u origin feature/-greeting - ``` - The `-u` flag sets up tracking so future pushes are simpler. - -5. Go back to main: - ```powershell - git switch main - ``` - ---- - -## Task 7: The Number Challenge - -This is the main collaborative exercise. Your team will work together to sort numbers 0-20 into the correct order. - -### The Setup - -The repository contains a file called `numbers.txt` with numbers 0-20 in random order: - -``` -17 -3 -12 -8 -... -``` - -Your goal: Work as a team to rearrange the numbers so they appear in order from 0 to 20. - -### The Rules - -1. **Each person moves ONE number per commit** -2. **You must pull before making changes** -3. **Communicate with your team** - decide who moves which number - -### Steps - -1. Pull the latest changes: - ```powershell - git pull - ``` - -2. Open `numbers.txt` in VS Code - -3. Find a number that's out of place and move it to the correct position - - For example, if `5` is at the bottom, move it between `4` and `6` - -4. Save the file (`Ctrl+S`) - -5. Commit your change with a clear message: - ```powershell - git add numbers.txt - git commit -m "fix: move 5 to correct position" - ``` - -6. Push your change: - ```powershell - git push - ``` - -7. If push fails (someone else pushed first): - ```powershell - git pull - ``` - Resolve any conflicts, then push again. - -8. Repeat until all numbers are in order! - -### Handling Conflicts - -When two people edit the same part of the file, you'll see conflict markers: - -``` -<<<<<<< HEAD -4 -5 -6 -======= -4 -6 ->>>>>>> origin/main -``` - -To resolve: -1. Decide what the correct order should be -2. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) -3. Keep only the correct content: - ``` - 4 - 5 - 6 - ``` -4. Save, commit, and push - -### Success - -When complete, `numbers.txt` should look like: - -``` -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -``` - -Celebrate with your team! - ---- - -## Quick Reference - -| Command | What It Does | -|---------|--------------| -| `git clone ` | Download a repository | -| `git push` | Upload your commits | -| `git pull` | Download and merge commits | -| `git fetch` | Download commits (don't merge) | -| `git switch -c ` | Create and switch to a branch | -| `git push -u origin ` | Push a new branch | - ---- - -## Common Issues - -### "Permission denied (publickey)" -Your SSH key isn't set up correctly. See the SSH setup guide or ask your facilitator. - -### "Updates were rejected" -Someone pushed before you. Run `git pull` first, then `git push`. - -### "Merge conflict" -Two people edited the same lines. See BEST-PRACTICES.md for how to handle this. - -### "There is no tracking information" -Run `git push -u origin ` to set up tracking. diff --git a/01-essentials/08-multiplayer/numbers.txt b/01-essentials/08-multiplayer/numbers.txt new file mode 100644 index 0000000..b033488 --- /dev/null +++ b/01-essentials/08-multiplayer/numbers.txt @@ -0,0 +1,11 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 From 356b6268babcb2d8571a6377b2cc1550c4d68b51 Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 17:27:35 +0100 Subject: [PATCH 60/61] refactor: remove advanced for now --- 02-advanced/01-reset/README.md | 717 ------------------- 02-advanced/01-reset/reset.ps1 | 24 - 02-advanced/01-reset/setup.ps1 | 359 ---------- 02-advanced/01-reset/verify.ps1 | 231 ------ 02-advanced/02-rebasing/README.md | 130 ---- 02-advanced/02-rebasing/reset.ps1 | 22 - 02-advanced/02-rebasing/setup.ps1 | 123 ---- 02-advanced/02-rebasing/verify.ps1 | 154 ---- 02-advanced/03-interactive-rebase/README.md | 159 ---- 02-advanced/03-interactive-rebase/reset.ps1 | 22 - 02-advanced/03-interactive-rebase/setup.ps1 | 133 ---- 02-advanced/03-interactive-rebase/verify.ps1 | 172 ----- 02-advanced/04-worktrees/README.md | 208 ------ 02-advanced/04-worktrees/reset.ps1 | 22 - 02-advanced/04-worktrees/setup.ps1 | 123 ---- 02-advanced/04-worktrees/verify.ps1 | 165 ----- 02-advanced/05-bisect/README.md | 237 ------ 02-advanced/05-bisect/reset.ps1 | 22 - 02-advanced/05-bisect/setup.ps1 | 311 -------- 02-advanced/05-bisect/verify.ps1 | 133 ---- 02-advanced/06-blame/README.md | 169 ----- 02-advanced/06-blame/reset.ps1 | 24 - 02-advanced/06-blame/setup.ps1 | 323 --------- 02-advanced/06-blame/verify.ps1 | 114 --- 02-advanced/07-merge-strategies/README.md | 448 ------------ 02-advanced/07-merge-strategies/reset.ps1 | 24 - 02-advanced/07-merge-strategies/setup.ps1 | 221 ------ 02-advanced/07-merge-strategies/verify.ps1 | 140 ---- 28 files changed, 4930 deletions(-) delete mode 100644 02-advanced/01-reset/README.md delete mode 100644 02-advanced/01-reset/reset.ps1 delete mode 100644 02-advanced/01-reset/setup.ps1 delete mode 100644 02-advanced/01-reset/verify.ps1 delete mode 100644 02-advanced/02-rebasing/README.md delete mode 100644 02-advanced/02-rebasing/reset.ps1 delete mode 100644 02-advanced/02-rebasing/setup.ps1 delete mode 100644 02-advanced/02-rebasing/verify.ps1 delete mode 100644 02-advanced/03-interactive-rebase/README.md delete mode 100644 02-advanced/03-interactive-rebase/reset.ps1 delete mode 100644 02-advanced/03-interactive-rebase/setup.ps1 delete mode 100644 02-advanced/03-interactive-rebase/verify.ps1 delete mode 100644 02-advanced/04-worktrees/README.md delete mode 100644 02-advanced/04-worktrees/reset.ps1 delete mode 100644 02-advanced/04-worktrees/setup.ps1 delete mode 100644 02-advanced/04-worktrees/verify.ps1 delete mode 100644 02-advanced/05-bisect/README.md delete mode 100644 02-advanced/05-bisect/reset.ps1 delete mode 100644 02-advanced/05-bisect/setup.ps1 delete mode 100644 02-advanced/05-bisect/verify.ps1 delete mode 100644 02-advanced/06-blame/README.md delete mode 100644 02-advanced/06-blame/reset.ps1 delete mode 100644 02-advanced/06-blame/setup.ps1 delete mode 100644 02-advanced/06-blame/verify.ps1 delete mode 100644 02-advanced/07-merge-strategies/README.md delete mode 100644 02-advanced/07-merge-strategies/reset.ps1 delete mode 100644 02-advanced/07-merge-strategies/setup.ps1 delete mode 100644 02-advanced/07-merge-strategies/verify.ps1 diff --git a/02-advanced/01-reset/README.md b/02-advanced/01-reset/README.md deleted file mode 100644 index 7cd2862..0000000 --- a/02-advanced/01-reset/README.md +++ /dev/null @@ -1,717 +0,0 @@ -# Module 06: Git Reset - Dangerous History Rewriting - -## ⚠️ CRITICAL SAFETY WARNING ⚠️ - -**Git reset is DESTRUCTIVE and DANGEROUS when misused!** - -Before using `git reset`, always ask yourself: - -``` -Have I pushed these commits to a remote repository? -├─ YES → ❌ DO NOT USE RESET! -│ Use git revert instead (Module 05) -│ Rewriting pushed history breaks collaboration! -│ -└─ NO → ✅ Proceed with reset (local cleanup only) - Choose your mode carefully: - --soft (safest), --mixed (moderate), --hard (DANGEROUS) -``` - -**The Golden Rule:** NEVER reset commits that have been pushed/shared. - -## About This Module - -Welcome to Module 06, where you'll learn the powerful but dangerous `git reset` command. Unlike `git revert` (Module 05) which safely creates new commits, **reset erases commits from history**. - -**Why reset exists:** -- ✅ Clean up messy local commit history before pushing -- ✅ Undo commits you haven't shared yet -- ✅ Unstage files from the staging area -- ✅ Recover from mistakes (with reflog) - -**Why reset is dangerous:** -- ⚠️ Erases commits permanently (without reflog) -- ⚠️ Breaks repositories if used on pushed commits -- ⚠️ Can lose work if used incorrectly -- ⚠️ Confuses teammates if they have your commits - -**Key principle:** Reset is for polishing LOCAL history before sharing. - -## Learning Objectives - -By completing this module, you will: - -1. Understand the three reset modes: --soft, --mixed, --hard -2. Reset commits while keeping changes staged (--soft) -3. Reset commits and unstage changes (--mixed) -4. Reset commits and discard everything (--hard) -5. Know when reset is appropriate (local only!) -6. Understand when to use revert instead -7. Use reflog to recover from mistakes - -## Prerequisites - -Before starting this module, you should: -- Be comfortable with commits and staging (`git add`, `git commit`) -- Understand `git revert` from Module 05 -- **Know the difference between local and pushed commits!** - -## Setup - -Run the setup script to create the challenge environment: - -```powershell -./setup.ps1 -``` - -This creates a `challenge/` directory with three branches demonstrating different reset modes: -- `soft-reset` - Reset with --soft (keep changes staged) -- `mixed-reset` - Reset with --mixed (unstage changes) -- `hard-reset` - Reset with --hard (discard everything) - -**Remember:** These are all LOCAL commits that have NEVER been pushed! - -## Understanding Reset Modes - -Git reset has three modes that control what happens to your changes: - -| Mode | Commits | Staging Area | Working Directory | -|------|---------|--------------|-------------------| -| **--soft** | ✂️ Removed | ✅ Kept (staged) | ✅ Kept | -| **--mixed** (default) | ✂️ Removed | ✂️ Cleared | ✅ Kept (unstaged) | -| **--hard** | ✂️ Removed | ✂️ Cleared | ✂️ **LOST!** | - -**Visual explanation:** - -``` -Before reset (3 commits): -A → B → C → HEAD - -After git reset --soft HEAD~1: -A → B → HEAD - ↑ - C's changes are staged - -After git reset --mixed HEAD~1 (or just git reset HEAD~1): -A → B → HEAD - ↑ - C's changes are unstaged (in working directory) - -After git reset --hard HEAD~1: -A → B → HEAD - ↑ - C's changes are GONE (discarded completely!) -``` - -## Challenge 1: Soft Reset (Safest) - -### Scenario - -You committed "feature C" but immediately realized the implementation is wrong. You want to undo the commit but keep the changes staged so you can edit and re-commit them properly. - -**Use case:** Fixing the last commit's message or contents. - -### Your Task - -1. Navigate to the challenge directory: - ```bash - cd challenge - ``` - -2. You should be on the `soft-reset` branch. View the commits: - ```bash - git log --oneline - ``` - - You should see: - - "Add feature C - needs better implementation!" - - "Add feature B" - - "Add feature A" - - "Initial project setup" - -3. View the current state: - ```bash - git status - # Should be clean - ``` - -4. Reset the last commit with --soft: - ```bash - git reset --soft HEAD~1 - ``` - -5. Check what happened: - ```bash - # Commit is gone - git log --oneline - # Should only show 3 commits now (feature C commit removed) - - # Changes are still staged - git status - # Should show "Changes to be committed" - - # View the staged changes - git diff --cached - # Should show feature C code ready to be re-committed - ``` - -### What to Observe - -After `--soft` reset: -- ✅ Commit removed from history -- ✅ Changes remain in staging area -- ✅ Working directory unchanged -- ✅ Ready to edit and re-commit - -**When to use --soft:** -- Fix the last commit message (though `commit --amend` is simpler) -- Combine multiple commits into one -- Re-do a commit with better changes - -## Challenge 2: Mixed Reset (Default, Moderate) - -### Scenario - -You committed two experimental features that aren't ready. You want to remove both commits and have the changes back in your working directory (unstaged) so you can review and selectively re-commit them. - -**Use case:** Undoing commits and starting over with more careful staging. - -### Your Task - -1. Switch to the mixed-reset branch: - ```bash - git switch mixed-reset - ``` - -2. View the commits: - ```bash - git log --oneline - ``` - - You should see: - - "Add debug mode - REMOVE THIS TOO!" - - "Add experimental feature X - REMOVE THIS!" - - "Add logging system" - - "Add application lifecycle" - -3. Reset the last TWO commits (default is --mixed): - ```bash - git reset HEAD~2 - # This is equivalent to: git reset --mixed HEAD~2 - ``` - -4. Check what happened: - ```bash - # Commits are gone - git log --oneline - # Should only show 2 commits (lifecycle + logging) - - # NO staged changes - git diff --cached - # Should be empty - - # Changes are in working directory (unstaged) - git status - # Should show "Changes not staged for commit" - - # View the unstaged changes - git diff - # Should show experimental and debug code - ``` - -### What to Observe - -After `--mixed` reset (the default): -- ✅ Commits removed from history -- ✅ Staging area cleared -- ✅ Changes moved to working directory (unstaged) -- ✅ Can selectively stage and re-commit parts - -**When to use --mixed (default):** -- Undo commits and start over with clean staging -- Split one large commit into multiple smaller ones -- Review changes before re-committing -- Most common reset mode for cleanup - -## Challenge 3: Hard Reset (MOST DANGEROUS!) - -### ⚠️ EXTREME CAUTION REQUIRED ⚠️ - -**This will PERMANENTLY DELETE your work!** - -Only use `--hard` when you're absolutely sure you want to throw away changes. - -### Scenario - -You committed completely broken code that you want to discard entirely. There's no salvaging it—you just want it gone. - -**Use case:** Throwing away failed experiments or completely wrong code. - -### Your Task - -1. Switch to the hard-reset branch: - ```bash - git switch hard-reset - ``` - -2. View the commits and the broken code: - ```bash - git log --oneline - # Shows "Add broken helper D - DISCARD COMPLETELY!" - - cat utils.py - # Shows the broken helper_d function - ``` - -3. Reset the last commit with --hard: - ```bash - git reset --hard HEAD~1 - ``` - - **WARNING:** This will permanently discard all changes from that commit! - -4. Check what happened: - ```bash - # Commit is gone - git log --oneline - # Should only show 2 commits - - # NO staged changes - git diff --cached - # Empty - - # NO unstaged changes - git diff - # Empty - - # Working directory clean - git status - # "nothing to commit, working tree clean" - - # File doesn't have broken code - cat utils.py - # helper_d is completely gone - ``` - -### What to Observe - -After `--hard` reset: -- ✅ Commit removed from history -- ✅ Staging area cleared -- ✅ Working directory reset to match -- ⚠️ All changes from that commit PERMANENTLY DELETED - -**When to use --hard:** -- Discarding failed experiments completely -- Throwing away work you don't want (CAREFUL!) -- Cleaning up after mistakes (use reflog to recover if needed) -- Resetting to a known good state - -**⚠️ WARNING:** Files in the discarded commit are NOT gone forever—they're still in reflog for about 90 days. See "Recovery with Reflog" section below. - -## Understanding HEAD~N Syntax - -When resetting, you specify where to reset to: - -```bash -# Reset to the commit before HEAD -git reset HEAD~1 - -# Reset to 2 commits before HEAD -git reset HEAD~2 - -# Reset to 3 commits before HEAD -git reset HEAD~3 - -# Reset to a specific commit hash -git reset abc123 - -# Reset to a branch -git reset main -``` - -**Visualization:** - -``` -HEAD~3 HEAD~2 HEAD~1 HEAD - ↓ ↓ ↓ ↓ -A → B → C → D → E - ↑ - Current commit -``` - -- `git reset HEAD~1` moves HEAD from E to D -- `git reset HEAD~2` moves HEAD from E to C -- `git reset abc123` moves HEAD to that specific commit - -## Verification - -Verify your solutions by running the verification script: - -```bash -cd .. # Return to module directory -./verify.ps1 -``` - -The script checks that: -- ✅ Commits were reset (count decreased) -- ✅ --soft: Changes remain staged -- ✅ --mixed: Changes are unstaged -- ✅ --hard: Everything is clean - -## Recovery with Reflog - -**Good news:** Even `--hard` reset doesn't immediately destroy commits! - -Git keeps a "reflog" (reference log) of where HEAD has been for about 90 days. You can use this to recover "lost" commits. - -### How to Recover from a Reset - -1. View the reflog: - ```bash - git reflog - ``` - - Output example: - ``` - abc123 HEAD@{0}: reset: moving to HEAD~1 - def456 HEAD@{1}: commit: Add broken helper D - ... - ``` - -2. Find the commit you want to recover (def456 in this example) - -3. Reset back to it: - ```bash - git reset def456 - # Or use the reflog reference: - git reset HEAD@{1} - ``` - -4. Your "lost" commit is back! - -### Reflog Safety Net - -**Important:** -- Reflog entries expire after ~90 days (configurable) -- Reflog is LOCAL to your repository (not shared) -- `git gc` can clean up old reflog entries -- If you really lose a commit, check reflog first! - -**Pro tip:** Before doing dangerous operations, note your current commit hash: -```bash -git log --oneline | head -1 -# abc123 Current work -``` - -## When to Use Git Reset - -Use `git reset` when: - -- ✅ **Commits are LOCAL only** (never pushed) -- ✅ **Cleaning up messy history** before sharing -- ✅ **Undoing recent commits** you don't want -- ✅ **Combining commits** into one clean commit -- ✅ **Unstaging files** (mixed mode) -- ✅ **Polishing commit history** before pull request - -**Golden Rule:** Only reset commits that are local to your machine! - -## When NOT to Use Git Reset - -DO NOT use `git reset` when: - -- ❌ **Commits are pushed/shared** with others -- ❌ **Teammates have your commits** (breaks their repos) -- ❌ **In public repositories** (use revert instead) -- ❌ **Unsure if pushed** (check `git log origin/main`) -- ❌ **On main/master branch** after push -- ❌ **Need audit trail** of changes - -**Use git revert instead** (Module 05) for pushed commits! - -## Decision Tree: Reset vs Revert - -``` -Need to undo a commit? -│ -├─ Have you pushed this commit? -│ │ -│ ├─ YES → Use git revert (Module 05) -│ │ Safe for shared history -│ │ Preserves complete audit trail -│ │ -│ └─ NO → Can use git reset (local only) -│ │ -│ ├─ Want to keep changes? -│ │ │ -│ │ ├─ Keep staged → git reset --soft -│ │ └─ Keep unstaged → git reset --mixed -│ │ -│ └─ Discard everything? → git reset --hard -│ (CAREFUL!) -``` - -## Reset vs Revert vs Rebase - -| Command | History | Safety | Use Case | -|---------|---------|--------|----------| -| **reset** | Erases | ⚠️ Dangerous | Local cleanup before push | -| **revert** | Preserves | ✅ Safe | Undo pushed commits | -| **rebase** | Rewrites | ⚠️ Dangerous | Polish history before push | - -**This module teaches reset.** You learned revert in Module 05. - -## Command Reference - -### Basic Reset - -```bash -# Reset last commit, keep changes staged -git reset --soft HEAD~1 - -# Reset last commit, unstage changes (default) -git reset HEAD~1 -git reset --mixed HEAD~1 # Same as above - -# Reset last commit, discard everything (DANGEROUS!) -git reset --hard HEAD~1 - -# Reset multiple commits -git reset --soft HEAD~3 # Last 3 commits - -# Reset to specific commit -git reset --soft abc123 -``` - -### Unstaging Files - -```bash -# Unstage a specific file (common use of reset) -git reset HEAD filename.txt - -# Unstage all files -git reset HEAD . - -# This is the same as: -git restore --staged filename.txt # Modern syntax -``` - -### Reflog and Recovery - -```bash -# View reflog -git reflog - -# Recover from reset -git reset --hard HEAD@{1} -git reset --hard abc123 -``` - -### Check Before Reset - -```bash -# Check if commits are pushed -git log origin/main..HEAD -# If output is empty, commits are pushed (DO NOT RESET) -# If output shows commits, they're local (safe to reset) - -# Another way to check -git log --oneline --graph --all -# Look for origin/main marker -``` - -## Common Mistakes - -### 1. Resetting Pushed Commits - -```bash -# ❌ NEVER do this if you've pushed! -git push -# ... time passes ... -git reset --hard HEAD~3 # BREAKS teammate repos! - -# ✅ Do this instead -git revert HEAD~3..HEAD # Safe for shared history -``` - -### 2. Using --hard Without Thinking - -```bash -# ❌ Dangerous - loses work! -git reset --hard HEAD~1 - -# ✅ Better - keep changes to review -git reset --mixed HEAD~1 -# Now you can review changes and decide -``` - -### 3. Resetting Without Checking If Pushed - -```bash -# ❌ Risky - are these commits pushed? -git reset HEAD~5 - -# ✅ Check first -git log origin/main..HEAD # Local commits only -git reset HEAD~5 # Now safe if output showed commits -``` - -### 4. Forgetting Reflog Exists - -```bash -# ❌ Panic after accidental --hard reset -# "I lost my work!" - -# ✅ Check reflog first! -git reflog # Find the "lost" commit -git reset --hard HEAD@{1} # Recover it -``` - -## Best Practices - -1. **Always check if commits are pushed before reset:** - ```bash - git log origin/main..HEAD - ``` - -2. **Prefer --mixed over --hard:** - - You can always discard changes later - - Hard to recover if you use --hard by mistake - -3. **Commit often locally, reset before push:** - - Make many small local commits - - Reset/squash into clean commits before pushing - -4. **Use descriptive commit messages even for local commits:** - - Helps when reviewing before reset - - Useful when checking reflog - -5. **Know your escape hatch:** - ```bash - git reflog # Your safety net! - ``` - -6. **Communicate with team:** - - NEVER reset shared branches (main, develop, etc.) - - Only reset your personal feature branches - - Only before pushing! - -## Troubleshooting - -### "I accidentally reset with --hard and lost work!" - -**Solution:** Check reflog: -```bash -git reflog -# Find the commit before your reset -git reset --hard HEAD@{1} # Or the commit hash -``` - -**Prevention:** Always use --mixed first, then discard if really needed. - -### "I reset but teammates still have my commits" - -**Problem:** You reset and pushed with --force after they pulled. - -**Impact:** Their repository is now broken/inconsistent. - -**Solution:** Communicate! They need to: -```bash -git fetch -git reset --hard origin/main # Or whatever branch -``` - -**Prevention:** NEVER reset pushed commits! - -### "Reset didn't do what I expected" - -**Issue:** Wrong mode or wrong HEAD~N count. - -**Solution:** Check current state: -```bash -git status -git diff -git diff --cached -git log --oneline -``` - -Undo the reset: -```bash -git reflog -git reset HEAD@{1} # Go back to before your reset -``` - -### "Can't reset - 'fatal: ambiguous argument HEAD~1'" - -**Issue:** No commits to reset (probably first commit). - -**Solution:** You can't reset before the first commit. If you want to remove the first commit entirely: -```bash -rm -rf .git # Nuclear option - deletes entire repo -git init # Start over -``` - -## Advanced: Reset Internals - -Understanding what reset does under the hood: - -```bash -# Reset moves the branch pointer -# Before: -main → A → B → C (HEAD) - -# After git reset --soft HEAD~1: -main → A → B (HEAD) - ↑ - C still exists in reflog, just not in branch history - -# The commit object C is still in .git/objects -# It's just unreachable from any branch -``` - -**Key insight:** Reset moves the HEAD and branch pointers backward. The commits still exist temporarily in reflog until garbage collection. - -## Going Further - -Now that you understand reset, you're ready for: - -- **Module 07: Git Stash** - Temporarily save uncommitted work -- **Module 08: Multiplayer Git** - Collaborate with complex workflows -- **Interactive Rebase** - Advanced history polishing (beyond this workshop) - -## Summary - -You've learned: - -- ✅ `git reset` rewrites history by moving HEAD backward -- ✅ `--soft` keeps changes staged (safest) -- ✅ `--mixed` (default) unstages changes -- ✅ `--hard` discards everything (most dangerous) -- ✅ NEVER reset pushed/shared commits -- ✅ Use reflog to recover from mistakes -- ✅ Check if commits are pushed before resetting -- ✅ Use revert (Module 05) for shared commits - -**The Critical Rule:** Reset is for LOCAL commits ONLY. Once you push, use revert! - -## Next Steps - -1. Complete all three challenge scenarios -2. Run `./verify.ps1` to check your solutions -3. Practice checking if commits are pushed before reset -4. Move on to Module 07: Git Stash - ---- - -**⚠️ FINAL REMINDER ⚠️** - -**Before any `git reset` command, ask yourself:** - -> "Have I pushed these commits?" - -If YES → Use `git revert` instead! - -If NO → Proceed carefully, choose the right mode. - -**When in doubt, use --mixed instead of --hard!** diff --git a/02-advanced/01-reset/reset.ps1 b/02-advanced/01-reset/reset.ps1 deleted file mode 100644 index 4cdc736..0000000 --- a/02-advanced/01-reset/reset.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Resets the Module 06 challenge environment to start fresh. - -.DESCRIPTION - This script removes the challenge directory and re-runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "`n=== Resetting Module 06: Git Reset Challenge ===" -ForegroundColor Cyan - -# Check if challenge directory exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" - Write-Host "[OK] Challenge directory removed" -ForegroundColor Green -} else { - Write-Host "[INFO] No existing challenge directory found" -ForegroundColor Yellow -} - -# Run setup to create fresh environment -Write-Host "`nRunning setup to create fresh challenge environment..." -ForegroundColor Cyan -& "$PSScriptRoot/setup.ps1" diff --git a/02-advanced/01-reset/setup.ps1 b/02-advanced/01-reset/setup.ps1 deleted file mode 100644 index ccdaf38..0000000 --- a/02-advanced/01-reset/setup.ps1 +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Sets up the Module 06 challenge environment for learning git reset. - -.DESCRIPTION - This script creates a challenge directory with three branches demonstrating - different reset scenarios: - - soft-reset: Reset with --soft (keeps changes staged) - - mixed-reset: Reset with --mixed (unstages changes) - - hard-reset: Reset with --hard (discards everything) + reflog recovery -#> - -Write-Host "`n=== Setting up Module 06: Git Reset Challenge ===" -ForegroundColor Cyan -Write-Host "⚠️ WARNING: Git reset is DANGEROUS - use with extreme caution! ⚠️" -ForegroundColor Red - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} - -# Create fresh challenge directory -Write-Host "Creating challenge directory..." -ForegroundColor Green -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize Git repository -Write-Host "Initializing Git repository..." -ForegroundColor Green -git init | Out-Null - -# Configure git for this repository -git config user.name "Workshop Student" -git config user.email "student@example.com" - -# Detect the default branch name after first commit (created below) -# Will be detected after the initial commit - -# ============================================================================ -# Create initial commit (shared by all scenarios) -# ============================================================================ -$readmeContent = @" -# Git Reset Practice - -This repository contains practice scenarios for learning git reset. -"@ -Set-Content -Path "README.md" -Value $readmeContent -git add . -git commit -m "Initial commit" | Out-Null - -# Detect the main branch name after first commit -$mainBranch = git branch --show-current -if (-not $mainBranch) { - $mainBranch = git config --get init.defaultBranch - if (-not $mainBranch) { $mainBranch = "main" } -} -Write-Host "Default branch detected: $mainBranch" -ForegroundColor Yellow - -# ============================================================================ -# SCENARIO 1: Soft Reset (--soft) -# ============================================================================ -Write-Host "`nScenario 1: Creating soft-reset branch..." -ForegroundColor Cyan - -# Create soft-reset branch from initial commit -git switch -c soft-reset | Out-Null - -# Build up scenario 1 commits -$projectContent = @" -# project.py - Main project file - -def initialize(): - """Initialize the project.""" - print("Project initialized") - -def main(): - initialize() - print("Running application...") -"@ -Set-Content -Path "project.py" -Value $projectContent -git add . -git commit -m "Initial project setup" | Out-Null - -# Good commit: Add feature A -$projectContent = @" -# project.py - Main project file - -def initialize(): - """Initialize the project.""" - print("Project initialized") - -def feature_a(): - """Feature A implementation.""" - print("Feature A is working") - -def main(): - initialize() - feature_a() - print("Running application...") -"@ -Set-Content -Path "project.py" -Value $projectContent -git add . -git commit -m "Add feature A" | Out-Null - -# Good commit: Add feature B -$projectContent = @" -# project.py - Main project file - -def initialize(): - """Initialize the project.""" - print("Project initialized") - -def feature_a(): - """Feature A implementation.""" - print("Feature A is working") - -def feature_b(): - """Feature B implementation.""" - print("Feature B is working") - -def main(): - initialize() - feature_a() - feature_b() - print("Running application...") -"@ -Set-Content -Path "project.py" -Value $projectContent -git add . -git commit -m "Add feature B" | Out-Null - -# BAD commit: Add feature C (wrong implementation) -$projectContent = @" -# project.py - Main project file - -def initialize(): - """Initialize the project.""" - print("Project initialized") - -def feature_a(): - """Feature A implementation.""" - print("Feature A is working") - -def feature_b(): - """Feature B implementation.""" - print("Feature B is working") - -def feature_c(): - """Feature C implementation - WRONG!""" - print("Feature C has bugs!") # This needs to be re-implemented - -def main(): - initialize() - feature_a() - feature_b() - feature_c() - print("Running application...") -"@ -Set-Content -Path "project.py" -Value $projectContent -git add . -git commit -m "Add feature C - needs better implementation!" | Out-Null - -Write-Host "[CREATED] soft-reset branch with commit to reset --soft" -ForegroundColor Green - -# ============================================================================ -# SCENARIO 2: Mixed Reset (--mixed, default) -# ============================================================================ -Write-Host "`nScenario 2: Creating mixed-reset branch..." -ForegroundColor Cyan - -# Switch back to initial commit and create mixed-reset branch -git switch $mainBranch | Out-Null -git switch -c mixed-reset | Out-Null - -# Build up scenario 2 commits -$appContent = @" -# app.py - Application entry point - -def start(): - """Start the application.""" - print("Application started") - -def stop(): - """Stop the application.""" - print("Application stopped") -"@ -Set-Content -Path "app.py" -Value $appContent -git add . -git commit -m "Add application lifecycle" | Out-Null - -# Good commit: Add logging -$appContent = @" -# app.py - Application entry point - -def log(message): - """Log a message.""" - print(f"[LOG] {message}") - -def start(): - """Start the application.""" - log("Application started") - -def stop(): - """Stop the application.""" - log("Application stopped") -"@ -Set-Content -Path "app.py" -Value $appContent -git add . -git commit -m "Add logging system" | Out-Null - -# BAD commit 1: Add experimental feature X -$appContent = @" -# app.py - Application entry point - -def log(message): - """Log a message.""" - print(f"[LOG] {message}") - -def experimental_feature_x(): - """Experimental feature - NOT READY!""" - log("Feature X is experimental and buggy") - -def start(): - """Start the application.""" - log("Application started") - experimental_feature_x() - -def stop(): - """Stop the application.""" - log("Application stopped") -"@ -Set-Content -Path "app.py" -Value $appContent -git add . -git commit -m "Add experimental feature X - REMOVE THIS!" | Out-Null - -# BAD commit 2: Add debug mode (also not ready) -$appContent = @" -# app.py - Application entry point - -DEBUG_MODE = True # Should not be committed! - -def log(message): - """Log a message.""" - print(f"[LOG] {message}") - -def experimental_feature_x(): - """Experimental feature - NOT READY!""" - log("Feature X is experimental and buggy") - -def start(): - """Start the application.""" - if DEBUG_MODE: - log("DEBUG MODE ACTIVE!") - log("Application started") - experimental_feature_x() - -def stop(): - """Stop the application.""" - log("Application stopped") -"@ -Set-Content -Path "app.py" -Value $appContent -git add . -git commit -m "Add debug mode - REMOVE THIS TOO!" | Out-Null - -Write-Host "[CREATED] mixed-reset branch with commits to reset --mixed" -ForegroundColor Green - -# ============================================================================ -# SCENARIO 3: Hard Reset (--hard) + Reflog Recovery -# ============================================================================ -Write-Host "`nScenario 3: Creating hard-reset branch..." -ForegroundColor Cyan - -# Switch back to main and create hard-reset branch -git switch $mainBranch | Out-Null -git switch -c hard-reset | Out-Null - -# Reset to basic state -$utilsContent = @" -# utils.py - Utility functions - -def helper_a(): - """Helper function A.""" - return "Helper A" - -def helper_b(): - """Helper function B.""" - return "Helper B" -"@ -Set-Content -Path "utils.py" -Value $utilsContent -git add . -git commit -m "Add utility helpers" | Out-Null - -# Good commit: Add helper C -$utilsContent = @" -# utils.py - Utility functions - -def helper_a(): - """Helper function A.""" - return "Helper A" - -def helper_b(): - """Helper function B.""" - return "Helper B" - -def helper_c(): - """Helper function C.""" - return "Helper C" -"@ -Set-Content -Path "utils.py" -Value $utilsContent -git add . -git commit -m "Add helper C" | Out-Null - -# BAD commit: Add broken helper D (completely wrong) -$utilsContent = @" -# utils.py - Utility functions - -def helper_a(): - """Helper function A.""" - return "Helper A" - -def helper_b(): - """Helper function B.""" - return "Helper B" - -def helper_c(): - """Helper function C.""" - return "Helper C" - -def helper_d(): - """COMPLETELY BROKEN - throw away!""" - # This is all wrong and needs to be discarded - broken_code = "This doesn't even make sense" - return broken_code.nonexistent_method() # Will crash! -"@ -Set-Content -Path "utils.py" -Value $utilsContent -git add . -git commit -m "Add broken helper D - DISCARD COMPLETELY!" | Out-Null - -Write-Host "[CREATED] hard-reset branch with commit to reset --hard" -ForegroundColor Green - -# ============================================================================ -# Return to soft-reset to start -# ============================================================================ -git switch soft-reset | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n=== Setup Complete! ===`n" -ForegroundColor Green -Write-Host "Three reset scenarios have been created:" -ForegroundColor Cyan -Write-Host " 1. soft-reset - Reset --soft (keep changes staged)" -ForegroundColor White -Write-Host " 2. mixed-reset - Reset --mixed (unstage changes)" -ForegroundColor White -Write-Host " 3. hard-reset - Reset --hard (discard everything) + reflog recovery" -ForegroundColor White -Write-Host "`n⚠️ CRITICAL SAFETY REMINDER ⚠️" -ForegroundColor Red -Write-Host "NEVER use git reset on commits that have been PUSHED!" -ForegroundColor Red -Write-Host "These scenarios are LOCAL ONLY for practice." -ForegroundColor Yellow -Write-Host "`nYou are currently on the 'soft-reset' branch." -ForegroundColor Cyan -Write-Host "`nNext steps:" -ForegroundColor Cyan -Write-Host " 1. cd challenge" -ForegroundColor White -Write-Host " 2. Read the README.md for detailed instructions" -ForegroundColor White -Write-Host " 3. Complete each reset challenge" -ForegroundColor White -Write-Host " 4. Run '..\\verify.ps1' to check your solutions" -ForegroundColor White -Write-Host "" diff --git a/02-advanced/01-reset/verify.ps1 b/02-advanced/01-reset/verify.ps1 deleted file mode 100644 index bb0f592..0000000 --- a/02-advanced/01-reset/verify.ps1 +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Verifies the Module 06 challenge solutions. - -.DESCRIPTION - Checks that all three reset scenarios have been completed correctly: - - soft-reset: Commit reset but changes remain staged - - mixed-reset: Commits reset and changes unstaged - - hard-reset: Everything reset and discarded -#> - -Write-Host "`n=== Verifying Module 06: Git Reset Solutions ===" -ForegroundColor Cyan -Write-Host "⚠️ Remember: NEVER reset pushed commits! ⚠️" -ForegroundColor Red - -$allChecksPassed = $true -$originalDir = Get-Location - -# Check if challenge directory exists -if (-not (Test-Path "challenge")) { - Write-Host "[FAIL] Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - exit 1 -} - -Set-Location "challenge" - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] Not a git repository. Run setup.ps1 first." -ForegroundColor Red - Set-Location $originalDir - exit 1 -} - -# ============================================================================ -# SCENARIO 1: Soft Reset Verification -# ============================================================================ -Write-Host "`n=== Scenario 1: Soft Reset ===`n" -ForegroundColor Cyan - -git switch soft-reset 2>&1 | Out-Null - -if ($LASTEXITCODE -ne 0) { - Write-Host "[FAIL] soft-reset branch not found" -ForegroundColor Red - $allChecksPassed = $false -} else { - # Count commits (should be 4: Initial + project setup + feature A + feature B) - $commitCount = [int](git rev-list --count HEAD 2>$null) - - if ($commitCount -eq 4) { - Write-Host "[PASS] Commit count is 4 (feature C commit was reset)" -ForegroundColor Green - } else { - Write-Host "[FAIL] Expected 4 commits, found $commitCount" -ForegroundColor Red - Write-Host "[HINT] Use: git reset --soft HEAD~1" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check if changes are staged - $stagedChanges = git diff --cached --name-only 2>$null - if ($stagedChanges) { - Write-Host "[PASS] Changes are staged (feature C code in staging area)" -ForegroundColor Green - - # Verify the staged changes contain feature C code - $stagedContent = git diff --cached 2>$null - if ($stagedContent -match "feature_c") { - Write-Host "[PASS] Staged changes contain feature C code" -ForegroundColor Green - } else { - Write-Host "[INFO] Staged changes don't seem to contain feature C" -ForegroundColor Yellow - } - } else { - Write-Host "[FAIL] No staged changes found" -ForegroundColor Red - Write-Host "[HINT] After --soft reset, changes should remain staged" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check working directory has no unstaged changes to tracked files - $unstagedChanges = git diff --name-only 2>$null - if (-not $unstagedChanges) { - Write-Host "[PASS] No unstaged changes (all changes are staged)" -ForegroundColor Green - } else { - Write-Host "[INFO] Found unstaged changes (expected only staged changes)" -ForegroundColor Yellow - } -} - -# ============================================================================ -# SCENARIO 2: Mixed Reset Verification -# ============================================================================ -Write-Host "`n=== Scenario 2: Mixed Reset ===`n" -ForegroundColor Cyan - -git switch mixed-reset 2>&1 | Out-Null - -if ($LASTEXITCODE -ne 0) { - Write-Host "[FAIL] mixed-reset branch not found" -ForegroundColor Red - $allChecksPassed = $false -} else { - # Count commits (should be 3: Initial + lifecycle + logging, bad commits removed) - $commitCount = [int](git rev-list --count HEAD 2>$null) - - if ($commitCount -eq 3) { - Write-Host "[PASS] Commit count is 3 (both bad commits were reset)" -ForegroundColor Green - } elseif ($commitCount -eq 4) { - Write-Host "[INFO] Commit count is 4 (one commit reset, need to reset one more)" -ForegroundColor Yellow - Write-Host "[HINT] Use: git reset HEAD~1 (or git reset --mixed HEAD~1)" -ForegroundColor Yellow - $allChecksPassed = $false - } else { - Write-Host "[FAIL] Expected 3 commits, found $commitCount" -ForegroundColor Red - Write-Host "[HINT] Use: git reset --mixed HEAD~2 to remove both bad commits" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check that there are NO staged changes - $stagedChanges = git diff --cached --name-only 2>$null - if (-not $stagedChanges) { - Write-Host "[PASS] No staged changes (--mixed unstages everything)" -ForegroundColor Green - } else { - Write-Host "[FAIL] Found staged changes (--mixed should unstage)" -ForegroundColor Red - Write-Host "[HINT] After --mixed reset, changes should be unstaged" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check that there ARE unstaged changes in working directory - $unstagedChanges = git diff --name-only 2>$null - if ($unstagedChanges) { - Write-Host "[PASS] Unstaged changes present in working directory" -ForegroundColor Green - - # Verify unstaged changes contain the experimental/debug code - $workingContent = git diff 2>$null - if ($workingContent -match "experimental|DEBUG") { - Write-Host "[PASS] Unstaged changes contain the reset code" -ForegroundColor Green - } else { - Write-Host "[INFO] Unstaged changes don't contain expected code" -ForegroundColor Yellow - } - } else { - Write-Host "[FAIL] No unstaged changes found" -ForegroundColor Red - Write-Host "[HINT] After --mixed reset, changes should be in working directory (unstaged)" -ForegroundColor Yellow - $allChecksPassed = $false - } -} - -# ============================================================================ -# SCENARIO 3: Hard Reset Verification -# ============================================================================ -Write-Host "`n=== Scenario 3: Hard Reset ===`n" -ForegroundColor Cyan - -git switch hard-reset 2>&1 | Out-Null - -if ($LASTEXITCODE -ne 0) { - Write-Host "[FAIL] hard-reset branch not found" -ForegroundColor Red - $allChecksPassed = $false -} else { - # Count commits (should be 3: Initial + utilities + helper C, bad commit removed) - $commitCount = [int](git rev-list --count HEAD 2>$null) - - if ($commitCount -eq 3) { - Write-Host "[PASS] Commit count is 3 (broken commit was reset)" -ForegroundColor Green - } else { - Write-Host "[FAIL] Expected 3 commits, found $commitCount" -ForegroundColor Red - Write-Host "[HINT] Use: git reset --hard HEAD~1" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check that there are NO staged changes - $stagedChanges = git diff --cached --name-only 2>$null - if (-not $stagedChanges) { - Write-Host "[PASS] No staged changes (--hard discards everything)" -ForegroundColor Green - } else { - Write-Host "[FAIL] Found staged changes (--hard should discard all)" -ForegroundColor Red - $allChecksPassed = $false - } - - # Check that there are NO unstaged changes - $unstagedChanges = git diff --name-only 2>$null - if (-not $unstagedChanges) { - Write-Host "[PASS] No unstaged changes (--hard discards everything)" -ForegroundColor Green - } else { - Write-Host "[FAIL] Found unstaged changes (--hard should discard all)" -ForegroundColor Red - $allChecksPassed = $false - } - - # Check working directory is clean - $statusOutput = git status --porcelain 2>$null - if (-not $statusOutput) { - Write-Host "[PASS] Working directory is completely clean" -ForegroundColor Green - } else { - Write-Host "[INFO] Working directory has some changes" -ForegroundColor Yellow - } - - # Verify the file doesn't have the broken code - if (Test-Path "utils.py") { - $utilsContent = Get-Content "utils.py" -Raw - if ($utilsContent -notmatch "helper_d") { - Write-Host "[PASS] Broken helper_d function is gone" -ForegroundColor Green - } else { - Write-Host "[FAIL] Broken helper_d still exists (wasn't reset)" -ForegroundColor Red - $allChecksPassed = $false - } - - if ($utilsContent -match "helper_c") { - Write-Host "[PASS] Good helper_c function is preserved" -ForegroundColor Green - } else { - Write-Host "[FAIL] Good helper_c function missing" -ForegroundColor Red - $allChecksPassed = $false - } - } -} - -Set-Location $originalDir - -# Final summary -Write-Host "" -if ($allChecksPassed) { - Write-Host "==========================================" -ForegroundColor Green - Write-Host " CONGRATULATIONS! ALL SCENARIOS PASSED!" -ForegroundColor Green - Write-Host "==========================================" -ForegroundColor Green - Write-Host "`nYou've mastered git reset!" -ForegroundColor Cyan - Write-Host "You now understand:" -ForegroundColor Cyan - Write-Host " ✓ Resetting commits with --soft (keep staged)" -ForegroundColor White - Write-Host " ✓ Resetting commits with --mixed (unstage)" -ForegroundColor White - Write-Host " ✓ Resetting commits with --hard (discard all)" -ForegroundColor White - Write-Host " ✓ The DANGER of reset on shared history" -ForegroundColor White - Write-Host "`n⚠️ CRITICAL REMINDER ⚠️" -ForegroundColor Red - Write-Host "NEVER use 'git reset' on commits you've already PUSHED!" -ForegroundColor Red - Write-Host "Always use 'git revert' (Module 05) for shared commits!" -ForegroundColor Yellow - Write-Host "`nReady for Module 07: Git Stash!" -ForegroundColor Green - Write-Host "" - exit 0 -} else { - Write-Host "[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red - Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow - Write-Host "[REMINDER] Reset is ONLY for local, un-pushed commits!" -ForegroundColor Yellow - Write-Host "" - exit 1 -} diff --git a/02-advanced/02-rebasing/README.md b/02-advanced/02-rebasing/README.md deleted file mode 100644 index 71ada94..0000000 --- a/02-advanced/02-rebasing/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Module 07: Rebasing - -## Learning Objectives - -By the end of this module, you will: -- Understand what rebasing is and how it works -- Know the difference between merge and rebase -- Perform a rebase to integrate changes from one branch to another -- Understand when to use rebase vs merge -- Know the golden rule of rebasing - -## Challenge Description - -You have a repository with a `main` branch and a `feature` branch. While you were working on the feature branch, new commits were added to `main`. You want to incorporate those changes into your feature branch while maintaining a clean, linear history. - -Your task is to: -1. Review the current commit history -2. Rebase the `feature` branch onto `main` -3. Verify that the history is now linear - -## Key Concepts - -### What is Rebasing? - -Rebasing is the process of moving or combining a sequence of commits to a new base commit. Instead of creating a merge commit like `git merge` does, rebasing rewrites the commit history by replaying your commits on top of another branch. - -### Rebase vs Merge - -**Merge:** -``` - A---B---C feature - / \ - D---E---F---G main -``` -Creates a merge commit (G) that ties the histories together. - -**Rebase:** -``` - A'--B'--C' feature - / - D---E---F main -``` -Replays commits A, B, C on top of F, creating new commits A', B', C' with the same changes but different commit hashes. - -### Benefits of Rebasing - -- **Cleaner history**: Linear history is easier to read and understand -- **Simpler log**: No merge commits cluttering the history -- **Easier bisecting**: Finding bugs with `git bisect` is simpler with linear history - -### The Golden Rule of Rebasing - -**Never rebase commits that have been pushed to a public/shared repository.** - -Why? Because rebasing rewrites history by creating new commits. If others have based work on the original commits, rebasing will cause serious problems for collaborators. - -**Safe to rebase:** -- Local commits not yet pushed -- Feature branches you're working on alone -- Cleaning up your work before creating a pull request - -**Never rebase:** -- Commits already pushed to a shared branch (like `main` or `develop`) -- Commits that others might have based work on - -## Useful Commands - -```bash -# Rebase current branch onto another branch -git rebase - -# View commit history as a graph -git log --oneline --graph --all - -# If conflicts occur during rebase: -# 1. Resolve conflicts in files -# 2. Stage the resolved files -git add -# 3. Continue the rebase -git rebase --continue - -# Abort a rebase if something goes wrong -git rebase --abort - -# Check which branch you're on -git branch - -# Switch to a branch -git switch -``` - -## Verification - -Run the verification script to check your solution: - -```bash -.\verify.ps1 -``` - -The verification will check that: -- You're on the feature branch -- The rebase was completed successfully -- The history is linear (no merge commits) -- All commits from both branches are present - -## Tips - -- Always check your commit graph with `git log --oneline --graph --all` before and after rebasing -- If you encounter conflicts during rebase, resolve them just like merge conflicts -- Use `git rebase --abort` if you want to cancel the rebase and start over -- Rebasing rewrites history, so the commit hashes will change -- Only rebase local commits that haven't been shared with others - -## When to Use Rebase vs Merge - -**Use Rebase when:** -- You want a clean, linear history -- Working on a local feature branch that hasn't been shared -- Updating your feature branch with the latest changes from main -- You want to clean up commits before submitting a pull request - -**Use Merge when:** -- Working on a shared/public branch -- You want to preserve the complete history including when branches diverged -- You're merging a completed feature into main -- You want to be safe and avoid rewriting history - -## What You'll Learn - -Rebasing is a powerful tool for maintaining a clean project history. While merging is safer and preserves exact history, rebasing creates a more readable linear timeline. Understanding both techniques and knowing when to use each is essential for effective Git workflow management. diff --git a/02-advanced/02-rebasing/reset.ps1 b/02-advanced/02-rebasing/reset.ps1 deleted file mode 100644 index e08550d..0000000 --- a/02-advanced/02-rebasing/reset.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Resets the rebasing challenge environment. - -.DESCRIPTION - Removes the existing challenge directory and runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "Resetting challenge environment..." -ForegroundColor Yellow - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Remove-Item -Path "challenge" -Recurse -Force - Write-Host "Removed existing challenge directory." -ForegroundColor Cyan -} - -# Run setup script -Write-Host "Running setup script...`n" -ForegroundColor Cyan -& ".\setup.ps1" diff --git a/02-advanced/02-rebasing/setup.ps1 b/02-advanced/02-rebasing/setup.ps1 deleted file mode 100644 index 1372419..0000000 --- a/02-advanced/02-rebasing/setup.ps1 +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Sets up the rebasing challenge environment. - -.DESCRIPTION - Creates a Git repository with diverged branches to practice rebasing. - The feature branch needs to be rebased onto main. -#> - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Path "challenge" -Recurse -Force -} - -# Create challenge directory -Write-Host "Creating challenge environment..." -ForegroundColor Cyan -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize git repository -git init | Out-Null -git config user.name "Workshop User" | Out-Null -git config user.email "user@workshop.local" | Out-Null - -# Create initial file and commit -$readme = @" -# My Project - -A sample project for learning Git rebasing. -"@ - -Set-Content -Path "README.md" -Value $readme -git add README.md -git commit -m "Initial commit" | Out-Null - -# Create and switch to feature branch -git checkout -b feature | Out-Null - -# Add commits on feature branch -$feature1 = @" -# My Project - -A sample project for learning Git rebasing. - -## Features - -- Feature A: User authentication -"@ - -Set-Content -Path "README.md" -Value $feature1 -git add README.md -git commit -m "Add feature A" | Out-Null - -$feature2 = @" -# My Project - -A sample project for learning Git rebasing. - -## Features - -- Feature A: User authentication -- Feature B: Data validation -"@ - -Set-Content -Path "README.md" -Value $feature2 -git add README.md -git commit -m "Add feature B" | Out-Null - -# Switch back to main and add commits (simulating other work happening on main) -git checkout main | Out-Null - -$main1 = @" -# My Project - -A sample project for learning Git rebasing. - -## Installation - -Run \`npm install\` to install dependencies. -"@ - -Set-Content -Path "README.md" -Value $main1 -git add README.md -git commit -m "Add installation instructions" | Out-Null - -$main2 = @" -# My Project - -A sample project for learning Git rebasing. - -## Installation - -Run \`npm install\` to install dependencies. - -## Configuration - -Copy \`config.example.json\` to \`config.json\` and update settings. -"@ - -Set-Content -Path "README.md" -Value $main2 -git add README.md -git commit -m "Add configuration instructions" | Out-Null - -# Switch back to feature branch -git checkout feature | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "Challenge environment created!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou are now on the 'feature' branch." -ForegroundColor Cyan -Write-Host "The 'main' branch has new commits that aren't in your feature branch." -ForegroundColor Cyan -Write-Host "`nYour task:" -ForegroundColor Yellow -Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White -Write-Host "2. View the commit history: git log --oneline --graph --all" -ForegroundColor White -Write-Host "3. Rebase the 'feature' branch onto 'main'" -ForegroundColor White -Write-Host "4. View the history again to see the linear result" -ForegroundColor White -Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/02-advanced/02-rebasing/verify.ps1 b/02-advanced/02-rebasing/verify.ps1 deleted file mode 100644 index f5434de..0000000 --- a/02-advanced/02-rebasing/verify.ps1 +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Verifies the rebasing challenge solution. - -.DESCRIPTION - Checks that the user successfully rebased the feature branch onto main, - resulting in a clean, linear history. -#> - -Set-Location "challenge" -ErrorAction SilentlyContinue - -# Check if challenge directory exists -if (-not (Test-Path "../verify.ps1")) { - Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path ".")) { - Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Write-Host "Verifying your solution..." -ForegroundColor Cyan - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] No git repository found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check current branch -$currentBranch = git branch --show-current 2>$null -if ($currentBranch -ne "feature") { - Write-Host "[FAIL] You should be on the 'feature' branch." -ForegroundColor Red - Write-Host "Current branch: $currentBranch" -ForegroundColor Yellow - Write-Host "Hint: Use 'git checkout feature' to switch to feature branch" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check if there's an ongoing rebase -if (Test-Path ".git/rebase-merge") { - Write-Host "[FAIL] Rebase is not complete. There may be unresolved conflicts." -ForegroundColor Red - Write-Host "Hint: Resolve any conflicts, then use:" -ForegroundColor Yellow - Write-Host " git add " -ForegroundColor White - Write-Host " git rebase --continue" -ForegroundColor White - Write-Host "Or abort with: git rebase --abort" -ForegroundColor White - Set-Location .. - exit 1 -} - -# Get all commits on feature branch -$featureCommits = git log --oneline feature 2>$null -if (-not $featureCommits) { - Write-Host "[FAIL] No commits found on feature branch." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check commit count (should be 5: 1 initial + 2 main + 2 feature) -$commitCount = (git rev-list --count feature 2>$null) -if ($commitCount -ne 5) { - Write-Host "[FAIL] Expected 5 commits on feature branch, found $commitCount" -ForegroundColor Red - Write-Host "Hint: The rebased feature branch should contain:" -ForegroundColor Yellow - Write-Host " - 1 initial commit" -ForegroundColor White - Write-Host " - 2 commits from main" -ForegroundColor White - Write-Host " - 2 commits from feature" -ForegroundColor White - Set-Location .. - exit 1 -} - -# Get commit messages in reverse chronological order (newest first) -$commits = git log --pretty=format:"%s" feature 2>$null - -# Convert to array and reverse to get chronological order (oldest first) -$commitArray = $commits -split "`n" -[array]::Reverse($commitArray) - -# Check that commits are in the expected order after rebase -$expectedOrder = @( - "Initial commit", - "Add installation instructions", - "Add configuration instructions", - "Add feature A", - "Add feature B" -) - -$orderCorrect = $true -for ($i = 0; $i -lt $expectedOrder.Length; $i++) { - if ($commitArray[$i] -ne $expectedOrder[$i]) { - $orderCorrect = $false - break - } -} - -if (-not $orderCorrect) { - Write-Host "[FAIL] Commit order is incorrect after rebase." -ForegroundColor Red - Write-Host "Expected order (oldest to newest):" -ForegroundColor Yellow - $expectedOrder | ForEach-Object { Write-Host " $_" -ForegroundColor White } - Write-Host "`nActual order:" -ForegroundColor Yellow - $commitArray | ForEach-Object { Write-Host " $_" -ForegroundColor White } - Write-Host "`nHint: The feature commits should come AFTER the main commits" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that main branch still has only 3 commits (initial + 2 main commits) -$mainCommitCount = (git rev-list --count main 2>$null) -if ($mainCommitCount -ne 3) { - Write-Host "[FAIL] Main branch should have 3 commits, found $mainCommitCount" -ForegroundColor Red - Write-Host "Hint: You should rebase feature onto main, not the other way around" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that there are no merge commits (parent count should be 1 for all commits) -$mergeCommits = git log --merges --oneline feature 2>$null -if ($mergeCommits) { - Write-Host "[FAIL] Found merge commits in history. Rebasing should create a linear history." -ForegroundColor Red - Write-Host "Hint: Use 'git rebase main' instead of 'git merge main'" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Verify that feature branch contains all commits from main -$mainCommits = git log --pretty=format:"%s" main 2>$null -$featureCommitMessages = git log --pretty=format:"%s" feature 2>$null - -foreach ($mainCommit in ($mainCommits -split "`n")) { - if ($featureCommitMessages -notcontains $mainCommit) { - Write-Host "[FAIL] Feature branch is missing commits from main." -ForegroundColor Red - Write-Host "Missing: $mainCommit" -ForegroundColor Yellow - Set-Location .. - exit 1 - } -} - -# Success! -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Rebased the feature branch onto main" -ForegroundColor White -Write-Host "- Created a clean, linear commit history" -ForegroundColor White -Write-Host "- Preserved all commits from both branches" -ForegroundColor White -Write-Host "`nYour feature branch now has a linear history!" -ForegroundColor Green -Write-Host "Run 'git log --oneline --graph --all' to see the result.`n" -ForegroundColor Cyan - -Set-Location .. -exit 0 diff --git a/02-advanced/03-interactive-rebase/README.md b/02-advanced/03-interactive-rebase/README.md deleted file mode 100644 index 6ba19e6..0000000 --- a/02-advanced/03-interactive-rebase/README.md +++ /dev/null @@ -1,159 +0,0 @@ -# Module 08: Interactive Rebase - -## Learning Objectives - -By the end of this module, you will: -- Understand what interactive rebase is and when to use it -- Learn the different interactive rebase commands (pick, reword, squash, fixup, drop) -- Clean up commit history by squashing related commits -- Reword commit messages to be more descriptive -- Use reset and commit techniques to achieve similar results - -## Challenge Description - -You have a feature branch with messy commit history - multiple "WIP" commits, typos in commit messages, and commits that should be combined. Before submitting a pull request, you want to clean up this history. - -Your task is to: -1. Review the current commit history with its messy commits -2. Combine the four feature commits into a single, well-described commit -3. Keep the initial commit unchanged -4. End up with a clean, professional commit history - -## Key Concepts - -### What is Interactive Rebase? - -Interactive rebase (`git rebase -i`) is a powerful tool that lets you modify commit history. Unlike regular rebase which simply moves commits, interactive rebase lets you: -- Reorder commits -- Edit commit messages (reword) -- Combine multiple commits (squash/fixup) -- Split commits into smaller ones -- Delete commits entirely -- Edit the contents of commits - -### Interactive Rebase Commands - -When you run `git rebase -i HEAD~3`, Git opens an editor with a list of commits and commands: - -``` -pick abc1234 First commit -pick def5678 Second commit -pick ghi9012 Third commit -``` - -You can change `pick` to other commands: - -- **pick**: Keep the commit as-is -- **reword**: Keep the commit but edit its message -- **edit**: Pause the rebase to amend the commit -- **squash**: Combine this commit with the previous one, keeping both messages -- **fixup**: Like squash, but discard this commit's message -- **drop**: Remove the commit entirely - -### When to Use Interactive Rebase - -Interactive rebase is perfect for: -- Cleaning up work-in-progress commits before creating a pull request -- Fixing typos in commit messages -- Combining related commits into logical units -- Removing debug commits or experimental code -- Creating a clean, professional history - -**Remember**: Only rebase commits that haven't been pushed to a shared branch! - -## Alternative Approach: Reset and Recommit - -Since interactive rebase requires an interactive editor, this challenge uses an alternative approach that achieves the same result: - -1. **Reset soft**: Move HEAD back while keeping changes staged - ```bash - git reset --soft HEAD~4 - ``` - This keeps all changes from the last 4 commits but "uncommits" them. - -2. **Create a new commit**: Commit all changes with a clean message - ```bash - git commit -m "Your new clean commit message" - ``` - -This technique is useful when you want to combine multiple commits into one without using interactive rebase. - -## Useful Commands - -```bash -# View commit history -git log --oneline - -# See the last N commits in detail -git log -n 5 - -# Reset to N commits back, keeping changes staged -git reset --soft HEAD~N - -# Reset to N commits back, keeping changes unstaged -git reset --mixed HEAD~N - -# Reset to N commits back, discarding all changes (DANGEROUS!) -git reset --hard HEAD~N - -# Create a new commit -git commit -m "message" - -# Amend the last commit -git commit --amend -m "new message" - -# Interactive rebase (requires interactive editor) -git rebase -i HEAD~N - -# Check current status -git status -``` - -## Verification - -Run the verification script to check your solution: - -```bash -.\verify.ps1 -``` - -The verification will check that: -- You have exactly 2 commits total (initial + your combined feature commit) -- The feature commit contains all the changes from the original 4 feature commits -- The commit message is clean and descriptive -- All expected files are present - -## Challenge Steps - -1. Navigate to the challenge directory -2. View the current messy commit history: `git log --oneline` -3. Use `git reset --soft HEAD~4` to uncommit the last 4 commits while keeping changes -4. Check status with `git status` - you should see all changes staged -5. Create a new commit with a clean message: `git commit -m "Add user profile feature with validation"` -6. Verify with `git log --oneline` - you should now have clean history -7. Run the verification script - -## Tips - -- Always check your history with `git log --oneline` before and after -- `git reset --soft` is safer than `--hard` because it keeps your changes -- You can use `git reset --soft HEAD~N` where N is the number of commits to undo -- If you make a mistake, run `.\reset.ps1` to start over -- In real projects with interactive rebase, you would use `git rebase -i HEAD~N` and modify the commit list in the editor - -## What is Git Reset? - -`git reset` moves the current branch pointer to a different commit: - -- **--soft**: Moves HEAD but keeps changes staged -- **--mixed** (default): Moves HEAD and unstages changes, but keeps them in working directory -- **--hard**: Moves HEAD and discards all changes (dangerous!) - -Reset is useful for: -- Undoing commits while keeping changes (`--soft`) -- Unstaging files (`--mixed`) -- Discarding commits entirely (`--hard`) - -## What You'll Learn - -Cleaning up commit history is an important skill for professional development. Whether you use interactive rebase or reset-and-recommit, the goal is the same: create a clear, logical history that makes your code review easier and your project history more understandable. Messy WIP commits are fine during development, but cleaning them up before merging shows attention to detail and makes your codebase more maintainable. diff --git a/02-advanced/03-interactive-rebase/reset.ps1 b/02-advanced/03-interactive-rebase/reset.ps1 deleted file mode 100644 index e672af5..0000000 --- a/02-advanced/03-interactive-rebase/reset.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Resets the interactive rebase challenge environment. - -.DESCRIPTION - Removes the existing challenge directory and runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "Resetting challenge environment..." -ForegroundColor Yellow - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Remove-Item -Path "challenge" -Recurse -Force - Write-Host "Removed existing challenge directory." -ForegroundColor Cyan -} - -# Run setup script -Write-Host "Running setup script...`n" -ForegroundColor Cyan -& ".\setup.ps1" diff --git a/02-advanced/03-interactive-rebase/setup.ps1 b/02-advanced/03-interactive-rebase/setup.ps1 deleted file mode 100644 index ba73b0d..0000000 --- a/02-advanced/03-interactive-rebase/setup.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Sets up the interactive rebase challenge environment. - -.DESCRIPTION - Creates a Git repository with messy commit history that needs to be - cleaned up using reset and recommit techniques. -#> - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Path "challenge" -Recurse -Force -} - -# Create challenge directory -Write-Host "Creating challenge environment..." -ForegroundColor Cyan -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize git repository -git init | Out-Null -git config user.name "Workshop User" | Out-Null -git config user.email "user@workshop.local" | Out-Null - -# Create initial commit -$readme = @" -# User Management System - -A simple user management application. -"@ - -Set-Content -Path "README.md" -Value $readme -git add README.md -git commit -m "Initial commit" | Out-Null - -# Create messy commits that should be squashed - -# Commit 1: WIP user profile -$userProfile = @" -class UserProfile: - def __init__(self, name, email): - self.name = name - self.email = email -"@ - -Set-Content -Path "user_profile.py" -Value $userProfile -git add user_profile.py -git commit -m "WIP: user profile" | Out-Null - -# Commit 2: Add validation (typo in message) -$userProfileWithValidation = @" -class UserProfile: - def __init__(self, name, email): - self.name = name - self.email = email - - def validate(self): - if not self.name or not self.email: - raise ValueError('Name and email are required') - return True -"@ - -Set-Content -Path "user_profile.py" -Value $userProfileWithValidation -git add user_profile.py -git commit -m "add validaton" | Out-Null # Intentional typo - -# Commit 3: Fix validation -$userProfileFixed = @" -class UserProfile: - def __init__(self, name, email): - self.name = name - self.email = email - - def validate(self): - if not self.name or not self.email: - raise ValueError('Name and email are required') - if '@' not in self.email: - raise ValueError('Invalid email format') - return True -"@ - -Set-Content -Path "user_profile.py" -Value $userProfileFixed -git add user_profile.py -git commit -m "fix validation bug" | Out-Null - -# Commit 4: Add tests (another WIP commit) -$tests = @" -import unittest -from user_profile import UserProfile - -class TestUserProfile(unittest.TestCase): - def test_validate_correct_user_data(self): - user = UserProfile('John', 'john@example.com') - self.assertTrue(user.validate()) - - def test_reject_missing_name(self): - user = UserProfile('', 'john@example.com') - with self.assertRaises(ValueError): - user.validate() - - def test_reject_invalid_email(self): - user = UserProfile('John', 'invalid-email') - with self.assertRaises(ValueError): - user.validate() - -if __name__ == '__main__': - unittest.main() -"@ - -Set-Content -Path "test_user_profile.py" -Value $tests -git add test_user_profile.py -git commit -m "WIP tests" | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "Challenge environment created!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have a repository with messy commit history:" -ForegroundColor Cyan -Write-Host "- WIP commits" -ForegroundColor Yellow -Write-Host "- Typos in commit messages" -ForegroundColor Yellow -Write-Host "- Multiple commits that should be combined" -ForegroundColor Yellow -Write-Host "`nYour task:" -ForegroundColor Yellow -Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White -Write-Host "2. View the messy history: git log --oneline" -ForegroundColor White -Write-Host "3. Use 'git reset --soft HEAD~4' to uncommit the last 4 commits" -ForegroundColor White -Write-Host "4. Create a single clean commit with a descriptive message" -ForegroundColor White -Write-Host " Example: git commit -m 'Add user profile feature with validation'" -ForegroundColor White -Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/02-advanced/03-interactive-rebase/verify.ps1 b/02-advanced/03-interactive-rebase/verify.ps1 deleted file mode 100644 index 18ab563..0000000 --- a/02-advanced/03-interactive-rebase/verify.ps1 +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Verifies the interactive rebase challenge solution. - -.DESCRIPTION - Checks that the user successfully cleaned up the commit history - by combining multiple messy commits into a single clean commit. -#> - -Set-Location "challenge" -ErrorAction SilentlyContinue - -# Check if challenge directory exists -if (-not (Test-Path "../verify.ps1")) { - Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path ".")) { - Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Write-Host "Verifying your solution..." -ForegroundColor Cyan - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] No git repository found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check commit count (should be exactly 2: initial + combined feature commit) -$commitCount = (git rev-list --count HEAD 2>$null) -if ($commitCount -ne 2) { - Write-Host "[FAIL] Expected exactly 2 commits, found $commitCount" -ForegroundColor Red - if ($commitCount -gt 2) { - Write-Host "Hint: You have too many commits. Use 'git reset --soft HEAD~N' to combine them." -ForegroundColor Yellow - Write-Host " Where N is the number of commits to undo (should be 4 in this case)." -ForegroundColor Yellow - } else { - Write-Host "Hint: You may have reset too far back. Run ../reset.ps1 to start over." -ForegroundColor Yellow - } - Set-Location .. - exit 1 -} - -# Get commit messages -$commits = git log --pretty=format:"%s" 2>$null -$commitArray = $commits -split "`n" - -# Check that first commit is still the initial commit -if ($commitArray[1] -ne "Initial commit") { - Write-Host "[FAIL] The initial commit should remain unchanged." -ForegroundColor Red - Write-Host "Expected first commit: 'Initial commit'" -ForegroundColor Yellow - Write-Host "Found: '$($commitArray[1])'" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that the second commit message is clean (not WIP, not with typos) -$featureCommit = $commitArray[0] -if ($featureCommit -match "WIP|wip") { - Write-Host "[FAIL] Commit message still contains 'WIP'." -ForegroundColor Red - Write-Host "Current message: '$featureCommit'" -ForegroundColor Yellow - Write-Host "Hint: Create a clean, descriptive commit message." -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -if ($featureCommit -match "validaton") { - Write-Host "[FAIL] Commit message contains a typo ('validaton')." -ForegroundColor Red - Write-Host "Current message: '$featureCommit'" -ForegroundColor Yellow - Write-Host "Hint: Use a properly spelled commit message." -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that commit message is not empty and has reasonable length -if ($featureCommit.Length -lt 10) { - Write-Host "[FAIL] Commit message is too short. Be more descriptive." -ForegroundColor Red - Write-Host "Current message: '$featureCommit'" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that required files exist -if (-not (Test-Path "user_profile.py")) { - Write-Host "[FAIL] user_profile.py not found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -if (-not (Test-Path "test_user_profile.py")) { - Write-Host "[FAIL] test_user_profile.py not found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check that user_profile.py contains all expected features -$userProfileContent = Get-Content "user_profile.py" -Raw - -# Should have the class -if ($userProfileContent -notmatch "class UserProfile") { - Write-Host "[FAIL] user_profile.py should contain UserProfile class." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Should have validation method -if ($userProfileContent -notmatch "def validate\(") { - Write-Host "[FAIL] user_profile.py should contain validate() method." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Should have email format validation (the final fix from commit 3) -if ($userProfileContent -notmatch "'@'.*in.*email|email.*in.*'@'") { - Write-Host "[FAIL] user_profile.py should contain email format validation." -ForegroundColor Red - Write-Host "Hint: Make sure all changes from all 4 commits are included." -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check that test file has content -$testContent = Get-Content "test_user_profile.py" -Raw - -if ($testContent -notmatch "class.*TestUserProfile") { - Write-Host "[FAIL] test_user_profile.py should contain TestUserProfile tests." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check that we have at least 3 test cases -$testMatches = ([regex]::Matches($testContent, "def test_")).Count -if ($testMatches -lt 3) { - Write-Host "[FAIL] test_user_profile.py should contain at least 3 test cases." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Verify that the latest commit contains changes to both files -$filesInLastCommit = git diff-tree --no-commit-id --name-only -r HEAD 2>$null -if ($filesInLastCommit -notcontains "user_profile.py") { - Write-Host "[FAIL] The feature commit should include user_profile.py" -ForegroundColor Red - Set-Location .. - exit 1 -} - -if ($filesInLastCommit -notcontains "test_user_profile.py") { - Write-Host "[FAIL] The feature commit should include test_user_profile.py" -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Success! -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Combined 4 messy commits into 1 clean commit" -ForegroundColor White -Write-Host "- Created a descriptive commit message" -ForegroundColor White -Write-Host "- Preserved all code changes" -ForegroundColor White -Write-Host "- Cleaned up the commit history" -ForegroundColor White -Write-Host "`nYour commit history is now clean and professional!" -ForegroundColor Green -Write-Host "Run 'git log --oneline' to see the result.`n" -ForegroundColor Cyan -Write-Host "Key takeaway: Clean commit history makes code review easier" -ForegroundColor Yellow -Write-Host "and your project more maintainable.`n" -ForegroundColor Yellow - -Set-Location .. -exit 0 diff --git a/02-advanced/04-worktrees/README.md b/02-advanced/04-worktrees/README.md deleted file mode 100644 index b6d9ec7..0000000 --- a/02-advanced/04-worktrees/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# Module 13: Worktrees - -## Learning Objectives - -By the end of this module, you will: -- Understand what Git worktrees are and when to use them -- Create and manage multiple working directories for the same repository -- Work on multiple branches simultaneously -- Understand the benefits of worktrees over stashing or cloning -- Remove and clean up worktrees - -## Challenge Description - -You're working on a feature when an urgent bug report comes in. Instead of stashing your work or creating a separate clone, you'll use Git worktrees to work on both the feature and the bugfix simultaneously in different directories. - -Your task is to: -1. Create a worktree for the bugfix on a separate branch -2. Fix the bug in the worktree -3. Commit and verify the fix -4. Continue working on your feature in the main working directory -5. Clean up the worktree when done - -## Key Concepts - -### What are Git Worktrees? - -A worktree is an additional working directory attached to the same repository. Each worktree can have a different branch checked out, allowing you to work on multiple branches simultaneously without switching. - -### Traditional Workflow vs Worktrees - -**Traditional (switching branches):** -``` -main-repo/ - - Switch to bugfix branch - - Fix bug - - Switch back to feature branch - - Continue feature work - - (Requires stashing or committing incomplete work) -``` - -**With Worktrees:** -``` -main-repo/ <- feature branch -worktrees/bugfix/ <- bugfix branch - -Work in both simultaneously! -``` - -### Why Use Worktrees? - -**Advantages:** -- Work on multiple branches at the same time -- No need to stash or commit incomplete work -- Each worktree has its own working directory and index -- Share the same Git history (one `.git` directory) -- Faster than cloning the entire repository -- Perfect for code reviews, comparisons, or parallel development - -**Use Cases:** -- Urgent bug fixes while working on a feature -- Code reviews (checkout PR in separate worktree) -- Comparing implementations side by side -- Running tests on one branch while coding on another -- Building different versions simultaneously - -## Useful Commands - -```bash -# List all worktrees -git worktree list - -# Add a new worktree -git worktree add -git worktree add ../bugfix bugfix-branch - -# Create new branch in worktree -git worktree add -b -git worktree add ../feature-new -b feature-new - -# Remove a worktree -git worktree remove -git worktree remove ../bugfix - -# Prune stale worktree information -git worktree prune - -# Move a worktree -git worktree move - -# Lock a worktree (prevent deletion) -git worktree lock -git worktree unlock -``` - -## Verification - -Run the verification script to check your solution: - -```bash -.\verify.ps1 -``` - -The verification will check that: -- You created a worktree for the bugfix -- The bug was fixed and committed in the worktree -- Your feature work continued in the main directory -- Both branches have the expected changes - -## Challenge Steps - -1. Navigate to the challenge directory -2. You're in main-repo with a feature branch checked out -3. View current worktrees: `git worktree list` -4. Create a worktree for bugfix: `git worktree add ../bugfix-worktree -b bugfix` -5. Navigate to the worktree: `cd ../bugfix-worktree` -6. Fix the bug in calculator.js (fix the divide by zero check) -7. Commit the fix: `git add . && git commit -m "Fix divide by zero bug"` -8. Go back to main repo: `cd ../main-repo` -9. Continue working on your feature -10. Add a new method to calculator.js -11. Commit your feature -12. List worktrees: `git worktree list` -13. Remove the worktree: `git worktree remove ../bugfix-worktree` -14. Run verification - -## Tips - -- Worktree paths are typically siblings of your main repo (use `../worktree-name`) -- Each worktree must have a different branch checked out -- Can't checkout the same branch in multiple worktrees -- The main `.git` directory is shared, so commits in any worktree are visible everywhere -- Worktrees are listed in `.git/worktrees/` -- Use `git worktree remove` to clean up, or just delete the directory and run `git worktree prune` -- Worktrees persist across restarts until explicitly removed - -## Common Worktree Workflows - -### Urgent Bugfix -```bash -# Currently on feature branch with uncommitted changes -git worktree add ../hotfix -b hotfix - -cd ../hotfix -# Fix the bug -git add . -git commit -m "Fix critical bug" -git push origin hotfix - -cd ../main-repo -# Continue working on feature -``` - -### Code Review -```bash -# Review a pull request without switching branches -git fetch origin pull/123/head:pr-123 -git worktree add ../review-pr-123 pr-123 - -cd ../review-pr-123 -# Review code, test it -# Run: npm test, npm start, etc. - -cd ../main-repo -git worktree remove ../review-pr-123 -``` - -### Parallel Development -```bash -# Work on two features simultaneously -git worktree add ../feature-a -b feature-a -git worktree add ../feature-b -b feature-b - -# Terminal 1 -cd feature-a && code . - -# Terminal 2 -cd feature-b && code . -``` - -### Build Comparison -```bash -# Compare builds between branches -git worktree add ../release-build release-v2.0 - -cd ../release-build -npm run build -# Test production build - -# Meanwhile, continue development in main repo -``` - -## Worktree vs Other Approaches - -### vs Stashing -- **Stash**: Temporary, one at a time, requires branch switching -- **Worktree**: Persistent, multiple simultaneously, no switching - -### vs Cloning -- **Clone**: Full copy, separate `.git`, uses more disk space -- **Worktree**: Shared `.git`, less disk space, instant sync - -### vs Branch Switching -- **Switching**: Requires clean working directory, one branch at a time -- **Worktree**: Keep dirty working directory, multiple branches active - -## What You'll Learn - -Git worktrees are a powerful but underutilized feature that can significantly improve your workflow. They eliminate the need for constant branch switching, stashing, or maintaining multiple clones. Whether you're handling urgent fixes, reviewing code, or comparing implementations, worktrees provide a clean and efficient solution. Once you understand worktrees, you'll find many situations where they're the perfect tool for the job. diff --git a/02-advanced/04-worktrees/reset.ps1 b/02-advanced/04-worktrees/reset.ps1 deleted file mode 100644 index 286aa1b..0000000 --- a/02-advanced/04-worktrees/reset.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Resets the worktrees challenge environment. - -.DESCRIPTION - Removes the existing challenge directory and runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "Resetting challenge environment..." -ForegroundColor Yellow - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Remove-Item -Path "challenge" -Recurse -Force - Write-Host "Removed existing challenge directory." -ForegroundColor Cyan -} - -# Run setup script -Write-Host "Running setup script...`n" -ForegroundColor Cyan -& ".\setup.ps1" diff --git a/02-advanced/04-worktrees/setup.ps1 b/02-advanced/04-worktrees/setup.ps1 deleted file mode 100644 index 4360a04..0000000 --- a/02-advanced/04-worktrees/setup.ps1 +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Sets up the worktrees challenge environment. - -.DESCRIPTION - Creates a Git repository with a feature in progress, ready for - demonstrating the use of worktrees for parallel work. -#> - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Path "challenge" -Recurse -Force -} - -# Create challenge directory -Write-Host "Creating challenge environment..." -ForegroundColor Cyan -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Create main repository -New-Item -ItemType Directory -Path "main-repo" | Out-Null -Set-Location "main-repo" - -# Initialize git repository -git init | Out-Null -git config user.name "Workshop User" | Out-Null -git config user.email "user@workshop.local" | Out-Null - -# Create initial calculator with a bug -$calculator = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - # BUG: No division by zero check! - def divide(self, a, b): - return a / b -"@ - -Set-Content -Path "calculator.py" -Value $calculator -git add calculator.py -git commit -m "Initial calculator implementation" | Out-Null - -$readme = @" -# Calculator Project - -A simple calculator with basic operations. - -## Features -- Addition -- Subtraction -- Multiplication -- Division (has a bug!) -"@ - -Set-Content -Path "README.md" -Value $readme -git add README.md -git commit -m "Add README" | Out-Null - -# Create feature branch and start working on it -git checkout -b feature-advanced-math | Out-Null - -# Add work in progress on feature branch -$calculatorWithFeature = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - # BUG: No division by zero check! - def divide(self, a, b): - return a / b - - # New feature: power function (work in progress) - def power(self, a, b): - return a ** b - - # TODO: Add square root function - # TODO: Add logarithm function -"@ - -Set-Content -Path "calculator.py" -Value $calculatorWithFeature -git add calculator.py -git commit -m "Add power function (WIP: more math functions coming)" | Out-Null - -# Return to challenge directory -Set-Location .. - -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "Challenge environment created!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nSituation:" -ForegroundColor Cyan -Write-Host "You're working on the 'feature-advanced-math' branch" -ForegroundColor White -Write-Host "You have plans to add more math functions (see TODOs)" -ForegroundColor White -Write-Host "`nUrgent: A critical bug was discovered in the divide function!" -ForegroundColor Red -Write-Host "It doesn't check for division by zero." -ForegroundColor Red -Write-Host "`nInstead of stashing your feature work, use a worktree:" -ForegroundColor Yellow -Write-Host "`nYour task:" -ForegroundColor Yellow -Write-Host "1. Navigate to main-repo: cd challenge/main-repo" -ForegroundColor White -Write-Host "2. Create a worktree: git worktree add ../bugfix-worktree -b bugfix" -ForegroundColor White -Write-Host "3. Go to worktree: cd ../bugfix-worktree" -ForegroundColor White -Write-Host "4. Fix the bug in calculator.py:" -ForegroundColor White -Write-Host " Add a check: if (b === 0) throw new Error('Division by zero');" -ForegroundColor White -Write-Host "5. Commit the fix: git add . && git commit -m 'Fix divide by zero bug'" -ForegroundColor White -Write-Host "6. Return to main-repo: cd ../main-repo" -ForegroundColor White -Write-Host "7. Complete your feature: Add square root method to calculator.py" -ForegroundColor White -Write-Host "8. Commit: git add . && git commit -m 'Add square root function'" -ForegroundColor White -Write-Host "9. Clean up worktree: git worktree remove ../bugfix-worktree" -ForegroundColor White -Write-Host "`nRun '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/02-advanced/04-worktrees/verify.ps1 b/02-advanced/04-worktrees/verify.ps1 deleted file mode 100644 index d5e779f..0000000 --- a/02-advanced/04-worktrees/verify.ps1 +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Verifies the worktrees challenge solution. - -.DESCRIPTION - Checks that the user successfully used worktrees to fix a bug - while continuing work on a feature. -#> - -Set-Location "challenge" -ErrorAction SilentlyContinue - -# Check if challenge directory exists -if (-not (Test-Path "../verify.ps1")) { - Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path ".")) { - Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Write-Host "Verifying your solution..." -ForegroundColor Cyan - -# Check if main-repo exists -if (-not (Test-Path "main-repo")) { - Write-Host "[FAIL] main-repo directory not found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Set-Location "main-repo" - -# Check if it's a git repository -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] main-repo is not a git repository." -ForegroundColor Red - Set-Location ../.. - exit 1 -} - -# Check if bugfix branch exists -$branches = git branch --all 2>$null -if ($branches -notmatch "bugfix") { - Write-Host "[FAIL] bugfix branch not found." -ForegroundColor Red - Write-Host "Hint: Create a worktree with: git worktree add ../bugfix-worktree -b bugfix" -ForegroundColor Yellow - Set-Location ../.. - exit 1 -} - -Write-Host "[PASS] Bugfix branch exists!" -ForegroundColor Green - -# Check bugfix branch for the fix -git checkout bugfix 2>$null | Out-Null - -if (-not (Test-Path "calculator.py")) { - Write-Host "[FAIL] calculator.py not found on bugfix branch." -ForegroundColor Red - Set-Location ../.. - exit 1 -} - -$bugfixCalc = Get-Content "calculator.py" -Raw - -# Check if division by zero check was added -if ($bugfixCalc -notmatch "b === 0|b == 0|division by zero|divide by zero") { - Write-Host "[FAIL] Division by zero check not found in bugfix branch." -ForegroundColor Red - Write-Host "Hint: Add a check in the divide method to prevent division by zero" -ForegroundColor Yellow - Set-Location ../.. - exit 1 -} - -# Check for commit on bugfix branch -$bugfixCommits = git log --pretty=format:"%s" bugfix 2>$null -if ($bugfixCommits -notmatch "bug|fix|division|divide") { - Write-Host "[FAIL] No bugfix commit found on bugfix branch." -ForegroundColor Red - Write-Host "Hint: Commit your fix with a descriptive message" -ForegroundColor Yellow - Set-Location ../.. - exit 1 -} - -Write-Host "[PASS] Bug fixed on bugfix branch!" -ForegroundColor Green - -# Check feature branch for continued work -git checkout feature-advanced-math 2>$null | Out-Null - -$featureCalc = Get-Content "calculator.py" -Raw - -# Check if square root function was added -if ($featureCalc -notmatch "sqrt|squareRoot") { - Write-Host "[FAIL] Square root function not found on feature branch." -ForegroundColor Red - Write-Host "Hint: Add the square root method to calculator.py on feature-advanced-math branch" -ForegroundColor Yellow - Set-Location ../.. - exit 1 -} - -# Check that feature work was committed -$featureCommits = git log --pretty=format:"%s" feature-advanced-math 2>$null -$featureCommitArray = $featureCommits -split "`n" - -# Should have at least 3 commits: initial + README + power + sqrt -if ($featureCommitArray.Count -lt 4) { - Write-Host "[FAIL] Not enough commits on feature branch." -ForegroundColor Red - Write-Host "Expected: initial, README, power, and square root commits" -ForegroundColor Yellow - Write-Host "Found $($featureCommitArray.Count) commits" -ForegroundColor Yellow - Set-Location ../.. - exit 1 -} - -# Check if the latest feature commit is about square root -if ($featureCommitArray[0] -notmatch "sqrt|square|root") { - Write-Host "[FAIL] Latest commit on feature branch should be about square root." -ForegroundColor Red - Write-Host "Latest commit: $($featureCommitArray[0])" -ForegroundColor Yellow - Set-Location ../.. - exit 1 -} - -Write-Host "[PASS] Feature work completed!" -ForegroundColor Green - -# Check if worktree was cleaned up (bugfix-worktree should not exist or be removed) -Set-Location .. -$worktreeStillExists = Test-Path "bugfix-worktree" - -if ($worktreeStillExists) { - Write-Host "[WARNING] bugfix-worktree directory still exists." -ForegroundColor Yellow - Write-Host "Hint: Clean up with: git worktree remove ../bugfix-worktree" -ForegroundColor Yellow - # Don't fail on this, just warn -} - -# Check worktree list -Set-Location "main-repo" -$worktrees = git worktree list 2>$null - -# Verify that the concept was understood (they should have created the worktree at some point) -# We can check this by looking for the bugfix branch existence -if ($branches -notmatch "bugfix") { - Write-Host "[FAIL] No evidence of worktree usage." -ForegroundColor Red - Set-Location ../.. - exit 1 -} - -# Success! -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Created a worktree for the bugfix" -ForegroundColor White -Write-Host "- Fixed the division by zero bug" -ForegroundColor White -Write-Host "- Committed the fix on the bugfix branch" -ForegroundColor White -Write-Host "- Continued feature work in parallel" -ForegroundColor White -Write-Host "- Added the square root function" -ForegroundColor White -Write-Host "- Committed the feature work" -ForegroundColor White - -if (-not $worktreeStillExists) { - Write-Host "- Cleaned up the worktree" -ForegroundColor White -} - -Write-Host "`nYou now understand Git worktrees!" -ForegroundColor Green -Write-Host "`nKey takeaway:" -ForegroundColor Yellow -Write-Host "Worktrees let you work on multiple branches simultaneously" -ForegroundColor White -Write-Host "without stashing, switching, or cloning the repository.`n" -ForegroundColor White - -Set-Location ../.. -exit 0 diff --git a/02-advanced/05-bisect/README.md b/02-advanced/05-bisect/README.md deleted file mode 100644 index 10870b9..0000000 --- a/02-advanced/05-bisect/README.md +++ /dev/null @@ -1,237 +0,0 @@ -# Module 14: Bisect - Finding Bugs with Binary Search - -## Learning Objectives - -By the end of this module, you will: -- Understand what git bisect is and when to use it -- Use binary search to find the commit that introduced a bug -- Mark commits as good or bad during bisection -- Automate bisect with test scripts -- Understand the efficiency of binary search for debugging - -## Challenge Description - -A bug has appeared in your calculator application, but you don't know which commit introduced it. The project has many commits, and manually checking each one would take too long. You'll use `git bisect` to efficiently find the culprit commit using binary search. - -Your task is to: -1. Start a bisect session -2. Mark the current commit as bad (bug exists) -3. Mark an old commit as good (bug didn't exist) -4. Test commits and mark them good or bad -5. Let Git find the first bad commit -6. Identify what change introduced the bug - -## Key Concepts - -### What is Git Bisect? - -Git bisect uses binary search to find the commit that introduced a bug. Instead of checking every commit linearly, it cuts the search space in half with each test, making it extremely efficient. - -### Binary Search Efficiency - -**Linear Search (manual checking):** -- 100 commits = up to 100 tests -- 1000 commits = up to 1000 tests - -**Binary Search (bisect):** -- 100 commits = ~7 tests -- 1000 commits = ~10 tests - -Formula: log₂(n) tests needed for n commits - -### How Bisect Works - -``` -Commits: A---B---C---D---E---F---G---H - ✓ ✓ ✓ ? ? ? ? ✗ - -1. Start: Mark H (bad) and A (good) -2. Git checks middle: E -3. You test E: bad ✗ - -Commits: A---B---C---D---E - ✓ ✓ ✓ ? ✗ - -4. Git checks middle: C -5. You test C: good ✓ - -Commits: C---D---E - ✓ ? ✗ - -6. Git checks: D -7. You test D: bad ✗ - -Result: D is the first bad commit! -``` - -### When to Use Bisect - -Use bisect when: -- You know a bug exists now but didn't exist in the past -- You have many commits to check -- You can reliably test for the bug -- You want to find exactly when something broke - -## Useful Commands - -```powershell -# Start bisect session -git bisect start - -# Mark current commit as bad -git bisect bad - -# Mark a commit as good -git bisect good -git bisect good HEAD~10 - -# After testing current commit -git bisect good # This commit is fine -git bisect bad # This commit has the bug - -# Skip a commit (if you can't test it) -git bisect skip - -# End bisect session and return to original state -git bisect reset - -# Visualize bisect process -git bisect visualize -git bisect view - -# Automate with a test script -git bisect run -git bisect run npm test -git bisect run ./test.sh - -# Show bisect log -git bisect log -``` - -## Verification - -Run the verification script to check your solution: - -```powershell -.\verify.ps1 -``` - -The verification will check that: -- You completed a bisect session -- You identified the correct commit that introduced the bug -- You understand which change caused the problem - -## Challenge Steps - -1. Navigate to the challenge directory -2. View the bug: run the calculator and see it fails -3. Start bisect: `git bisect start` -4. Mark current as bad: `git bisect bad` -5. Mark old commit as good: `git bisect good HEAD~10` -6. Git will checkout a middle commit -7. Test the current commit (run the test or check manually) -8. Mark it: `git bisect good` or `git bisect bad` -9. Repeat testing until Git identifies the bad commit -10. Note the commit hash and message -11. End bisect: `git bisect reset` -12. Check the identified commit: `git show ` -13. Create a file named `bug-commit.txt` with the bad commit hash -14. Run verification - -## Tips - -- Always start with a known good commit (far enough back) -- Keep a clear way to test each commit (script or manual steps) -- Use `git bisect log` to see your progress -- `git bisect reset` returns you to your original state -- You can bisect on any criteria, not just bugs (performance, features, etc.) -- Automate with `git bisect run` for faster results -- Each bisect step cuts remaining commits in half -- Skip commits you can't build/test with `git bisect skip` - -## Manual vs Automated Bisect - -### Manual Bisect -```powershell -git bisect start -git bisect bad -git bisect good HEAD~20 - -# For each commit Git checks out: -npm test -git bisect good # or bad - -git bisect reset -``` - -### Automated Bisect -```powershell -git bisect start -git bisect bad -git bisect good HEAD~20 -git bisect run npm test -# Git automatically tests each commit -git bisect reset -``` - -The test script should exit with: -- 0 for good (test passes) -- 1-127 (except 125) for bad (test fails) -- 125 for skip (can't test this commit) - -## Bisect Workflow Example - -### Finding a Performance Regression -```powershell -# App is slow now, was fast 50 commits ago -git bisect start -git bisect bad -git bisect good HEAD~50 - -# Create test script -@' -$output = npm start 2>&1 | Select-String "Started in" -if ($output -match "Started in (\d+)") { - if ([int]$Matches[1] -gt 5000) { exit 1 } # Slow - else { exit 0 } # Fast -} -exit 1 -'@ | Out-File -FilePath test.ps1 - -git bisect run pwsh test.ps1 -# Git finds the commit that made it slow -``` - -### Finding When a Feature Broke -```powershell -git bisect start -git bisect bad -git bisect good v1.0.0 # Last known good version - -# For each commit -npm test -- user-login.test.js -git bisect good # or bad - -# Found! Commit abc123 broke login -git show abc123 -``` - -## Common Bisect Pitfalls - -### Pitfall 1: Testing Incorrectly -- Make sure your test is consistent -- Automate when possible to avoid human error -- Use the same test for every commit - -### Pitfall 2: Wrong Good Commit -- If the "good" commit actually has the bug, bisect will fail -- Choose a commit you're confident was working - -### Pitfall 3: Multiple Bugs -- Bisect finds one commit at a time -- If multiple bugs exist, they might confuse the search -- Fix found bugs and bisect again for others - -## What You'll Learn - -Git bisect is a powerful debugging tool that turns a tedious manual search into an efficient automated process. By leveraging binary search, you can quickly pinpoint problematic commits even in repositories with thousands of commits. This is invaluable for debugging regressions, performance issues, or any situation where something that worked before is now broken. Mastering bisect makes you a more effective debugger and shows deep Git proficiency. diff --git a/02-advanced/05-bisect/reset.ps1 b/02-advanced/05-bisect/reset.ps1 deleted file mode 100644 index 1790ec1..0000000 --- a/02-advanced/05-bisect/reset.ps1 +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Resets the bisect challenge environment. - -.DESCRIPTION - Removes the existing challenge directory and runs setup.ps1 - to create a fresh challenge environment. -#> - -Write-Host "Resetting challenge environment..." -ForegroundColor Yellow - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Remove-Item -Path "challenge" -Recurse -Force - Write-Host "Removed existing challenge directory." -ForegroundColor Cyan -} - -# Run setup script -Write-Host "Running setup script...`n" -ForegroundColor Cyan -& ".\setup.ps1" diff --git a/02-advanced/05-bisect/setup.ps1 b/02-advanced/05-bisect/setup.ps1 deleted file mode 100644 index 037c158..0000000 --- a/02-advanced/05-bisect/setup.ps1 +++ /dev/null @@ -1,311 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Sets up the bisect challenge environment. - -.DESCRIPTION - Creates a Git repository with multiple commits where a bug is - introduced in one of them. Students use bisect to find it. -#> - -# Remove existing challenge directory if present -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Path "challenge" -Recurse -Force -} - -# Create challenge directory -Write-Host "Creating challenge environment..." -ForegroundColor Cyan -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize git repository -git init | Out-Null -git config user.name "Workshop User" | Out-Null -git config user.email "user@workshop.local" | Out-Null - -# Commit 1: Initial calculator -$calc1 = @" -class Calculator: - def add(self, a, b): - return a + b -"@ -Set-Content -Path "calculator.py" -Value $calc1 -git add calculator.py -git commit -m "Initial calculator with add function" | Out-Null - -# Commit 2: Add subtract -$calc2 = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b -"@ -Set-Content -Path "calculator.py" -Value $calc2 -git add calculator.py -git commit -m "Add subtract function" | Out-Null - -# Commit 3: Add multiply -$calc3 = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b -"@ -Set-Content -Path "calculator.py" -Value $calc3 -git add calculator.py -git commit -m "Add multiply function" | Out-Null - -# Commit 4: Add divide -$calc4 = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b -"@ -Set-Content -Path "calculator.py" -Value $calc4 -git add calculator.py -git commit -m "Add divide function" | Out-Null - -# Commit 5: Add modulo -$calc5 = @" -class Calculator: - def add(self, a, b): - return a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b - - def modulo(self, a, b): - return a % b -"@ -Set-Content -Path "calculator.py" -Value $calc5 -git add calculator.py -git commit -m "Add modulo function" | Out-Null - -# Commit 6: BUG - Introduce error in add function -$calc6 = @" -class Calculator: - def add(self, a, b): - return a - b # BUG: Should be a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b - - def modulo(self, a, b): - return a % b -"@ -Set-Content -Path "calculator.py" -Value $calc6 -git add calculator.py -git commit -m "Refactor add function for clarity" | Out-Null - -# Commit 7: Add power function (bug still exists) -$calc7 = @" -class Calculator: - def add(self, a, b): - return a - b # BUG: Should be a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b - - def modulo(self, a, b): - return a % b - - def power(self, a, b): - return a ** b -"@ -Set-Content -Path "calculator.py" -Value $calc7 -git add calculator.py -git commit -m "Add power function" | Out-Null - -# Commit 8: Add square root -$calc8 = @" -import math - -class Calculator: - def add(self, a, b): - return a - b # BUG: Should be a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b - - def modulo(self, a, b): - return a % b - - def power(self, a, b): - return a ** b - - def sqrt(self, a): - return math.sqrt(a) -"@ -Set-Content -Path "calculator.py" -Value $calc8 -git add calculator.py -git commit -m "Add square root function" | Out-Null - -# Commit 9: Add absolute value -$calc9 = @" -import math - -class Calculator: - def add(self, a, b): - return a - b # BUG: Should be a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b - - def modulo(self, a, b): - return a % b - - def power(self, a, b): - return a ** b - - def sqrt(self, a): - return math.sqrt(a) - - def abs(self, a): - return abs(a) -"@ -Set-Content -Path "calculator.py" -Value $calc9 -git add calculator.py -git commit -m "Add absolute value function" | Out-Null - -# Commit 10: Add max function -$calc10 = @" -import math - -class Calculator: - def add(self, a, b): - return a - b # BUG: Should be a + b - - def subtract(self, a, b): - return a - b - - def multiply(self, a, b): - return a * b - - def divide(self, a, b): - if b == 0: - raise ValueError('Division by zero') - return a / b - - def modulo(self, a, b): - return a % b - - def power(self, a, b): - return a ** b - - def sqrt(self, a): - return math.sqrt(a) - - def abs(self, a): - return abs(a) - - def max(self, a, b): - return a if a > b else b -"@ -Set-Content -Path "calculator.py" -Value $calc10 -git add calculator.py -git commit -m "Add max function" | Out-Null - -# Create a test file -$test = @" -import sys -from calculator import Calculator - -calc = Calculator() - -# Test addition (this will fail due to bug) -result = calc.add(5, 3) -if result != 8: - print(f'FAIL: add(5, 3) returned {result}, expected 8') - sys.exit(1) - -print('PASS: All tests passed') -sys.exit(0) -"@ -Set-Content -Path "test.py" -Value $test - -# Return to module directory -Set-Location .. - -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "Challenge environment created!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nSituation:" -ForegroundColor Cyan -Write-Host "The calculator has a bug - addition doesn't work correctly!" -ForegroundColor Red -Write-Host "calc.add(5, 3) returns 2 instead of 8" -ForegroundColor Red -Write-Host "`nThe bug was introduced somewhere in the last 10 commits." -ForegroundColor Yellow -Write-Host "Manually checking each commit would be tedious." -ForegroundColor Yellow -Write-Host "Use git bisect to find it efficiently!" -ForegroundColor Green -Write-Host "`nYour task:" -ForegroundColor Yellow -Write-Host "1. Navigate to the challenge directory: cd challenge" -ForegroundColor White -Write-Host "2. Test the bug: python test.py (it will fail)" -ForegroundColor White -Write-Host "3. Start bisect: git bisect start" -ForegroundColor White -Write-Host "4. Mark current as bad: git bisect bad" -ForegroundColor White -Write-Host "5. Mark old commit as good: git bisect good HEAD~10" -ForegroundColor White -Write-Host "6. Git will checkout a commit - test it: python test.py" -ForegroundColor White -Write-Host "7. Mark result: git bisect good (if test passes) or git bisect bad (if it fails)" -ForegroundColor White -Write-Host "8. Repeat until Git finds the first bad commit" -ForegroundColor White -Write-Host "9. Note the commit hash" -ForegroundColor White -Write-Host "10. End bisect: git bisect reset" -ForegroundColor White -Write-Host "11. Create bug-commit.txt with the bad commit hash" -ForegroundColor White -Write-Host "`nHint: The bug is in commit 6 ('Refactor add function for clarity')" -ForegroundColor Cyan -Write-Host "Run '../verify.ps1' from the challenge directory to check your solution.`n" -ForegroundColor Cyan diff --git a/02-advanced/05-bisect/verify.ps1 b/02-advanced/05-bisect/verify.ps1 deleted file mode 100644 index 2647577..0000000 --- a/02-advanced/05-bisect/verify.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env pwsh - -<# -.SYNOPSIS - Verifies the bisect challenge solution. - -.DESCRIPTION - Checks that the user successfully used git bisect to find - the commit that introduced the bug. -#> - -Set-Location "challenge" -ErrorAction SilentlyContinue - -# Check if challenge directory exists -if (-not (Test-Path "../verify.ps1")) { - Write-Host "Error: Please run this script from the module directory" -ForegroundColor Red - exit 1 -} - -if (-not (Test-Path ".")) { - Write-Host "Error: Challenge directory not found. Run setup.ps1 first." -ForegroundColor Red - Set-Location .. - exit 1 -} - -Write-Host "Verifying your solution..." -ForegroundColor Cyan - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] No git repository found." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Make sure we're not in a bisect session -$bisectHead = Test-Path ".git/BISECT_HEAD" -if ($bisectHead) { - Write-Host "[FAIL] You're still in a bisect session." -ForegroundColor Red - Write-Host "Hint: End the bisect with: git bisect reset" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check if bug-commit.txt exists -if (-not (Test-Path "bug-commit.txt")) { - Write-Host "[FAIL] bug-commit.txt not found." -ForegroundColor Red - Write-Host "Hint: After finding the bad commit, create a file with its hash:" -ForegroundColor Yellow - Write-Host " echo 'commit-hash' > bug-commit.txt" -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Read the commit hash from the file -$userCommit = (Get-Content "bug-commit.txt" -Raw).Trim() - -if (-not $userCommit) { - Write-Host "[FAIL] bug-commit.txt is empty." -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Get all commit hashes -$allCommits = git log --pretty=format:"%H" --reverse 2>$null -$commitArray = $allCommits -split "`n" - -# The bug was introduced in commit 6 (index 5 in 0-based array) -# This is the "Refactor add function for clarity" commit -$badCommitMessage = git log --pretty=format:"%s" --grep="Refactor add function" 2>$null -$actualBadCommit = git log --pretty=format:"%H" --grep="Refactor add function" 2>$null - -if (-not $actualBadCommit) { - Write-Host "[FAIL] Could not find the expected bad commit." -ForegroundColor Red - Write-Host "Something may be wrong with the repository setup." -ForegroundColor Yellow - Set-Location .. - exit 1 -} - -# Check if user found the correct commit -if ($userCommit -ne $actualBadCommit) { - # Maybe they provided a short hash - if ($actualBadCommit -like "$userCommit*") { - Write-Host "[PASS] Correct commit identified (using short hash)!" -ForegroundColor Green - } else { - Write-Host "[FAIL] Incorrect commit identified." -ForegroundColor Red - Write-Host "You identified: $userCommit" -ForegroundColor Yellow - Write-Host "Expected: $actualBadCommit" -ForegroundColor Yellow - Write-Host "Expected commit message: 'Refactor add function for clarity'" -ForegroundColor Yellow - Write-Host "`nHint: Use git bisect to find where the add function broke:" -ForegroundColor Yellow - Write-Host " git bisect start" -ForegroundColor White - Write-Host " git bisect bad" -ForegroundColor White - Write-Host " git bisect good HEAD~10" -ForegroundColor White - Write-Host " # Then test with: node test.py" -ForegroundColor White - Write-Host " git bisect good # or bad" -ForegroundColor White - Set-Location .. - exit 1 - } -} else { - Write-Host "[PASS] Correct commit identified!" -ForegroundColor Green -} - -# Verify the commit actually has the bug -git checkout $actualBadCommit 2>$null | Out-Null -$calcContent = Get-Content "calculator.py" -Raw - -if ($calcContent -notmatch "add\(a, b\)[\s\S]*?return a - b") { - Write-Host "[WARNING] The identified commit doesn't seem to have the expected bug." -ForegroundColor Yellow -} - -# Return to latest commit -git checkout $(git branch --show-current 2>$null) 2>$null | Out-Null -if (-not $?) { - git checkout main 2>$null | Out-Null -} - -# Success! -Write-Host "`n========================================" -ForegroundColor Green -Write-Host "SUCCESS! Challenge completed!" -ForegroundColor Green -Write-Host "========================================" -ForegroundColor Green -Write-Host "`nYou have successfully:" -ForegroundColor Cyan -Write-Host "- Used git bisect to perform a binary search" -ForegroundColor White -Write-Host "- Tested commits systematically" -ForegroundColor White -Write-Host "- Identified the exact commit that introduced the bug" -ForegroundColor White -Write-Host "- Found: '$badCommitMessage'" -ForegroundColor White -Write-Host "`nThe bug was in commit 6 out of 10 commits." -ForegroundColor Yellow -Write-Host "Manual checking: up to 10 tests" -ForegroundColor Yellow -Write-Host "With bisect: only ~4 tests needed!" -ForegroundColor Green -Write-Host "`nYou now understand git bisect!" -ForegroundColor Green -Write-Host "`nKey takeaway:" -ForegroundColor Yellow -Write-Host "Bisect uses binary search to efficiently find bugs," -ForegroundColor White -Write-Host "saving massive amounts of time in large codebases.`n" -ForegroundColor White - -Set-Location .. -exit 0 diff --git a/02-advanced/06-blame/README.md b/02-advanced/06-blame/README.md deleted file mode 100644 index 01de914..0000000 --- a/02-advanced/06-blame/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# Module 05: Git Blame - Code Archaeology - -## Learning Objectives - -In this module, you will: -- Use `git blame` to find who made specific changes -- Understand blame output format and information -- Track down problematic code changes -- Learn when and why to use `git blame` -- Investigate code history to understand context - -## Challenge - -### Setup - -Run the setup script to create your challenge environment: - -```powershell -.\setup.ps1 -``` - -This will create a `challenge/` directory with a Git repository that has a security issue - someone committed hardcoded credentials! - -### Your Task - -Your team has discovered a security vulnerability: hardcoded credentials were added to the codebase. Your job is to investigate who made this change and document your findings. - -The setup script will create an `investigation.md` file in the challenge directory with questions for you to answer. Use `git blame` and other Git commands to track down the responsible developer. - -**Scenario:** -- Someone added hardcoded login credentials (`username: "admin"`, `password: "admin123"`) to `app.py` -- This is a critical security issue -- You need to identify who made this change so the team can discuss it with them - -**Suggested Approach:** - -1. Navigate to the challenge directory: `cd challenge` -2. Open `investigation.md` to see the questions -3. Examine `app.py` to find the suspicious line -4. Use `git blame` to find who wrote that line -5. Use `git blame -e` to see email addresses -6. Use `git show` to see the full commit details -7. Document your findings in `investigation.md` - -> **Important Notes:** -> - `git blame` shows who last modified each line -> - Each line shows: commit hash, author, date, line number, and content -> - Use `-e` flag to show email addresses -> - Use `-L` to focus on specific line ranges - -## Key Concepts - -- **Git Blame**: Shows the revision and author who last modified each line of a file -- **Code Archaeology**: Using Git history to understand when and why code changed -- **Author Attribution**: Identifying who wrote specific code for context, not punishment -- **Commit Context**: Understanding the full story behind a change - -## Understanding Git Blame Output - -When you run `git blame app.py`, you'll see output like this: - -``` -a1b2c3d4 (John Doe 2024-01-15 10:30:45 +0000 1) # app.py - Main application -a1b2c3d4 (John Doe 2024-01-15 10:30:45 +0000 2) -e5f6g7h8 (Jane Smith 2024-01-16 14:20:10 +0000 3) from auth import login -e5f6g7h8 (Jane Smith 2024-01-16 14:20:10 +0000 4) -i9j0k1l2 (Bob Wilson 2024-01-17 09:15:30 +0000 5) def main(): -i9j0k1l2 (Bob Wilson 2024-01-17 09:15:30 +0000 6) login("admin", "admin123") -``` - -### Breaking It Down - -Each line shows: -1. **Commit Hash** (`a1b2c3d4`) - The commit that last changed this line -2. **Author Name** (`John Doe`) - Who made the change -3. **Date/Time** (`2024-01-15 10:30:45 +0000`) - When it was changed -4. **Line Number** (`1`) - The line number in the current file -5. **Line Content** (`# app.py - Main application`) - The actual code - -### Useful Git Blame Options - -```bash -git blame # Basic blame output -git blame -e # Show email addresses instead of names -git blame -L 10,20 # Only show lines 10-20 -git blame -L 10,+5 # Show 5 lines starting from line 10 -git blame -w # Ignore whitespace changes -git blame # Blame as of specific commit -``` - -### Following Up After Blame - -Once you find the commit hash: - -```bash -git show # See the full commit details -git log -p # See commit with diff -git show --stat # See which files were changed -``` - -## When to Use Git Blame - -**Good reasons to use `git blame`:** -- 🔍 Understanding why code was written a certain way -- 📚 Finding context for a piece of code -- 🐛 Identifying when a bug was introduced -- 💡 Discovering the thought process behind a decision -- 👥 Finding who to ask about specific code - -**Not for blaming:** -- ❌ Finding someone to blame for mistakes -- ❌ Tracking "productivity" or code ownership -- ❌ Punishing developers for old code - -**Remember:** Code archaeology is about understanding, not blaming! - -## Useful Commands - -### Investigation Commands - -```bash -# Find who changed each line -git blame -git blame -e # With email addresses - -# Focus on specific lines -git blame -L 10,20 # Lines 10-20 -git blame -L :function_name # Specific function (Git 2.20+) - -# See historical blame -git blame ^ # Blame before a specific commit - -# Combine with grep -git blame | grep "pattern" # Find who wrote lines matching pattern -``` - -### Context Commands - -```bash -# See full commit details -git show -git log -1 # Just the commit message - -# See all commits by author -git log --author="name" - -# See what else changed in that commit -git show --stat -``` - -## Verification - -Once you've completed your investigation in `investigation.md`, verify your solution: - -```powershell -.\verify.ps1 -``` - -The verification script will check that you've identified the correct developer. - -## Need to Start Over? - -If you want to reset the challenge and start fresh: - -```powershell -.\reset.ps1 -``` - -This will remove the challenge directory and run the setup script again, giving you a clean slate. diff --git a/02-advanced/06-blame/reset.ps1 b/02-advanced/06-blame/reset.ps1 deleted file mode 100644 index 17a7649..0000000 --- a/02-advanced/06-blame/reset.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Resets the Module 05 challenge environment. - -.DESCRIPTION - This script removes the challenge directory and re-runs the setup script - to give you a fresh start. -#> - -Write-Host "`n=== Resetting Module 05 Challenge ===" -ForegroundColor Cyan - -# Remove challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" - Write-Host "Challenge directory removed." -ForegroundColor Green -} else { - Write-Host "No challenge directory found to remove." -ForegroundColor Yellow -} - -# Run setup script -Write-Host "`nRunning setup script..." -ForegroundColor Cyan -& "./setup.ps1" diff --git a/02-advanced/06-blame/setup.ps1 b/02-advanced/06-blame/setup.ps1 deleted file mode 100644 index 50ead77..0000000 --- a/02-advanced/06-blame/setup.ps1 +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Sets up the Module 05 challenge environment for git blame investigation. - -.DESCRIPTION - This script creates a challenge directory with a Git repository that - contains a security vulnerability (hardcoded credentials) for students - to investigate using git blame. -#> - -Write-Host "`n=== Setting up Module 05 Challenge ===" -ForegroundColor Cyan - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} - -# Create fresh challenge directory -Write-Host "Creating challenge directory..." -ForegroundColor Green -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize Git repository -Write-Host "Initializing Git repository..." -ForegroundColor Green -git init | Out-Null - -# Commit 1: Initial project structure (by Alice) -Write-Host "Creating initial project structure..." -ForegroundColor Green -git config user.name "Alice Johnson" -git config user.email "alice@example.com" - -$appContent = @" -# app.py - Main application file - -def main(): - print("Welcome to My App!") - # Application initialization code here - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Initial project structure" | Out-Null - -# Commit 2: Add authentication module (by Bob) -Write-Host "Adding authentication module..." -ForegroundColor Green -git config user.name "Bob Chen" -git config user.email "bob@example.com" - -$authContent = @" -# auth.py - Authentication module - -def login(username, password): - # Authenticate user - print(f"Logging in user: {username}") - return True - -def logout(username): - # Log out user - print(f"Logging out user: {username}") - return True -"@ -Set-Content -Path "auth.py" -Value $authContent - -$appContent = @" -# app.py - Main application file -from auth import login, logout - -def main(): - print("Welcome to My App!") - # Application initialization code here - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Add authentication module" | Out-Null - -# Commit 3: Add database connection (by Carol) -Write-Host "Adding database connection..." -ForegroundColor Green -git config user.name "Carol Martinez" -git config user.email "carol@example.com" - -$databaseContent = @" -# database.py - Database connection module - -def connect(): - # Connect to database - print("Connecting to database...") - return True - -def disconnect(): - # Disconnect from database - print("Disconnecting from database...") - return True -"@ -Set-Content -Path "database.py" -Value $databaseContent - -$appContent = @" -# app.py - Main application file -from auth import login, logout -from database import connect, disconnect - -def main(): - print("Welcome to My App!") - connect() - # Application initialization code here - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Add database connection" | Out-Null - -# Commit 4: Add hardcoded credentials (THE SECURITY ISSUE - by Suspicious Developer) -Write-Host "Adding suspicious change..." -ForegroundColor Green -git config user.name "Suspicious Developer" -git config user.email "guilty@email.com" - -$appContent = @" -# app.py - Main application file -from auth import login, logout -from database import connect, disconnect - -def main(): - print("Welcome to My App!") - connect() - # Quick fix for testing - TODO: Remove before production! - if login("admin", "admin123"): - print("Admin logged in successfully") - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Add quick test login for debugging" | Out-Null - -# Commit 5: Add logging (by David - innocent commit after the security issue) -Write-Host "Adding logging module..." -ForegroundColor Green -git config user.name "David Lee" -git config user.email "david@example.com" - -$loggingContent = @" -# logging_config.py - Logging configuration - -import logging - -def setup_logging(): - logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - return logging.getLogger(__name__) -"@ -Set-Content -Path "logging_config.py" -Value $loggingContent - -git add . -git commit -m "Add logging configuration" | Out-Null - -# Reset git config -git config user.name "Workshop Student" -git config user.email "student@example.com" - -# Create investigation.md template -Write-Host "Creating investigation template..." -ForegroundColor Green -$investigationTemplate = @" -# Security Investigation Report - -## Incident Overview - -A security vulnerability has been discovered in the codebase: hardcoded credentials in `app.py`. - -**Your task:** Use git blame and related Git commands to investigate this security issue and document your findings. - ---- - -## Question 1: What line number contains the hardcoded password? - -Look at `app.py` and find the line with `"admin123"`. - -**Your Answer:** - - - ---- - -## Question 2: Who added the hardcoded credentials? - -Use `git blame` to find the email address of the developer who wrote the line with the hardcoded credentials. - -**Suggested commands:** -``````bash -# View blame with email addresses -git blame -e app.py - -# Or focus on specific lines (if you know the line range) -git blame -L 8,10 app.py - -# Look for the line containing login("admin", "admin123") -`````` - -**Your Answer (provide the email address):** - - - ---- - -## Question 3: What was the commit message for the change that introduced the hardcoded credentials? - -Once you've found the commit hash from git blame, use `git show` or `git log` to see the full commit message. - -**Suggested commands:** -``````bash -# After finding the commit hash from git blame -git show -git log -1 -`````` - -**Your Answer:** - - - ---- - -## Question 4: How many files were modified in the commit that added the hardcoded credentials? - -Use `git show` with the `--stat` flag to see which files were changed. - -**Suggested commands:** -``````bash -git show --stat -git show --name-only -`````` - -**Your Answer:** - - - ---- - -## Question 5: When was this security vulnerability introduced? - -Use the timestamp from git blame to determine when the vulnerable code was committed. - -**Your Answer (date and time):** - - - ---- - -## Recommendations - -Based on your investigation, what actions should the team take? - -**Your Recommendations:** - - - ---- - -## Quick Reference - Investigation Commands - -**Finding Who Changed What:** -``````bash -git blame # Show who last modified each line -git blame -e # Show with email addresses -git blame -L 10,20 # Blame specific line range -`````` - -**Getting Commit Details:** -``````bash -git show # See full commit details -git show --stat # See files changed -git log -1 # See commit message only -git log -p # See commit with diff -`````` - -**Searching History:** -``````bash -git log --all --grep="keyword" # Search commit messages -git log --author="name" # See commits by author -git log --since="2 weeks ago" # Recent commits -`````` - ---- - -When you're done with your investigation, run ``..\verify.ps1`` to check your answers! -"@ - -Set-Content -Path "investigation.md" -Value $investigationTemplate - -# Return to module directory -Set-Location .. - -Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green -Write-Host "`nYour investigation environment is ready in the 'challenge/' directory." -ForegroundColor Cyan -Write-Host "`nScenario: Someone committed hardcoded credentials to app.py!" -ForegroundColor Yellow -Write-Host "`nNext steps:" -ForegroundColor Cyan -Write-Host " 1. cd challenge" -ForegroundColor White -Write-Host " 2. Open 'investigation.md' to see the investigation questions" -ForegroundColor White -Write-Host " 3. Use 'git blame -e app.py' to start your investigation" -ForegroundColor White -Write-Host " 4. Fill in your findings in 'investigation.md'" -ForegroundColor White -Write-Host " 5. Run '..\verify.ps1' to check your investigation" -ForegroundColor White -Write-Host "" diff --git a/02-advanced/06-blame/verify.ps1 b/02-advanced/06-blame/verify.ps1 deleted file mode 100644 index 8b843ae..0000000 --- a/02-advanced/06-blame/verify.ps1 +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Verifies the Module 05 challenge solution. - -.DESCRIPTION - This script checks that: - - The challenge directory exists - - A Git repository exists - - investigation.md exists with correct findings about the security issue -#> - -Write-Host "`n=== Verifying Module 05 Solution ===" -ForegroundColor Cyan - -$allChecksPassed = $true - -# Check if challenge directory exists -if (-not (Test-Path "challenge")) { - Write-Host "[FAIL] Challenge directory not found. Did you run setup.ps1?" -ForegroundColor Red - exit 1 -} - -Set-Location "challenge" - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] Not a git repository. Did you run setup.ps1?" -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check if investigation.md exists -if (-not (Test-Path "investigation.md")) { - Write-Host "[FAIL] investigation.md not found. Did you run setup.ps1?" -ForegroundColor Red - Write-Host "[HINT] The setup script should have created investigation.md for you" -ForegroundColor Yellow - $allChecksPassed = $false -} else { - Write-Host "[PASS] investigation.md exists" -ForegroundColor Green - - # Read the investigation file - $investigation = Get-Content "investigation.md" -Raw - $investigationLower = $investigation.ToLower() - - # Check 1: Line number (line 8 contains the hardcoded password) - if ($investigationLower -match "8") { - Write-Host "[PASS] Correct line number identified" -ForegroundColor Green - } else { - Write-Host "[FAIL] Line number not found or incorrect" -ForegroundColor Red - Write-Host "[HINT] Look at app.py to find which line contains 'admin123'" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check 2: Email address (guilty@email.com) - if ($investigationLower -match "guilty@email\.com") { - Write-Host "[PASS] Correct email address found using git blame!" -ForegroundColor Green - } else { - Write-Host "[FAIL] Developer's email address not found" -ForegroundColor Red - Write-Host "[HINT] Use 'git blame -e app.py' to see who changed each line with email addresses" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check 3: Commit message (contains "test" or "debug" or "quick") - if ($investigationLower -match "test|debug|quick") { - Write-Host "[PASS] Commit message identified" -ForegroundColor Green - } else { - Write-Host "[FAIL] Commit message not found" -ForegroundColor Red - Write-Host "[HINT] Use 'git show ' to see the commit message" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check 4: Number of files (1 file - only app.py) - if ($investigationLower -match "1|one|app\.py") { - Write-Host "[PASS] Number of files modified identified" -ForegroundColor Green - } else { - Write-Host "[FAIL] Number of files modified not found" -ForegroundColor Red - Write-Host "[HINT] Use 'git show --stat' to see which files were changed" -ForegroundColor Yellow - $allChecksPassed = $false - } - - # Check 5: Some mention of timestamp/date (flexible check) - # We're just checking they attempted to answer this - if ($investigationLower -match "202|date|time|\d{4}-\d{2}-\d{2}") { - Write-Host "[PASS] Timestamp/date documented" -ForegroundColor Green - } else { - Write-Host "[FAIL] Timestamp/date not documented" -ForegroundColor Red - Write-Host "[HINT] The git blame output shows the date and time of each change" -ForegroundColor Yellow - $allChecksPassed = $false - } -} - -Set-Location .. - -# Final summary -if ($allChecksPassed) { - Write-Host "`n" -NoNewline - Write-Host "=====================================" -ForegroundColor Green - Write-Host " INVESTIGATION COMPLETE!" -ForegroundColor Green - Write-Host "=====================================" -ForegroundColor Green - Write-Host "`nExcellent detective work! You've successfully used git blame to track down the security issue." -ForegroundColor Cyan - Write-Host "`nYou now know how to:" -ForegroundColor Cyan - Write-Host " - Use git blame to find who modified each line" -ForegroundColor White - Write-Host " - Read and interpret git blame output" -ForegroundColor White - Write-Host " - Use git blame with -e flag to show email addresses" -ForegroundColor White - Write-Host " - Find commit details after identifying changes with blame" -ForegroundColor White - Write-Host " - Conduct code archaeology to understand code history" -ForegroundColor White - Write-Host "`nRemember: git blame is for understanding, not blaming!" -ForegroundColor Yellow - Write-Host "`nReady for the next module!" -ForegroundColor Green - Write-Host "" -} else { - Write-Host "`n[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red - Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow - Write-Host "" - exit 1 -} diff --git a/02-advanced/07-merge-strategies/README.md b/02-advanced/07-merge-strategies/README.md deleted file mode 100644 index eed5a26..0000000 --- a/02-advanced/07-merge-strategies/README.md +++ /dev/null @@ -1,448 +0,0 @@ -# Module 06: Merge Strategies - Fast-Forward vs Three-Way - -## Learning Objectives - -In this module, you will: -- Understand the difference between fast-forward and three-way merges -- Learn when Git automatically chooses each strategy -- Force specific merge behavior with `--no-ff` and `--ff-only` flags -- Understand the trade-offs between linear and branched history -- Make informed decisions about merge strategies for different workflows - -## Prerequisites - -Before starting this module, you should have completed: -- Module 03: Branching Basics -- Module 04: Merging Branches - -## Challenge - -### Setup - -Run the setup script to create your challenge environment: - -```powershell -.\setup.ps1 -``` - -This will create a `challenge/` directory with scenarios for both fast-forward and three-way merges. - -### Your Task - -You'll experiment with both types of merges and learn how to control Git's merge behavior. - -**Part 1: Fast-Forward Merge** -1. Merge `feature-fast-forward` into main -2. Observe Git's "Fast-forward" message -3. Examine the linear history - -**Part 2: Three-Way Merge** -4. Merge `feature-divergent` into main -5. Observe Git creates a merge commit -6. Examine the branched history - -**Part 3: Force Merge Commit** -7. Merge `feature-optional` into main using `--no-ff` -8. Compare with the fast-forward from Part 1 - -**Steps:** - -1. Navigate to the challenge directory: `cd challenge` -2. View all branches: `git branch -a` -3. View the current graph: `git log --oneline --graph --all` -4. Merge feature-fast-forward: `git merge feature-fast-forward` -5. Check the log: `git log --oneline --graph` -6. Merge feature-divergent: `git merge feature-divergent` -7. Check the log: `git log --oneline --graph --all` -8. Merge feature-optional with --no-ff: `git merge --no-ff feature-optional` -9. Compare the results: `git log --oneline --graph --all` - -> **Key Questions to Consider:** -> - Which merges created merge commits? -> - Which merge kept a linear history? -> - How does `--no-ff` change Git's behavior? -> - When would you prefer each approach? - -## Understanding Merge Strategies - -### Fast-Forward Merge - -A **fast-forward merge** happens when the target branch (e.g., `main`) hasn't changed since the feature branch was created. Git simply "fast-forwards" the branch pointer to the latest commit on the feature branch. - -**Before the merge:** -``` -main: A---B - \ -feature: C---D -``` - -In this scenario: -- Commit B is where `feature` branched off from `main` -- Commits C and D are new commits on the `feature` branch -- `main` has NO new commits since the branch split -- The history is **linear** (straight line from A to D) - -**After `git merge feature` (on main):** -``` -main: A---B---C---D - ↑ - feature -``` - -**What happened:** -- Git moved the `main` pointer forward to commit D -- NO merge commit was created -- The history remains linear (a straight line) -- Both `main` and `feature` now point to the same commit (D) - -**Command:** -```bash -git switch main -git merge feature-fast-forward -``` - -**Output you'll see:** -``` -Updating abc123..def456 -Fast-forward - new-feature.py | 10 ++++++++++ - 1 file changed, 10 insertions(+) -``` - -**Notice the "Fast-forward" message!** - -**Characteristics:** -- ✅ Keeps history linear and clean -- ✅ Simpler to read in `git log` -- ✅ No extra merge commit -- ❌ Loses visibility that work was done on a branch -- ❌ Harder to revert entire features at once - ---- - -### Three-Way Merge - -A **three-way merge** happens when BOTH branches have new commits since they diverged. Git must combine changes from both branches, which creates a special merge commit. - -**Before the merge:** -``` -main: A---B---C---E - \ -feature: D---F -``` - -In this scenario: -- Commit B is where `feature` branched off from `main` -- Commits C and E are new commits on `main` -- Commits D and F are new commits on `feature` -- The branches have **diverged** (both have unique commits) - -**After `git merge feature` (on main):** -``` -main: A---B---C---E---M - \ / -feature: D---F---/ -``` - -**What happened:** -- Git created a new **merge commit** (M) -- Commit M has TWO parent commits: E (from main) and F (from feature) -- The merge commit combines changes from both branches -- The history shows the branches converging - -**Why it's called "three-way":** -Git uses THREE commits to perform the merge: -1. **Commit B** - The common ancestor (where branches split) -2. **Commit E** - The latest commit on `main` -3. **Commit F** - The latest commit on `feature` - -Git compares all three to figure out what changed on each branch and how to combine them. - -**Command:** -```bash -git switch main -git merge feature-divergent -``` - -**Output you'll see:** -``` -Merge made by the 'ort' strategy. - feature-code.py | 5 +++++ - 1 file changed, 5 insertions(+) -``` - -**Notice it says "Merge made by the 'ort' strategy" instead of "Fast-forward"!** - -**Characteristics:** -- ✅ Preserves feature branch history -- ✅ Shows when features were merged -- ✅ Easier to revert entire features (revert the merge commit) -- ✅ Clear visualization in git log --graph -- ❌ Creates more commits (merge commits) -- ❌ History can become complex with many merges - ---- - -### When Does Each Type Happen? - -| Situation | Merge Type | Merge Commit? | Git's Behavior | -|-----------|------------|---------------|----------------| -| Target branch (main) has NO new commits | Fast-Forward | ❌ No | Automatic | -| Target branch (main) HAS new commits | Three-Way | ✅ Yes | Automatic | -| You use `--no-ff` flag | Three-Way | ✅ Yes | Forced | -| You use `--ff-only` flag | Fast-Forward | ❌ No | Fails if not possible | - -**Git chooses automatically** based on the branch state, but you can override this behavior! - ---- - -## Controlling Merge Behavior - -### Force a Merge Commit with `--no-ff` - -Even if a fast-forward is possible, you can force Git to create a merge commit: - -```bash -git merge --no-ff feature-optional -``` - -**Before (fast-forward would be possible):** -``` -main: A---B - \ -feature: C---D -``` - -**After `git merge --no-ff feature` (on main):** -``` -main: A---B-------M - \ / -feature: C---D -``` - -Notice that even though main didn't change, a merge commit (M) was created! - -**When to use `--no-ff`:** -- ✅ When you want to preserve the feature branch in history -- ✅ For important features that might need to be reverted -- ✅ In team workflows where you want to see when features were merged -- ✅ When following a branching model like Git Flow - -**Example:** -```bash -# You've finished a major feature on feature-auth -git switch main -git merge --no-ff feature-auth -m "Merge feature-auth: Add user authentication system" -``` - -This creates a clear marker in history showing when and what was merged. - ---- - -### Require Fast-Forward with `--ff-only` - -You can make Git fail the merge if a fast-forward isn't possible: - -```bash -git merge --ff-only feature-branch -``` - -**What happens:** -- ✅ If fast-forward is possible, Git merges -- ❌ If branches have diverged, Git refuses to merge - -**Output when it fails:** -``` -fatal: Not possible to fast-forward, aborting. -``` - -**When to use `--ff-only`:** -- ✅ When you want to keep history strictly linear -- ✅ To ensure you rebase before merging -- ✅ In workflows that prohibit merge commits - -**Example workflow:** -```bash -# Try to merge -git merge --ff-only feature-branch - -# If it fails, rebase first -git switch feature-branch -git rebase main -git switch main -git merge --ff-only feature-branch # Now it works! -``` - ---- - -## Visualizing the Difference - -### Linear History (Fast-Forward) - -```bash -git log --oneline --graph -``` - -``` -* d1e2f3g (HEAD -> main, feature) Add feature C -* a4b5c6d Add feature B -* 7e8f9g0 Add feature A -* 1a2b3c4 Initial commit -``` - -Notice the straight line! No branching visible. - ---- - -### Branched History (Three-Way Merge) - -```bash -git log --oneline --graph --all -``` - -``` -* m1e2r3g (HEAD -> main) Merge branch 'feature' -|\ -| * f4e5a6t (feature) Add feature implementation -| * u7r8e9s Feature setup -* | a1b2c3d Update documentation on main -|/ -* 0i1n2i3t Initial commit -``` - -Notice the branching pattern! You can see: -- Where the branch split (`|/`) -- Commits on each branch (`|`) -- Where branches merged (`* merge commit`) - ---- - -## Decision Guide: Which Strategy to Use? - -### Use Fast-Forward When: -- ✅ Working on personal projects -- ✅ Want simplest, cleanest history -- ✅ Small changes or bug fixes -- ✅ Don't need to track feature branches -- ✅ History readability is top priority - -### Use Three-Way Merge (or force with --no-ff) When: -- ✅ Working in teams -- ✅ Want to preserve feature context -- ✅ Need to revert features as units -- ✅ Following Git Flow or similar workflow -- ✅ Important to see when features were integrated - -### Force Fast-Forward (--ff-only) When: -- ✅ Enforcing rebase workflow -- ✅ Maintaining strictly linear history -- ✅ Integration branch requires clean history - ---- - -## Comparison Table - -| Aspect | Fast-Forward | Three-Way | -|--------|-------------|-----------| -| **When it happens** | Target branch unchanged | Both branches have new commits | -| **Merge commit created?** | ❌ No | ✅ Yes | -| **History appearance** | Linear | Branched | -| **Command output** | "Fast-forward" | "Merge made by..." | -| **Git log --graph** | Straight line | Fork and merge pattern | -| **Can revert entire feature?** | ❌ No (must revert each commit) | ✅ Yes (revert merge commit) | -| **Force it** | `--ff-only` | `--no-ff` | -| **Best for** | Solo work, small changes | Team work, features | - ---- - -## Useful Commands - -### Merging with Strategy Control - -```bash -git merge # Let Git decide automatically -git merge --no-ff # Force a merge commit -git merge --ff-only # Only merge if fast-forward possible -git merge --abort # Cancel a merge in progress -``` - -### Viewing Different Merge Types - -```bash -# See all merges -git log --merges # Only merge commits -git log --no-merges # Hide merge commits - -# Visualize history -git log --oneline --graph # See branch structure -git log --oneline --graph --all # Include all branches -git log --first-parent # Follow only main branch line - -# Check if merge would be fast-forward -git merge-base main feature # Find common ancestor -git log main..feature # See commits unique to feature -``` - -### Configuring Default Behavior - -```bash -# Disable fast-forward by default -git config merge.ff false # Always create merge commits - -# Only allow fast-forward -git config merge.ff only # Refuse non-fast-forward merges - -# Reset to default -git config --unset merge.ff -``` - ---- - -## Common Patterns in the Wild - -### GitHub Flow (Simple) -- Use fast-forward when possible -- Short-lived feature branches -- Merge to main frequently - -### Git Flow (Structured) -- Always use `--no-ff` for features -- Preserve branch history -- Complex release management - -### Rebase Workflow -- Always use `--ff-only` -- Rebase before merging -- Strictly linear history - ---- - -## Verification - -Once you've completed all three merges, verify your solution: - -```powershell -.\verify.ps1 -``` - -The verification script will check that you've experienced both types of merges and used the `--no-ff` flag. - -## Need to Start Over? - -If you want to reset the challenge and start fresh: - -```powershell -.\reset.ps1 -``` - -This will remove the challenge directory and run the setup script again, giving you a clean slate. - -## What's Next? - -Now that you understand merge strategies, you can make informed decisions about your workflow. Consider: - -- **For personal projects:** Fast-forward merges keep history simple -- **For team projects:** Three-way merges preserve context -- **For open source:** Follow the project's contribution guidelines - -The best strategy depends on your team's needs and workflow! diff --git a/02-advanced/07-merge-strategies/reset.ps1 b/02-advanced/07-merge-strategies/reset.ps1 deleted file mode 100644 index ed0ffa9..0000000 --- a/02-advanced/07-merge-strategies/reset.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Resets the Module 06 challenge environment. - -.DESCRIPTION - This script removes the challenge directory and re-runs the setup script - to give you a fresh start. -#> - -Write-Host "`n=== Resetting Module 06 Challenge ===" -ForegroundColor Cyan - -# Remove challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" - Write-Host "Challenge directory removed." -ForegroundColor Green -} else { - Write-Host "No challenge directory found to remove." -ForegroundColor Yellow -} - -# Run setup script -Write-Host "`nRunning setup script..." -ForegroundColor Cyan -& "./setup.ps1" diff --git a/02-advanced/07-merge-strategies/setup.ps1 b/02-advanced/07-merge-strategies/setup.ps1 deleted file mode 100644 index 2eca7f9..0000000 --- a/02-advanced/07-merge-strategies/setup.ps1 +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Sets up the Module 06 challenge environment for learning merge strategies. - -.DESCRIPTION - This script creates a challenge directory with a Git repository that - contains scenarios for both fast-forward and three-way merges, allowing - students to compare different merge strategies. -#> - -Write-Host "`n=== Setting up Module 06 Challenge ===" -ForegroundColor Cyan - -# Remove existing challenge directory if it exists -if (Test-Path "challenge") { - Write-Host "Removing existing challenge directory..." -ForegroundColor Yellow - Remove-Item -Recurse -Force "challenge" -} - -# Create fresh challenge directory -Write-Host "Creating challenge directory..." -ForegroundColor Green -New-Item -ItemType Directory -Path "challenge" | Out-Null -Set-Location "challenge" - -# Initialize Git repository -Write-Host "Initializing Git repository..." -ForegroundColor Green -git init | Out-Null - -# Configure git for this repository -git config user.name "Workshop Student" -git config user.email "student@example.com" - -# ======================================== -# Scenario 1: Fast-Forward Merge Setup -# ======================================== - -Write-Host "Setting up fast-forward merge scenario..." -ForegroundColor Green - -# Commit 1: Initial structure on main -$appContent = @" -# app.py - Main application - -def main(): - print("Application started") - pass - -if __name__ == "__main__": - main() -"@ -Set-Content -Path "app.py" -Value $appContent - -git add . -git commit -m "Initial application structure" | Out-Null - -# Create feature-fast-forward branch (main won't change after this) -git switch -c feature-fast-forward | Out-Null - -# Commit on feature-fast-forward -$utilsContent = @" -# utils.py - Utility functions - -def format_string(text): - """Format a string to title case.""" - return text.title() - -def validate_input(text): - """Validate user input.""" - return text and len(text) > 0 -"@ -Set-Content -Path "utils.py" -Value $utilsContent - -git add . -git commit -m "Add utility functions" | Out-Null - -# Second commit on feature-fast-forward -$utilsContent = @" -# utils.py - Utility functions - -def format_string(text): - """Format a string to title case.""" - return text.title() - -def validate_input(text): - """Validate user input.""" - return text and len(text) > 0 - -def sanitize_input(text): - """Remove dangerous characters from input.""" - return text.replace("<", "").replace(">", "") -"@ -Set-Content -Path "utils.py" -Value $utilsContent - -git add . -git commit -m "Add input sanitization" | Out-Null - -# ======================================== -# Scenario 2: Three-Way Merge Setup -# ======================================== - -Write-Host "Setting up three-way merge scenario..." -ForegroundColor Green - -# Switch back to main -git switch main | Out-Null - -# Create feature-divergent branch -git switch -c feature-divergent | Out-Null - -# Commit on feature-divergent -$authContent = @" -# auth.py - Authentication module - -def authenticate(username, password): - """Authenticate a user.""" - print(f"Authenticating: {username}") - # TODO: Implement actual authentication - return True -"@ -Set-Content -Path "auth.py" -Value $authContent - -git add . -git commit -m "Add authentication module" | Out-Null - -# Second commit on feature-divergent -$authContent = @" -# auth.py - Authentication module - -def authenticate(username, password): - """Authenticate a user.""" - print(f"Authenticating: {username}") - # TODO: Implement actual authentication - return True - -def check_permissions(user, resource): - """Check if user has permission for resource.""" - print(f"Checking permissions for {user}") - return True -"@ -Set-Content -Path "auth.py" -Value $authContent - -git add . -git commit -m "Add permission checking" | Out-Null - -# Switch back to main and make a commit (creates divergence) -git switch main | Out-Null - -$readmeContent = @" -# My Application - -A Python application with utilities and authentication. - -## Features - -- String formatting and validation -- User authentication -- Permission management - -## Setup - -1. Install Python 3.8+ -2. Run: python app.py -"@ -Set-Content -Path "README.md" -Value $readmeContent - -git add . -git commit -m "Add README documentation" | Out-Null - -# ======================================== -# Scenario 3: Optional Fast-Forward for --no-ff -# ======================================== - -Write-Host "Setting up --no-ff demonstration scenario..." -ForegroundColor Green - -# Create feature-optional branch (main won't change after this) -git switch -c feature-optional | Out-Null - -# Commit on feature-optional -$configContent = @" -# config.py - Configuration settings - -DEBUG_MODE = False -LOG_LEVEL = "INFO" -DATABASE_URL = "sqlite:///app.db" -"@ -Set-Content -Path "config.py" -Value $configContent - -git add . -git commit -m "Add configuration module" | Out-Null - -# Switch back to main -git switch main | Out-Null - -# Return to module directory -Set-Location .. - -Write-Host "`n=== Setup Complete! ===" -ForegroundColor Green -Write-Host "`nYour challenge environment is ready in the 'challenge/' directory." -ForegroundColor Cyan -Write-Host "`nYou have THREE scenarios set up:" -ForegroundColor Yellow -Write-Host "`n Scenario 1: Fast-Forward Merge" -ForegroundColor White -Write-Host " Branch: feature-fast-forward" -ForegroundColor Cyan -Write-Host " Status: main has NOT changed since branch was created" -ForegroundColor Cyan -Write-Host " Result: Will fast-forward (no merge commit)" -ForegroundColor Green -Write-Host "`n Scenario 2: Three-Way Merge" -ForegroundColor White -Write-Host " Branch: feature-divergent" -ForegroundColor Cyan -Write-Host " Status: BOTH main and branch have new commits" -ForegroundColor Cyan -Write-Host " Result: Will create merge commit" -ForegroundColor Green -Write-Host "`n Scenario 3: Force Merge Commit" -ForegroundColor White -Write-Host " Branch: feature-optional" -ForegroundColor Cyan -Write-Host " Status: Could fast-forward, but we'll use --no-ff" -ForegroundColor Cyan -Write-Host " Result: Will create merge commit even though fast-forward is possible" -ForegroundColor Green -Write-Host "`nNext steps:" -ForegroundColor Cyan -Write-Host " 1. cd challenge" -ForegroundColor White -Write-Host " 2. View initial state: git log --oneline --graph --all" -ForegroundColor White -Write-Host " 3. Merge fast-forward: git merge feature-fast-forward" -ForegroundColor White -Write-Host " 4. View result: git log --oneline --graph" -ForegroundColor White -Write-Host " 5. Merge divergent: git merge feature-divergent" -ForegroundColor White -Write-Host " 6. View result: git log --oneline --graph --all" -ForegroundColor White -Write-Host " 7. Merge with --no-ff: git merge --no-ff feature-optional" -ForegroundColor White -Write-Host " 8. View final result: git log --oneline --graph --all" -ForegroundColor White -Write-Host " 9. Compare all three merges!" -ForegroundColor White -Write-Host " 10. Run '..\verify.ps1' to check your solution" -ForegroundColor White -Write-Host "" diff --git a/02-advanced/07-merge-strategies/verify.ps1 b/02-advanced/07-merge-strategies/verify.ps1 deleted file mode 100644 index 222c251..0000000 --- a/02-advanced/07-merge-strategies/verify.ps1 +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env pwsh -<# -.SYNOPSIS - Verifies the Module 06 challenge solution. - -.DESCRIPTION - This script checks that: - - The challenge directory exists - - A Git repository exists - - All three feature branches have been merged - - Appropriate merge strategies were used - - Student understands the difference between fast-forward and three-way merges -#> - -Write-Host "`n=== Verifying Module 06 Solution ===" -ForegroundColor Cyan - -$allChecksPassed = $true - -# Check if challenge directory exists -if (-not (Test-Path "challenge")) { - Write-Host "[FAIL] Challenge directory not found. Did you run setup.ps1?" -ForegroundColor Red - exit 1 -} - -Set-Location "challenge" - -# Check if git repository exists -if (-not (Test-Path ".git")) { - Write-Host "[FAIL] Not a git repository. Did you run setup.ps1?" -ForegroundColor Red - Set-Location .. - exit 1 -} - -# Check current branch is main -$currentBranch = git branch --show-current 2>$null -if ($currentBranch -eq "main") { - Write-Host "[PASS] Currently on main branch" -ForegroundColor Green -} else { - Write-Host "[FAIL] Not on main branch (currently on: $currentBranch)" -ForegroundColor Red - Write-Host "[HINT] Switch to main with: git switch main" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check 1: Fast-Forward Merge - utils.py should exist -if (Test-Path "utils.py") { - Write-Host "[PASS] utils.py exists (feature-fast-forward merged)" -ForegroundColor Green -} else { - Write-Host "[FAIL] utils.py not found" -ForegroundColor Red - Write-Host "[HINT] Merge feature-fast-forward: git merge feature-fast-forward" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check 2: Three-Way Merge - auth.py should exist -if (Test-Path "auth.py") { - Write-Host "[PASS] auth.py exists (feature-divergent merged)" -ForegroundColor Green -} else { - Write-Host "[FAIL] auth.py not found" -ForegroundColor Red - Write-Host "[HINT] Merge feature-divergent: git merge feature-divergent" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check 3: --no-ff Merge - config.py should exist -if (Test-Path "config.py") { - Write-Host "[PASS] config.py exists (feature-optional merged)" -ForegroundColor Green -} else { - Write-Host "[FAIL] config.py not found" -ForegroundColor Red - Write-Host "[HINT] Merge feature-optional: git merge --no-ff feature-optional" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Check for merge commits -$mergeCommits = git log --merges --oneline 2>$null -$mergeCount = ($mergeCommits | Measure-Object -Line).Lines - -if ($mergeCount -ge 2) { - Write-Host "[PASS] Found $mergeCount merge commit(s)" -ForegroundColor Green - Write-Host "[INFO] Expected: 1 from three-way merge + 1 from --no-ff = 2 total" -ForegroundColor Cyan -} else { - Write-Host "[FAIL] Only found $mergeCount merge commit(s), expected at least 2" -ForegroundColor Red - Write-Host "[HINT] Make sure to:" -ForegroundColor Yellow - Write-Host " 1. Merge feature-divergent (creates merge commit)" -ForegroundColor Yellow - Write-Host " 2. Merge feature-optional with --no-ff flag (forces merge commit)" -ForegroundColor Yellow - $allChecksPassed = $false -} - -# Provide detailed merge analysis -Write-Host "`n--- Merge Analysis ---" -ForegroundColor Cyan - -# Count total commits -$totalCommits = git rev-list --count HEAD 2>$null -Write-Host "[INFO] Total commits on main: $totalCommits" -ForegroundColor Cyan - -# List merge commits -if ($mergeCommits) { - Write-Host "[INFO] Merge commits found:" -ForegroundColor Cyan - $mergeCommits | ForEach-Object { - Write-Host " $_" -ForegroundColor White - } -} - -# Check if branches still exist -$branches = git branch 2>$null -Write-Host "[INFO] Existing branches:" -ForegroundColor Cyan -$branches | ForEach-Object { - Write-Host " $_" -ForegroundColor White -} - -Set-Location .. - -# Final summary -if ($allChecksPassed) { - Write-Host "`n" -NoNewline - Write-Host "=====================================" -ForegroundColor Green - Write-Host " CONGRATULATIONS! CHALLENGE PASSED!" -ForegroundColor Green - Write-Host "=====================================" -ForegroundColor Green - Write-Host "`nYou've mastered Git merge strategies!" -ForegroundColor Cyan - Write-Host "`nYou now understand:" -ForegroundColor Cyan - Write-Host " - Fast-forward merges (linear history, no merge commit)" -ForegroundColor White - Write-Host " - Three-way merges (divergent branches, creates merge commit)" -ForegroundColor White - Write-Host " - How to force merge commits with --no-ff flag" -ForegroundColor White - Write-Host " - When to use each merge strategy" -ForegroundColor White - Write-Host " - Trade-offs between linear and branched history" -ForegroundColor White - Write-Host "`nKey Takeaways:" -ForegroundColor Yellow - Write-Host " - Git chooses the strategy automatically based on branch state" -ForegroundColor Cyan - Write-Host " - Use --no-ff to preserve feature branch history" -ForegroundColor Cyan - Write-Host " - Use --ff-only to enforce linear history" -ForegroundColor Cyan - Write-Host " - Different workflows prefer different strategies" -ForegroundColor Cyan - Write-Host "`nNow you can make informed decisions about merge strategies for your projects!" -ForegroundColor Green - Write-Host "" -} else { - Write-Host "`n[SUMMARY] Some checks failed. Review the hints above and try again." -ForegroundColor Red - Write-Host "[INFO] You can run this verification script as many times as needed." -ForegroundColor Yellow - Write-Host "" - Write-Host "Quick Reference:" -ForegroundColor Cyan - Write-Host " git merge feature-fast-forward # Fast-forward merge" -ForegroundColor White - Write-Host " git merge feature-divergent # Three-way merge" -ForegroundColor White - Write-Host " git merge --no-ff feature-optional # Force merge commit" -ForegroundColor White - Write-Host "" - exit 1 -} From a895abdd0317051a10b74ea982cf0757997ae0ee Mon Sep 17 00:00:00 2001 From: Bjarke Sporring Date: Thu, 15 Jan 2026 17:30:03 +0100 Subject: [PATCH 61/61] feat: drastically simplify tasks for multiplayer --- 01-essentials/08-multiplayer/README.md | 148 +++++++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 01-essentials/08-multiplayer/README.md diff --git a/01-essentials/08-multiplayer/README.md b/01-essentials/08-multiplayer/README.md new file mode 100644 index 0000000..60b9022 --- /dev/null +++ b/01-essentials/08-multiplayer/README.md @@ -0,0 +1,148 @@ +# Multiplayer Git + +Work with others using branches and pull requests. + +## Goal + +Learn to collaborate on a shared repository using: +- **Branches** - work independently without breaking main +- **Pull Requests** - review and merge changes safely + +## The Workflow + +``` +1. Create branch → 2. Make changes → 3. Push branch + ↓ +6. Delete branch ← 5. Merge PR ← 4. Create PR +``` + +This is how professional teams work together on code. + +--- + +## Step 1: Clone the Repository + +Get the repository URL from your facilitator, then: + +```powershell +git clone +code +``` + +--- + +## Step 2: Create a Branch + +Never work directly on `main`. Create your own branch: + +```powershell +git switch -c +``` + +This creates a new branch and switches to it. + +--- + +## Step 3: Make Changes + +1. Open `numbers.txt` in VS Code +2. Move one number to its correct position +3. Save the file (`Ctrl+S`) + +--- + +## Step 4: Commit and Push + +```powershell +git add . +git commit -m "fix: move 7 to correct position" +git push feature-2 +``` + +Your branch is now on Azure DevOps. + +--- + +## Step 5: Create a Pull Request + +1. Go to Azure DevOps in your browser +2. Navigate to **Repos** → **Pull Requests** +3. Click **New Pull Request** +4. Set: + - **Source branch:** `feature/` + - **Target branch:** `main` +5. Add a title describing your change +6. Click **Create** + +--- + +## Step 6: Review and Merge + +1. Review the changes shown in the PR +2. If everything looks good, click **Complete** +3. Select **Complete merge** +4. Your changes are now in `main` + +--- + +## Step 7: Update Your Local Main + +After merging, update your local copy: + +```powershell +git switch main +git pull +``` + +--- + +## Step 8: Repeat + +1. Create a new branch for your next change +2. Make changes, commit, push +3. Create another PR +4. Continue until all numbers are sorted + +--- + +## Quick Reference + +| Command | What It Does | +|---------|--------------| +| `git switch -c ` | Create and switch to new branch | +| `git push -u origin ` | Push branch to Azure DevOps | +| `git switch main` | Switch to main branch | +| `git pull` | Get latest changes from remote | + +--- + +## Common Issues + +### "I accidentally committed to main" + +Switch to a new branch and push from there: +```powershell +git switch -c feature/ +git push -u origin feature/ +``` + +### "My PR has conflicts" + +1. Update your branch with latest main: + ```powershell + git switch main + git pull + git switch feature/ + git merge main + ``` +2. Resolve conflicts in VS Code +3. Commit and push again + +### "I need to make more changes to my PR" + +Just commit and push to the same branch - the PR updates automatically: +```powershell +git add . +git commit -m "fix: address review feedback" +git push +```