Skip to content

Commit c3e7095

Browse files
committed
feat: use mcpp.lock commit as offline anchor for git deps
- 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 #329
1 parent 7f1489d commit c3e7095

4 files changed

Lines changed: 221 additions & 12 deletions

File tree

src/build/prepare.cppm

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import mcpp.pm.index_refresh;
4646
import mcpp.pm.mangle;
4747
import mcpp.pm.compat;
4848
import mcpp.pm.dep_spec;
49+
import mcpp.pm.lock_io;
4950
import mcpp.version_req;
5051
import mcpp.ui;
5152
import mcpp.log;
@@ -838,6 +839,26 @@ prepare_build(bool print_fingerprint,
838839
mcpp::diag::warning("manifest/schema", w);
839840
}
840841

842+
// Load mcpp.lock once. Git-based deps can use it as an offline anchor:
843+
// if a branch is already resolved to a commit and the local cache matches,
844+
// no network round-trip is needed.
845+
std::map<std::string, mcpp::pm::LockedGitSource> gitLockAnchors;
846+
{
847+
auto lockPath = *root / "mcpp.lock";
848+
if (std::filesystem::exists(lockPath)) {
849+
if (auto lock = mcpp::lockfile::load(lockPath); lock) {
850+
for (auto const& p : lock->packages) {
851+
if (auto parsed = mcpp::pm::parse_git_source(p.source); parsed) {
852+
gitLockAnchors.emplace(p.name, std::move(*parsed));
853+
}
854+
}
855+
} else {
856+
mcpp::diag::warning("lockfile",
857+
std::format("ignoring mcpp.lock: {}", lock.error().message));
858+
}
859+
}
860+
}
861+
841862
// Global-cache mode: --cache > MCPP_BUILD_CACHE > [build] cache > global.
842863
// An unparseable value is a warning (error under --strict) and falls
843864
// through to the next source rather than silently meaning "global" — a typo
@@ -2987,24 +3008,102 @@ prepare_build(bool print_fingerprint,
29873008
// them to a commit before forming the cache key; this lets
29883009
// `mcpp update <dep>` pick up a moved branch without deleting
29893010
// unrelated git caches.
3011+
//
3012+
// mcpp.lock acts as an offline anchor: if a branch is already
3013+
// resolved to a commit and the local cache still matches it,
3014+
// we skip the network round-trip entirely.
29903015
auto mcppHome = mcpp::home::root(); // single resolver (#311)
29913016
std::string resolvedGitRev = spec.gitRev;
3017+
bool skipLsRemote = false;
3018+
3019+
// Look up an offline anchor for this git dep.
3020+
std::optional<std::string> lockedCommit;
3021+
if (auto it = gitLockAnchors.find(name); it != gitLockAnchors.end()) {
3022+
auto const& anchor = it->second;
3023+
if (anchor.refKind == spec.gitRefKind && anchor.ref == spec.gitRev) {
3024+
lockedCommit = anchor.resolvedCommit;
3025+
}
3026+
}
3027+
3028+
auto computeGitRoot = [&](const std::string& rev) {
3029+
std::hash<std::string> H;
3030+
auto urlHash = std::format("{:016x}",
3031+
H(spec.git + "|" + spec.gitRefKind + "|" + spec.gitRev
3032+
+ "|" + rev));
3033+
return mcppHome / "git" / urlHash;
3034+
};
3035+
3036+
auto readCacheHead = [&](const std::filesystem::path& gitRoot)
3037+
-> std::string
3038+
{
3039+
auto cmd = std::format("git -C {} rev-parse HEAD 2>&1",
3040+
mcpp::platform::shell::quote(gitRoot.string()));
3041+
auto r = mcpp::platform::process::capture(cmd);
3042+
if (r.exit_code != 0) return {};
3043+
std::string head = r.output;
3044+
head.erase(head.find_last_not_of(" \r\n\t") + 1);
3045+
return head;
3046+
};
3047+
29923048
if (spec.gitRefKind == "branch") {
29933049
auto ref = std::format("refs/heads/{}", spec.gitRev);
2994-
auto cmd = std::format(
2995-
"git ls-remote {} {} 2>&1",
2996-
mcpp::platform::shell::quote(spec.git),
2997-
mcpp::platform::shell::quote(ref));
2998-
auto r = mcpp::platform::process::capture(cmd);
2999-
if (r.exit_code != 0) {
3000-
return std::unexpected(std::format(
3001-
"git ls-remote of '{}' failed:\n{}", spec.git, r.output));
3050+
if (lockedCommit && !lockedCommit->empty()) {
3051+
resolvedGitRev = *lockedCommit;
3052+
auto gitRoot = computeGitRoot(resolvedGitRev);
3053+
if (std::filesystem::exists(gitRoot / ".git") &&
3054+
readCacheHead(gitRoot) == resolvedGitRev) {
3055+
skipLsRemote = true;
3056+
mcpp::ui::info("Resolved",
3057+
std::format("{} ({} = {}) from lock",
3058+
spec.git, spec.gitRefKind, spec.gitRev));
3059+
}
3060+
}
3061+
if (!skipLsRemote) {
3062+
if (mcpp::platform::env::offline_mode()) {
3063+
if (lockedCommit) {
3064+
return std::unexpected(std::format(
3065+
"git dep '{}' locked to commit {} but its local cache is missing or stale; "
3066+
"run without --offline to refresh, or `mcpp update {}` to re-resolve",
3067+
name, *lockedCommit, name));
3068+
} else {
3069+
return std::unexpected(std::format(
3070+
"git dep '{}' uses branch '{}' and mcpp.lock has no commit; "
3071+
"cannot resolve offline. Run `mcpp update {}` or build without --offline.",
3072+
name, spec.gitRev, name));
3073+
}
3074+
}
3075+
auto cmd = std::format(
3076+
"git ls-remote {} {} 2>&1",
3077+
mcpp::platform::shell::quote(spec.git),
3078+
mcpp::platform::shell::quote(ref));
3079+
auto r = mcpp::platform::process::capture(cmd);
3080+
if (r.exit_code != 0) {
3081+
return std::unexpected(std::format(
3082+
"git ls-remote of '{}' failed:\n{}", spec.git, r.output));
3083+
}
3084+
std::istringstream is(r.output);
3085+
is >> resolvedGitRev;
3086+
if (resolvedGitRev.empty()) {
3087+
return std::unexpected(std::format(
3088+
"git branch '{}' not found in '{}'", spec.gitRev, spec.git));
3089+
}
3090+
}
3091+
} else {
3092+
// tag/rev: the declared ref is already a stable identity.
3093+
// If the lock already recorded this dep and the clone is present,
3094+
// we still use it; otherwise fall through to clone.
3095+
auto gitRoot = computeGitRoot(resolvedGitRev);
3096+
if (std::filesystem::exists(gitRoot / ".git")) {
3097+
mcpp::ui::info("Resolved",
3098+
std::format("{} ({} = {}) from cache",
3099+
spec.git, spec.gitRefKind, spec.gitRev));
30023100
}
3003-
std::istringstream is(r.output);
3004-
is >> resolvedGitRev;
3005-
if (resolvedGitRev.empty()) {
3101+
if (!std::filesystem::exists(gitRoot / ".git") &&
3102+
mcpp::platform::env::offline_mode()) {
30063103
return std::unexpected(std::format(
3007-
"git branch '{}' not found in '{}'", spec.gitRev, spec.git));
3104+
"git dep '{}' is locked but its local cache is missing; "
3105+
"run without --offline to clone, or `mcpp update {}` to re-resolve.",
3106+
name, name));
30083107
}
30093108
}
30103109

src/pm/lock_io.cppm

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,22 @@ struct LockedPackage {
3333
std::string hash; // "sha256:..." or "fnv1a:..."
3434
};
3535

36+
// Parsed form of a git source string as written to mcpp.lock.
37+
// Supported forms:
38+
// git+https://host/repo#branch=develop@5848943...
39+
// git+https://host/repo#tag=v1.0.0
40+
// git+https://host/repo#rev=5848943...
41+
// The resolvedCommit field is optional because old lock entries or
42+
// tag/rev sources may not carry a resolved commit.
43+
struct LockedGitSource {
44+
std::string url;
45+
std::string refKind; // "branch", "tag", or "rev"
46+
std::string ref;
47+
std::optional<std::string> resolvedCommit; // for branch entries with @commit
48+
};
49+
50+
std::optional<LockedGitSource> parse_git_source(std::string_view source);
51+
3652
struct Lockfile {
3753
int schemaVersion = 2;
3854
std::vector<LockedIndex> indices;
@@ -159,4 +175,37 @@ std::string compute_hash(const Lockfile& lock) {
159175
return std::format("{:016x}", h);
160176
}
161177

178+
std::optional<LockedGitSource> parse_git_source(std::string_view source) {
179+
constexpr std::string_view prefix = "git+";
180+
if (!source.starts_with(prefix)) return std::nullopt;
181+
182+
auto rest = source.substr(prefix.size());
183+
auto hashPos = rest.find('#');
184+
if (hashPos == std::string_view::npos) return std::nullopt;
185+
186+
LockedGitSource out;
187+
out.url = std::string(rest.substr(0, hashPos));
188+
auto fragment = rest.substr(hashPos + 1);
189+
190+
// fragment is one of: branch=develop@commit, tag=v1.0.0, rev=abc123
191+
auto eqPos = fragment.find('=');
192+
if (eqPos == std::string_view::npos) return std::nullopt;
193+
194+
out.refKind = std::string(fragment.substr(0, eqPos));
195+
if (out.refKind != "branch" && out.refKind != "tag" && out.refKind != "rev")
196+
return std::nullopt;
197+
198+
auto refPart = fragment.substr(eqPos + 1);
199+
auto atPos = refPart.find('@');
200+
if (atPos == std::string_view::npos) {
201+
out.ref = std::string(refPart);
202+
} else {
203+
out.ref = std::string(refPart.substr(0, atPos));
204+
out.resolvedCommit = std::string(refPart.substr(atPos + 1));
205+
}
206+
207+
if (out.ref.empty()) return std::nullopt;
208+
return out;
209+
}
210+
162211
} // namespace mcpp::pm

tests/e2e/24_git_dependency.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ out=$(${triple}${fp_dir}/bin/branchapp)
153153
echo "FAIL: branch dep v1 not invoked: $out"
154154
cat branch-v1.log; exit 1; }
155155

