For developers

This section documents the design of the Making With Code CLI at a technical level. It is intended for software developers who want to understand, modify, or extend the codebase. Students and teachers do not need to read this.

The source code is hosted at github.com/cproctor/making-with-code-courseware.

Architecture overview

The CLI has four main concerns, each with a corresponding module or package:

  • Account management (mwc_accounts_api.py): HTTP client to the MWC accounts server. Handles authentication and provides the roster of students and their per-student git tokens.

  • Curriculum (curriculum.py): Fetches the course manifest from a curriculum site URL. The manifest is a JSON document listing every course, unit, and module, including each module’s template repo URL.

  • Git backend (git_backend/): Abstracts over the git server. Currently only MWCBackend is implemented, targeting the Gitea instance at git.makingwithcode.org.

  • Settings (settings.py): Reads and writes a TOML config file, defaulting to ~/.config/mwc/settings.toml. The path can be overridden with the MWC_CONFIG environment variable or --config flag, which supports running multiple MWC identities on one machine.

How mwc update wraps git

Each curriculum module is distributed as a git repository. When a student runs mwc update, the CLI:

  1. Calls the accounts server to get the student’s section memberships.

  2. For each section, fetches the curriculum manifest to get the list of modules.

  3. For each module, calls MWCBackend.init_module() (if the repo does not yet exist locally) or MWCBackend.update() (if it does).

init_module() uses the Gitea API to fork the template repo into the student’s account, then clones it with credentials embedded in the HTTPS URL. It also writes a local git config (user.name, user.email, core.editor, commit.template) so students are not prompted for global git identity.

update() delegates to safe_pull(), which is the main focus of this section.

safe_pull()

A bare git pull fails in surprisingly many ways for beginner students who may have left their repos in unusual states. safe_pull() in git_backend/mwc_backend.py wraps the pull in a sequence of pre-flight checks and recovery steps before attempting the pull, and classifies failures into typed exceptions afterward.

Pre-flight recovery

Before pulling, safe_pull() uses helpers from git_wrapper.py to repair common bad states:

  • In-progress merge, rebase, or cherry-pick — Detected by checking for MERGE_HEAD, rebase-merge/, rebase-apply/, or CHERRY_PICK_HEAD in the .git directory. Fixed by running the appropriate --abort command.

  • Detached HEAD — Detected when git branch --show-current returns an empty string. Fixed by stashing any local changes, checking out the default branch (discovered from refs/remotes/origin/HEAD), and popping the stash.

  • Stale index lock.git/index.lock is removed if present.

Conflict resolution policy

The pull uses git pull -X theirs, which means incoming changes (from the teacher) always win in a merge conflict. This is appropriate because teacher pushes are typically feedback annotations, test updates, or bug-fixes to starter code. Student work is preserved: if the student has uncommitted changes to files that the incoming pull would overwrite, those changes are auto-committed before the pull with the message [auto-saved before update].

The -X theirs extended option also handles the case where local and remote histories have diverged (e.g. after a force-push), since it implies a merge strategy and does not require pull.rebase to be configured.

Error classification

If the pull fails after pre-flight and conflict handling, safe_pull() parses the git stderr output and raises a typed exception from the GitError hierarchy (defined in errors.py):

Exception

Condition

GitNetworkError

Host resolution failure, connection timeout, or refused connection.

GitAuthError

Authentication failure or HTTP 403. Student should re-run mwc setup.

GitCorruptError

Corrupt objects or bad refs in the local repo.

GitError (base)

Any other non-zero exit. Git output is echoed in dim style above the friendly message.

These exceptions all inherit from MWCError, so the caller in update/__init__.py can catch them by type and display a beginner-friendly message without a Python traceback.

How mwc submit wraps git

mwc submit (in submit.py) is a wrapper around git add --all && git commit && git push. Running it from anywhere inside a module directory commits all changes and pushes to the MWC git server.

Pre-flight

The same git_wrapper helpers used by safe_pull() run before any git operations: in-progress operations are aborted, detached HEAD is recovered, and the index lock is cleared.

Unpushed commits (S1)

repo_has_changes() checks git status --porcelain, which only reflects the working tree and index. If a previous mwc submit run committed successfully but the push failed (for example, due to a network dropout), the working tree appears clean while commits are stranded locally. has_unpushed_commits() catches this by running git log @{u}..HEAD. When it detects stranded commits, the submit skips straight to the push step.

Empty commit message (S2)

git commit opens the configured editor. If the student closes it without writing anything, git exits non-zero with "Aborting commit due to empty commit message" in its output. The submit detects this, unstages all files with git restore --staged ., and prompts the student to try again.

Push rejection (S3)

If git push is rejected as non-fast-forward, it means the teacher pushed to the student’s repo (typically feedback or corrected starter files) since the student last pulled. The submit auto-recovers by running git pull -X ours (student’s committed code wins in any conflict, since this is a submit operation) and retrying the push.

Network and authentication errors (S4–S5)

If push fails due to a network error, the student is told explicitly that their commit is safe locally and that re-running mwc submit will send it when connectivity is restored. Authentication failures direct the student to re-run mwc setup.

Error handling design

Two design constraints shape the error handling throughout:

  1. Student work is never silently lost. Any operation that modifies the working tree (auto-commit before pull, stash during detached-HEAD recovery) either commits the work into git history or restores it afterward.

  2. Friendly messages, raw output as secondary. When a git operation fails, the raw git stderr is echoed in dim style (using the debug formatter from styles.py) before the friendly message, so a teacher who is helping a student can still see what git actually said.

Key source files

File

Purpose

git_wrapper.py

Stateless helpers for inspecting and repairing repo state. Used by both submit.py and MWCBackend.

git_backend/mwc_backend.py

MWCBackend: clones, configures, and updates student repos. Contains safe_pull() and _raise_pull_error().

git_backend/base_backend.py

Abstract base class. work_dir() and relative_path() helpers used for display.

submit.py

submit Click command and _push() / _preflight_repo() helpers.

errors.py

MWCError hierarchy, including GitError and its subclasses.

update/__init__.py

update Click command. Iterates over modules, calls init_module() or update(), handles GitError with a clean message.

teach/gitea_api/api.py

Low-level Gitea API client used by teacher-side commands (teach update, teach patch). Parallel to MWCBackend but uses per-student tokens from the roster.