feat(pm): make mcpp.lock authoritative for git deps (2026.8.1.2) - #330
Conversation
- Add LockedGitSource + parse_git_source to lock_io.cppm. - Load mcpp.lock in prepare_build and use recorded branch commit to skip git ls-remote when the local cache still matches. - For tag/rev, reuse cached clone and fail early in --offline when missing. - Update e2e test 24 to assert branch deps do not re-ls-remote on rebuild. - Add unit tests for git source parsing. Closes mcpp-community#329
Code reviewThe main path is correct: Found 4 issues:
mcpp/tests/e2e/24_git_dependency.sh Lines 155 to 160 in c3e7095
Lines 3027 to 3035 in c3e7095 Lines 3109 to 3117 in c3e7095
Lines 3018 to 3027 in c3e7095
Lines 3061 to 3074 in c3e7095 Two smaller notes, same PR:
Behaviour note, not a defect: |
- tests: assert 'from lock' on rebuild instead of vacuous 'ls-remote' - prepare: dedupe cache-key formula via computeGitRoot(resolvedGitRev) - prepare: add anchor.url == spec.git to offline anchor match - prepare: drop 'mcpp update' from offline diagnostics (offline no-op) - commands: refresh stale comment in cmd_update about lockfile semantics - lock_io: split fragment on last '@' (branch names with '@') and reset empty resolvedCommit so the offline branch prints the right hint
…heck A bare second `mcpp build` takes the try_fast_build path when build.ninja is fresh (no source change), so prepare_build is skipped and neither the lock-anchor code nor `git ls-remote` runs — leaving the `from lock` assertion vacuous (it cannot match; same failure mode as the original ls-remote assertion the reviewer flagged). Run `mcpp clean` first so the fast-path cannot fire; mcpp.lock and the git cache (in MCPP_HOME/git) both survive `mcpp clean`, so the next build re-prepares, hits the anchor, prints 'from lock' and skips both ls-remote and the clone.
The lock was read for the first time in the previous commits of this PR, but only as a hint the local clone had to confirm: a recorded commit counted while `~/.mcpp/git/<hash>` existed, and otherwise the branch was re-resolved over the network and the lock rewritten. That hands "which commit do we build" to the survival of a cache directory. `mcpp new` does not gitignore `mcpp.lock` — it is meant to be committed — so the failure is ordinary: clone the project on a second machine, get a different commit, and see the lock quietly change under you. Measured on the new e2e assertion, with the branch moved and the cache evicted: the previous head built v2, this builds the v1 the lock recorded. `cargo build` / `cargo update` split it the same way. Making the lock authoritative also collapses the block into two independent questions, each with at most one network operation and therefore exactly one --offline gate: which commit (tag/rev name one; a branch is answered by the lock, else by ls-remote), and is it on disk (the commit picks the cache directory; a miss is a clone). The separate tag/rev leg goes with it, taking its per-build "from cache" noise line and its "is locked but its local cache is missing" message — which was emitted for deps that were never in the lock. Four defects fixed along the way: - --offline refused git remotes that are local directories. docs/05-mcpp-toml.md defines --offline as "never touch the network ... anything already installed still builds", and prepare.cppm's dependency-download gate draws the same line in a comment; ls-remote/clone against a local path are filesystem reads, so refusing them buys no isolation. Remotes are now classified by shape, which keeps a Windows drive letter (C:\repo — colon, no @) on the local side. - A clone killed between `git clone` and `git checkout` served the wrong commit forever: the directory is named after the commit but was only checked for existence. Branch deps now compare `git rev-parse HEAD` and re-clone on mismatch (tag/rev keep the ref name as identity, so there is nothing to compare). - The clone depended on `cd` changing drive on Windows. cmd.exe needs `cd /d`, and MCPP_HOME routinely sits on a different drive than the project; every other cross-drive site in the repo (process.cppm, msvc.cppm) writes /d, this one did not. `git -C <dir>` needs no shell at all. - An unreadable mcpp.lock was a plain warning, invisible to --strict. Per src/diag.cppm that is the degraded channel: the engine silently does less than asked — every git branch dep falls back to the network. Also: parse_git_source no longer splits a tag/rev whose name contains '@' (only branch entries carry @<commit>, which is what the writer emits), the cmd_update comment now records the right causality, and docs/CHANGELOG cover the new semantics in both languages. Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Summary
Makes
mcpp.locka resolution input for git dependencies — and an authoritativeone. Before this change the lock was write-only: a branch-based git dependency
(
branch = "develop") re-rangit ls-remoteevery timeprepare_buildran, andthe commit written into
mcpp.lockwas never read back by anything.After this change the lock is read first. A branch that already has a commit
recorded resolves from it with no network round-trip, and that commit is what
gets built whether or not the local clone survived — evicting
~/.mcpp/git, orcloning the project on another machine, can no longer put the build on a newer
branch tip.
mcpp update <dep>drops the entry and remains the one way a branchadvances.
Closes #329.
Why authoritative, and not "an anchor the cache confirms"
A lock that only counts while the clone happens to exist hands the "which commit
do we build" decision to the survival of a cache directory.
mcpp newdoes notgitignore
mcpp.lock— it is meant to be committed — so the failure is ordinary:a teammate clones the project, has no
~/.mcpp/git, and silently builds adifferent commit while rewriting the lock.
This is also the dominant convention:
cargo buildon a git branch dependencybuilds the locked revision and
cargo updatere-resolves it — the same splitmcpp already follows for profile defaults.
Measured, with the branch moved to v2 and the cache removed (this is the new
assertion in
tests/e2e/24_git_dependency.sh):branch dep v2— silently advanced, lock rewrittenbranch dep v1— the commit the lock recordedShape
The block collapses into two independent questions, each with at most one network
operation and therefore exactly one
--offlinegate:tag/revname one outright. Abranchis floating:mcpp.lockanswers it, elsegit ls-remote.The separate tag/rev leg is gone with it — along with its per-build
from cachenoise line and its
"is locked but its local cache is missing"message, whichwas emitted for dependencies that were never in the lock.
Also fixed here
--offlineno longer refuses a local git remote.docs/05-mcpp-toml.mddefines
--offlineas "never touch the network … anything already installedstill builds", and the dependency-download gate in
prepare.cppmdraws thesame line in a comment. A
git =naming a local directory (or afile://URL) is served by filesystem reads, so refusing it buys no isolation. Remotes
are classified by shape, keeping a Windows drive letter (
C:\repo— colon,no
@) on the local side.git cloneandgit checkoutno longer serves thewrong commit forever. The cache directory is named after the commit but was
only checked for existence. Branch deps now compare
git rev-parse HEADandre-clone on mismatch (tag/rev keep their ref name as the identity, so there is
nothing to compare).
cdchanging drive.git clone … && cd <dir> && git checkoutdoes not change drive in cmd.exe without/d,and the cache root routinely lives on a different one than the project. Every
other cross-drive site in the repo (
process.cppm,msvc.cppm) writescd /d; this one did not. Nowgit -C <dir>, which needs no shell at all.mcpp.lockis reported as degraded, not as a plain warning.Per
src/diag.cppmthat channel is for "the engine did less than asked" andrequires an impact — here every git branch dep silently falls back to the
network and may move past the recorded commit. As a warning it was invisible
to
--strict.parse_git_sourceno longer splits a tag/rev whose name contains@.Only
branchentries carry@<commit>(that is what the writer emits), so atag legitimately named
v1.0@rc1was being read as refv1.0+ commitrc1.Branch entries still split on the last
@, sofeat@v2round-trips.Note on the original framing
The first revision described this as removing an
ls-remotefrom "everymcpp build". That overstates it: a no-op rebuild takes the fast path incmd_build.cppm, which returns beforeprepare_buildruns, so nols-remotehappened there either. The cost this removes is on builds that do re-prepare —
sources changed,
mcpp.tomltouched, a--profile/--target/--featuresflagpassed, or after
mcpp clean. That is also why the e2e has tomcpp cleanfirst: without it the assertion passes on
maintoo.Changes
src/pm/lock_io.cppm:LockedGitSource+parse_git_source().src/build/prepare.cppm: loadmcpp.lockup front; two-phase git resolution;local-remote classification; cache integrity check;
git -C.src/pm/commands.cppm: correct thecmd_updatecomment — dropping a lockentry is now the thing that lets a branch advance, not a no-op.
docs/05-mcpp-toml.md,docs/zh/05-mcpp-toml.md: git deps +mcpp.lock, andthe
--offlinerow.CHANGELOG.md,mcpp.toml,src/toolchain/fingerprint.cppm: 2026.8.1.2.tests/e2e/24_git_dependency.sh: offline rebuild from the lock; the branchdoes not advance when the cache is evicted.
tests/unit/test_pm_lock_io.cpp:@in branch names,@in tag/rev names,empty commit, scp-like URLs.
Test plan
mcpp buildmcpp test(46 passed, 0 failed)tests/e2e/24_git_dependency.sh.github/tools/check_version_pins.sh