156+
# Second build with the lock in place must not hit the network for ls-remote.
157+
build2=$("$MCPP" build 2>&1)
158+
echo "$build2" | grep -q 'ls-remote' && { echo "FAIL: branch dep re-ls-remoted on rebuild"; exit 1; } || true
159+
echo "$build2" | grep -q 'Cloning' && { echo "FAIL: branch dep re-cloned on rebuild"; exit 1; } || true
160+
156161
grep -q 'source = "git+' mcpp.lock || {
157162
echo "FAIL: git dep lock source is not marked as git"
158163
cat mcpp.lock; exit 1; }

tests/unit/test_pm_lock_io.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#include <gtest/gtest.h>
2+
3+
import std;
4+
import mcpp.pm.lock_io;
5+
6+
TEST(PmLockIo, ParseGitBranchWithCommit) {
7+
auto parsed = mcpp::pm::parse_git_source(
8+
"git+https://github.com/user/repo#branch=develop@584894315b7a4fe4d7957d3c29dc4052b8012860");
9+
ASSERT_TRUE(parsed.has_value());
10+
EXPECT_EQ(parsed->url, "https://github.com/user/repo");
11+
EXPECT_EQ(parsed->refKind, "branch");
12+
EXPECT_EQ(parsed->ref, "develop");
13+
ASSERT_TRUE(parsed->resolvedCommit.has_value());
14+
EXPECT_EQ(parsed->resolvedCommit.value(),
15+
"584894315b7a4fe4d7957d3c29dc4052b8012860");
16+
}
17+
18+
TEST(PmLockIo, ParseGitBranchWithoutCommit) {
19+
auto parsed = mcpp::pm::parse_git_source(
20+
"git+https://github.com/user/repo#branch=develop");
21+
ASSERT_TRUE(parsed.has_value());
22+
EXPECT_EQ(parsed->url, "https://github.com/user/repo");
23+
EXPECT_EQ(parsed->refKind, "branch");
24+
EXPECT_EQ(parsed->ref, "develop");
25+
EXPECT_FALSE(parsed->resolvedCommit.has_value());
26+
}
27+
28+
TEST(PmLockIo, ParseGitTag) {
29+
auto parsed = mcpp::pm::parse_git_source(
30+
"git+https://github.com/user/repo#tag=v1.0.0");
31+
ASSERT_TRUE(parsed.has_value());
32+
EXPECT_EQ(parsed->url, "https://github.com/user/repo");
33+
EXPECT_EQ(parsed->refKind, "tag");
34+
EXPECT_EQ(parsed->ref, "v1.0.0");
35+
EXPECT_FALSE(parsed->resolvedCommit.has_value());
36+
}
37+
38+
TEST(PmLockIo, ParseGitRev) {
39+
auto parsed = mcpp::pm::parse_git_source(
40+
"git+https://github.com/user/repo#rev=584894315b7a4fe4d7957d3c29dc4052b8012860");
41+
ASSERT_TRUE(parsed.has_value());
42+
EXPECT_EQ(parsed->url, "https://github.com/user/repo");
43+
EXPECT_EQ(parsed->refKind, "rev");
44+
EXPECT_EQ(parsed->ref, "584894315b7a4fe4d7957d3c29dc4052b8012860");
45+
EXPECT_FALSE(parsed->resolvedCommit.has_value());
46+
}
47+
48+
TEST(PmLockIo, ParseNonGitSourceReturnsNullopt) {
49+
EXPECT_FALSE(mcpp::pm::parse_git_source("index+mcpplibs@1.0.0").has_value());
50+
EXPECT_FALSE(mcpp::pm::parse_git_source("").has_value());
51+
EXPECT_FALSE(
52+
mcpp::pm::parse_git_source("https://github.com/user/repo").has_value());
53+
EXPECT_FALSE(mcpp::pm::parse_git_source("git+https://host/repo").has_value());
54+
EXPECT_FALSE(
55+
mcpp::pm::parse_git_source("git+https://host/repo#bad").has_value());
56+
}

0 commit comments

Comments
 (0)