{
  "version": 1,
  "id": "git-object-model",
  "title": "The Git Object Model",
  "summary": "Blobs, trees, commits and tags; what a branch really is; and how the index relates to the working tree.",
  "estimatedMinutes": 25,
  "prerequisites": [],
  "source": "courses-notebook ENV202_git_mastery 01_git_object_model",
  "quiz": [
    {
      "prompt": "Which four object types make up Git's content-addressable object database?",
      "options": [
        "blob, tree, commit, tag",
        "file, folder, snapshot, branch",
        "index, stash, ref, pack",
        "object, ref, pack, hook"
      ],
      "answer": 0
    },
    {
      "prompt": "Which statement most accurately describes a Git branch?",
      "options": [
        "A directory containing a copy of every tracked file",
        "A movable pointer (ref) to a single commit SHA",
        "A separate database with its own object store",
        "An immutable snapshot like a tag"
      ],
      "answer": 1
    },
    {
      "prompt": "In a default Git workflow, what does the staging area (index) represent?",
      "options": [
        "The set of changes that will be included in the next commit",
        "The set of files ignored by .gitignore",
        "The reflog of HEAD",
        "The remote tracking branches"
      ],
      "answer": 0
    },
    {
      "prompt": "What does a commit object contain (its payload)?",
      "options": [
        "A reference to a tree, parent commit reference(s), author/committer info, and a message",
        "A list of file diffs against the previous commit",
        "A copy of the working directory",
        "Only an SHA and a message"
      ],
      "answer": 0
    }
  ],
  "lesson": "## 1.1  Everything is a Hash\n\nGit stores 4 types of objects, all content-addressed by SHA-1:\n\n| Object | Contains |\n|--------|----------|\n| **blob** | File contents |\n| **tree** | Directory (list of blobs + trees) |\n| **commit** | Tree + parent + metadata |\n| **tag** | Named commit pointer |\n\n```bash\n# Inspect objects\ngit cat-file -t HEAD          # type\ngit cat-file -p HEAD          # content of commit\ngit cat-file -p HEAD^{tree}   # content of tree\n\n# See the object database\nls .git/objects/\n```\n\n## 1.2  Refs\n\nRefs are human-readable pointers to SHA-1 hashes:\n```\n.git/\n\u251c\u2500\u2500 refs/\n\u2502   \u251c\u2500\u2500 heads/main     \u2190 branch pointer\n\u2502   \u251c\u2500\u2500 heads/feature  \u2190 another branch\n\u2502   \u2514\u2500\u2500 remotes/origin/main\n\u251c\u2500\u2500 HEAD               \u2190 current branch\n\u2514\u2500\u2500 ORIG_HEAD          \u2190 pre-merge/rebase state\n```\n\n## 1.3  The Three Trees\n\n```\nWorking Directory \u2192 Index (Staging) \u2192 Repository\n      git add \u2192          git commit \u2192\n```\n\n- **Working directory**: what you see in the filesystem\n- **Index/Stage**: snapshot prepared for next commit\n- **HEAD**: last commit\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('=== Git Object Model Exploration ===')\nprint('HEAD commit:', git('rev-parse HEAD')[:16], '...')\nprint('HEAD type:  ', git('cat-file -t HEAD'))\nprint()\nprint('Last commit:')\nprint(git('log --oneline -5'))\nprint()\nprint('Object database size:')\nprint(git('count-objects -vH'))\n```\n\n## Git Object Database\n\nGit stores 4 types of objects, all content-addressed (SHA-1/SHA-256 hash):\n\n| Object | Content |\n|--------|---------|\n| **blob** | File contents |\n| **tree** | Directory listing (blob + tree refs) |\n| **commit** | Snapshot: root tree + parent(s) + message + author |\n| **tag** | Annotated tag: points to commit + message |\n\n```bash\ngit cat-file -t <hash>   # type of object\ngit cat-file -p <hash>   # print object content\ngit log --oneline        # commit hashes + messages\ngit show HEAD:src/main.py  # view file at HEAD\ngit ls-tree HEAD         # list root tree\n```\n\n```python\nimport subprocess, pathlib, tempfile, os\n\n# Show git object model on current repo\ntry:\n    # Get HEAD commit hash\n    head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], text=True).strip()\n    print(f'HEAD commit: {head[:12]}')\n\n    # Show commit object\n    commit = subprocess.check_output(['git', 'cat-file', '-p', 'HEAD'], text=True)\n    print('Commit object:')\n    for line in commit.splitlines()[:6]:\n        print(f'  {line}')\n\n    # Get tree from commit\n    tree_hash = [l for l in commit.splitlines() if l.startswith('tree')][0].split()[1]\n    print(f'\\nRoot tree: {tree_hash[:12]}')\n    tree = subprocess.check_output(['git', 'cat-file', '-p', tree_hash], text=True)\n    for line in tree.splitlines()[:5]:\n        print(f'  {line}')\nexcept subprocess.CalledProcessError:\n    print('Not in a git repo or git not available')\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- **Detached HEAD**: checkout a commit directly \u2014 you're not on a branch, new commits will be orphaned; always `git checkout -b new-branch` from detached state\n- **Shallow clones**: `git clone --depth 1` creates shallow repo \u2014 `git log`, `git blame`, `git rebase` all break; use `git fetch --unshallow` to fix\n\n## Exercises\n1. Walk the object graph: `git log --oneline | head -3` \u2192 pick a commit \u2192 `git cat-file -p <hash>` \u2192 find the tree \u2192 read a blob\n2. Manually create a commit: `git hash-object -w file.txt` \u2192 `git write-tree` \u2192 `git commit-tree`\n3. Explain in your own words why git history is immutable (hint: hash of commit includes parent hash)\n\n## Key Takeaways\n\n- **Four git object types**: blob (file contents), tree (directory), commit (snapshot + metadata), tag (named reference) \u2014 all content-addressed by SHA-1 hash.\n- **`git cat-file -t <hash>`**: reveals the type of any git object; `git cat-file -p <hash>` prints its contents \u2014 essential for understanding what git actually stores.\n- **`git reflog`**: records every HEAD movement \u2014 the safety net for recovering from accidental resets, rebases, and branch deletions for 90 days.\n- **Detached HEAD**: occurs when you checkout a commit directly (not a branch) \u2014 any new commits will be orphaned unless you immediately `git checkout -b new-branch`.\n- **Shallow clones (`--depth 1`)**: truncate history to save bandwidth, but break `git log`, `git blame`, and `git rebase` \u2014 avoid in development repos.\n\n## \ud83e\udde0 Quick Quiz\n\n**Q1.** `git rebase` vs `git merge`: rebase:\n- A) Is safer for shared branches\n- B) **Replays commits on top of another branch, creating a linear history** \u2713\n- C) Creates a merge commit\n- D) Is always faster\n\n**Q2.** `git stash` saves:\n- A) Committed changes to remote\n- B) **Uncommitted working directory changes to a temporary stack** \u2713\n- C) The entire repository state\n- D) Only staged changes\n\n**Q3.** `git bisect` helps:\n- A) Split a branch\n- B) **Find the commit that introduced a bug via binary search** \u2713\n- C) Compare two branches\n- D) Squash commits\n\n<details><summary>Answers</summary>1-B, 2-B, 3-B</details>\n"
}
