feat(root03): author Git & Collaborative Development
Adds all 6 ROOT 03 modules (why version control exists, the Git mental model, everyday Git, branching & merging, remote repositories, engineering Git workflow) — 31 activities. Uses only real LOCAL Git repositories throughout — no Gitea account, no network access, and no RootNode-canonical-repo credential is ever needed or exposed in a student Lab (docs/ROOT_FOUNDATIONS_LAB_STRATEGY.md §4). Modules 1, 2, and 6 are conceptual bookends with no lab, matching the flexible readiness standard already used in ROOT 01/02. Adds 3 new hands-on Git scenarios plus 2 MCQs: git-everyday-basics (init/add/commit, validated by inspecting real `git log`/`git show` state, not just command output strings — Step 6's explicit requirement), git-branching-and-merge (a REAL merge conflict caused on purpose by two branches editing the same line, resolved by the student, validated by confirming the conflict markers are gone and a real two-parent merge commit exists), and git-remote-clone-push (a real local bare repository standing in for a remote — clone/commit/push validated by inspecting the REMOTE side directly, not just the student's own clone). All tested end-to-end via the real scenario-validation API, including negative tests. Wires rootnode-git-fundamentals-assessment (2 required + 3 random-draw scenarios) using the existing Phase 3B assessment engine. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"id": "git-branching-and-merge",
|
||||
"title": "Resolve a Real Merge Conflict",
|
||||
"category": "Git",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Medium",
|
||||
"type": "task",
|
||||
"weight": 6,
|
||||
"description": "## Resolve a Real Merge Conflict\n\nA repository at `/srv/git-lab-branch/` already has two branches (`main` and `feature`) that both changed the SAME line of `shared.txt` differently — merging them WILL conflict.\n\n**Your task:**\n\n1. Make sure you're on the `main` branch\n2. Merge `feature` into it: `git merge feature`\n3. Git will report a conflict — open `shared.txt` and you'll see conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`)\n4. Edit the file to remove the markers and decide on final content (either version, both, or something new — your choice)\n5. Stage the resolved file and commit to complete the merge",
|
||||
"hints": [
|
||||
{
|
||||
"title": "What the conflict markers mean",
|
||||
"body": "Everything between `<<<<<<< HEAD` and `=======` is YOUR current branch's version; everything between `=======` and `>>>>>>> feature` is the incoming branch's version. Delete all three marker lines and keep whichever content (or combination) you actually want.",
|
||||
"command": "cat /srv/git-lab-branch/shared.txt"
|
||||
},
|
||||
{
|
||||
"title": "Completing the merge",
|
||||
"body": "Once the file no longer has conflict markers, staging and committing tells Git the conflict is resolved.",
|
||||
"command": "cd /srv/git-lab-branch\ngit add shared.txt\ngit commit -m 'Merge feature, resolve conflict'"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "git config --global init.defaultBranch main" },
|
||||
{ "command": "git config --global user.email [email protected]" },
|
||||
{ "command": "git config --global user.name 'RootNode Student'" },
|
||||
{ "command": "rm -rf /srv/git-lab-branch && mkdir -p /srv/git-lab-branch" },
|
||||
{ "command": "cd /srv/git-lab-branch && git init -q && echo 'original line' > shared.txt && git add shared.txt && git commit -q -m 'Initial commit'" },
|
||||
{ "command": "cd /srv/git-lab-branch && git branch feature && git checkout -q feature && echo 'feature version' > shared.txt && git commit -qam 'Update from feature branch'" },
|
||||
{ "command": "cd /srv/git-lab-branch && git checkout -q main && echo 'main version' > shared.txt && git commit -qam 'Update from main branch'" },
|
||||
{ "command": "cd /srv/git-lab-branch && git merge feature 2>/dev/null; true" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "the feature branch still exists", "command": "git -C /srv/git-lab-branch branch --list feature | tr -d ' *'", "expected_output": "feature", "match": "exact" },
|
||||
{ "description": "a real merge commit (two parents) exists", "command": "git -C /srv/git-lab-branch log --merges --oneline 2>/dev/null | wc -l | tr -d ' '", "expected_output": "^[1-9][0-9]*$", "match": "regex" },
|
||||
{ "description": "the conflict markers were actually removed", "command": "grep -c '<<<<<<<' /srv/git-lab-branch/shared.txt", "expected_output": "0", "match": "exact" },
|
||||
{ "description": "the working tree is clean (nothing left uncommitted)", "command": "git -C /srv/git-lab-branch status --short | wc -l | tr -d ' '", "expected_output": "0", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/git-lab-branch" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "git-diverged-branches-mcq",
|
||||
"title": "Your Branch and the Remote Have Diverged",
|
||||
"category": "Git",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Medium",
|
||||
"type": "mcq",
|
||||
"weight": 4,
|
||||
"description": "## Your Branch and the Remote Have Diverged\n\n`git status` reports \"Your branch and 'origin/main' have diverged, and have 2 and 3 different commits each, respectively.\" What should you understand BEFORE blindly running `git pull`?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Nothing — git pull always resolves this automatically and safely with no side effects" },
|
||||
{ "id": "b", "text": "That a pull here will attempt to merge (or rebase, depending on config) the remote's 3 commits with your own 2 local commits, which can itself produce a conflict you'll need to resolve — the same way two diverged branches did in this course's merge exercise" },
|
||||
{ "id": "c", "text": "That you must delete your local commits first" },
|
||||
{ "id": "d", "text": "That the remote repository is corrupted" }
|
||||
],
|
||||
"correct_option": "b",
|
||||
"explanation": "\"Diverged\" means both sides have commits the other doesn't have — a plain `git pull` is really fetch-then-merge (or rebase), and merging two sets of independent commits can conflict exactly like the local branch-merge exercise did, just with a remote branch instead of a local one. Understanding this before pulling means you won't be surprised by a conflict, and you definitely don't need to delete anything or assume corruption.",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "git-everyday-basics",
|
||||
"title": "Init, Track, Commit — Your First Real Repository",
|
||||
"category": "Git",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "task",
|
||||
"weight": 4,
|
||||
"description": "## Init, Track, Commit — Your First Real Repository\n\n**Your task**, inside `/srv/git-lab-everyday/`:\n\n1. Initialize a new Git repository\n2. Create a file named `notes.txt` containing any text you like\n3. Stage and commit it, with a commit message that includes the word `notes`\n4. Confirm your work with `git log` and `git status`",
|
||||
"hints": [
|
||||
{
|
||||
"title": "The full sequence",
|
||||
"body": "`git init` creates the repository. `git add` stages a file (moves it from working tree to staging area). `git commit` records the staged snapshot permanently.",
|
||||
"command": "cd /srv/git-lab-everyday\ngit init\necho 'my first notes' > notes.txt\ngit add notes.txt\ngit commit -m 'Add notes file'"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "mkdir -p /srv/git-lab-everyday" },
|
||||
{ "command": "git config --global user.email [email protected]" },
|
||||
{ "command": "git config --global user.name 'RootNode Student'" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "a real Git repository was initialized", "command": "git -C /srv/git-lab-everyday rev-parse --is-inside-work-tree 2>/dev/null", "expected_output": "true", "match": "exact" },
|
||||
{ "description": "at least one commit exists", "command": "git -C /srv/git-lab-everyday log --oneline 2>/dev/null | wc -l | tr -d ' '", "expected_output": "^[1-9][0-9]*$", "match": "regex" },
|
||||
{ "description": "notes.txt is tracked in the latest commit", "command": "git -C /srv/git-lab-everyday show --name-only --format= HEAD 2>/dev/null", "expected_output": "notes.txt", "match": "exact" },
|
||||
{ "description": "the commit message mentions notes", "command": "git -C /srv/git-lab-everyday log -1 --pretty=%s 2>/dev/null", "expected_output": "notes", "match": "contains" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/git-lab-everyday" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "git-remote-clone-push",
|
||||
"title": "Clone, Commit, and Push to a Real Remote",
|
||||
"category": "Git",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Medium",
|
||||
"type": "task",
|
||||
"weight": 5,
|
||||
"description": "## Clone, Commit, and Push to a Real Remote\n\nA bare Git repository already exists at `/srv/git-lab-remote.git` — this plays the role of a real remote (like Gitea or GitHub) without needing any account or network access. It is currently empty.\n\n**Your task:**\n\n1. Clone it to `/srv/git-lab-clone`\n2. Inside the clone, create a file `README.md` with any content\n3. Add and commit it\n4. Push your commit back to the remote (`origin`)",
|
||||
"hints": [
|
||||
{
|
||||
"title": "The full sequence",
|
||||
"body": "`git clone` copies a remote repository (bare or not) into a new working directory, automatically configuring it as `origin`. `git push origin main` sends your local commits back to it.",
|
||||
"command": "git clone /srv/git-lab-remote.git /srv/git-lab-clone\ncd /srv/git-lab-clone\necho '# My Project' > README.md\ngit add README.md\ngit commit -m 'Add README'\ngit push origin main"
|
||||
}
|
||||
],
|
||||
"setup_commands": [
|
||||
{ "command": "git config --global init.defaultBranch main" },
|
||||
{ "command": "git config --global user.email [email protected]" },
|
||||
{ "command": "git config --global user.name 'RootNode Student'" },
|
||||
{ "command": "rm -rf /srv/git-lab-remote.git /srv/git-lab-clone" },
|
||||
{ "command": "git init -q --bare /srv/git-lab-remote.git" }
|
||||
],
|
||||
"validation": {
|
||||
"commands": [
|
||||
{ "description": "the clone has origin configured pointing at the remote", "command": "git -C /srv/git-lab-clone remote get-url origin 2>/dev/null", "expected_output": "/srv/git-lab-remote.git", "match": "exact" },
|
||||
{ "description": "the commit actually reached the remote (not just the local clone)", "command": "git --git-dir=/srv/git-lab-remote.git log --oneline 2>/dev/null | wc -l | tr -d ' '", "expected_output": "^[1-9][0-9]*$", "match": "regex" },
|
||||
{ "description": "README.md is part of what was pushed", "command": "git --git-dir=/srv/git-lab-remote.git show --name-only --format= HEAD 2>/dev/null", "expected_output": "README.md", "match": "exact" }
|
||||
]
|
||||
},
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": [
|
||||
{ "command": "rm -rf /srv/git-lab-remote.git /srv/git-lab-clone" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"id": "git-staging-area-mcq",
|
||||
"title": "A Changed File That Won't Commit",
|
||||
"category": "Git",
|
||||
"isolation_type": "shared_namespace",
|
||||
"difficulty": "Easy",
|
||||
"type": "mcq",
|
||||
"weight": 3,
|
||||
"description": "## A Changed File That Won't Commit\n\nYou edit `config.py`, then run `git commit -m \"update config\"`. The commit succeeds, but when you check `git show`, your changes to `config.py` aren't in it. What Git state should you inspect first?",
|
||||
"options": [
|
||||
{ "id": "a", "text": "Whether config.py is even tracked by Git at all" },
|
||||
{ "id": "b", "text": "The staging area — run `git status` to check whether your changes were actually staged with `git add` before you committed" },
|
||||
{ "id": "c", "text": "Whether your internet connection is working" },
|
||||
{ "id": "d", "text": "The remote repository's branch protection settings" }
|
||||
],
|
||||
"correct_option": "b",
|
||||
"explanation": "`git commit` only records what's currently in the STAGING AREA, not every change in your working tree. A modified-but-unstaged file is invisible to the next commit — `git status` shows exactly this distinction (\"Changes not staged for commit\" vs. \"Changes to be committed\"), which is why checking the staging area is the right first step, not assuming the file isn't tracked or a network issue (Git is entirely local for this operation).",
|
||||
"hints": [],
|
||||
"setup_commands": [],
|
||||
"default_namespace": "default",
|
||||
"teardown_commands": []
|
||||
}
|
||||
Reference in New Issue
Block a user