Skip to content

Commit 8b1e880

Browse files
committed
feat(project): add Git online metadata
1 parent b56062b commit 8b1e880

7 files changed

Lines changed: 568 additions & 13 deletions

File tree

‎include/vix/cli/project/native/GitPackageContentSource.hpp‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
#include <filesystem>
88
#include <memory>
9+
#include <optional>
910
#include <string>
11+
#include <string_view>
1012
#include <vector>
1113

1214
#include <vix/cli/project/native/PackageContentAcquisition.hpp>
@@ -34,6 +36,20 @@ namespace vix::cli::project::native
3436
[[nodiscard]] virtual GitCommandResult run(const GitCommand &command) = 0;
3537
};
3638

39+
enum class GitObjectTreeReadStatus { available, transport_failure, invalid_object_data };
40+
struct GitObjectTreeReadResult
41+
{
42+
GitObjectTreeReadStatus status{GitObjectTreeReadStatus::transport_failure};
43+
std::optional<CanonicalPackageTree> tree;
44+
std::string diagnostic;
45+
};
46+
47+
// Shared object-database reader used by content and metadata acquisition.
48+
// It reads one exact commit tree without creating a worktree or admitting
49+
// bytes to the persistent package-content store.
50+
[[nodiscard]] GitObjectTreeReadResult read_git_object_tree(
51+
GitCommandRunner &runner, std::string_view gitDirectory, std::string_view fullCommit);
52+
3753
class NativeGitCommandRunner final : public GitCommandRunner
3854
{
3955
public:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/** @file GitPackageMetadataSource.hpp
2+
* Exact revision Git metadata observation without checkout or P10P admission.
3+
*/
4+
#ifndef VIX_CLI_PROJECT_NATIVE_GIT_PACKAGE_METADATA_SOURCE_HPP
5+
#define VIX_CLI_PROJECT_NATIVE_GIT_PACKAGE_METADATA_SOURCE_HPP
6+
7+
#include <filesystem>
8+
#include <utility>
9+
10+
#include <vix/cli/project/native/GitPackageContentSource.hpp>
11+
#include <vix/cli/project/native/MetadataAcquisition.hpp>
12+
13+
namespace vix::cli::project::native
14+
{
15+
class GitPackageMetadataSource final
16+
{
17+
public:
18+
GitPackageMetadataSource(std::filesystem::path workRoot, GitCommandRunner &runner)
19+
: workRoot_(std::move(workRoot)), runner_(runner) {}
20+
21+
[[nodiscard]] GitQueryResult resolve_git(
22+
const GitPackageIdentity &identity, const GitRevisionRequirement &revision);
23+
24+
private:
25+
std::filesystem::path workRoot_;
26+
GitCommandRunner &runner_;
27+
};
28+
}
29+
30+
#endif
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/** @file OnlinePackageMetadataSource.hpp
2+
* Typed production composition for online Registry and Git metadata.
3+
*/
4+
#ifndef VIX_CLI_PROJECT_NATIVE_ONLINE_PACKAGE_METADATA_SOURCE_HPP
5+
#define VIX_CLI_PROJECT_NATIVE_ONLINE_PACKAGE_METADATA_SOURCE_HPP
6+
7+
#include <vix/cli/project/native/GitPackageMetadataSource.hpp>
8+
#include <vix/cli/project/native/RegistryMetadataSource.hpp>
9+
10+
namespace vix::cli::project::native
11+
{
12+
class OnlinePackageMetadataSource final : public PackageMetadataSource
13+
{
14+
public:
15+
OnlinePackageMetadataSource(PackageMetadataSource &registry,
16+
GitPackageMetadataSource &git) : registry_(registry), git_(git) {}
17+
18+
[[nodiscard]] RegistryQueryResult registry_releases(
19+
const RegistryPackageIdentity &identity) override
20+
{ return registry_.registry_releases(identity); }
21+
22+
[[nodiscard]] GitQueryResult resolve_git(const GitPackageIdentity &identity,
23+
const GitRevisionRequirement &revision) override
24+
{ return git_.resolve_git(identity, revision); }
25+
26+
private:
27+
PackageMetadataSource &registry_;
28+
GitPackageMetadataSource &git_;
29+
};
30+
}
31+
32+
#endif

‎src/project/native/GitPackageContentSource.cpp‎

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,36 @@ namespace vix::cli::project::native
321321
#endif
322322
}
323323

