{
  "version": 1,
  "id": "git-branching-strategies",
  "title": "Branching Strategies",
  "summary": "GitHub Flow, Git Flow and trunk-based development, plus merge, fast-forward and fetch versus pull.",
  "estimatedMinutes": 25,
  "prerequisites": [],
  "source": "courses-notebook ENV202_git_mastery 02_branching_strategies",
  "quiz": [
    {
      "prompt": "What does `git fetch` do that `git pull` does not?",
      "options": [
        "`git fetch` rewrites local history",
        "`git fetch` downloads remote refs and objects WITHOUT merging or rebasing them into the current branch",
        "`git fetch` requires SSH whereas `git pull` works over HTTPS only",
        "`git fetch` runs hooks; `git pull` skips them"
      ],
      "answer": 1
    },
    {
      "prompt": "Which scenario is a typical fast-forward merge?",
      "options": [
        "The target branch has no commits that the source branch does not also have, so the branch pointer is simply moved forward",
        "Both branches have divergent commits since a common ancestor",
        "The user runs `git merge --no-ff` explicitly",
        "There are merge conflicts but they are auto-resolved"
      ],
      "answer": 0
    },
    {
      "prompt": "Which describes trunk-based development most accurately?",
      "options": [
        "All work happens on long-lived feature branches that are merged once per release",
        "Developers integrate into a single shared trunk frequently via short-lived branches, often using feature flags",
        "Every developer maintains a personal fork that is never merged",
        "Each environment has its own trunk and changes flow between them"
      ],
      "answer": 1
    }
  ],
  "lesson": "## 2.1  Core Commands\n\n```bash\ngit branch                    # list local branches\ngit branch -a                 # list all (including remote)\ngit branch feature/new-thing  # create branch\ngit checkout feature/new-thing # switch\ngit switch -c feature/new      # create + switch (modern)\ngit branch -d branch-name      # delete merged\ngit branch -D branch-name      # force delete\n```\n\n## 2.2  Branching Models\n\n### GitHub Flow (recommended for most projects)\n```\nmain  \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n           \u2191 PR merge\nfeature  \u2500\u2500\u2518\n```\n1. Branch from `main`\n2. Make commits\n3. Open PR\n4. Merge (squash merge preferred)\n\n### Git Flow (complex projects)\n```\nmain    \u2500\u2500\u25cf\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25cf\u2500\u2500\n           \u2191 release                      \u2191\ndevelop \u2500\u2500\u25cf\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25cf\u2500\u2500\n           \u2191                     \u2191\nfeature \u2500\u2500\u2500\u2518        hotfix \u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## 2.3  Useful Branch Commands\n\n```bash\n# See merged branches (safe to delete)\ngit branch --merged main | grep -v main\n\n# Delete all merged feature branches\ngit branch --merged main | grep feature/ | xargs git branch -d\n\n# See which branch contains a commit\ngit branch --contains <sha>\n\n# Stash work-in-progress\ngit stash push -m \"WIP: feature X\"\ngit stash list\ngit stash pop\n```\n\n```python\nimport subprocess\n\ndef git(cmd):\n    r = subprocess.run(f'git {cmd}', shell=True, capture_output=True, text=True)\n    return r.stdout.strip()\n\nprint('=== Branch Status ===')\nprint(git('branch -v'))\nprint()\nprint('=== Recent Commits ===')\nprint(git('log --oneline --graph --decorate -10'))\n```\n\n## Branching Strategies\n\n| Strategy | Main branches | When to use |\n|----------|--------------|-------------|\n| **GitHub Flow** | main + feature branches | Continuous deployment |\n| **Git Flow** | main + develop + release + hotfix | Scheduled releases |\n| **Trunk-based** | main only + short-lived feature flags | Large teams, CI/CD |\n\n```bash\n# GitHub Flow (most common)\ngit switch -c feature/user-auth  # create feature branch\ngit push -u origin feature/user-auth\n# ... work, commit ...\ngh pr create                     # open PR\n# ... review, merge to main ...\ngit switch main && git pull\ngit branch -d feature/user-auth  # clean up\n```\n\n```python\nbranching_comparison = '''\nGitHub Flow (recommended for most teams):\n  + Simple: one protected branch (main)\n  + Fast: feature \u2192 PR \u2192 merge to main \u2192 deploy\n  - Requires feature flags for in-progress work\n  - Hard if supporting multiple versions\n\nGit Flow (complex projects with releases):\n  + Clear release management\n  + Separate develop buffer\n  - Complex merge hell\n  - Slow: features \u2192 develop \u2192 release \u2192 main\n\nTrunk-Based (Google, Facebook scale):\n  + Fastest integration\n  + No merge conflicts (integrate many times per day)\n  - Requires sophisticated feature flag infrastructure\n  - Needs excellent test coverage\n\nChoose: GitHub Flow for most projects.\n         Git Flow only if you ship installable software with versioned releases.\n         Trunk-based for large teams with mature CI/CD.\n'''\nprint(branching_comparison)\n```\n\n```python\n# Verification helpers \u2014 exercise checker\n\ndef assert_equal(got, want, label=\"\"):\n    status = \"\u2713\" if got == want else f\"\u2717 got={got!r} want={want!r}\"\n    print(f\"  {label or 'test'}: {status}\")\n\ndef assert_approx(got, want, tol=1e-6, label=\"\"):\n    diff = abs(got - want)\n    status = \"\u2713\" if diff < tol else f\"\u2717 diff={diff:.2e}\"\n    print(f\"  {label or 'test'}: {status}\")\n\ndef run_tests(fn, cases):\n    \"\"\"cases: list of (args_tuple, expected)\"\"\"\n    passed = sum(1 for args, want in cases\n                 if (result := fn(*args) if isinstance(args, tuple) else fn(args)) == want\n                 or (print(f\"  FAIL {fn.__name__}{args}: got {result!r}, want {want!r}\"), False))\n    print(f\"{fn.__name__}: {passed}/{len(cases)} passed {'\u2713' if passed==len(cases) else '\u2717'}\")\n\n# \u2500\u2500\u2500 Example: test your implementations below \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef flatten(nested):\n    \"\"\"Flatten arbitrarily nested lists.\"\"\"\n    result = []\n    for item in nested:\n        if isinstance(item, list):\n            result.extend(flatten(item))\n        else:\n            result.append(item)\n    return result\n\nrun_tests(flatten, [\n    (([1,[2,[3,[4]]],5],),  [1,2,3,4,5]),\n    (([],),                  []),\n    (([[1],[2],[3]],),        [1,2,3]),\n])\n\ndef chunk(lst, size):\n    \"\"\"Split list into chunks of given size.\"\"\"\n    return [lst[i:i+size] for i in range(0, len(lst), size)]\n\nrun_tests(chunk, [\n    (([1,2,3,4,5], 2),  [[1,2],[3,4],[5]]),\n    (([1,2,3], 3),       [[1,2,3]]),\n    (([], 2),            []),\n])\n```\n\n```python\n# Advanced: Git object model \u2014 blob, tree, commit simulation\nimport hashlib, json\nfrom dataclasses import dataclass, field\nfrom typing import Optional\n\ndef sha1(content: str) -> str:\n    return hashlib.sha1(content.encode()).hexdigest()[:8]\n\n@dataclass\nclass GitObject:\n    kind:    str   # blob | tree | commit\n    content: str\n    hash:    str = field(init=False)\n    def __post_init__(self):\n        self.hash = sha1(f\"{self.kind}:{self.content}\")\n\nclass GitRepo:\n    def __init__(self):\n        self.objects: dict[str, GitObject] = {}\n        self.refs:    dict[str, str] = {\"HEAD\": None}\n        self.index:   dict[str, str] = {}   # filename \u2192 blob hash\n\n    def hash_object(self, kind: str, content: str) -> str:\n        obj = GitObject(kind=kind, content=content)\n        self.objects[obj.hash] = obj\n        return obj.hash\n\n    def add(self, filename: str, content: str) -> str:\n        blob_hash = self.hash_object(\"blob\", content)\n        self.index[filename] = blob_hash\n        print(f\"  staged {filename} \u2192 blob {blob_hash}\")\n        return blob_hash\n\n    def commit(self, message: str, author: str = \"dev\") -> str:\n        tree_content = json.dumps(dict(sorted(self.index.items())))\n        tree_hash    = self.hash_object(\"tree\", tree_content)\n        parent       = self.refs.get(\"HEAD\")\n        commit_data  = json.dumps({\"tree\": tree_hash, \"parent\": parent,\n                                   \"author\": author, \"message\": message})\n        commit_hash  = self.hash_object(\"commit\", commit_data)\n        self.refs[\"HEAD\"] = commit_hash\n        print(f\"  commit {commit_hash}  '{message}'  tree={tree_hash}\")\n        return commit_hash\n\n    def log(self):\n        cur = self.refs.get(\"HEAD\")\n        while cur:\n            obj = self.objects.get(cur)\n            if not obj: break\n            data = json.loads(obj.content)\n            print(f'  commit {cur}  \"{data['message']}\"  tree={data['tree']}')\n            cur  = data.get(\"parent\")\n\nrepo = GitRepo()\nprint(\"=== git add ===\")\nrepo.add(\"README.md\",  \"# My Project\")\nrepo.add(\"main.py\",    \"print('hello')\")\nprint(\"=== git commit ===\")\nrepo.commit(\"Initial commit\")\n\nrepo.add(\"main.py\", \"print('hello world')\")   # modify file\nrepo.commit(\"Update main.py\")\n\nprint(\"=== git log ===\")\nrepo.log()\nprint(f\"=== Objects stored: {len(repo.objects)} ===\")\nfor h, obj in list(repo.objects.items())[:6]:\n    print(f\"  {h}  {obj.kind}\")\n```\n\n## Git Quick Reference\n\n```bash\n# Daily workflow\ngit status -s                     # compact status\ngit diff --staged                 # review before commit\ngit commit -m \"type: description\" # conventional commit\ngit push -u origin feature/name   # first push\n\n# Branching\ngit switch -c feature/name        # create + switch\ngit switch main && git pull        # update main\ngit rebase main                    # rebase feature onto main\ngit merge --no-ff feature/name     # merge with history\n\n# Undoing things (safe)\ngit restore <file>                 # discard working tree changes\ngit restore --staged <file>        # unstage\ngit revert HEAD                    # new commit that undoes last\n\n# History inspection\ngit log --oneline --graph --all    # visual branch graph\ngit log -p --follow -- <file>      # file history with diffs\ngit blame -L 10,20 <file>          # who changed lines 10-20\ngit bisect start                   # binary search for bug commit\n\n# Stash\ngit stash push -m \"WIP: feature\"\ngit stash list\ngit stash pop                      # restore latest\n\n# Remote\ngit remote -v\ngit fetch --prune                  # remove stale remote branches\ngit pull --rebase origin main      # rebase instead of merge pull\n```\n\n**Commit types (Conventional Commits):**\n`feat` \u00b7 `fix` \u00b7 `refactor` \u00b7 `docs` \u00b7 `test` \u00b7 `chore` \u00b7 `perf` \u00b7 `ci`\n\n## Failure Modes\n\n- **Long-lived feature branches**: diverge from main, merge conflicts accumulate \u2014 rebase weekly or use trunk-based dev\n- **Merging main into feature repeatedly**: creates noisy merge commits \u2014 use `git rebase origin/main` for clean linear history\n\n## Exercises\n1. Practice the GitHub Flow: create feature branch, make 3 commits, squash to 1, open PR, merge\n2. Set up branch protection rules: require PR reviews, passing CI, no force push to main\n3. Resolve a merge conflict deliberately: create conflicting changes on 2 branches, merge and resolve\n\n## Key Takeaways\n\n- **GitHub Flow**: `main` is always deployable; features live in short-lived branches merged via PRs \u2014 the standard for continuous deployment teams.\n- **`git switch -c feature/name`**: the modern command to create and switch to a branch simultaneously \u2014 prefer over the older `git checkout -b`.\n- **Trunk-based development (TBD)**: all developers commit to `main` at least daily using feature flags for incomplete work \u2014 reduces merge conflicts and speeds up CI.\n- **`git rebase origin/main`**: replays your feature branch commits on top of the latest main \u2014 keeps history linear and avoids noisy merge commits on long-lived branches.\n- **Branch protection**: enforce PR reviews and passing CI before merging to `main` via GitHub repo settings \u2014 prevents broken code from reaching the main branch.\n\n## \ud83e\udde0 Quick Quiz\n\n**Q1.** Trunk-based development (TBD) recommends:\n- A) Long-lived feature branches\n- B) **All developers commit to main/trunk frequently (at least daily)** \u2713\n- C) One release branch per version\n- D) GitFlow with develop and release branches\n\n**Q2.** Feature flags in trunk-based development allow:\n- A) Skipping code review\n- B) **Merging incomplete features without exposing them to users** \u2713\n- C) Merging without CI\n- D) Automatic versioning\n\n**Q3.** `git cherry-pick <commit>` applies:\n- A) All commits from a branch\n- B) The oldest commit in a branch\n- C) **A specific commit from another branch to the current branch** \u2713\n- D) The latest changes without committing\n\n<details><summary>Answers</summary>1-B, 2-B, 3-C</details>\n"
}
