diff --git a/.github/ISSUE_TEMPLATE/bug-or-broken-content.md b/.github/ISSUE_TEMPLATE/bug-or-broken-content.md new file mode 100644 index 0000000..d65de01 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-or-broken-content.md @@ -0,0 +1,15 @@ +--- +name: Bug / Broken Content +about: Report a broken link, a wrong command, or an outdated explanation +title: "[Module 0X] Short description" +labels: bug +--- + +**Where** +Which module/file, and roughly which section (a heading or a direct link is perfect)? + +**What's wrong** +What does the course currently say, and why is it wrong or broken? + +**What it should say instead** +If you know the fix, say it here — if not, that's fine too. diff --git a/.github/ISSUE_TEMPLATE/content-suggestion.md b/.github/ISSUE_TEMPLATE/content-suggestion.md new file mode 100644 index 0000000..3c29f21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/content-suggestion.md @@ -0,0 +1,15 @@ +--- +name: Content Suggestion +about: Suggest a missing concept, a clearer explanation, or a new Try It / lab +title: "[Suggestion] Short description" +labels: enhancement +--- + +**What's missing or could be clearer** +A concept, command, or scenario you think belongs in the course. + +**Where it would fit** +Which existing module, if any? If it's genuinely new ground, say that instead. + +**Why it's worth adding** +What would a learner be able to do afterward that they can't today? diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..601ede9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## What + + + +## Why + + + +## Checklist + +- [ ] Ran `python3 .github/scripts/check_docs.py` locally — no issues +- [ ] Kept existing examples/analogies/labs intact (or has a good reason not to) +- [ ] Used dummy data only — no real names, emails, credentials, or URLs +- [ ] New commands/concepts include a `callout-tryit` block the reader can actually run diff --git a/.github/scripts/check_docs.py b/.github/scripts/check_docs.py new file mode 100644 index 0000000..43253fd --- /dev/null +++ b/.github/scripts/check_docs.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Validate this repo's own Markdown: balanced code fences, resolvable +relative links, and balanced custom HTML components (
/
). +Used locally and by .github/workflows/validate-docs.yml in CI. +""" +import os +import re +import sys +import glob + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +def check_file(path): + problems = [] + with open(path, encoding="utf-8") as fh: + content = fh.read() + + fence_count = len(re.findall(r"^```", content, re.MULTILINE)) + if fence_count % 2 != 0: + problems.append(f"Unbalanced code fences ({fence_count} found)") + + div_open = len(re.findall(r"") + if div_open != div_close: + problems.append(f"Unbalanced
tags ({div_open} open, {div_close} close)") + + details_open = content.count("
") + details_close = content.count("
") + if details_open != details_close: + problems.append(f"Unbalanced
tags ({details_open} open, {details_close} close)") + + for match in re.finditer(r"\]\(([^)]+)\)", content): + link = match.group(1) + if link.startswith(("http://", "https://", "mailto:", "#")): + continue + path_part = link.split("#")[0] + if not path_part: + continue + resolved = os.path.normpath(os.path.join(os.path.dirname(path), path_part)) + if not os.path.exists(resolved): + problems.append(f"Broken relative link: {link}") + + return problems + + +def main(): + md_files = glob.glob(os.path.join(REPO_ROOT, "**", "*.md"), recursive=True) + all_problems = {} + for f in md_files: + problems = check_file(f) + if problems: + all_problems[os.path.relpath(f, REPO_ROOT)] = problems + + if not all_problems: + print(f"OK — {len(md_files)} Markdown files checked, no issues found.") + return 0 + + for f, problems in sorted(all_problems.items()): + for p in problems: + print(f"{f}: {p}") + print(f"\n{sum(len(p) for p in all_problems.values())} issue(s) in {len(all_problems)} file(s).") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/validate-docs.yml b/.github/workflows/validate-docs.yml new file mode 100644 index 0000000..1305d9d --- /dev/null +++ b/.github/workflows/validate-docs.yml @@ -0,0 +1,14 @@ +name: Validate Docs + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: python3 .github/scripts/check_docs.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0a4b93a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Jekyll build output (local preview only — GitHub Pages builds this itself) +_site/ +.jekyll-cache/ +.jekyll-metadata +.sass-cache/ diff --git a/00-introduction/README.md b/00-introduction/README.md index e87a33f..e8b7ae4 100644 --- a/00-introduction/README.md +++ b/00-introduction/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 00 — Introduction & Setup" +description: "What version control actually is, installing Git, and the one-time config that saves you from a confusing commit history." +--- +
# 🏠 Module 00 — Introduction & Setup @@ -9,10 +14,23 @@
+
+ +**By the end of this module you will be able to:** + +✓ Explain what Git actually *is*, and why it beat older version control systems +✓ Install Git and set your identity correctly (global vs. local) +✓ Create a real Git repository from scratch with `git init` +✓ Read `git status`'s output and know what it's telling you to do next +✓ Pull up Git's built-in help without leaving the terminal + +
+ --- ## 📑 In This Module +- [Set Up Your Safe Sandbox](#-set-up-your-safe-sandbox) - [Git Home — What Even Is This](#-git-home--what-even-is-this) - [Git Intro — Version Control, Explained Like You're Five (a smart five-year-old)](#-git-intro--version-control-explained-like-youre-five-a-smart-five-year-old) - [Git Install](#-git-install) @@ -23,6 +41,33 @@ --- +## 🛟 Set Up Your Safe Sandbox + +Every "Try It" and "Your Turn" block in this course — starting right now — assumes you have one +disposable folder to actually type commands into. Not a real project. Something you can break on +purpose, over and over, with zero consequences. + +
+ +**⌨️ TRY IT YOURSELF** + +```bash +mkdir ~/git-playground +cd ~/git-playground +``` + +That's it for now — you don't even have `git init` yet, because the very next section is about +to teach you what that command does and why. Just get the folder made and remember where it is; +every module from here forward will say "in your playground" and mean this exact folder. + +
+ +> [!TIP] +> Keep this folder for the entire course. Delete everything inside it whenever you want a clean +> slate — nothing in here should ever be work you actually care about keeping. + +--- + ## 🏠 Git Home — What Even Is This **Git** is a program that tracks changes to files over time, so you can: @@ -101,9 +146,9 @@ graph TB E1 -.->|Works fine offline,
syncs later| E1 end - style C1 fill:#ffcccc - style E1 fill:#d4edda - style E2 fill:#d4edda + style C1 fill:#ffcccc,color:#1a1a1a + style E1 fill:#d4edda,color:#1a1a1a + style E2 fill:#d4edda,color:#1a1a1a ``` In a **centralized** system, only the server has the full history — if it goes down, nobody can @@ -151,6 +196,21 @@ git --version > added to it. (The other 20% of the time, you'll be Googling PATH environment variables at > midnight. We've all been there.) +
+ +**⌨️ TRY IT YOURSELF** + +```bash +git --version +git help -a +``` + +**What should I see?** A version number on the first command, and a long alphabetical list of +every Git subcommand that exists on the second. You don't need to read all of it — just confirm +it printed something. That list existing at all is the whole point of the next section. + +
+ --- ## ⚙️ Git Config @@ -195,30 +255,83 @@ git config user.email "you@company.com" > override it with a `--local` (no flag) config inside your work repos specifically — not the > other way around, unless you enjoy explaining commit history to HR. +
+ +**⌨️ TRY IT YOURSELF** + +```bash +git config --global user.name "Your Name" +git config --global user.email "you@example.com" +git config --list +``` + +**What should I see?** A list of key=value settings, including `user.name=Your Name` and +`user.email=you@example.com` somewhere in it. If you don't see them, the config didn't save — +check for a typo in the command before moving on. + +
+ +
+ +**🎯 YOUR TURN** + +Without looking at the example above: set your email to one thing **globally**, then go into +your `~/git-playground` folder and override it to something different, **locally, just for that +one folder**. Then run `git config user.email` *inside* the playground folder, and run it again +from somewhere *outside* it. Can you explain out loud why the two commands print different +answers? + +
+💡 Show solution + +```bash +git config --global user.email "global@example.com" + +cd ~/git-playground +git config user.email "local-override@example.com" + +git config user.email # prints "local-override@example.com" — local wins here +cd .. +git config user.email # prints "global@example.com" — no local override outside the playground +``` + +Local config lives in that specific repo's `.git/config` file and always wins over global when +both exist — that's the entire mechanism behind "why do my personal commits show my work email" +bugs: someone set a local override once, forgot about it, and every commit since has used it. + +
+ +
+ --- ## 🚀 Git Get Started -Time to actually create a repository. +Time to actually create a repository — in the `~/git-playground` folder you made earlier. + +
-### Step by step +**⌨️ TRY IT YOURSELF** ```bash -# 1. Make a new folder and go into it -mkdir my-first-repo -cd my-first-repo +cd ~/git-playground -# 2. Turn it into a Git repository +# Turn this folder into a Git repository git init -# Initialized empty Git repository in .../my-first-repo/.git/ +# Initialized empty Git repository in .../git-playground/.git/ -# 3. Check what Git thinks is going on (you'll run this CONSTANTLY) +# Check what Git thinks is going on (you'll run this CONSTANTLY) git status # On branch main # No commits yet # nothing to commit (create/copy files and use "git add" to track) ``` +**What should I see?** `git init`'s one-line confirmation, then `git status` reporting a brand +new, empty repo on branch `main` with nothing to commit yet. + +
+ That `.git` folder `git init` just created is where **all** of Git's magic lives — the entire history, every branch, every commit. Delete that folder, and as far as Git is concerned, the project's history never existed (the files themselves are untouched — only the tracking is gone). @@ -229,6 +342,48 @@ project's history never existed (the files themselves are untouched — only the > staging, after committing. It always tells you exactly what state you're in and, usually, the > exact next command to run. +
+ +**🎯 YOUR TURN** + +Before running anything — predict out loud what `git status` will print if you create a brand +new empty file right now and run it, **without** running `git add` first. Then actually do it +and check your prediction. + +
+💡 Show solution + +```bash +touch notes.txt +git status +``` + +``` +Untracked files: + (use "git add ..." to include in what will be committed) + notes.txt +``` + +If you predicted "it'll show up as untracked, not staged" — that's exactly right, and it's the +single most important mental model from this entire module: Git notices new files immediately, +but never tracks them until you explicitly say so with `git add`. + +
+ +
+ +
+ +**🧪 SDET SCENARIO** + +Every automation repo you'll ever work in — a Playwright suite, a Selenium framework, a Postman +collection exported to Newman — starts with exactly this: `git init` (or `git clone`, if someone +already created it), then an immediate `.gitignore` for `node_modules/`, test reports, and +screenshots nobody wants committed. You'll see the real version of this in +[Module 05](../05-advanced-git/#-gitignore). + +
+ --- ## 🆘 Git Help @@ -278,6 +433,18 @@ If any of those feel shaky, that's completely normal for a first pass — skim b click. If you're solid on all three, you're ready for the module where things actually get typed into files. +
+ +**🧠 You can now:** + +✓ Install Git and verify it from the terminal +✓ Set your identity globally, and override it locally when you need to +✓ Turn any folder into a Git repository with `git init` +✓ Read `git status` and know the difference between "untracked" and "nothing to commit" +✓ Reach for `git help` instead of leaving the terminal to Google a flag + +
+ ---
diff --git a/01-git-fundamentals/README.md b/01-git-fundamentals/README.md index 0e3bfd8..f547815 100644 --- a/01-git-fundamentals/README.md +++ b/01-git-fundamentals/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 01 — Git Fundamentals" +description: "Staging, commits, branching, and merging — the core loop you'll run hundreds of times a week, with hands-on labs for every concept." +--- +
# 🧬 Module 01 — Git Fundamentals @@ -9,6 +14,22 @@
+
+ +**By the end of this module you will be able to:** + +✓ Explain why Git has a staging area at all, not just "what it does" +✓ Stage exactly the changes you want, even out of a messy working directory +✓ Write commit messages your future self will actually thank you for +✓ Create, switch, and merge branches — the loop you'll run dozens of times a day +✓ Use `git stash` to context-switch without committing half-finished work + +
+ +> [!TIP] +> Still have `~/git-playground` from [Module 00](../00-introduction/#-set-up-your-safe-sandbox)? +> Use it. Every Try It block below assumes a real terminal, not just reading along. + --- ## 📑 In This Module @@ -46,6 +67,20 @@ Untracked files: **Untracked** = "Git sees this file exists, but has zero interest in it until you tell it otherwise." This is a feature, not Git being lazy. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +echo "console.log('hello world')" > app.js +git status +``` + +**What should I see?** `app.js` listed under "Untracked files," with Git explicitly suggesting +`git add` — not an error, just Git telling you exactly what to do next. + +
+ --- ## 📦 Staging — The Most Important Concept in Git @@ -65,9 +100,9 @@ graph LR A[Working Directory
📝 your messy desk] -->|git add| B[Staging Area
📦 the box you're packing] B -->|git commit| C[Repository / .git history
🚚 shipped & permanent] - style A fill:#fff3cd - style B fill:#cfe2ff - style C fill:#d4edda + style A fill:#fff3cd,color:#1a1a1a + style B fill:#cfe2ff,color:#1a1a1a + style C fill:#d4edda,color:#1a1a1a ``` ### 💻 The technical example @@ -113,6 +148,50 @@ git reset file.txt # UN-stage a file (doesn't touch its actual content > accidentally left a `console.log('here')` debug line in the same file, and want to commit only > the fix. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +echo "fix bug" >> login.js +echo "new feature" >> signup.js +git add login.js +git status +``` + +**What should I see?** `login.js` under "Changes to be committed," `signup.js` still sitting +under "Changes not staged for commit" — proof the staging area really does let you pick exactly +what goes in, even when more than one file changed. + +
+ +
+ +**🎯 YOUR TURN** + +You have two modified files: `login.js` and `signup.js`. Stage **only** `signup.js`, confirm it +with `git status`, then **unstage** it again without touching its actual content. Can you do it +without looking at the solution first? + +
+💡 Show solution + +```bash +git add signup.js +git status # signup.js now under "Changes to be committed" +git reset signup.js +git status # signup.js back under "Changes not staged" — content untouched +``` + +`git reset ` (no flags, just a filename) only moves a file *out of the box* — it never +discards the edit itself. That distinction matters a lot more once you hit +[Module 04](../04-undo-and-recovery/), where `reset` can also discard real work if you're not +careful with it. + +
+ +
+ --- ## ✅ Commit @@ -152,6 +231,32 @@ The convention `type: description` (feat/fix/refactor/docs/test/chore) isn't req itself, but it's so widely adopted that `git log --oneline --grep="fix:"` instantly showing every bug fix in your project's history is now basically an industry standard trick. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git commit -m "fix: login validation bug" +git log --oneline -1 +``` + +**What should I see?** A one-line confirmation from `git commit` (branch, short hash, file stats), +then that same commit as a single line from `git log`. + +
+ +
+ +**🚨 COMMON MISTAKE** + +Committing "today's work" as one giant commit at the end of the day. It feels efficient in the +moment and is genuinely painful later — `git revert` (Module 04) undoes *one commit*, so a +50-file mega-commit means reverting the one real bug inside it also reverts 49 unrelated changes +you didn't want touched. Small, focused commits aren't a style preference; they're what makes +every later Git operation (revert, cherry-pick, bisect) actually usable. + +
+ --- ## 📜 History @@ -178,6 +283,21 @@ without asking anyone or guessing. git log -p --follow -- path/to/file.js ``` +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git log --oneline +git log --graph --oneline --all +``` + +**What should I see?** The same commits, but the second command draws them as a small ASCII +tree. On one branch with no merges yet it'll look like a straight line — that's expected. It gets +genuinely useful once branches exist, a few sections from now. + +
+ --- ## 🏷️ Tagging @@ -199,6 +319,21 @@ shipped to customers as Version 2.0."** Branches move forward constantly; tags d — once set, `v1.0.0` always points at the exact same commit, forever, so you can always answer "what code was actually in production on release day" with certainty. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git tag -a v0.1.0 -m "First playground milestone" +git tag +git show v0.1.0 +``` + +**What should I see?** `v0.1.0` listed by `git tag`, and `git show` printing the tag's own +message plus the full commit it points at. + +
+ --- ## 🧳 Stash @@ -227,6 +362,36 @@ git stash push -m "half-done payment form" # stash WITH a note, for when you'll > clearly if this happens, don't panic, it's the same conflict-resolution process as a merge > conflict (covered below). +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +echo "half-done work" >> login.js +git status # shows login.js modified +git stash +git status # clean — login.js looks untouched +git stash pop +git status # the change is back +``` + +**What should I see?** `git status` reporting a clean working directory right after `stash`, then +the exact same modification reappearing after `pop` — nothing was lost, it was just parked. + +
+ +
+ +**🧪 SDET SCENARIO** + +You're mid-way through writing a new Playwright test, it's not passing yet, and a flaky test in +CI just paged you to investigate *right now*. You can't commit broken, half-finished test code — +but you also can't lose 40 minutes of locator work. `git stash`, switch branches, debug the +flaky test, fix it, switch back, `git stash pop`, and you're exactly where you left off. This is +the single most common real reason test engineers reach for stash. + +
+ --- ## 🌿 Branch @@ -270,6 +435,46 @@ git branch -D feature/checkout # force-delete (no safety check — know what > branch per feature, per bug fix, per experiment) in a way that would've been unthinkably slow in > older version control systems. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git switch -c feature/login +git branch +``` + +**What should I see?** `feature/login` listed with a `*` next to it, confirming you're now on +the new branch — `main` is still exactly where you left it. + +
+ +
+ +**🎯 YOUR TURN** + +You're currently on `feature/login`. Create a second branch called `feature/payment`, make one +commit on it (any trivial change), then switch back to `main`. Do NOT look at the solution first. + +
+💡 Show solution + +```bash +git switch -c feature/payment +echo "payment stub" >> payment.js +git add payment.js +git commit -m "feat: stub out payment module" +git switch main +``` + +Run `git log --oneline --graph --all` afterward — you'll see `main`, `feature/login`, and +`feature/payment` as three separate pointers, with `feature/payment` one commit ahead of where +it branched off. + +
+ +
+ --- ## 🔀 Merge @@ -291,8 +496,8 @@ graph TB subgraph "Three-Way Merge" B1[main moved forward too] --> B2[Git combines BOTH histories] --> B3[Creates a new
merge commit] end - style A3 fill:#d4edda - style B3 fill:#cfe2ff + style A3 fill:#d4edda,color:#1a1a1a + style B3 fill:#cfe2ff,color:#1a1a1a ``` - **Fast-forward**: nobody touched `main` while you were on your branch — Git just moves `main`'s @@ -309,6 +514,30 @@ one final document (three-way merge) — and if both edited the *exact same sent that's a **merge conflict**, which needs a human to decide. (Full conflict-resolution workflow is in [Module 05](../05-advanced-git/).) +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git switch main +git merge feature/login +git log --oneline --graph --all +``` + +**What should I see?** Since `main` hasn't moved since you branched, this is a **fast-forward** — +no merge commit, `main`'s pointer just catches up to `feature/login`'s latest commit. + +
+ +
+🧠 Quick Check: If you'd made a NEW commit directly on main before merging feature/login in, would this still be a fast-forward? + +No — the moment `main` has a commit `feature/login` doesn't know about, Git can't just "catch up" +a pointer anymore. It has to combine two divergent histories, which means a three-way merge and a +dedicated merge commit, even if there's no actual conflict between the changes. + +
+ --- ## 🔁 Workflow — Putting It All Together @@ -325,8 +554,8 @@ flowchart LR E -->|No, ready| F[git checkout main] F --> G[git merge
your branch] - style D fill:#cfe2ff - style G fill:#d4edda + style D fill:#cfe2ff,color:#1a1a1a + style G fill:#d4edda,color:#1a1a1a ``` ```bash @@ -346,6 +575,41 @@ That's genuinely most of Git, in one loop. Everything else in this course is eit on this loop (Module 02's GitHub push/pull), a safety net for when it goes wrong (Module 04), or a specialization for specific situations (Module 05). +
+ +**🎯 YOUR TURN — Run the whole loop yourself** + +In `~/git-playground`, from `main`: + +1. Create a branch called `feature/dark-mode` +2. Make two separate, small commits on it (anything — even two one-line `echo` changes to + different files) +3. Switch back to `main` and merge it in +4. Delete the now-merged branch + +Do all four steps without looking at the solution first — this is the exact loop you'll run for +the rest of your career, so it's worth getting your fingers to do it without thinking. + +
+💡 Show solution + +```bash +git switch -c feature/dark-mode +echo "dark mode css" >> styles.css +git add styles.css +git commit -m "feat: add dark mode styles" +echo "dark mode toggle" >> settings.js +git add settings.js +git commit -m "feat: add dark mode toggle" +git switch main +git merge feature/dark-mode +git branch -d feature/dark-mode +``` + +
+ +
+ --- ## 🧠 Best Practices @@ -395,6 +659,18 @@ a specialization for specific situations (Module 05). If #3 didn't immediately bring "`git stash`" to mind, that's worth a re-read of that section — it's one of the most practically useful commands in this entire module. +
+ +**🧠 You can now:** + +✓ Explain why the staging area exists, not just recite what it does +✓ Stage exactly the changes you want, chunk by chunk if needed +✓ Write commit messages that'll still make sense to you in six months +✓ Create, switch, merge, and delete branches — the daily loop +✓ Shelve half-finished work with `git stash` without committing it + +
+ ---
diff --git a/02-git-and-github/README.md b/02-git-and-github/README.md index ec3b8ef..03b551e 100644 --- a/02-git-and-github/README.md +++ b/02-git-and-github/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 02 — Git + GitHub" +description: "SSH keys, remotes, push/pull, GitHub Flow, and GitHub Pages — where Git on your laptop becomes Git the internet can see." +--- +
# 🌐 Module 02 — Git + GitHub @@ -9,6 +14,24 @@
+
+ +**By the end of this module you will be able to:** + +✓ Authenticate to GitHub with SSH instead of typing a password every time +✓ Connect a local repo to a real GitHub repo and push to it +✓ Explain the actual difference between `fetch` and `pull` — and why cautious people fetch first +✓ Push a branch and know what happens to it on GitHub's side +✓ Describe GitHub Flow well enough to follow it on a real team + +
+ +> [!NOTE] +> Unlike Modules 00–01, the Try It blocks below need a **real, free GitHub account** and (for the +> push/pull ones) an empty repo you created on github.com — this module is specifically about +> talking to the internet, so there's no way around that. Nothing here touches real production +> code; create a throwaway repo, same spirit as `~/git-playground`. + --- ## 📑 In This Module @@ -83,6 +106,20 @@ ssh -T git@github.com > anywhere but your own machine) and `id_ed25519.pub` (public — this is the one that's safe to > give to GitHub). Mixing these up is a genuine security incident, not a typo. +
+ +**⌨️ TRY IT YOURSELF** + +```bash +ls -la ~/.ssh +``` + +**What should I see?** If you see `id_ed25519` and `id_ed25519.pub` (or an `id_rsa` pair from an +older setup), you already have a key and can skip straight to adding the `.pub` one to GitHub. +If the folder doesn't exist or is empty, run the `ssh-keygen` command above first. + +
+ --- ## 🔗 Set Remote @@ -110,6 +147,24 @@ git remote remove origin `banana` and Git wouldn't care. Everyone calls it `origin` because everyone else does, which is its own kind of consistency worth keeping. +
+ +**⌨️ TRY IT YOURSELF** + +1. On github.com, create a new **empty** repository (no README, no .gitignore — genuinely empty) +2. Copy its SSH URL (the green "Code" button → SSH tab) + +```bash +cd ~/git-playground +git remote add origin git@github.com:yourusername/your-repo-name.git +git remote -v +``` + +**What should I see?** Two lines back from `git remote -v` — `origin` listed for both fetch and +push, pointing at the repo you just created. + +
+ --- ## ✏️ Edit Code — the GitHub Web Editor @@ -145,9 +200,9 @@ graph LR A[Local commits
on your laptop] -->|git push| B[GitHub
the shared remote] B -->|git pull| C[Someone else's laptop] - style A fill:#fff3cd - style B fill:#cfe2ff - style C fill:#d4edda + style A fill:#fff3cd,color:#1a1a1a + style B fill:#cfe2ff,color:#1a1a1a + style C fill:#d4edda,color:#1a1a1a ``` > [!WARNING] @@ -156,6 +211,20 @@ graph LR > teammate's just-pushed work. `git push --force-with-lease` is the safer version: it refuses to > push if the remote has changes you haven't seen yet, instead of blindly overwriting. +
+ +**⌨️ TRY IT YOURSELF** (using the `origin` you set up above) + +```bash +git push -u origin main +``` + +**What should I see?** Git prints upload progress, then confirms `main -> main` with the +upstream now set. Refresh the repo's page on github.com — your commits from Modules 00–01 are +now visible there, publicly (or privately, if you made the repo private). + +
+ --- ## ⬇️ Pull from GitHub @@ -177,6 +246,33 @@ cabinet, grabbing those new pages, AND immediately filing them into your own not right place. `git fetch` alone is just "go look at the cabinet and see what's new" — without touching your own notebook yet. Cautious people `fetch` first, review, then `merge` or `pull`. +
+ +**🎯 YOUR TURN** + +Make a small edit directly on GitHub's web editor (press `.` on your repo, edit a file, commit) +— *without* touching your local clone. Then, locally, run `git fetch origin` followed by +`git log origin/main`. Can you see that remote commit before running `git pull`? What does that +prove about what `fetch` actually does? + +
+💡 Show solution + +```bash +git fetch origin +git log origin/main -1 # the web-editor commit shows up here already +git log main -1 # your LOCAL main still doesn't have it +git pull origin main # now it does +``` + +This proves `fetch` genuinely downloads the data without touching your working branch — +`origin/main` and `main` are two separate references, and only `merge` (or `pull`, which runs it +for you) moves your local branch forward. + +
+ +
+ --- ## 🌿 GitHub Branch — Push/Pull a Branch @@ -195,6 +291,24 @@ git checkout feature/their-branch Once pushed, that branch is visible to your whole team on GitHub — anyone can view its diff, comment on it, or open a **pull request** from it (Module 03 goes deep on this). +
+ +**⌨️ TRY IT YOURSELF** + +```bash +git checkout -b feature/new-navbar +echo "navbar stub" >> navbar.js +git add navbar.js +git commit -m "feat: stub out new navbar" +git push -u origin feature/new-navbar +``` + +**What should I see?** GitHub's output includes a direct link to open a pull request for the +branch you just pushed — and refreshing the repo's "Branches" page on github.com shows it listed +there, separate from `main`. + +
+ --- ## 🔄 GitHub Flow @@ -212,8 +326,8 @@ flowchart LR E -->|Changes needed| C F --> G[Deploy] - style A fill:#d4edda - style F fill:#d4edda + style A fill:#d4edda,color:#1a1a1a + style F fill:#d4edda,color:#1a1a1a ``` 1. `main` is always in a deployable state @@ -226,6 +340,17 @@ It's deliberately simpler than Git Flow (an older model with `develop`, `release branches) — GitHub Flow assumes you can deploy frequently and safely, which most modern teams with good CI/CD can. +
+ +**🧪 SDET SCENARIO** + +In a real automation repo, step 4 ("CI passes") is almost always your test suite — a pull request +against `main` triggers a GitHub Actions run of the full Playwright/Selenium suite automatically +(Module 05 covers the actual YAML for this). Opening the PR as a Draft early, per step 3, means +you find out your new test is flaky in CI *before* a reviewer looks at it, not after. + +
+ --- ## 📄 GitHub Pages @@ -293,6 +418,18 @@ If #3 surprised you, that's the most commonly-missed point in this whole module early (as a Draft) is a feature, not a mistake, because it makes your in-progress work visible for early feedback instead of a surprise 400-line diff at the end. +
+ +**🧠 You can now:** + +✓ Authenticate to GitHub over SSH instead of a password every time +✓ Connect a local repo to GitHub and push real commits to it +✓ Tell `fetch` and `pull` apart, and know why cautious people fetch first +✓ Push a branch and find it on GitHub, ready for a pull request +✓ Follow GitHub Flow on a real team without needing it explained again + +
+ ---
diff --git a/03-contributing-to-open-source/README.md b/03-contributing-to-open-source/README.md index 05ea7dc..aa60a92 100644 --- a/03-contributing-to-open-source/README.md +++ b/03-contributing-to-open-source/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 03 — Contributing to Open Source" +description: "Fork, clone, branch, pull request — the exact sequence behind every open-source contribution you've ever seen on GitHub." +--- +
# 🤝 Module 03 — Contributing to Open Source @@ -9,6 +14,17 @@
+
+ +**By the end of this module you will be able to:** + +✓ Fork a repository and explain what that actually created +✓ Keep your fork in sync with the original project (`origin` vs. `upstream`) +✓ Open a pull request that's likely to get merged quickly +✓ Find a genuinely beginner-friendly issue to contribute to, today + +
+ --- ## 📑 In This Module @@ -39,6 +55,18 @@ You do **not** have push access to the original repo — that's the entire point how strangers on the internet can propose changes to projects they don't have write access to, without any risk to the original. +
+ +**⌨️ TRY IT YOURSELF** + +Pick any small public repo on GitHub (even this `git-tutorials` repo works fine for practice) and +click **Fork** (top right). That's the whole action — no terminal needed yet. + +**What should I see?** A new repo under *your* account, named the same thing, with "forked from" +and a link back to the original shown right under the repo name. + +
+ --- ## 📥 Clone from GitHub @@ -73,6 +101,33 @@ git merge upstream/main git push origin main # update YOUR fork's copy on GitHub too ``` +
+ +**⌨️ TRY IT YOURSELF** (using the fork you just created) + +```bash +git clone git@github.com:yourusername/your-fork-name.git +cd your-fork-name +git remote add upstream +git remote -v +``` + +**What should I see?** `origin` pointing at your fork, `upstream` pointing at the original — four +lines total from `git remote -v` (fetch + push for each). + +
+ +
+🧠 Quick Check: You accidentally run git push upstream main instead of git push origin main. What happens? + +It fails — you don't have write access to `upstream` (the original repo), which is the entire +point of the fork workflow. This is a genuinely safe mistake to make: Git will refuse with a +permission error, not silently do something destructive. The more dangerous version of this +mix-up is the opposite direction — accidentally *pulling* from `origin` when you meant +`upstream`, which doesn't error, it just silently leaves you out of sync with the real project. + +
+ --- ## 📬 Send a Pull Request @@ -120,6 +175,27 @@ Ran the corrected command on a fresh clone — build script now completes succes > harder, and sits unreviewed far longer. If you notice something else worth fixing, that's a > **second** PR, not scope creep on the first one. +
+ +**🎯 YOUR TURN — Your first real contribution** + +Using the fork you set up above (any public repo, including this one): find one genuinely small, +real thing worth fixing — a typo, a broken link, an outdated command — make the fix on a properly +named branch, push it to `origin`, and open the pull request. This is a real PR, not a drill. + +
+💡 Not sure where to find something to fix? + +Search GitHub for issues labeled `good first issue` across any language you're comfortable in +(or check [goodfirstissue.dev](https://goodfirstissue.dev/)). If genuinely nothing jumps out, even +fixing a typo in this course's own docs and opening a PR against +[ghanendra-sdet/git-tutorials](https://github.com/ghanendra-sdet/git-tutorials) counts — the +mechanism is identical regardless of project size. + +
+ +
+ --- ## 🔁 The Full Contribution Flow, End to End @@ -137,9 +213,9 @@ flowchart TD I -->|Changes requested| F I -->|Approved| J["Merged! 🎉"] - style B fill:#fff3cd - style H fill:#cfe2ff - style J fill:#d4edda + style B fill:#fff3cd,color:#1a1a1a + style H fill:#cfe2ff,color:#1a1a1a + style J fill:#d4edda,color:#1a1a1a ``` That's genuinely the entire mechanism behind every open-source contribution you've ever seen — @@ -177,6 +253,17 @@ from a one-line typo fix to a massive feature added to a project with a million Opening a new PR for review feedback is a surprisingly common beginner instinct, and it's unnecessary extra noise for the maintainer.) +
+ +**🧠 You can now:** + +✓ Fork a repo and explain exactly what that did and didn't give you access to +✓ Keep a fork in sync with the real project via `upstream` +✓ Open a pull request that's actually likely to get reviewed and merged quickly +✓ Find a real `good first issue` and make your first genuine open-source contribution + +
+ ---
diff --git a/04-undo-and-recovery/README.md b/04-undo-and-recovery/README.md index 5f9aae5..f809ee5 100644 --- a/04-undo-and-recovery/README.md +++ b/04-undo-and-recovery/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 04 — Undo & Recovery" +description: "Revert, reset, amend, rebase, and reflog — plus hands-on disaster labs for every 'I think I just lost three days of work' scenario." +--- +
# ↩️ Module 04 — Undo & Recovery @@ -9,6 +14,23 @@
+
+ +**By the end of this module you will be able to:** + +✓ Choose correctly between `revert` and `reset` based on whether history is shared +✓ Explain what `--soft`/`--mixed`/`--hard` actually do to your changes, not just recite the flags +✓ Fix the last commit cleanly with `--amend`, before it's pushed +✓ Use `reflog` to recover a commit you were sure was gone forever +✓ Walk through 7 realistic Git disasters and fix every one of them yourself + +
+ +> [!CAUTION] +> This module teaches commands that can discard real work. Do every Try It and Lab below in +> `~/git-playground` (from [Module 00](../00-introduction/#-set-up-your-safe-sandbox)), never in +> a repo you actually care about, until the commands are second nature. + --- ## 📑 In This Module @@ -18,7 +40,7 @@ - [Amend](#-amend) - [Rebase](#-rebase) - [Reflog — The Undo Button for Your Undo Button](#-reflog--the-undo-button-for-your-undo-button) -- [Recovery Scenarios, Worked](#-recovery-scenarios-worked) +- [💥 Disaster Labs](#-disaster-labs) - [Glossary (Module 04 Terms)](#-glossary-module-04-terms) --- @@ -41,13 +63,31 @@ fix," rather than pretending it never happened. graph LR A[commit 1] --> B[commit 2
the bug] --> C[commit 3] --> D["commit 4
git revert commit 2"] - style B fill:#ffcccc - style D fill:#d4edda + style B fill:#ffcccc,color:#1a1a1a + style D fill:#d4edda,color:#1a1a1a ``` **This is why `revert` is the safe choice for anything already pushed and shared** — it doesn't rewrite history other people may have already pulled, so nobody's local copy gets confused. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +echo "a deliberate bug" >> app.js +git add app.js +git commit -m "feat: add a feature (with a bug)" +git log --oneline -1 # note this commit's hash +git revert --no-edit HEAD +git log --oneline -3 +``` + +**What should I see?** A brand new commit on top, undoing the previous one — the "buggy" line is +gone from `app.js`, but both commits are still visible in `git log`. Nothing was erased. + +
+ --- ## ⏮️ Reset — Rewrite History Locally @@ -78,9 +118,9 @@ graph TB subgraph "git reset --hard" H1[Commit undone] --> H2[Changes GONE completely] end - style S2 fill:#d4edda - style M2 fill:#fff3cd - style H2 fill:#ffcccc + style S2 fill:#d4edda,color:#1a1a1a + style M2 fill:#fff3cd,color:#1a1a1a + style H2 fill:#ffcccc,color:#1a1a1a ``` > [!WARNING] @@ -96,6 +136,32 @@ graph TB | Safe on a pushed/shared branch? | ✅ Yes | ❌ No — never reset a branch others have pulled | | Safe on your own local, unpushed work? | Overkill, but fine | ✅ Yes — this is exactly what it's for | +
⚠️ Destructive command
+ +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground` — this one discards changes on purpose) + +```bash +echo "temporary experiment" >> app.js && git add app.js && git commit -m "wip: experiment" +git reset --soft HEAD~1 +git status # staged, ready to re-commit — nothing lost + +echo "temporary experiment" >> app.js && git add app.js && git commit -m "wip: experiment" +git reset --mixed HEAD~1 +git status # the change exists but is UNSTAGED — still nothing lost + +echo "temporary experiment" >> app.js && git add app.js && git commit -m "wip: experiment" +git reset --hard HEAD~1 +git status # clean — the change is genuinely gone from app.js +``` + +**What should I see?** The exact same commit, undone three different ways — `git status` showing +staged, then unstaged, then nothing at all. That progression *is* the whole lesson: same command, +one flag, three completely different outcomes for your actual work. + +
+ --- ## 🔧 Amend @@ -115,6 +181,25 @@ item, and reseals it as if it was always right. This **only** works cleanly on t and **only** before you've pushed it — amending a commit that's already been pushed and pulled by someone else creates the exact "rewriting shared history" problem `revert` exists to avoid. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git commit --allow-empty -m "fx: typo in this very message" +git log --oneline -1 # note this hash +git commit --amend -m "fix: corrected commit message" +git log --oneline -1 # the message is fixed... +git log --oneline -2 # ...but there's still only ONE commit here, not two +``` + +**What should I see?** The commit message corrected, and a *different* hash than before — +`--amend` doesn't edit the old commit in place, it replaces it with a brand new commit object and +moves the branch pointer to it. `git log --oneline -2` confirms there's still exactly one commit +here, not a correction stacked on top of the original. + +
+ --- ## 🌱 Rebase @@ -139,8 +224,8 @@ graph TB subgraph "After: git rebase main" C1["feature: A-B-C-D'-E'
(D and E replayed on top of C)"] end - style A2 fill:#fff3cd - style C1 fill:#d4edda + style A2 fill:#fff3cd,color:#1a1a1a + style C1 fill:#d4edda,color:#1a1a1a ``` **Merge vs. rebase — genuinely one of the most debated topics in all of Git:** @@ -157,6 +242,30 @@ graph TB > commits, their history and yours now permanently disagree, and reconciling that is a genuinely > painful mess. Rebase your own local, not-yet-shared work freely — never rebase `main`. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git switch main +git switch -c feature/rebase-practice +echo "feature work" >> feature.js && git add feature.js && git commit -m "feat: feature work" + +git switch main +echo "main moved forward" >> readme.md && git add readme.md && git commit -m "docs: update readme" + +git log --oneline --graph --all # two diverged branches, same starting point +git switch feature/rebase-practice +git rebase main # replay feature's commit on top of main's latest +git log --oneline --graph --all # now a straight line — no merge commit +``` + +**What should I see?** Before the rebase, `--graph` shows two separate lines diverging. After, +one straight line — `feature/rebase-practice`'s commit now sits directly on top of `main`'s, as +if it had been written after it, not alongside it. + +
+ --- ## 🕵️ Reflog — The Undo Button for Your Undo Button @@ -189,36 +298,294 @@ actually gone. It's sitting in Git's internal object database, unreferenced by a > reachable commits, 30 for unreachable) — but that's an enormous safety window for "I just did > this 30 seconds ago." +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +echo "important work" >> app.js && git add app.js && git commit -m "feat: important work" +git log --oneline -1 # note this hash + +git reset --hard HEAD~1 # "lose" the commit +git log --oneline -1 # it's really gone from here + +git reflog # ...but it's right there in the reflog +git branch recovered HEAD@{1} # bring it back as a new branch, without touching current HEAD +git log --oneline recovered -1 # the "lost" commit, fully intact +``` + +**What should I see?** The commit missing from `git log` after the reset, then reappearing the +moment you check `reflog` and recover it onto a new branch — proof that "gone" and "unreferenced" +aren't the same thing in Git. + +
+ --- -## 🚑 Recovery Scenarios, Worked +## 💥 Disaster Labs + +Everything above was one concept at a time. Real Git panic is never that clean — it's always +"wait, which command do I even start with." These seven labs are realistic, worked disasters. +Read the scenario, try to fix it yourself first, then check the solution. + +### 💥 Lab 1 — Wrong Branch -**"I committed to the wrong branch"** +You meant to be on `feature/checkout`, but you committed directly to `main` instead. + +
+ +**Set up the disaster:** ```bash -git log --oneline -1 # note the commit hash -git reset --hard HEAD~1 # remove it from the wrong branch -git checkout correct-branch -git cherry-pick # apply it to the right one +git switch main +echo "this should NOT be on main" >> checkout.js +git add checkout.js +git commit -m "feat: checkout logic" ``` -**"I deleted a branch and need it back"** +**🎯 Your task:** move that commit off `main` and onto a new `feature/checkout` branch, leaving +`main` exactly as it was before. + +
+💡 Show solution + ```bash -git reflog # find the last commit hash the deleted branch pointed to -git branch recovered-branch +git log --oneline -1 # note the commit hash +git reset --hard HEAD~1 # remove it from main +git switch -c feature/checkout +git cherry-pick # bring it onto the new branch ``` -**"I ran `reset --hard` and lost uncommitted work"** +`main` is back to clean, and `feature/checkout` has exactly the one commit that belonged there. + +
+ +
+ +### 💥 Lab 2 — Lost Commit + +You've already done this one above in the Reflog Try It — this lab is the same mechanism, framed +as a cold-open disaster instead of a guided walkthrough. + +
+ +**Set up the disaster:** ```bash -# Unfortunately: reflog only helps with COMMITTED work. -# Uncommitted changes discarded by --hard are NOT in reflog — this is the one true "gone" scenario. -# This is exactly why "commit often, even messy WIP commits" beats "wait until it's perfect." +echo "three hours of work" >> big-feature.js && git add big-feature.js +git commit -m "feat: three hours of work" +git reset --hard HEAD~1 ``` +**🎯 Your task:** get that commit back, without undoing anything else. + +
+💡 Show solution + +```bash +git reflog +git branch recovered HEAD@{1} +``` + +
+ +
+ > [!WARNING] -> That last scenario is the one real gap in Git's safety net: **reflog protects commits, not -> uncommitted changes.** This is the strongest possible argument for committing early and often — -> even an ugly `"wip: half done, don't judge"` commit is fully recoverable. Uncommitted work that -> gets discarded is genuinely gone. +> **This lab only works because the work was committed first.** If the three hours of work had +> never been committed — just sitting in the working directory when `reset --hard` ran — reflog +> cannot help you; uncommitted changes aren't tracked by it at all. That's the one real gap in +> Git's entire safety net, and it's the single strongest argument for committing early and often, +> even an ugly `"wip: half done, don't judge"` commit. + +### 💥 Lab 3 — Merge Conflict + +Create conflicting changes on two branches and resolve them by hand. The full marker-by-marker +mechanics live in [Module 05](../05-advanced-git/#-merge-conflicts-properly) — this lab +is the hands-on version. + +
+ +**Set up the disaster:** +```bash +git switch main +echo "const rate = 0.10;" > pricing.js && git add pricing.js && git commit -m "feat: add pricing" + +git switch -c feature/pricing-update +echo "const rate = 0.15;" > pricing.js && git add pricing.js && git commit -m "feat: raise rate" + +git switch main +echo "const rate = 0.12;" > pricing.js && git add pricing.js && git commit -m "feat: adjust rate" +git merge feature/pricing-update +``` + +**🎯 Your task:** Git will stop with a conflict in `pricing.js`. Open it, resolve it to whatever +value you decide is correct, then complete the merge. + +
+💡 Show solution + +```bash +# Edit pricing.js by hand — remove the <<<<<<<, =======, >>>>>>> markers, +# keep whichever line (or a new one) is actually correct +git add pricing.js +git commit +``` + +Changed your mind entirely? `git merge --abort` backs out to exactly how things were before you +started the merge — no harm done. + +
+ +
+ +### 💥 Lab 4 — Detached HEAD + +```bash +git checkout +``` + +Git will print a long, slightly alarming note about being in **"detached HEAD"** state. + +
+ +**💡 CONCEPT** + +Normally `HEAD` points at a *branch*, which points at a commit. Checking out a specific commit +(or tag) directly skips the branch entirely — `HEAD` now points **straight at a commit**, with +no branch name attached. You can look around freely, but any new commit you make here has no +branch pointing at it, and is one `git switch` away from becoming an orphaned, reflog-only commit. + +
+ +
+ +**Set up the scenario:** +```bash +git switch main +git log --oneline -3 # pick a commit a couple back +git checkout # "You are in 'detached HEAD' state" +``` + +**🎯 Your task:** did you lose any work? Make one commit here out of curiosity, then get safely +back to `main` — with or without keeping that commit. + +
+💡 Show solution + +```bash +echo "experiment" >> scratch.js && git add scratch.js && git commit -m "wip: detached experiment" + +# To KEEP it: turn this detached commit into a real branch before leaving +git switch -c rescued-from-detached + +# To DISCARD it and just go back: +git switch main +``` + +Nothing was ever truly at risk — the commit you made while detached is sitting in the reflog +either way, exactly like Lab 2, for as long as the reflog retention window lasts. + +
+ +
+ +### 💥 Lab 5 — Bad Commit Message + +You already practiced the mechanism in the Amend Try It above — this is the realistic version of +*when* you'd reach for it. + +
+ +**Set up the disaster:** +```bash +git commit --allow-empty -m "fix bug" +``` + +**🎯 Your task:** that message tells nobody anything. Fix it — but only because you **haven't +pushed it yet**. + +
+💡 Show solution + +```bash +git commit --amend -m "fix: prevent duplicate charge on double-click checkout" +``` + +
+ +
+ +### 💥 Lab 6 — Wrong Commit on the Wrong Branch + +A teammate's fix landed on `main`, but a `release/v1.2` branch — already out and in use — also +needs that exact fix, without pulling in anything else `main` has picked up since. + +
+ +**Set up the scenario:** +```bash +git switch main +echo "critical fix" >> auth.js && git add auth.js && git commit -m "fix: critical auth bug" +git tag v1.2-base # pretend this is where release/v1.2 branched from, earlier + +git switch -c release/v1.2 v1.2-base # the release branch, WITHOUT the fix above yet +``` + +**🎯 Your task:** get just that one fix onto `release/v1.2`, without merging all of `main`. + +
+💡 Show solution + +```bash +git log main --oneline -1 # find the fix commit's hash +git switch release/v1.2 +git cherry-pick +``` + +Full cherry-pick mechanics (and the patch-file alternative for when you can't push directly) are +in [Module 05](../05-advanced-git/#-cherry-pick--patch). + +
+ +
+ +### 💥 Lab 7 — Production Hotfix + +The realistic, end-to-end version of everything above: `v1.0.0` is live in production. A critical +bug is found. `main` has unrelated, half-finished work on it that absolutely cannot ship yet. + +
+ +**Set up the scenario:** +```bash +git switch main +git tag v1.0.0 # pretend this is the live production release +echo "unfinished feature" >> big-feature.js && git add big-feature.js +git commit -m "wip: half-finished feature, not ready to ship" +``` + +**🎯 Your task:** fix the production bug based on the `v1.0.0` tag specifically — not on `main`, +which now has unshippable work on it — then get the fix both released and merged back into +`main`, without shipping the unfinished feature. + +
+💡 Show solution + +```bash +git switch -c hotfix/critical-bug v1.0.0 # branch from the TAG, not main +echo "fix applied" >> auth.js && git add auth.js +git commit -m "fix: critical production bug" + +git tag v1.0.1 hotfix/critical-bug # the new, fixed release +git switch main +git merge hotfix/critical-bug # bring the fix into main too, unfinished work untouched +``` + +`main`'s half-finished feature was never touched — the hotfix branch came from the tag, not from +`main`'s current tip, so the only thing that reached production was the fix itself. + +
+ +
--- @@ -232,6 +599,7 @@ git branch recovered-branch | **Rebase** | Replay commits on top of a new base, producing linear history — only safe on unshared branches | | **Reflog** | A log of every place `HEAD` has ever pointed, including "lost" commits — Git's real safety net | | **Cherry-pick** | Apply one specific commit from anywhere onto your current branch | +| **Detached HEAD** | `HEAD` pointing directly at a commit instead of at a branch — new commits here aren't on any branch until you make one | --- @@ -246,6 +614,29 @@ If #2 didn't immediately bring "uncommitted changes, protected by committing oft that's the single most practically important lesson in this entire module — worth re-reading the Reflog section above. +
+🧠 Quick Check: You run git checkout <old-commit-hash> directly and Git warns you about "detached HEAD." Have you broken something? + +No — you're just looking at history from a point that has no branch name attached to it. You can +look around freely. The only thing to be careful about is making NEW commits while detached and +then switching away without turning them into a real branch first (`git switch -c some-name`) — +otherwise that commit becomes reachable only through the reflog, like any other "lost" commit. + +
+ +
+ +**🧠 You can now:** + +✓ Choose `revert` vs. `reset` correctly based on whether history is shared +✓ Explain what `--soft`/`--mixed`/`--hard` each actually do to your changes +✓ Fix a commit message or forgotten file with `--amend`, before pushing +✓ Recover a commit you were sure was gone forever, using `reflog` +✓ Walk through a wrong branch, a lost commit, a merge conflict, a detached HEAD, a bad commit + message, a misplaced commit, and a production hotfix — and fix every one of them + +
+ ---
diff --git a/05-advanced-git/README.md b/05-advanced-git/README.md index 4486078..f757c26 100644 --- a/05-advanced-git/README.md +++ b/05-advanced-git/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 05 — Advanced Git" +description: ".gitignore/.gitattributes, LFS, signed commits, cherry-picking, merge conflict resolution, CI/CD, hooks, submodules, and git bisect." +--- +
# 🚀 Module 05 — Advanced Git @@ -9,6 +14,18 @@
+
+ +**By the end of this module you will be able to:** + +✓ Keep junk and secrets out of a repo with `.gitignore` — and know its one real limitation +✓ Apply one specific commit anywhere you need it with cherry-pick +✓ Resolve a real merge conflict by hand, calmly +✓ Hunt down exactly which commit introduced a regression, using `git bisect` +✓ Explain what a pre-commit hook checks vs. what CI checks, and why teams use both + +
+ --- ## 📑 In This Module @@ -19,6 +36,7 @@ - [Signing Commits & Tags](#-signing-commits--tags) - [Cherry-pick & Patch](#-cherry-pick--patch) - [Merge Conflicts, Properly](#-merge-conflicts-properly) +- [🔍 git bisect — Finding the Commit That Broke Things](#-git-bisect--finding-the-commit-that-broke-things) - [Git in CI/CD](#-git-in-cicd) - [Git Hooks](#-git-hooks) - [Submodules](#-submodules) @@ -62,6 +80,23 @@ git commit -m "chore: stop tracking .env, add to gitignore" > not just removing the file. [gitignore.io](https://www.toptal.com/developers/gitignore) generates > solid starter files for any language/framework combination. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +echo "node_modules/" > .gitignore +echo ".env" >> .gitignore +mkdir node_modules && touch node_modules/some-package.js +touch .env +git status +``` + +**What should I see?** Neither `node_modules/` nor `.env` show up anywhere in `git status` — +not even as untracked. `.gitignore` is working. + +
+ --- ## 🏷️ .gitattributes @@ -155,6 +190,27 @@ git checkout release/v1.2 git cherry-pick ``` +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +git switch main +git switch -c release/v1.2 # pretend this is an earlier release branch +git switch main +echo "shared fix" >> shared.js && git add shared.js && git commit -m "fix: shared bug" +git log --oneline -1 # note this hash + +git switch release/v1.2 +git cherry-pick +git log --oneline -1 # the fix, now here too — with a NEW hash +``` + +**What should I see?** The exact same change, now on `release/v1.2`, but as a brand new commit +with its own hash — cherry-pick copies the *change*, not the original commit object. + +
+ **Patch** — export changes as a portable file, apply them elsewhere (useful when you can't push directly, e.g. emailing a fix to an offline system): @@ -214,6 +270,98 @@ do, and writing the version that's genuinely correct. > conflicting changes. A branch merged after 2 days has far fewer conflicts than the same branch > merged after 3 weeks. +
+🧠 Quick Check: In a conflict block, which side is HEAD and which is "incoming"? + +``` +<<<<<<< HEAD +const discountRate = 0.10; +======= +const discountRate = 0.15; +>>>>>>> feature/pricing-update +``` + +Everything between `<<<<<<< HEAD` and `=======` is **your current branch's** version (the one +you're merging *into*). Everything between `=======` and `>>>>>>> branch-name` is the **incoming** +branch's version (the one you're merging *in*). Here, `0.10` is what's on your current branch; +`0.15` is what `feature/pricing-update` wants to change it to. + +
+ +> [!TIP] +> Want to actually resolve one hands-on instead of just reading markers? [Module 04's Disaster +> Lab 3](../04-undo-and-recovery/#-lab-3--merge-conflict) sets up a real conflict and walks +> through fixing it. + +--- + +## 🔍 git bisect — Finding the Commit That Broke Things + +A regression test started failing. You know it passed a week ago, across maybe 40 commits since. +Reading every diff by hand would work — eventually — but there's a faster, structured way: +**binary search through your own commit history.** + +```bash +git bisect start +git bisect bad # the current commit is confirmed broken +git bisect good + +# Git checks out a commit exactly halfway between good and bad. +# Run your test / reproduce the bug, then tell Git the result: +git bisect good # this commit is fine — the bug is somewhere later +# or +git bisect bad # this commit already has the bug — it's somewhere earlier + +# Git keeps halving the range automatically. Repeat until it reports: +# " is the first bad commit" + +git bisect reset # done — return to your original HEAD +``` + +### 🎭 The real-life example + +It's the "guess a number between 1 and 100" game, except the number is "which of these 40 +commits broke the checkout flow," and each guess costs you running the test suite once instead +of just saying a number. 40 commits, checked one at a time, is 40 checks worst case. Bisected, +it's **at most ~6** (log₂ 40) — Git throws away half the remaining suspects every single round. + +
+ +**🧪 SDET SCENARIO** + +Your regression suite flags that `test_checkout_total_with_discount` started failing sometime in +the last 40 commits, but nobody noticed exactly when because it only runs nightly. Instead of +checking out commits one at a time and re-running the full suite by hand, point bisect at the +test itself and let it fully automate the search: + +```bash +git bisect start HEAD +git bisect run npm test -- --grep "checkout_total_with_discount" +``` + +`git bisect run` checks out each candidate commit, runs that exact command, and uses its **exit +code** to decide good/bad automatically — 0 means good, nonzero means bad. It reports the exact +first broken commit without you touching a checkout command yourself. This is one of the +highest-leverage automation-adjacent Git skills a test engineer can have: it turns "something +broke somewhere in the last two weeks" from a multi-hour manual hunt into one unattended command. + +
+ +> [!TIP] +> `git bisect` needs a command (or your own judgment) that can cleanly say "good" or "bad" for +> any commit in the range — which is exactly what an automated test assertion already does. This +> is why bisect and test automation pair so naturally: the test *is* the oracle bisect needs. + +
+🧠 Quick Check: Why does git bisect run need a command, not just a test file path? + +Because bisect decides good/bad purely from the command's **exit code** — it needs something it +can actually execute and check the result of, not a file to read. `npm test -- --grep "..."` (or +`pytest -k "..."`, or any equivalent) is a command that exits 0 on pass and nonzero on failure, +which is exactly the signal bisect's automated loop depends on. + +
+ --- ## ⚙️ Git in CI/CD @@ -222,7 +370,7 @@ Git isn't just for humans typing commands — CI/CD pipelines (GitHub Actions, J entirely around Git events. ```yaml -# .github/workflows/test.yml — a real, minimal example +# .github/workflows/test.yml — a real, minimal example for a Playwright test suite name: Run Tests on: push: @@ -236,7 +384,8 @@ jobs: steps: - uses: actions/checkout@v4 # this step IS a git clone, done for you - run: npm install - - run: npm test + - run: npx playwright install --with-deps + - run: npx playwright test ``` ### 🎭 The real-life example @@ -245,6 +394,19 @@ CI/CD is an automatic inspector standing at the shared filing cabinet, who reads the moment it's filed (`git push`) and immediately checks it for problems — without waiting for a human to ask. `on: push` / `on: pull_request` are literally Git events triggering that inspection. +
+ +**🧪 SDET SCENARIO** + +This is the exact mechanism behind "every pull request automatically runs the test suite" — +which is what Module 02's GitHub Flow called "CI passes" without showing the YAML behind it. +`actions/checkout@v4` is doing a `git clone` of the exact commit the PR is built on, and the +suite then runs against that precise snapshot — which is also why a flaky test that only fails +in CI, not locally, is so often a real environment/data difference, not a ghost: it's running +against genuinely different, freshly-cloned code every time. + +
+ --- ## 🪝 Git Hooks @@ -278,6 +440,28 @@ before allowing a push). > [Husky](https://typicode.github.io/husky/) solve this by making hooks a normal, committed, > shareable part of the project instead. +
+ +**⌨️ TRY IT YOURSELF** (in `~/git-playground`) + +```bash +cat > .git/hooks/pre-commit << 'EOF' +#!/bin/sh +echo "🪝 pre-commit hook running..." +exit 1 +EOF +chmod +x .git/hooks/pre-commit + +echo "test" >> app.js && git add app.js +git commit -m "this should get blocked" +``` + +**What should I see?** The commit is refused — `exit 1` tells Git the hook failed, so the commit +never happens, even though you ran `git commit` correctly. Change `exit 1` to `exit 0` and try +again to see it succeed instead. + +
+ --- ## 🧩 Submodules @@ -336,6 +520,7 @@ told yet. | **Signed commit** | A commit cryptographically verified to actually be from you | | **Cherry-pick** | Apply one specific commit from any branch onto your current one | | **Merge conflict** | When Git can't automatically reconcile two changes to the same lines — needs human resolution | +| **`git bisect`** | Binary search through commit history to find exactly which commit introduced a regression | | **Git hook** | A script that runs automatically at a specific point in Git's workflow (e.g. before a commit) | | **Submodule** | A Git repository nested inside another, tracked as a reference to a specific commit | @@ -345,13 +530,26 @@ told yet. 1. Why doesn't adding a file to `.gitignore` remove it from history if it was already committed? 2. When would you reach for `cherry-pick` instead of a full `merge`? -3. What's the actual difference between what a `pre-commit` hook checks vs. what CI checks — and +3. Why does `git bisect` need a pass/fail signal (like a test's exit code) to actually automate? +4. What's the actual difference between what a `pre-commit` hook checks vs. what CI checks — and why do teams often use both, not just one? -(#3: a `pre-commit` hook is fast, local, and optional (someone COULD skip it with `--no-verify`) +(#4: a `pre-commit` hook is fast, local, and optional (someone COULD skip it with `--no-verify`) — CI is slower, remote, and mandatory. Teams use both: hooks for a fast local safety net, CI as the actual, unskippable gate.) +
+ +**🧠 You can now:** + +✓ Keep junk and secrets out of a repo with `.gitignore`, and know what it can't undo +✓ Apply one specific commit anywhere you need it with cherry-pick +✓ Read merge conflict markers and resolve a conflict calmly +✓ Hunt down the exact commit that broke something with `git bisect` — manually or automated +✓ Tell a local hook apart from a CI check, and know why real teams use both + +
+ ---
diff --git a/06-certification-prep/README.md b/06-certification-prep/README.md index 074310a..e5de5c0 100644 --- a/06-certification-prep/README.md +++ b/06-certification-prep/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 06 — Certification Prep" +description: "A structured review pass across every module, framed as certification-style, scenario-based practice." +--- +
# 🎓 Module 06 — Certification Prep @@ -94,8 +99,14 @@ module before moving on — don't just re-read, actually re-explain it to yourse **Advanced ([Module 05](../05-advanced-git/))** - [ ] `.gitignore` vs. `.gitattributes` — different jobs entirely - [ ] What a merge conflict actually is, and how to resolve one calmly +- [ ] How `git bisect` finds a regression, and why it needs a pass/fail signal to automate - [ ] The difference between a local hook and a CI check +**Disaster Labs ([Module 04](../04-undo-and-recovery/#-disaster-labs))** +- [ ] Moving a commit off the wrong branch with `cherry-pick` +- [ ] What "detached HEAD" actually means, and why it isn't automatically dangerous +- [ ] Why a production hotfix branches from a tag, not from `main`'s current tip + --- ## ⚡ Rapid-Fire Concept Check @@ -146,6 +157,21 @@ you use, and why?** > created — Git just moves `main`'s pointer forward to match, since there's no divergent history > to actually combine. +**Q: A regression test started failing sometime in the last 30 commits, but nobody knows which +one. Walk through how you'd find it.** +> `git bisect start`, mark the current commit `bad` and a known-working older commit `good`. Git +> checks out the midpoint; run the test, report `good` or `bad`, and Git halves the remaining +> range each time — at most ~5 checks for 30 commits, instead of up to 30. If the check is itself +> a test command, `git bisect run ` automates the entire loop using the command's exit +> code, with no manual checkout steps at all. + +**Q: You run `git checkout` on an old commit hash directly and Git warns about "detached HEAD." +Is this a problem?** +> Not by itself — you're just viewing history with no branch name attached to `HEAD`. The only +> real risk is making a new commit here and switching away without turning it into a real branch +> first (`git switch -c name`); until then it's one `git switch` away from being reachable only +> through the reflog. + --- ## 🗣️ The "Explain It Out Loud" Test diff --git a/07-exercises-and-study-plan/README.md b/07-exercises-and-study-plan/README.md index 95af21a..de8c637 100644 --- a/07-exercises-and-study-plan/README.md +++ b/07-exercises-and-study-plan/README.md @@ -1,3 +1,8 @@ +--- +title: "Module 07 — Exercises & Study Plan" +description: "Progressive exercises, a capstone Git Mastery Challenge, and a 20-question quiz bank — where reading turns into muscle memory." +--- +
# 🏁 Module 07 — Exercises & Study Plan (Capstone) @@ -9,6 +14,16 @@
+
+ +**By the end of this module you will be able to:** + +✓ Run the full branch → commit → PR → merge → recover → bisect loop from memory, not notes +✓ Self-assess exactly which modules need a second pass, using the quiz bank +✓ Point at a real, well-organized Git history as proof you know this, not just a certificate + +
+ --- ## 📑 In This Module @@ -16,7 +31,7 @@ - [Syllabus — The Whole Course, One Page](#-syllabus--the-whole-course-one-page) - [Study Plan — Suggested Pacing](#-study-plan--suggested-pacing) - [Exercises — Progressive, By Module](#-exercises--progressive-by-module) -- [Capstone Exercise — One Continuous Scenario](#-capstone-exercise--one-continuous-scenario) +- [🏆 Git Mastery Challenge (Capstone)](#-git-mastery-challenge-capstone) - [Quiz Bank](#-quiz-bank) - [You're Done — Now What](#-youre-done--now-what) @@ -113,27 +128,125 @@ gantt --- -## 🎯 Capstone Exercise — One Continuous Scenario +## 🏆 Git Mastery Challenge (Capstone) + +This is the final "prove you can actually use Git" exercise — one continuous scenario using +everything from Modules 00–05 together, the single best way to confirm the concepts connected +into one coherent mental model instead of staying isolated facts. + +
+ +**🎯 THE SCENARIO** + +You're working on a small automation project. Starting from a fresh GitHub repo, you need to: + +1. Set up `.gitignore` properly from the start +2. Create a feature branch, make commits, push it +3. Open a real Pull Request and merge it via GitHub +4. Deliberately create a merge conflict between two branches, and resolve it +5. Rebase a local feature branch onto `main` before opening its PR +6. Make a commit with a typo in the message, `--amend` it before pushing +7. Push a bad commit, then `revert` it cleanly on a branch that's already shared +8. Deliberately "lose" a commit with `reset --hard`, then recover it with `reflog` +9. Use `git bisect` to identify exactly which of several commits introduced a deliberately + planted bug +10. Tag a release, add a `pre-commit` hook, and write a clean, genuinely good README + +Don't look at the hints yet. Try the whole thing from memory first — that's the actual point. + +
+ +
+💡 Stuck? Hints (not the solution) + +- Steps 1–4 are the GitHub Flow loop from Module 02, plus the conflict lab from Module 04 +- Step 5: rebase your feature branch *before* opening the PR, not after — that's the whole reason + teams rebase local branches instead of `main` +- Step 8 only works if the "lost" commit was actually committed first — `reflog` can't rescue + uncommitted changes, by design +- Step 9: plant a bug in one commit among several on purpose, mark the current state `bad`, an + earlier known-good commit `good`, and either bisect manually or write one script you can hand + to `git bisect run` + +
+ +
+💡 Partial solution — steps 1–4 + +```bash +git init my-automation-project && cd my-automation-project +echo "node_modules/" > .gitignore && echo ".env" >> .gitignore +git add .gitignore && git commit -m "chore: initial gitignore" +git remote add origin +git push -u origin main + +git switch -c feature/first-change +echo "feature work" >> feature.js && git add feature.js +git commit -m "feat: first change" +git push -u origin feature/first-change +# Open the PR on GitHub, merge it, then locally: +git switch main && git pull +``` + +
+ +
+💡 Complete solution — all 10 steps + +Steps 5–10, continuing from the state above: + +```bash +# 5. Rebase before opening a PR +git switch -c feature/second-change +echo "more work" >> feature2.js && git add feature2.js && git commit -m "feat: second change" +git fetch origin && git rebase origin/main + +# 6. Amend a typo'd message before pushing +git commit --allow-empty -m "fx: typo" +git commit --amend -m "fix: corrected message" + +# 7. Revert a bad commit already shared +git push -u origin feature/second-change +echo "oops" >> feature2.js && git add feature2.js && git commit -m "feat: broken change" && git push +git revert --no-edit HEAD && git push + +# 8. Lose and recover a commit +echo "important" >> feature2.js && git add feature2.js && git commit -m "feat: important work" +git reset --hard HEAD~1 +git reflog +git branch recovered HEAD@{1} + +# 9. Bisect a planted regression (simplified — exit code stands in for a real test) +git bisect start +git bisect bad HEAD +git bisect good +# Git checks out a midpoint each round — inspect it, then: +git bisect good # or: git bisect bad +# ...until Git reports the first bad commit, then: +git bisect reset + +# 10. Tag, hook, README +git tag -a v1.0.0 -m "First release" +echo '#!/bin/sh\necho "checking..."' > .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit +# Write the README by hand — this part doesn't have a shortcut +``` + +
+ +
+💡 Explanation — why these 10 steps specifically -Do this as one continuous exercise, using everything from Modules 00–05 together — this is the -single best way to confirm the concepts actually connected into one coherent mental model, not -just isolated facts: +Every step maps to a real failure mode this course spent a whole module on: steps 1–4 are the +daily loop (Modules 01–02), step 5 is the "clean up before review" habit senior engineers expect, +steps 6–8 are Module 04's safety net exercised for real, step 9 is the single highest-leverage +debugging skill this course teaches, and step 10 is "does your own repo practice what this course +preaches" — the same bar [Module 04's Disaster Labs](../04-undo-and-recovery/#-disaster-labs) and +this very repository hold themselves to. -1. Create a new GitHub repo for a small project (anything — a script, a simple app, doesn't - matter) -2. Set up `.gitignore` properly from the start -3. Work in feature branches for every change — never commit directly to `main` -4. Push every branch, open a real Pull Request for each one (even solo, reviewing your own PR is - good practice), merge via GitHub -5. Deliberately create one merge conflict between two branches and resolve it -6. Make one commit with a typo in the message, `--amend` it before pushing -7. Push a bad commit, then `revert` it cleanly -8. Tag a "release" with an annotated tag -9. Add a `pre-commit` hook that checks something trivial -10. Write a clean, genuinely good README for the finished project +
-If you can do all 10 steps without needing to look anything up beyond a quick syntax check, you've -completed this course in the way that actually matters — not "read about Git" but "can use Git." +If you can do all 10 steps — even needing the hints, not the full solution — you've completed +this course in the way that actually matters: not "read about Git" but "can use Git." --- @@ -161,6 +274,8 @@ Quick self-test — cover the answers, go through all 20, then check yourself. 18. What's the actual job of `.gitignore` vs `.gitattributes`? 19. What are the three sections inside a merge conflict's `<<<<<<<`/`=======`/`>>>>>>>` markers? 20. What's the difference between a Git hook and a CI/CD check? +21. What does "detached HEAD" actually mean, and is it dangerous by itself? +22. Why does `git bisect run` need a command with an exit code, not just a file to look at? **Answers:** every single one of these is directly covered in Modules 00–05 — if any felt shaky, that's your specific, targeted review list. Don't re-read whole modules; jump straight to the diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9bbd820 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,18 @@ +--- +title: "Code of Conduct" +description: "A short, genuine code of conduct for contributing to GIT Tutorials." +--- + +# Code of Conduct + +This is a learning resource. People contributing here are, by definition, often still learning — +including the beginner fixing their first-ever typo PR. + +- Be patient with questions that seem basic. Everyone in this course's own target audience was + once the person who didn't know what a staging area was. +- Critique the content, not the person. "This command is wrong" is useful; anything aimed at the + contributor instead of the contribution isn't. +- No harassment, discrimination, or personal attacks, in issues, PRs, or comments — no exceptions. + +Violations can be reported by opening an issue or contacting the maintainer directly through +GitHub. Reports are taken seriously and handled privately. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cd8a46b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +--- +title: "Contributing" +description: "How to contribute to GIT Tutorials — the exact fork/clone/branch/PR flow this course itself teaches in Module 03." +--- + +# Contributing to GIT Tutorials + +This course explicitly invites you to practice [Module 03](./03-contributing-to-open-source/) +on this very repo — a typo fix, a broken link, an outdated command, or a clearer explanation are +all genuinely welcome. This file is the specific version of the generic flow that module teaches. + +## The flow + +1. **Fork** this repo (top right on GitHub) +2. **Clone your fork**, then add this repo as `upstream`: + ```bash + git clone git@github.com:yourusername/git-tutorials.git + cd git-tutorials + git remote add upstream git@github.com:ghanendra-sdet/git-tutorials.git + ``` +3. **Branch** for your specific change — never commit directly to `main`: + ```bash + git switch -c fix/broken-link-in-module-04 + ``` +4. **Make the change.** Keep it small and focused — one fix per PR, per + [Module 03](./03-contributing-to-open-source/#-send-a-pull-request)'s own advice. +5. **Check it** before pushing — see Content Checks below. +6. **Push to your fork, open a PR** against `ghanendra-sdet/git-tutorials` `main`. + +## Content checks before opening a PR + +This repo validates two things automatically in CI on every PR (see +[`.github/workflows/validate-docs.yml`](./.github/workflows/validate-docs.yml)): + +- Every fenced code block (` ``` `) has a matching close +- Every relative Markdown link resolves to a real file + +You can run the same check locally before pushing: + +```bash +python3 .github/scripts/check_docs.py +``` + +## What makes a good contribution here specifically + +- **Preserve the teaching philosophy.** Every section pairs a real-life analogy with a runnable + command — if you're adding a new concept, keep that pairing, not just the command. +- **Hands-on over read-only.** If you're adding a new command or concept, a `callout-tryit` block + the reader can actually run beats another paragraph of explanation. +- **Dummy data only.** Every example uses placeholder names/emails/URLs — never anything that + looks like a real credential or real personal data. +- **Don't remove existing examples, analogies, or labs** to make room for new ones unless they're + factually wrong — this course's own ground rule for itself. + +## Reporting an issue instead of fixing it yourself + +That's genuinely fine too — open an issue using the templates under **New Issue**. A clearly +described problem is still a real contribution. diff --git a/README.md b/README.md index e688cd5..7f4534b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +--- +title: "GIT Tutorials" +description: "A free, hands-on Git + GitHub course. Learn → See → Do → Break → Fix → Remember, with real commands, real recovery labs, and zero corporate-training energy." +--- +
# 🐙 GIT Tutorials — From `git init` to "wait, how do I undo this" @@ -11,11 +16,23 @@ **A structured, hands-on Git + GitHub course — real commands, real analogies, real "oh no I force-pushed to main" recovery stories, zero corporate-training energy.** +**Learn → See → Do → Break → Fix → Remember.** You don't just read about a command — you run it, +in a disposable sandbox, immediately. + +[![Start Learning](https://img.shields.io/badge/🚀-Start%20Learning-2563eb?style=for-the-badge)](./00-introduction/) +[![Course Roadmap](https://img.shields.io/badge/📚-Course%20Roadmap-16a34a?style=for-the-badge)](#️-the-path) +[![Practice Labs](https://img.shields.io/badge/🧪-Practice%20Labs-d97706?style=for-the-badge)](./04-undo-and-recovery/#-disaster-labs) +[![Cheat Sheet](https://img.shields.io/badge/⚡-Cheat%20Sheet-7c3aed?style=for-the-badge)](./resources/cheatsheet.md) + **🌐 Browse it live:** [ghanendra-sdet.github.io/git-tutorials](https://ghanendra-sdet.github.io/git-tutorials/)

**📦 View source on GitHub:** [github.com/ghanendra-sdet/git-tutorials](https://github.com/ghanendra-sdet/git-tutorials)
+
+ +

Progress is tracked only in your own browser (localStorage) — no login, nothing sent anywhere. The checklist above will stay empty until JavaScript runs, on the live site.

+ --- ## Why this exists @@ -43,16 +60,36 @@ This one is different for three reasons: ## 🗺️ The Path +```mermaid +flowchart TD + M00["00 · Introduction & Setup"] --> M01["01 · Git Fundamentals"] + M01 --> M02["02 · Git + GitHub"] + M02 --> M03["03 · Contributing to Open Source"] + M03 --> M04["04 · Undo & Recovery"] + M04 --> M05["05 · Advanced Git"] + M05 --> M06["06 · Certification Prep"] + M06 --> M07["07 · Exercises & Study Plan"] + + style M00 fill:#d4edda,color:#1a1a1a + style M01 fill:#d4edda,color:#1a1a1a + style M02 fill:#cfe2ff,color:#1a1a1a + style M03 fill:#cfe2ff,color:#1a1a1a + style M04 fill:#fff3cd,color:#1a1a1a + style M05 fill:#ffcccc,color:#1a1a1a + style M06 fill:#ede9fe,color:#1a1a1a + style M07 fill:#ede9fe,color:#1a1a1a ``` -00 → Introduction & Setup "What even is version control, and why does everyone use this one" -01 → Git Fundamentals "The 90% of Git you'll use every single day" -02 → Git + GitHub "Your laptop's Git talking to the internet's Git" -03 → Contributing to Open Source "Fork, clone, pull request, profit (reputation-wise)" -04 → Undo & Recovery "You didn't break it. Well — you did. But it's fixable." -05 → Advanced Git "The stuff that shows up in senior job descriptions" -06 → Certification Prep "Prove it, on paper" -07 → Exercises & Study Plan "Stop reading, start typing" -``` + +| # | Module | Level | "In one sentence" | +|---|---|---|---| +| 00 | [Introduction & Setup](./00-introduction/) | 🟢 Beginner | What even is version control, and why does everyone use this one | +| 01 | [Git Fundamentals](./01-git-fundamentals/) | 🟢 Beginner | The 90% of Git you'll use every single day | +| 02 | [Git + GitHub](./02-git-and-github/) | 🟢 Beginner→Intermediate | Your laptop's Git talking to the internet's Git | +| 03 | [Contributing to Open Source](./03-contributing-to-open-source/) | 🔵 Intermediate | Fork, clone, pull request, profit (reputation-wise) | +| 04 | [Undo & Recovery](./04-undo-and-recovery/) | 🔵 Intermediate | You didn't break it. Well — you did. But it's fixable | +| 05 | [Advanced Git](./05-advanced-git/) | 🔴 Advanced | The stuff that shows up in senior job descriptions | +| 06 | [Certification Prep](./06-certification-prep/) | 🟣 All levels | Prove it, on paper | +| 07 | [Exercises & Study Plan](./07-exercises-and-study-plan/) | 🟣 All levels | Stop reading, start typing | Follow the numbers in order for modules 00–02 — everything after that builds directly on branching and merging, which is the one concept you genuinely cannot skip. @@ -114,9 +151,13 @@ progression. This is where reading turns into muscle memory. ## 🧠 Practice & Recall -Reading a module once won't make the commands stick — using them will. Two resources built +Reading a module once won't make the commands stick — using them will. Three resources built specifically for that: +- **[Interactive Cheatsheet Matrix](./resources/interactive-cheatsheet.html)** — every command, + searchable and filterable by "how much you're panicking," with one-click copy and a live, + consequence-free visual Git tree sandbox (click "commit," "branch," "merge" and watch the graph + update in real time). - **[Command Recall Drill](./resources/command-recall-drill.md)** — scenario-based, fold-to-reveal quizzing across every module. Do 10-15 a day, not all of them at once — this is spaced repetition, not a reading list. diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..f2994d7 --- /dev/null +++ b/_config.yml @@ -0,0 +1,23 @@ +theme: minima +markdown: kramdown + +plugins: + - jekyll-readme-index + - jekyll-relative-links + +# Every README.md in this repo has front matter (added for SEO titles/ +# descriptions) — the plugin ignores README files with front matter +# unless this is explicitly enabled. +readme_index: + with_frontmatter: true + +defaults: + - scope: + path: "" + values: + layout: "default" + +exclude: + - Gemfile + - Gemfile.lock + - vendor diff --git a/_includes/head-custom.html b/_includes/head-custom.html index 74e481f..2baf12b 100644 --- a/_includes/head-custom.html +++ b/_includes/head-custom.html @@ -1,38 +1,21 @@ - + + + + + + + + + + + + + + - - - + diff --git a/_includes/head.html b/_includes/head.html new file mode 100644 index 0000000..4268e38 --- /dev/null +++ b/_includes/head.html @@ -0,0 +1,11 @@ + + + + + {%- seo -%} + {%- feed_meta -%} + {%- if jekyll.environment == 'production' and site.google_analytics -%} + {%- include google-analytics.html -%} + {%- endif -%} + {%- include head-custom.html -%} + diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 0000000..53886f6 --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,43 @@ + + + + {%- include head.html -%} + + +
+ + +
+
+ +
+ {{ content }} +
+
+
+ + +
+ + + diff --git a/assets/css/custom.css b/assets/css/custom.css new file mode 100644 index 0000000..0831934 --- /dev/null +++ b/assets/css/custom.css @@ -0,0 +1,572 @@ +/* ========================================================================== + GIT Tutorials — Design System v2 + Matches resources/interactive-cheatsheet.html's existing look (dark-first, + zinc/emerald/amber/rose, rounded cards, generous spacing) instead of + minima's default theme. This file now owns ALL visual styling — minima's + own compiled CSS is no longer linked (see _layouts/default.html). + ========================================================================== */ + +/* ------------------------------------------------------------------------- + 1. Color tokens — dark is the primary, designed palette. Light is a + deliberately-designed complement, not an inversion. + ------------------------------------------------------------------------- */ +:root { + --zinc-50: #fafafa; + --zinc-100: #f4f4f5; + --zinc-200: #e4e4e7; + --zinc-300: #d4d4d8; + --zinc-400: #a1a1aa; + --zinc-500: #71717a; + --zinc-600: #52525b; + --zinc-700: #3f3f46; + --zinc-800: #27272a; + --zinc-900: #18181b; + --zinc-950: #09090b; + + --emerald-400: #34d399; + --emerald-500: #10b981; + --emerald-600: #059669; + --amber-400: #fbbf24; + --amber-500: #f59e0b; + --amber-600: #d97706; + --rose-400: #fb7185; + --rose-500: #f43f5e; + --rose-600: #e11d48; + --sky-400: #38bdf8; + --sky-500: #0ea5e9; + --sky-600: #0284c7; + --violet-400: #a78bfa; + --violet-500: #8b5cf6; + --violet-600: #7c3aed; + + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 18px; + --radius-full: 999px; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; +} + +/* Dark (default) */ +:root, +:root[data-theme="dark"] { + --color-bg: var(--zinc-950); + --color-bg-elevated: var(--zinc-900); + --color-surface: rgba(39, 39, 42, 0.6); /* zinc-800/60 */ + --color-surface-hover: var(--zinc-900); + --color-border: var(--zinc-800); + --color-border-strong: var(--zinc-700); + --color-text: var(--zinc-200); + --color-text-strong: #ffffff; + --color-muted: var(--zinc-400); + --color-muted-2: var(--zinc-500); + --color-link: var(--emerald-400); + --color-link-hover: var(--emerald-500); + --color-code-bg: var(--zinc-950); + --color-primary: var(--emerald-500); + --color-primary-text: var(--zinc-950); + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.5); + --scrollbar-track: var(--zinc-900); + --scrollbar-thumb: var(--zinc-700); +} + +/* Light — only when explicitly chosen (manual toggle) or system prefers it + AND the user hasn't manually picked dark */ +@media (prefers-color-scheme: light) { + :root:not([data-theme="dark"]) { + --color-bg: #ffffff; + --color-bg-elevated: var(--zinc-50); + --color-surface: var(--zinc-50); + --color-surface-hover: var(--zinc-100); + --color-border: var(--zinc-200); + --color-border-strong: var(--zinc-300); + --color-text: var(--zinc-800); + --color-text-strong: var(--zinc-950); + --color-muted: var(--zinc-600); + --color-muted-2: var(--zinc-500); + --color-link: var(--emerald-600); + --color-link-hover: var(--emerald-500); + --color-code-bg: var(--zinc-50); + --color-primary: var(--emerald-600); + --color-primary-text: #ffffff; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.08); + --scrollbar-track: var(--zinc-100); + --scrollbar-thumb: var(--zinc-300); + } +} + +:root[data-theme="light"] { + --color-bg: #ffffff; + --color-bg-elevated: var(--zinc-50); + --color-surface: var(--zinc-50); + --color-surface-hover: var(--zinc-100); + --color-border: var(--zinc-200); + --color-border-strong: var(--zinc-300); + --color-text: var(--zinc-800); + --color-text-strong: var(--zinc-950); + --color-muted: var(--zinc-600); + --color-muted-2: var(--zinc-500); + --color-link: var(--emerald-600); + --color-link-hover: var(--emerald-500); + --color-code-bg: var(--zinc-50); + --color-primary: var(--emerald-600); + --color-primary-text: #ffffff; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.08); + --scrollbar-track: var(--zinc-100); + --scrollbar-thumb: var(--zinc-300); +} + +/* ------------------------------------------------------------------------- + 2. Reset + base (minima's CSS is no longer loaded — this owns everything) + ------------------------------------------------------------------------- */ +*, *::before, *::after { box-sizing: border-box; } +html { + -webkit-text-size-adjust: 100%; + /* Sticky header is ~64px tall — without this, jumping to a #heading + (TOC links, "On this page", same-page anchors) lands the heading + flush under the header, hiding it. This offsets every anchor jump + so the target lands visibly below the header instead. */ + scroll-padding-top: 5.5rem; +} +@media (max-width: 640px) { + html { scroll-padding-top: 4.5rem; } +} + +body { + margin: 0; + background-color: var(--color-bg); + color: var(--color-text); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.65; + -webkit-font-smoothing: antialiased; + transition: background-color 0.15s ease, color 0.15s ease; +} + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: var(--scrollbar-track); } +::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: var(--radius-full); } + +img { max-width: 100%; height: auto; } + +a { + color: var(--color-link); + text-decoration: none; +} +a:hover { color: var(--color-link-hover); text-decoration: underline; } + +h1, h2, h3, h4, h5, h6 { + color: var(--color-text-strong); + font-weight: 700; + line-height: 1.25; + margin: 1.6em 0 0.6em; + scroll-margin-top: 5.5rem; /* belt-and-suspenders with html's scroll-padding-top */ +} +@media (max-width: 640px) { + h1, h2, h3, h4, h5, h6 { scroll-margin-top: 4.5rem; } +} +h1 { font-size: 1.9rem; } +h2 { font-size: 1.5rem; border-bottom: 1px solid var(--color-border); padding-bottom: 0.4em; } +h3 { font-size: 1.2rem; } +h4 { font-size: 1.05rem; } + +p { margin: 0.9em 0; } +ul, ol { padding-left: 1.4em; } +li { margin: 0.35em 0; } + +strong { color: var(--color-text-strong); font-weight: 700; } + +hr { + border: none; + border-top: 1px solid var(--color-border); + margin: 2.2em 0; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 1.2em 0; + font-size: 0.92rem; + display: block; + overflow-x: auto; +} +th, td { + border: 1px solid var(--color-border); + padding: 0.55em 0.8em; + text-align: left; + vertical-align: top; +} +th { + background: var(--color-bg-elevated); + color: var(--color-text-strong); + font-weight: 700; +} +tr:nth-child(2n) td { background: rgba(255, 255, 255, 0.015); } + +blockquote { + margin: 1.2em 0; + padding: 0.8em 1.1em; + border-left: 3px solid var(--color-border-strong); + background: var(--color-surface); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + color: var(--color-muted); +} +blockquote p { margin: 0.4em 0; } + +/* ------------------------------------------------------------------------- + 3. Code + ------------------------------------------------------------------------- */ +code { + font-family: var(--font-mono); + font-size: 0.86em; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 5px; + padding: 0.15em 0.4em; +} + +pre { + background: var(--color-code-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 1em 1.2em; + overflow-x: auto; + font-size: 0.86rem; + line-height: 1.6; +} +pre code { + background: none; + border: none; + padding: 0; + font-size: 1em; +} +pre.mermaid { background: transparent; border: none; padding: 0; overflow-x: visible; } + +.code-block-wrap { position: relative; } +.copy-btn { + position: absolute; + top: 0.6em; + right: 0.6em; + z-index: 2; + font-size: 0.72rem; + line-height: 1; + padding: 0.4em 0.7em; + border-radius: var(--radius-full); + border: 1px solid var(--color-border); + background: var(--color-bg-elevated); + color: var(--color-muted); + cursor: pointer; + opacity: 0; + transition: opacity 0.12s ease, color 0.12s ease, border-color 0.12s ease; +} +.code-block-wrap:hover .copy-btn, +.code-block-wrap:focus-within .copy-btn, +.copy-btn:focus-visible { opacity: 1; } +.copy-btn:hover { color: var(--color-link); border-color: var(--color-link); } +.copy-btn[data-copied="true"] { color: var(--emerald-400); border-color: var(--emerald-500); opacity: 1; } +@media (max-width: 600px) { .copy-btn { opacity: 1; } } + +/* ------------------------------------------------------------------------- + 4. Callout components — the course's visual language + ------------------------------------------------------------------------- */ +.callout { + margin: 1.4em 0; + padding: 1.1em 1.25em; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); + border-left-width: 4px; + background: var(--color-surface); + box-shadow: var(--shadow-sm); +} +.callout > :first-child { margin-top: 0; } +.callout > :last-child { margin-bottom: 0; } +.callout-label { + display: flex; + align-items: center; + gap: 0.5em; + font-weight: 700; + font-size: 0.74rem; + letter-spacing: 0.06em; + text-transform: uppercase; + margin-bottom: 0.6em; + color: var(--color-text-strong); +} + +.callout-concept { border-left-color: var(--sky-400); } +.callout-important { border-left-color: var(--rose-500); } +.callout-protip { border-left-color: var(--emerald-400); } +.callout-mistake { border-left-color: var(--amber-400); } +.callout-tryit { border-left-color: var(--emerald-500); } +.callout-challenge { border-left-color: var(--violet-400); } +.callout-remember { border-left-color: var(--violet-400); } +.callout-recovery { border-left-color: var(--emerald-400); } +.callout-realworld { border-left-color: var(--zinc-500); } +.callout-sdet { border-left-color: var(--sky-400); } + +.gfm-alert-note { border-left-color: var(--sky-400); } +.gfm-alert-tip { border-left-color: var(--emerald-400); } +.gfm-alert-important { border-left-color: var(--violet-400); } +.gfm-alert-warning { border-left-color: var(--amber-400); } +.gfm-alert-caution { border-left-color: var(--rose-500); } + +.danger-badge { + display: inline-flex; + align-items: center; + gap: 0.4em; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--zinc-950); + background: var(--rose-500); + padding: 0.3em 0.75em; + border-radius: var(--radius-full); + margin-bottom: 0.6em; +} + +details { + margin: 1em 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 0.7em 1em; + background: var(--color-surface); +} +details summary { + cursor: pointer; + font-weight: 600; + color: var(--color-text-strong); +} +details[open] summary { margin-bottom: 0.6em; } + +/* Badges (shields.io etc.) sit awkwardly on dark bg at full brightness — + soften very slightly and keep them from stretching the layout */ +img[src*="shields.io"] { vertical-align: middle; } + +/* ------------------------------------------------------------------------- + 5. Site shell — header, footer, content width + ------------------------------------------------------------------------- */ +.site-shell { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.site-header { + position: sticky; + top: 0; + z-index: 30; + border-bottom: 1px solid var(--color-border); + background: color-mix(in srgb, var(--color-bg) 82%, transparent); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} +.site-header-inner { + max-width: 1000px; + margin: 0 auto; + padding: 1em 1.25em; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1em; +} +.site-logo { + display: flex; + align-items: baseline; + gap: 0.5em; + font-weight: 800; + font-size: 1.05rem; + color: var(--color-text-strong) !important; + text-decoration: none !important; +} +.site-logo .tag { font-size: 0.7rem; font-weight: 600; color: var(--color-muted); letter-spacing: 0.04em; } +.site-nav { + display: flex; + align-items: center; + gap: 1.1em; + font-size: 0.85rem; +} +.site-nav a { color: var(--color-muted); font-weight: 500; } +.site-nav a:hover { color: var(--color-text-strong); text-decoration: none; } + +.page-wrap { + flex: 1; + max-width: 1200px; + width: 100%; + margin: 0 auto; + padding: 2.2em 1.25em 4em; +} + +.page-grid { + display: grid; + grid-template-columns: 1fr; + gap: 2.2em; + align-items: start; +} +.page-content { min-width: 0; } /* lets pre/table overflow-x scroll instead of blowing out the grid */ + +.site-footer { + border-top: 1px solid var(--color-border); + padding: 2.4em 1.25em; + text-align: center; + font-size: 0.82rem; + color: var(--color-muted-2); +} +.site-footer a { color: var(--color-muted); } +.site-footer a:hover { color: var(--color-link); } +.footer-links { display: flex; justify-content: center; gap: 1.2em; margin-bottom: 0.8em; flex-wrap: wrap; } + +/* ------------------------------------------------------------------------- + 6. Nav chrome — breadcrumbs, TOC, back-to-top, theme toggle + ------------------------------------------------------------------------- */ +.chrome-btn { + position: fixed; + z-index: 50; + width: 42px; + height: 42px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-full); + border: 1px solid var(--color-border); + background: var(--color-bg-elevated); + color: var(--color-text); + box-shadow: var(--shadow-md); + cursor: pointer; + font-size: 1.05rem; + transition: transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease; +} +.chrome-btn:hover { transform: translateY(-2px); border-color: var(--color-border-strong); } + +#theme-toggle-btn { bottom: 1.25rem; right: 1.25rem; } +#back-to-top-btn { + bottom: 1.25rem; + right: 4.25rem; + opacity: 0; + pointer-events: none; + transform: translateY(8px); +} +#back-to-top-btn.visible { opacity: 1; pointer-events: auto; transform: translateY(0); } + +.breadcrumb-nav { + font-size: 0.82rem; + color: var(--color-muted-2); + margin: 0 0 1.4em 0; + display: flex; + flex-wrap: wrap; + gap: 0.4em; + align-items: center; +} +.breadcrumb-nav a { color: var(--color-muted-2); } +.breadcrumb-nav a:hover { color: var(--color-link); } +.breadcrumb-nav .sep { opacity: 0.5; } +.breadcrumb-nav .current { color: var(--color-muted); font-weight: 600; } + +.toc-sidebar:empty { display: none; } + +.toc-box { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 1em 1.2em; + margin: 0 0 1.6em 0; + font-size: 0.86rem; +} +.toc-box summary { + font-weight: 700; + font-size: 0.74rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-muted); +} +.toc-box ul { margin: 0.8em 0 0 0; padding-left: 0; list-style: none; } +.toc-box li { margin: 0.4em 0; } +.toc-box a { color: var(--color-text); } +.toc-box a:hover { color: var(--color-link); } +.toc-box a.active { color: var(--emerald-400); font-weight: 600; } + +/* Mobile/narrow: sidebar stacks above the content, collapsible */ +.toc-sidebar { margin: 0; } + +@media (min-width: 1000px) { + /* Desktop: real left sidebar, sticky, own grid column */ + .page-grid:has(.toc-sidebar:not(:empty)) { + grid-template-columns: 240px minmax(0, 1fr); + } + .toc-sidebar { + position: sticky; + top: 5.5rem; + max-height: calc(100vh - 8rem); + overflow-y: auto; + } + .toc-sidebar .toc-box { margin: 0; } + /* On desktop it's a persistent sidebar, not a disclosure widget */ + .toc-sidebar .toc-box summary { pointer-events: none; } + .toc-sidebar .toc-box summary::-webkit-details-marker { display: none; } + .toc-sidebar .toc-box summary { list-style: none; } +} + +/* ------------------------------------------------------------------------- + 7. Progress tracker + ------------------------------------------------------------------------- */ +.progress-tracker { margin: 1.6em 0; } +.progress-row { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + gap: 0.6em; + margin: 0.6em 0; + font-size: 0.85rem; +} +.progress-row .progress-label { display: flex; align-items: center; gap: 0.5em; } +.progress-row .progress-label a { color: var(--color-text); } +.progress-track { + grid-column: 1 / -1; + height: 8px; + border-radius: var(--radius-full); + background: var(--color-border); + overflow: hidden; +} +.progress-fill { height: 100%; background: var(--emerald-500); border-radius: var(--radius-full); transition: width 0.25s ease; } +.progress-check { color: var(--emerald-400); font-weight: 700; } +.progress-reset-btn { + font-size: 0.74rem; + color: var(--color-muted); + background: none; + border: 1px solid var(--color-border); + border-radius: var(--radius-full); + padding: 0.35em 0.8em; + cursor: pointer; +} +.progress-reset-btn:hover { color: var(--rose-400); border-color: var(--rose-500); } + +/* ------------------------------------------------------------------------- + 8. Buttons used in content (e.g. action-row links on the homepage) + ------------------------------------------------------------------------- */ +.btn-row { display: flex; flex-wrap: wrap; gap: 0.6em; justify-content: center; } + +/* ------------------------------------------------------------------------- + 9. Accessibility + motion + ------------------------------------------------------------------------- */ +a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible { + outline: 2px solid var(--emerald-400); + outline-offset: 2px; +} +@media (prefers-reduced-motion: reduce) { + * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; } +} +.visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } + +/* ------------------------------------------------------------------------- + 10. Mobile + ------------------------------------------------------------------------- */ +@media (max-width: 640px) { + body { font-size: 15.5px; } + .page-wrap { padding: 1.6em 1em 3em; } + .site-nav { gap: 0.8em; font-size: 0.78rem; } + #back-to-top-btn { right: 1.25rem; bottom: 4.25rem; } + .chrome-btn { width: 38px; height: 38px; font-size: 0.95rem; } + table { font-size: 0.82rem; } +} diff --git a/assets/js/custom.js b/assets/js/custom.js new file mode 100644 index 0000000..923dd85 --- /dev/null +++ b/assets/js/custom.js @@ -0,0 +1,467 @@ +/* ========================================================================== + GIT Tutorials — site-wide progressive enhancement + Pure JS, no build step, no dependencies beyond the Mermaid CDN script + already loaded by head-custom.html. Everything here degrades gracefully: + if JS fails, the page is still a complete, readable Markdown page. + ========================================================================== */ +(function () { + "use strict"; + + var prefersReducedMotion = window.matchMedia && + window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + // The real Jekyll baseurl ("" locally, "/git-tutorials" on GitHub Pages), + // rendered server-side into a data attribute — never guessed from the URL. + // Guessing "drop the first path segment" breaks the moment the site isn't + // served under a subpath, which is exactly what a local `jekyll serve` + // without --baseurl does. + function getBaseUrl() { + var raw = (document.body.dataset.baseurl || "").replace(/\/$/, ""); + return raw; + } + function stripBaseUrl(pathname) { + var base = getBaseUrl(); + return base && pathname.indexOf(base) === 0 ? pathname.slice(base.length) : pathname; + } + + /* ======================================================================== + 1. THEME — light / dark / system, persisted, manual toggle + ======================================================================== */ + var ThemeManager = { + STORAGE_KEY: "git-tutorials-theme", + + getStored: function () { + try { return localStorage.getItem(this.STORAGE_KEY); } catch (e) { return null; } + }, + setStored: function (value) { + try { localStorage.setItem(this.STORAGE_KEY, value); } catch (e) { /* private mode etc. */ } + }, + apply: function (theme) { + // theme is "light", "dark", or null (= follow system) + if (theme === "light" || theme === "dark") { + document.documentElement.setAttribute("data-theme", theme); + } else { + document.documentElement.removeAttribute("data-theme"); + } + this.updateButton(); + this.reinitMermaid(); + }, + effectiveTheme: function () { + var stored = this.getStored(); + if (stored === "light" || stored === "dark") return stored; + return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" : "light"; + }, + cycle: function () { + var current = this.getStored(); + // light -> dark -> system(null) -> light ... + var next = current === "light" ? "dark" : current === "dark" ? null : "light"; + if (next === null) { try { localStorage.removeItem(this.STORAGE_KEY); } catch (e) {} } + else { this.setStored(next); } + this.apply(next); + }, + updateButton: function () { + var btn = document.getElementById("theme-toggle-btn"); + if (!btn) return; + var stored = this.getStored(); + var label = stored === "light" ? "Light (click for dark)" + : stored === "dark" ? "Dark (click for system)" + : "System theme (click for light)"; + var icon = stored === "light" ? "☀️" : stored === "dark" ? "🌙" : "🖥️"; + btn.textContent = icon; + btn.setAttribute("aria-label", "Theme: " + label); + btn.title = "Theme: " + label; + }, + reinitMermaid: function () { + if (!window.mermaid) return; + var isDark = this.effectiveTheme() === "dark"; + try { + window.mermaid.initialize({ startOnLoad: false, theme: isDark ? "dark" : "default" }); + document.querySelectorAll("pre.mermaid[data-processed]").forEach(function (el) { + el.removeAttribute("data-processed"); + }); + window.mermaid.run ? window.mermaid.run() : window.mermaid.init(undefined, document.querySelectorAll("pre.mermaid")); + } catch (e) { /* older mermaid API shape — ignore, initial render already happened */ } + }, + init: function () { + this.apply(this.getStored()); + var btn = document.createElement("button"); + btn.id = "theme-toggle-btn"; + btn.type = "button"; + btn.className = "chrome-btn"; + document.body.appendChild(btn); + btn.addEventListener("click", this.cycle.bind(this)); + this.updateButton(); + } + }; + + /* ======================================================================== + 2. GFM ALERT UPGRADE — fix [!NOTE]/[!TIP]/etc. rendering on Pages + kramdown renders these as a plain

[!TYPE]\ntext

. + GitHub.com renders them with icons natively; this makes the Pages + site match that instead of showing the literal "[!TYPE]" text. + ======================================================================== */ + var ALERT_META = { + note: { icon: "ℹ️", label: "Note" }, + tip: { icon: "💡", label: "Tip" }, + important: { icon: "❗", label: "Important" }, + warning: { icon: "⚠️", label: "Warning" }, + caution: { icon: "🚨", label: "Caution" } + }; + + function upgradeGfmAlerts() { + document.querySelectorAll("blockquote").forEach(function (bq) { + var firstP = bq.querySelector("p:first-of-type"); + if (!firstP) return; + var html = firstP.innerHTML; + var match = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\s*/i.exec(html) || + /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*\n?/i.exec(html); + if (!match) return; + var type = match[1].toLowerCase(); + firstP.innerHTML = html.slice(match[0].length); + + var meta = ALERT_META[type]; + var wrapper = document.createElement("div"); + wrapper.className = "callout gfm-alert gfm-alert-" + type; + var labelEl = document.createElement("p"); + labelEl.className = "callout-label"; + labelEl.innerHTML = ' " + meta.label; + wrapper.appendChild(labelEl); + while (bq.firstChild) wrapper.appendChild(bq.firstChild); + bq.replaceWith(wrapper); + }); + } + + /* ======================================================================== + 3. COPY BUTTONS on fenced code blocks (skips Mermaid source blocks) + ======================================================================== */ + function addCopyButtons() { + document.querySelectorAll("div.highlighter-rouge, figure.highlight, pre").forEach(function (block) { + // Normalize to the actual
 we want a button on.
+      var pre = block.tagName === "PRE" ? block : block.querySelector("pre");
+      if (!pre || pre.classList.contains("mermaid") || pre.dataset.copyWired) return;
+      var codeEl = pre.querySelector("code");
+      var text = (codeEl || pre).textContent;
+      if (!text || !text.trim()) return;
+      pre.dataset.copyWired = "true";
+
+      var wrap = document.createElement("div");
+      wrap.className = "code-block-wrap";
+      pre.parentNode.insertBefore(wrap, pre);
+      wrap.appendChild(pre);
+
+      var btn = document.createElement("button");
+      btn.type = "button";
+      btn.className = "copy-btn";
+      btn.textContent = "Copy";
+      btn.setAttribute("aria-label", "Copy code to clipboard");
+      wrap.appendChild(btn);
+
+      btn.addEventListener("click", function () {
+        var doCopy = function () {
+          btn.dataset.copied = "true";
+          btn.textContent = "Copied!";
+          setTimeout(function () {
+            btn.textContent = "Copy";
+            delete btn.dataset.copied;
+          }, 1500);
+        };
+        if (navigator.clipboard && navigator.clipboard.writeText) {
+          navigator.clipboard.writeText(text).then(doCopy, doCopy);
+        } else {
+          var ta = document.createElement("textarea");
+          ta.value = text;
+          ta.style.position = "fixed";
+          ta.style.opacity = "0";
+          document.body.appendChild(ta);
+          ta.select();
+          try { document.execCommand("copy"); } catch (e) {}
+          document.body.removeChild(ta);
+          doCopy();
+        }
+      });
+    });
+  }
+
+  /* ========================================================================
+     4. BACK TO TOP
+     ======================================================================== */
+  function initBackToTop() {
+    var btn = document.createElement("button");
+    btn.id = "back-to-top-btn";
+    btn.type = "button";
+    btn.className = "chrome-btn";
+    btn.innerHTML = '';
+    btn.setAttribute("aria-label", "Back to top");
+    document.body.appendChild(btn);
+
+    window.addEventListener("scroll", function () {
+      btn.classList.toggle("visible", window.scrollY > 480);
+    }, { passive: true });
+
+    btn.addEventListener("click", function () {
+      window.scrollTo({ top: 0, behavior: prefersReducedMotion ? "auto" : "smooth" });
+      document.body.setAttribute("tabindex", "-1");
+      document.body.focus();
+      document.body.removeAttribute("tabindex");
+    });
+  }
+
+  /* ========================================================================
+     5. BREADCRUMBS — built from the URL path, e.g.
+        Git Tutorials -> Module 01 - Git Fundamentals
+     ======================================================================== */
+  function titleFromSlug(slug) {
+    var withoutExt = slug.replace(/\.(html?|md)$/i, "");
+    var withoutNum = withoutExt.replace(/^\d+-/, "");
+    var words = withoutNum.split("-").map(function (w) {
+      return w.charAt(0).toUpperCase() + w.slice(1);
+    });
+    var numMatch = /^(\d+)-/.exec(withoutExt);
+    return (numMatch ? numMatch[1] + " · " : "") + words.join(" ");
+  }
+
+  function initBreadcrumbs() {
+    var h1 = document.querySelector(".page-content h1, main h1");
+    if (!h1) return;
+    var pathname = stripBaseUrl(window.location.pathname);
+    var parts = pathname.split("/").filter(Boolean);
+    var segments = parts.filter(function (p) { return p !== "index.html"; });
+    if (segments.length === 0) return; // already on the home page
+
+    var nav = document.createElement("nav");
+    nav.className = "breadcrumb-nav";
+    nav.setAttribute("aria-label", "Breadcrumb");
+
+    var home = document.createElement("a");
+    home.href = getBaseUrl() + "/";
+    home.textContent = "🏠 Git Tutorials";
+    nav.appendChild(home);
+
+    segments.forEach(function (seg, i) {
+      var sep = document.createElement("span");
+      sep.className = "sep";
+      sep.textContent = "›";
+      nav.appendChild(sep);
+
+      var isLast = i === segments.length - 1;
+      if (isLast) {
+        var current = document.createElement("span");
+        current.className = "current";
+        current.textContent = titleFromSlug(seg);
+        nav.appendChild(current);
+      } else {
+        var link = document.createElement("a");
+        link.href = getBaseUrl() + "/" + parts.slice(0, i + 1).join("/") + "/";
+        link.textContent = titleFromSlug(seg);
+        nav.appendChild(link);
+      }
+    });
+
+    h1.parentNode.insertBefore(nav, h1);
+  }
+
+  /* ========================================================================
+     6. AUTO TABLE OF CONTENTS for long pages
+     ======================================================================== */
+  function initTOC() {
+    var mount = document.getElementById("toc-sidebar-mount");
+    var content = document.querySelector(".page-content") || document.querySelector("main");
+    if (!mount || !content) return;
+    var headings = Array.prototype.slice.call(content.querySelectorAll("h2, h3"));
+    if (headings.length < 4) return; // short pages don't need one — mount stays empty, CSS hides it
+
+    var details = document.createElement("details");
+    details.className = "toc-box";
+    details.open = true;
+    var summary = document.createElement("summary");
+    summary.textContent = "On this page";
+    details.appendChild(summary);
+
+    var list = document.createElement("ul");
+    var linkById = {};
+    headings.forEach(function (h, i) {
+      if (!h.id) h.id = "toc-heading-" + i;
+      var li = document.createElement("li");
+      if (h.tagName === "H3") li.style.marginLeft = "1em";
+      var a = document.createElement("a");
+      a.href = "#" + h.id;
+      a.textContent = h.textContent.replace(/^[^\w]*/, ""); // trim leading emoji clutter
+      li.appendChild(a);
+      list.appendChild(li);
+      linkById[h.id] = a;
+    });
+    details.appendChild(list);
+    mount.appendChild(details);
+
+    if ("IntersectionObserver" in window) {
+      var observer = new IntersectionObserver(function (entries) {
+        entries.forEach(function (entry) {
+          var link = linkById[entry.target.id];
+          if (!link) return;
+          link.classList.toggle("active", entry.isIntersecting);
+        });
+      }, { rootMargin: "-10% 0px -70% 0px" });
+      headings.forEach(function (h) { observer.observe(h); });
+    }
+  }
+
+  /* ========================================================================
+     7. EXTERNAL LINKS open in a new tab (same-repo links stay in-tab)
+        [carried over from the previous head-custom.html inline script]
+     ======================================================================== */
+  function externalLinksNewTab() {
+    var repoBase = getBaseUrl();
+    document.querySelectorAll("a[href]").forEach(function (a) {
+      var href = a.getAttribute("href");
+      if (!/^https?:\/\//i.test(href)) return;
+      try {
+        var url = new URL(href, location.href);
+        var isSameRepo = url.origin === location.origin &&
+          (repoBase ? url.pathname.indexOf(repoBase + "/") === 0 : true);
+        if (!isSameRepo) {
+          a.setAttribute("target", "_blank");
+          a.setAttribute("rel", "noopener noreferrer");
+        }
+      } catch (e) { /* malformed href — leave it alone */ }
+    });
+  }
+
+  /* ========================================================================
+     8. MERMAID — convert kramdown's fenced-block output, then render
+     ======================================================================== */
+  function initMermaid() {
+    document.querySelectorAll("code.language-mermaid").forEach(function (codeEl) {
+      var pre = document.createElement("pre");
+      pre.className = "mermaid";
+      pre.textContent = codeEl.textContent;
+      var oldPre = codeEl.closest("pre") || codeEl;
+      oldPre.replaceWith(pre);
+    });
+    if (window.mermaid) {
+      var isDark = ThemeManager.effectiveTheme() === "dark";
+      window.mermaid.initialize({ startOnLoad: true, theme: isDark ? "dark" : "default" });
+    }
+  }
+
+  /* ========================================================================
+     9. PROGRESS TRACKER — localStorage only, no login, no server
+     ======================================================================== */
+  var MODULES = [
+    { slug: "00-introduction", title: "Introduction & Setup" },
+    { slug: "01-git-fundamentals", title: "Git Fundamentals" },
+    { slug: "02-git-and-github", title: "Git + GitHub" },
+    { slug: "03-contributing-to-open-source", title: "Contributing to Open Source" },
+    { slug: "04-undo-and-recovery", title: "Undo & Recovery" },
+    { slug: "05-advanced-git", title: "Advanced Git" },
+    { slug: "06-certification-prep", title: "Certification Prep" },
+    { slug: "07-exercises-and-study-plan", title: "Exercises & Study Plan" }
+  ];
+  var ProgressTracker = {
+    STORAGE_KEY: "git-tutorials-progress",
+    read: function () {
+      try { return JSON.parse(localStorage.getItem(this.STORAGE_KEY) || "{}"); }
+      catch (e) { return {}; }
+    },
+    write: function (data) {
+      try { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); } catch (e) {}
+    },
+    markVisited: function (slug) {
+      var data = this.read();
+      if (!data[slug]) { data[slug] = "visited"; this.write(data); }
+    },
+    markComplete: function (slug, complete) {
+      var data = this.read();
+      data[slug] = complete ? "complete" : "visited";
+      this.write(data);
+    },
+    recordCurrentPage: function () {
+      var parts = stripBaseUrl(location.pathname).split("/").filter(Boolean);
+      var slug = parts[0];
+      if (slug && MODULES.some(function (m) { return m.slug === slug; })) {
+        this.markVisited(slug);
+      }
+    },
+    renderWidget: function () {
+      var mount = document.getElementById("progress-tracker-mount");
+      if (!mount) return;
+      var data = this.read();
+      mount.innerHTML = "";
+      var container = document.createElement("div");
+      container.className = "progress-tracker";
+
+      var completedCount = MODULES.filter(function (m) { return data[m.slug] === "complete"; }).length;
+
+      MODULES.forEach(function (m) {
+        var state = data[m.slug] || "not-started";
+        var row = document.createElement("div");
+        row.className = "progress-row";
+
+        var label = document.createElement("div");
+        label.className = "progress-label";
+        var link = document.createElement("a");
+        link.href = "./" + m.slug + "/";
+        link.textContent = m.title;
+        label.appendChild(link);
+        if (state === "complete") {
+          var check = document.createElement("span");
+          check.className = "progress-check";
+          check.setAttribute("aria-label", "Completed");
+          check.textContent = "✓";
+          label.appendChild(check);
+        }
+        row.appendChild(label);
+
+        var toggle = document.createElement("button");
+        toggle.type = "button";
+        toggle.className = "progress-reset-btn";
+        toggle.textContent = state === "complete" ? "Mark incomplete" : "Mark complete";
+        toggle.addEventListener("click", function () {
+          var isComplete = state === "complete";
+          ProgressTracker.markComplete(m.slug, !isComplete);
+          ProgressTracker.renderWidget();
+        });
+        row.appendChild(toggle);
+
+        var track = document.createElement("div");
+        track.className = "progress-track";
+        var fill = document.createElement("div");
+        fill.className = "progress-fill";
+        fill.style.width = (state === "complete" ? 100 : state === "visited" ? 35 : 0) + "%";
+        track.appendChild(fill);
+        row.appendChild(track);
+
+        container.appendChild(row);
+      });
+
+      var resetAll = document.createElement("button");
+      resetAll.type = "button";
+      resetAll.className = "progress-reset-btn";
+      resetAll.style.marginTop = "0.75em";
+      resetAll.textContent = "Reset all progress (" + completedCount + "/" + MODULES.length + " complete)";
+      resetAll.addEventListener("click", function () {
+        ProgressTracker.write({});
+        ProgressTracker.renderWidget();
+      });
+      container.appendChild(resetAll);
+
+      mount.appendChild(container);
+    }
+  };
+
+  /* ========================================================================
+     Boot
+     ======================================================================== */
+  document.addEventListener("DOMContentLoaded", function () {
+    ThemeManager.init();
+    initMermaid();
+    upgradeGfmAlerts();
+    addCopyButtons();
+    initBackToTop();
+    initBreadcrumbs();
+    initTOC();
+    externalLinksNewTab();
+    ProgressTracker.recordCurrentPage();
+    ProgressTracker.renderWidget();
+  });
+})();
diff --git a/resources/cheatsheet.md b/resources/cheatsheet.md
index f60e7fd..7cd6c78 100644
--- a/resources/cheatsheet.md
+++ b/resources/cheatsheet.md
@@ -1,3 +1,8 @@
+---
+title: "Git Cheatsheet"
+description: "A flat, one-page Git command reference — for when you already know the command and just blanked on the flag."
+---
+
 
# 📖 Git Cheatsheet — One-Page Reference diff --git a/resources/command-recall-drill.md b/resources/command-recall-drill.md index 19cc963..b059a68 100644 --- a/resources/command-recall-drill.md +++ b/resources/command-recall-drill.md @@ -1,3 +1,8 @@ +--- +title: "Command Recall Drill" +description: "Scenario-based, fold-to-reveal Git command drills across every module — spaced-repetition practice, not a reading list." +--- +
# 🧠 Command Recall Drill diff --git a/resources/interactive-cheatsheet.html b/resources/interactive-cheatsheet.html index fbea7d5..d13a930 100644 --- a/resources/interactive-cheatsheet.html +++ b/resources/interactive-cheatsheet.html @@ -6,33 +6,54 @@ Interactive Cheatsheet Matrix — GIT Tutorials + + - + -
+
-

+

Interactive Cheatsheet Matrix

-

+

Every command from the course, filterable by how much you're panicking.

- +
@@ -43,12 +64,12 @@

- 🔎 + 🔎
@@ -56,82 +77,82 @@

-

Panic Level

+

Panic Level

-

Action Type

+

Action Type

-

+

- -
-

+
+

Visual Git Tree Sandbox

-

+

A tiny, fake, entirely consequence-free Git repo. Click the buttons, watch the tree change. Nothing here can actually break — that's the whole point.

-
- +
+
-

- On main · 1 commit +

+ On main · 1 commit

-