324+
GitObjectTreeReadResult read_git_object_tree(
325+
GitCommandRunner &runner, std::string_view gitDirectory, std::string_view fullCommit)
326+
{
327+
auto run = [&](std::vector<std::string> arguments, std::string input = {})
328+
{ return runner.run({std::move(arguments), std::move(input)}); };
329+
const auto listing = run({"git", "--git-dir", std::string{gitDirectory}, "ls-tree", "-r", "-z",
330+
"--full-tree", std::string{fullCommit}});
331+
if (!listing.started || listing.exitCode != 0)
332+
return {GitObjectTreeReadStatus::transport_failure, std::nullopt, "cannot enumerate exact Git commit tree"};
333+
334+
std::vector<TreeLeaf> leaves;
335+
CanonicalPackageTree tree;
336+
std::string error;
337+
if (!parse_tree(listing.standardOutput, leaves, error))
338+
return {GitObjectTreeReadStatus::invalid_object_data, std::nullopt,
339+
"invalid exact-commit Git tree: " + error};
340+
if (leaves.empty()) return {GitObjectTreeReadStatus::available, std::move(tree), {}};
341+
342+
std::string input;
343+
for (const auto &leaf : leaves)
344+
{ input += leaf.oid; input.push_back('\n'); }
345+
const auto blobs = run({"git", "--git-dir", std::string{gitDirectory}, "cat-file", "--batch"}, std::move(input));
346+
if (!blobs.started || blobs.exitCode != 0)
347+
return {GitObjectTreeReadStatus::transport_failure, std::nullopt, "cannot read Git blob batch"};
348+
if (!parse_batch(blobs.standardOutput, leaves, tree, error))
349+
return {GitObjectTreeReadStatus::invalid_object_data, std::nullopt,
350+
"invalid Git cat-file batch protocol: " + error};
351+
return {GitObjectTreeReadStatus::available, std::move(tree), {}};
352+
}
353+
324354
GitCliPackageContentSource::GitCliPackageContentSource(fs::path workRoot, GitCommandRunner &runner)
325355
: workRoot_(std::move(workRoot)), runner_(runner) {}
326356

