.. _developer: 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``): .. list-table:: :header-rows: 1 :widths: 25 75 * - 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 ---------------- .. list-table:: :header-rows: 1 :widths: 40 60 * - 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.