Building interactive Git exercises

An author’s guide to the git-sandbox extension: what each panel means, and how to write your own exercises

What this extension does

A ```git-sandbox block in your Quarto document becomes a real Git repository running in the reader’s browser. Learners type real git commands; the box shows them what happened. You author everything in Markdown and YAML, with no JavaScript.

Nothing is installed and nothing leaves the page. The repository lives in memory and disappears on reload, so learners can experiment fearlessly.

This page is written for authors. Each section shows a working box, explains what the reader sees, and includes the exact block that produced it, ready to copy into your own lesson. For the full option and condition reference, see the README.

Reading a git box

Before writing exercises, know what your learners are looking at.

A git box is a stack. At the top, a terminal: the prompt shows the folder and, once a repository exists, the current branch. Learners type commands and press Enter; the Try: buttons under the input fill in a command so nobody is stuck guessing syntax; the up arrow recalls history, and help lists everything the sandbox supports.

Directly under the terminal sits Your turn, the task checklist you author. Tasks tick themselves when their condition becomes true (a just-completed task flashes green so it registers while eyes are on the terminal). Undo takes back the last state-changing command: repo, checklist and terminal rewind together, so experimenting is cheap. Reset returns the box to its starting state.

Below the checklist, a tabbed panel re-renders after every command. One tab shows at a time; the others fold behind a live badge, where a count means “something here” and a green check means clean or in sync. The active tab follows the last command (git add shows Files, git commit shows History), and clicking a tab holds it until the next command with an opinion.

  • Files: the working directory / staging area / repository diagram. Files appear as chips and move from one area to the next as they are edited, staged, and committed. This is the mental model that makes Git click.
  • Changes: a live diff since the last commit. Edited files show green + and red - lines; new files are all additions, deleted files all removals. It answers “what would I be committing right now?”
  • History: the graph. Branches get lanes, HEAD and branch pills mark the tips, merge commits are hollow circles.
  • Remote: appears only once a remote exists, with the origin’s own graph and an in-sync/ahead/behind summary.

Try it: this box is seeded with two commits so every panel has something to show:

Loading the Git sandbox…

Here is the complete block that produced the box above:

```git-sandbox
id: anatomy
title: A tour of the git box
prompt: "~/demo-project $"
intro: |
  This repository already has two commits. Run a few commands and watch
  the panels below; every one of them updates after each command.
  Type {y}help{/} to see what the sandbox supports.
hints: [git status, git log --oneline, 'echo "A new line." >> README.md', git diff, git add ., 'git commit -m "Extend the README"']
done-note: That is the whole feedback loop. Every command updates every panel. Your lessons ride on top of it.
seed: |
  git init
  echo "# Demo project" > README.md
  git add .
  git commit -m "Add the README"
  echo "library(tidyverse)" > analysis.R
  git add .
  git commit -m "Start the analysis"
tasks:
  - text: Ask where things stand with `git status`
    when: ran /^\s*git\s+status/
  - text: Change a file and find it in the Changes panel
    when: not clean
  - text: Stage it and watch it move to the staging area in the diagram
    when: staged
  - text: Commit it and watch the history graph grow
    when: commits >= 3
```

The smallest useful block

You need very little. title labels the box, intro prints in the terminal, hints gives learners clickable commands, and tasks is the checklist. With no seed, the box starts as an empty, untracked folder:

```git-sandbox
id: minimal
title: A minimal exercise
prompt: "~/scratch $"
intro: |
  This box was authored in a few lines of YAML. It starts as an
  empty folder; nothing is tracked yet.
hints: [git init, 'echo "hello" > notes.txt', git add notes.txt, 'git commit -m "First note"']
tasks:
  - text: Turn the folder into a repository
    when: repo
  - text: Commit a file
    when: commits >= 1