@@ -359,19 +389,12 @@ namespace vix::cli::project::native
359389
if (!type.started || type.exitCode != 0) return failed("exact locked Git commit is unavailable after object acquisition");
360390
while (!type.standardOutput.empty() && (type.standardOutput.back() == '\n' || type.standardOutput.back() == '\r')) type.standardOutput.pop_back();
361391
if (type.standardOutput != "commit") return invalid("locked Git object is not itself a commit; tag peeling is forbidden");
362-
auto listing = run({"git", "--git-dir", gitDir, "ls-tree", "-r", "-z", "--full-tree", commit});
363-
if (!listing.started || listing.exitCode != 0) return failed("cannot enumerate exact locked commit tree");
364-
std::vector<TreeLeaf> leaves; std::string error;
365-
if (!parse_tree(listing.standardOutput, leaves, error)) return invalid("invalid exact-commit Git tree: " + error);
366-
std::string input;
367-
for (const auto &leaf : leaves) { input += leaf.oid; input.push_back('\n'); }
368-
if (leaves.empty())
369-
return {PackageContentSourceStatus::available, std::make_shared<CanonicalPackageContentCandidate>(CanonicalPackageTree{}), {}};
370-
auto blobs = run({"git", "--git-dir", gitDir, "cat-file", "--batch"}, std::move(input));
371-
if (!blobs.started || blobs.exitCode != 0) return failed("cannot read Git blob batch");
372-
CanonicalPackageTree candidate;
373-
if (!parse_batch(blobs.standardOutput, leaves, candidate, error)) return invalid("invalid Git cat-file batch protocol: " + error);
392+
auto objectTree = read_git_object_tree(runner_, gitDir, commit);
393+
if (objectTree.status == GitObjectTreeReadStatus::transport_failure)
394+
return failed(std::move(objectTree.diagnostic));
395+
if (objectTree.status != GitObjectTreeReadStatus::available || !objectTree.tree)
396+
return invalid(std::move(objectTree.diagnostic));
374397
return {PackageContentSourceStatus::available,
375-
std::make_shared<CanonicalPackageContentCandidate>(std::move(candidate)), {}};
398+
std::make_shared<CanonicalPackageContentCandidate>(std::move(*objectTree.tree)), {}};
376399
}
377400
} // namespace vix::cli::project::native
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#include <vix/cli/project/native/GitPackageMetadataSource.hpp>
2+
3+
#include <vix/cli/project/native/PackageContentDigest.hpp>
4+
#include <vix/cli/project/native/TomlParser.hpp>
5+
#include <vix/cli/project/native/Validation.hpp>
6+
7+
#include <algorithm>
8+
#include <atomic>
9+
#include <chrono>
10+
#include <cstdint>
11+
#include <filesystem>
12+
#include <string_view>
13+
#include <system_error>
14+
#include <type_traits>
15+
#include <utility>
16+
17+
namespace vix::cli::project::native
18+
{
19+
namespace fs = std::filesystem;
20+
namespace
21+
{
22+
bool valid_full_oid(std::string_view oid)
23+
{
24+
return (oid.size() == 40 || oid.size() == 64) &&
25+
std::all_of(oid.begin(), oid.end(), [](char ch)
26+
{ return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f'); });
27+
}
28+
29+
std::string trim_line(std::string value)
30+
{
31+
while (!value.empty() && (value.back() == '\n' || value.back() == '\r')) value.pop_back();
32+
return value;
33+
}
34+
35+
GitCommandResult run(GitCommandRunner &runner, std::vector<std::string> arguments)
36+
{ return runner.run({std::move(arguments), {}}); }
37+
38+
bool valid_revision_name(GitCommandRunner &runner, std::string_view namespaceName,
39+
std::string_view value)
40+
{
41+
if (value.empty() || value.find_first_of("\r\n") != std::string_view::npos ||
42+
value.find('\0') != std::string_view::npos) return false;
43+
const auto checked = run(runner, {"git", "check-ref-format", std::string{namespaceName} + std::string{value}});
44+
return checked.started && checked.exitCode == 0;
45+
}
46+
47+
GitQueryResult failed(std::string message)
48+
{ return {GitQueryStatus::failed, std::nullopt, std::move(message)}; }
49+
50+
GitQueryResult missing(std::string message)
51+
{ return {GitQueryStatus::not_found, std::nullopt, std::move(message)}; }
52+
53+
std::optional<std::string> package_contract_bytes(const CanonicalPackageTree &tree)
54+
{
55+
const auto entry = std::find_if(tree.entries.begin(), tree.entries.end(), [](const auto &value)
56+
{ return value.path == "vix.package.toml"; });
57+
if (entry == tree.entries.end() || entry->kind != CanonicalPackageEntryKind::file) return std::nullopt;
58+
std::string bytes;
59+
bytes.reserve(entry->content.size());
60+
for (const auto byte : entry->content) bytes.push_back(static_cast<char>(std::to_integer<unsigned char>(byte)));
61+
return bytes;
62+
}
63+
}
64+
65+
GitQueryResult GitPackageMetadataSource::resolve_git(
66+
const GitPackageIdentity &identity, const GitRevisionRequirement &revision)
67+
{
68+
if (!identity.repositoryRoot || identity.normalizedRepository.empty() ||
69+
identity.normalizedRepository.find_first_of("\r\n") != std::string::npos ||
70+
identity.normalizedRepository.find('\0') != std::string::npos)
71+
return failed("Git metadata request has an invalid repository identity or unsupported non-root package identity");
72+
73+
std::string fullCommit;
74+
std::string sourceRef;
75+
std::string destinationRef;
76+
bool commitRequest = false;
77+
const bool revisionValid = std::visit([&](const auto &value)
78+
{
79+
using Revision = std::decay_t<decltype(value)>;
80+
if constexpr (std::is_same_v<Revision, CommitRevision>)
81+
{
82+
commitRequest = true;
83+
fullCommit = value.value;
84+
return valid_full_oid(value.value);
85+
}
86+
else if constexpr (std::is_same_v<Revision, BranchRevision>)
87+
{
88+
sourceRef = "refs/heads/" + value.value;
89+
destinationRef = "refs/vix-metadata/heads/" + value.value;
90+
return valid_revision_name(runner_, "refs/heads/", value.value);
91+
}
92+
else
93+
{
94+
sourceRef = "refs/tags/" + value.value;
95+
destinationRef = "refs/vix-metadata/tags/" + value.value;
96+
return valid_revision_name(runner_, "refs/tags/", value.value);
97+
}
98+
}, revision);
99+
if (!revisionValid)
100+
return failed("Git revision must be a full commit ID or a valid exact branch/tag name");
101+
102+
std::error_code ec;
103+
fs::create_directories(workRoot_, ec);
104+
if (ec) return failed("cannot create injected Git metadata work root");
105+
static std::atomic<std::uint64_t> counter{0};
106+
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
107+
fs::path workspace = workRoot_ / ("vix-git-metadata-" + std::to_string(stamp) + "-" +
108+
std::to_string(counter.fetch_add(1, std::memory_order_relaxed)));
109+
if (!fs::create_directory(workspace, ec) || ec)
110+
return failed("cannot create isolated Git metadata workspace");
111+
struct Cleanup
112+
{
113+
fs::path path;
114+
~Cleanup() { std::error_code ignored; fs::remove_all(path, ignored); }
115+
} cleanup{workspace};
116+
const auto gitDir = (workspace / "objects.git").string();
117+
const auto initialized = run(runner_, {"git", "init", "--bare", "--quiet", gitDir});
118+
if (!initialized.started || initialized.exitCode != 0)
119+
return failed("could not initialize isolated Git metadata object database");
120+
121+
if (commitRequest)
122+
{
123+
auto fetched = runner_.run({{"git", "--git-dir", gitDir, "fetch", "--no-tags", "--depth=1", "--",
124+
identity.normalizedRepository, fullCommit}, {}});
125+
if (!fetched.started) return failed("Git executable unavailable during exact-commit metadata fetch");
126+
auto type = run(runner_, {"git", "--git-dir", gitDir, "cat-file", "-t", fullCommit});
127+
if (!type.started || type.exitCode != 0)
128+
{
129+
fetched = runner_.run({{"git", "--git-dir", gitDir, "fetch", "--no-tags", "--depth=1", "--",
130+
identity.normalizedRepository, "+refs/*:refs/vix-metadata/all/*"}, {}});
131+
if (!fetched.started || fetched.exitCode != 0)
132+
return missing("the requested full Git commit is unavailable from the configured repository");
133+
type = run(runner_, {"git", "--git-dir", gitDir, "cat-file", "-t", fullCommit});
134+
}
135+
if (!type.started || type.exitCode != 0)
136+
return missing("the requested full Git commit is absent from the configured repository");
137+
if (trim_line(std::move(type.standardOutput)) != "commit")
138+
return failed("the requested full Git object is not a commit");
139+
}
140+
else
141+
{
142+
const auto refspec = "+" + sourceRef + ":" + destinationRef;
143+
const auto fetched = runner_.run({{"git", "--git-dir", gitDir, "fetch", "--no-tags", "--depth=1", "--",
144+
identity.normalizedRepository, refspec}, {}});
145+
if (!fetched.started) return failed("Git executable unavailable while resolving the requested ref");
146+
if (fetched.exitCode != 0)
147+
{
148+
const auto &diagnostic = fetched.standardError;
149+
if (diagnostic.find("couldn't find remote ref") != std::string::npos ||
150+
diagnostic.find("no such ref") != std::string::npos)
151+
return missing("the requested exact Git ref is not present in the configured repository");
152+
return failed("Git transport failed while fetching the requested exact ref");
153+
}
154+
const auto peel = run(runner_, {"git", "--git-dir", gitDir, "rev-parse", "--verify",
155+
destinationRef + "^{commit}"});
156+
if (!peel.started || peel.exitCode != 0)
157+
return failed("the requested Git ref does not resolve to a commit");
158+
fullCommit = trim_line(peel.standardOutput);
159+
if (!valid_full_oid(fullCommit)) return failed("Git returned a malformed full commit ID");
160+
}
161+
162+
const auto objectTree = read_git_object_tree(runner_, gitDir, fullCommit);
163+
if (objectTree.status != GitObjectTreeReadStatus::available || !objectTree.tree)
164+
return failed(objectTree.diagnostic.empty() ? "could not inspect exact Git commit objects" : objectTree.diagnostic);
165+
const auto contractBytes = package_contract_bytes(*objectTree.tree);
166+
if (!contractBytes) return failed("exact Git commit is missing a regular vix.package.toml at repository root");
167+
const auto parsedContract = parse_package_contract_text(*contractBytes, "Git vix.package.toml");
168+
if (!parsedContract.valid() || !parsedContract.value || !validate_package_contract(*parsedContract.value).valid())
169+
return failed("exact Git vix.package.toml is malformed or semantically invalid");
170+
if (!std::holds_alternative<GitPackageContractIdentity>(parsedContract.value->identity))
171+
return failed("exact Git vix.package.toml declares a non-Git package identity");
172+
const auto canonical = canonicalize_package_content_tree(*objectTree.tree);
173+
if (!canonical.valid() || !canonical.contentId)
174+
return failed("exact Git package tree cannot be represented by the P10O canonical contract");
175+
176+
return {GitQueryStatus::found,
177+
GitResolution{identity.normalizedRepository, revision, std::move(fullCommit),
178+
canonical.contentId->digest(), *parsedContract.value}, {}};
179+
}
180+
}

‎tests/CMakeLists.txt‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,29 @@ target_link_libraries(vix_cli_native_git_package_content_source_tests PRIVATE Th
566566
add_test(NAME vix_cli_native_git_package_content_source_tests
567567
COMMAND vix_cli_native_git_package_content_source_tests)
568568

569+
add_executable(vix_cli_native_git_package_metadata_source_tests
570+
NativeGitPackageMetadataSourceTests.cpp
571+
../src/project/native/GitPackageMetadataSource.cpp
572+
../src/project/native/GitPackageContentSource.cpp
573+
../src/project/native/MetadataAcquisition.cpp
574+
../src/project/native/ResolutionCatalog.cpp
575+
../src/project/native/PackageContentDigest.cpp
576+
../src/project/native/PackageContentStore.cpp
577+
../src/project/native/Lockfile.cpp
578+
../src/project/native/CanonicalHash.cpp
579+
../src/project/native/TomlParser.cpp
580+
../src/project/native/Validation.cpp
581+
)
582+
target_include_directories(vix_cli_native_git_package_metadata_source_tests PRIVATE
583+
${CMAKE_CURRENT_SOURCE_DIR}/../include
584+
)
585+
target_include_directories(vix_cli_native_git_package_metadata_source_tests SYSTEM PRIVATE
586+
${CMAKE_CURRENT_SOURCE_DIR}/../third_party/tomlplusplus
587+
)
588+
target_link_libraries(vix_cli_native_git_package_metadata_source_tests PRIVATE Threads::Threads)
589+
add_test(NAME vix_cli_native_git_package_metadata_source_tests
590+
COMMAND vix_cli_native_git_package_metadata_source_tests)
591+
569592
add_executable(vix_cli_ctest_execution_tests
570593
CTestExecutionTests.cpp
571594
../src/project/cmake/CTestExecution.cpp

0 commit comments

Comments
 (0)