```
Loading the Git sandbox…

Three authoring notes that save time:

  • intro is terminal output, not Markdown. Use {y}…{/} for yellow, {g} green, {r} red, {b} blue, {w} bold. Backticks appear literally.
  • Quoting in hints. In a flow sequence, wrap any entry containing a double quote in single quotes: 'git commit -m "First note"'.
  • prompt is cosmetic but load-bearing. The folder name it shows is also used in git init output and pwd, so pick something that fits your lesson’s story. The current branch is appended automatically.

Checking the learner’s work

Each task has a when: condition written in a small declarative language, the same language for everything, no JavaScript. A task ticks when its condition first becomes true and stays ticked, so you can check for states the learner merely passes through.

This box wears its conditions on its sleeve: every task shows the when: that drives it.

Loading the Git sandbox…

The conditions, by example

Every condition is a short phrase about repository state. The full table is in the README; these annotated examples cover the whole vocabulary:

# Repository basics
when: repo                     # git init has been run
when: staged                   # something is in the staging area
when: clean                    # nothing uncommitted or untracked

# Counting commits
when: commits >= 3             # at least three commits exist
when: commits on docs >= 2     # at least two reachable from `docs`

# Branches and merging
when: branch docs              # a branch named `docs` exists
when: on main                  # HEAD is currently on `main`
when: merged docs into main    # a real merge brought `docs` into `main`
when: merge commit             # some commit has two parents

# Files and their contents
when: file SETUP.md                    # the file exists
when: file SETUP.md contains "Setup"   # ...and contains that text
when: file notes.txt contains /TODO:/  # ...or matches a regex

# Commands the learner typed (use sparingly; see below)
when: ran "git status"                 # plain text matches as a substring
when: ran /git\s+log\s+--oneline/      # regex for exact shapes

# Remotes
when: remote                   # a remote has been configured
when: pushed                   # every local commit is on the remote

Conditions compose with and, or, not and parentheses (and binds tighter than or), which is where they earn their keep. Some realistic tasks:

tasks:
  # "Commit on the feature branch, not on main"
  - text: Commit your experiment on the `spike` branch
    when: on spike and commits on spike >= 2

  # "Merge it back, however they get there" - fast-forward or merge commit
  - text: Bring `spike` into `main`
    when: on main and merged spike into main

  # "Clean up after the merge" - the flag itself is the lesson here
  - text: Delete the merged branch with `git branch -d spike`
    when: not branch spike and ran /git\s+branch\s+-[dD]/

  # "Ship it" - state on both sides of the push
  - text: Publish your work
    when: pushed and clean

Two habits worth adopting:

  • Prefer state over ran. commits >= 1 is true however the learner got there; ran "git commit" breaks the moment someone uses a different flag. Use ran only when running a specific command is the point.
  • Trust the error reporting. A condition that cannot be parsed shows the problem next to the task, and a bad option produces a visible error box. You never need the browser console to debug an exercise.

Flow: branching and merging

seed runs commands before the learner arrives, so you can start an exercise in the middle of a story. This one seeds a forecast branch that has diverged from main: the learner commits on main, merges, and sees a genuine merge commit appear as a hollow circle joining two lanes.

Loading the Git sandbox…

The seed for the box above builds the whole situation in plain commands:

seed: |
  git init
  echo "# Sales analysis" > README.md
  git add .
  git commit -m "Add the project README"
  git checkout -b forecast
  echo "arima(sales)" > forecast.R
  git add .
  git commit -m "Add a first forecast model"
  echo "tune the model" >> forecast.R
  git add .
  git commit -m "Tune the forecast parameters"
  git checkout main

Seed failures are loud: if a seed command fails, the box says so instead of dropping the learner into a half-built repository. And Reset re-runs the seed, so learners always return to the exact state you designed.

Flow: a remote, push and pull

git remote add origin <url> in a seed creates a mock remote, a second repository inside the page, and reveals the Remote tab, drawn as its own commit graph with an in-sync/ahead/behind summary. Push, fetch, and pull behave like the real thing, including rejected non-fast-forward pushes.

One authoring-only command makes the classic scenario possible: rewind <n> moves the local branch back after a push, so the pushed commits exist only on the remote, exactly as if a colleague pushed while the learner was away.

Loading the Git sandbox…

The seed pattern, annotated:

seed: |
  git init
  git remote add origin https://github.com/acme/sales-analysis
  echo "# Sales analysis" > README.md
  git add .
  git commit -m "Add the project README"
  git push -u origin main                  # first push sets the upstream
  echo "arima(sales)" > forecast.R
  git add .
  git commit -m "Add a first forecast model"
  git push                                 # the "colleague's" commit
  rewind 1                                 # local forgets it; the remote keeps it

The remote and pushed conditions pair with this: remote is true once a remote is configured, and pushed once every commit on the current branch is on the remote.

Where to go from here

  • The README has the complete option table, the full when: condition reference, the js: escape hatch for conditions the language cannot express, and notes on CSP and page weight for LMS embedding.
  • example-embed.html in the repository shows the sandbox running in a plain HTML page with no Quarto at all, useful for platforms that accept an HTML block or iframe.
  • Install with quarto add ryjohnson09/quarto-git-sandbox, add git-sandbox to your document’s filters, and start with the minimal block above.