#ifdef NIX_VERSION
#include "repo.h"
#include "debug.h"
#include <chrono>
#include <nix/fetchers/cache.hh>
#include <nix/fetchers/fetch-settings.hh>
#include <nix/fetchers/fetchers.hh>
#include <nix/fetchers/store-path-accessor.hh>
#include <nix/store/store-api.hh>
#include <nix/util/posix-source-accessor.hh>
#include <date/date.h>
#include <nlohmann/json.hpp>
using nixpluginpijul::getRepoStatus;
using nixpluginpijul::getState;
using nixpluginpijul::getTrackedFiles;
using nixpluginpijul::isRepoDirty;
using nixpluginpijul::record;
using nixpluginpijul::RepoStatus;
using namespace std::string_literals;
using namespace std::string_view_literals;
namespace nix::fetchers
{
struct PijulInputScheme : InputScheme {
[[nodiscard]] std::optional<Input> inputFromURL(const Settings &settings, const ParsedURL &url, bool requireTree) const override
{
DBG_BEGIN
if (url.scheme != "pijul+http" && url.scheme != "pijul+https" && url.scheme != "pijul+ssh" && url.scheme != "pijul+file") {
return {};
}
auto url2(url);
url2.scheme = std::string(url2.scheme, 6);
url2.query.clear();
Attrs attrs;
attrs.emplace("type"s, "pijul"s);
for (const auto &[name, value] : url.query) {
if (name == "channel" || name == "state") {
attrs.emplace(name, value);
} else {
url2.query.emplace(name, value);
}
}
attrs.emplace("url"s, url2.to_string());
return inputFromAttrs(settings, attrs);
DBG_END
}
[[nodiscard]] std::optional<Input> inputFromAttrs(const Settings &settings, const Attrs &attrs) const override
{
DBG_BEGIN
if (maybeGetStrAttr(attrs, "type") != "pijul") {
return {};
}
parseURL(getStrAttr(attrs, "url"));
Input input(settings);
input.attrs = attrs;
return input;
DBG_END
}
[[nodiscard]] ParsedURL toURL(const Input &input) const override
{
DBG_BEGIN
auto url = parseURL(getStrAttr(input.attrs, "url"));
if (url.scheme != "pijul") {
url.scheme = "pijul+"s + url.scheme;
}
if (auto channel = maybeGetStrAttr(input.attrs, "channel"s)) {
url.query.insert_or_assign("channel"s, std::move(*channel));
}
if (auto state = maybeGetStrAttr(input.attrs, "state"s)) {
url.query.insert_or_assign("state"s, std::move(*state));
}
return url;
DBG_END
}
std::optional<std::filesystem::path> getSourcePath(const Input &input) const override
{
auto url = parseURL(getStrAttr(input.attrs, "url"));
if (url.scheme == "file" && !input.getRef() && !input.getRev()) {
return url.path;
}
return {};
DBG_END
}
void putFile(const Input &input, const CanonPath &path, std::string_view contents, std::optional<std::string> commitMsg) const override
{
DBG_BEGIN
auto root = getSourcePath(input);
assert(root);
auto rootStr = root->string();
writeFile((CanonPath(rootStr) / path).abs(), contents);
record(*commitMsg, rootStr, {Path(path.rel())});
DBG_END
}
std::string_view schemeName() const override
{
return "pijul"sv;
}
StringSet allowedAttrs() const override
{
return {"url"s, "channel"s, "state"s, "narHash"s, "lastModified"s};
}
bool isLocked(const Input &input) const override
{
DBG_BEGIN
return maybeGetStrAttr(input.attrs, "channel") && maybeGetStrAttr(input.attrs, "state");
DBG_END
}
{
DBG_BEGIN
auto [storePath, input] = fetchToStoreImpl(store, _input);
return {makeStorePathAccessor(store, storePath), input};
DBG_END
}
private:
std::pair<StorePath, Input> fetchToStoreImpl(ref<Store> store, const Input &_input) const
{
DBG_BEGIN
if (auto localPath = getSourcePath(_input)) {
return fetchLocal(store, _input, *localPath);
} else {
auto [storePath, infoAttrs] = doFetchCached(store, _input);
Input input(_input);
mergeAttrs(input.attrs, std::move(infoAttrs));
return {std::move(storePath), input};
}
DBG_END
}
static std::pair<StorePath, Attrs> doFetchCached(ref<Store> store, const Input &input)
{
DBG_BEGIN
auto cache = input.settings->getCache();
const auto &name = input.getName();
auto url = parseURL(getStrAttr(input.attrs, "url"));
auto baseUrl = url;
baseUrl.query.clear();
const auto repoUrl = baseUrl.to_string();
const auto channel = maybeGetStrAttr(input.attrs, "channel");
const auto state = maybeGetStrAttr(input.attrs, "state");
std::optional<Cache::Key> key;
bool isLockedInput = false;
if (channel && state) {
isLockedInput = true;
Attrs keyAttrs;
keyAttrs.emplace("name"s, name);
keyAttrs.emplace("channel"s, *channel);
keyAttrs.emplace("state"s, *state);
key = Cache::Key{"pijul"sv, std::move(keyAttrs)};
if (auto res = cache->lookupStorePath(*key, *store)) {
return std::make_pair(std::move(res->storePath), std::move(res->value));
}
}
Attrs impureKeyAttrs;
impureKeyAttrs.emplace("name"s, name);
impureKeyAttrs.emplace("url"s, repoUrl);
const Cache::Key impureKey{"pijul"sv, std::move(impureKeyAttrs)};
if (auto res = cache->lookupStorePath(impureKey, *store)) {
auto &infoAttrs = res->value;
if ((!channel || *channel == getStrAttr(infoAttrs, "channel")) && (!state || *state == getStrAttr(infoAttrs, "state"))) {
return std::make_pair(std::move(res->storePath), std::move(infoAttrs));
}
}
auto [storePath, rs] = doFetchClone(store, name, repoUrl, channel, state);
if (!key) {
Attrs keyAttrs;
keyAttrs.emplace("name"s, name);
key = Cache::Key{"pijul"sv, std::move(keyAttrs)};
}
Attrs toMerge;
toMerge.emplace("channel"s, rs.channel);
toMerge.emplace("state"s, rs.state);
mergeAttrs(key->second, std::move(toMerge));
Attrs infoAttrs;
infoAttrs.emplace("channel"s, std::move(rs.channel));
infoAttrs.emplace("state"s, std::move(rs.state));
infoAttrs.emplace("lastModified"s, rs.lastModified);
if (!isLockedInput) {
cache->upsert(impureKey, *store, infoAttrs, storePath);
}
cache->upsert(*key, *store, infoAttrs, storePath);
return std::make_pair(std::move(storePath), std::move(infoAttrs));
DBG_END
}
static std::pair<StorePath, RepoStatus> doFetchClone(ref<Store> store,
const std::string_view &inputName,
const std::string_view &repoUrl,
const std::optional<std::string_view> &channel,
const std::optional<std::string_view> &state)
{
DBG_BEGIN
const Path tmpDir = createTempDir();
const AutoDelete delTmpDir(tmpDir, true);
const auto repoDir = tmpDir + "/source"sv;
nixpluginpijul::clone(repoUrl, repoDir, channel, state);
RepoStatus rs = getRepoStatus(repoDir);
if (channel && *channel != rs.channel) {
throw Error("channel mismatch: requested %s, got %s"s, *channel, rs.channel);
}
if (state && *state != rs.state) {
throw Error("state mismatch: requested %s, got %s"s, *state, rs.state);
}
deletePath(repoDir + "/.pijul"sv);
auto path = PosixSourceAccessor::createAtRoot(repoDir);
auto storePath = store->addToStore(inputName, path);
return {std::move(storePath), std::move(rs)};
DBG_END
}
static std::pair<StorePath, Input> fetchLocal(ref<Store> store, const Input &_input, const std::filesystem::path &localPath)
{
DBG_BEGIN
if (_input.attrs.contains("channel"s) || _input.attrs.contains("state"s)) {
throw Error("no channel/state support for local Pijul repository yet"s);
}
Input input(_input);
bool dirty = isRepoDirty(localPath.string());
if (dirty) {
if (!_input.settings->allowDirty) {
throw Error("Pijul tree '%s' is dirty", localPath.string());
}
if (_input.settings->warnDirty) {
warn("Pijul tree '%s' is dirty", localPath.string());
}
}
auto files = getTrackedFiles(localPath.string());
Path actualPath(absPath(Path(localPath.string())));
PathFilter filter = [&](const Path &p) -> bool {
assert(p.starts_with(actualPath));
std::string file(p, actualPath.size() + 1);
auto st = lstat(p);
if (S_ISDIR(st.st_mode)) {
auto prefix = file + "/";
auto i = files.lower_bound(prefix);
return i != files.end() && i->starts_with(prefix);
}
return files.count(file);
};
auto sourcePath = PosixSourceAccessor::createAtRoot(actualPath);
auto storePath = store->addToStore(input.getName(), sourcePath, ContentAddressMethod::Raw::NixArchive, HashAlgorithm::SHA256, {}, filter);
try {
const auto [state, timestamp] = getState(localPath.string());
input.attrs.insert_or_assign("lastModified", timestamp);
} catch (...) {
input.attrs.insert_or_assign("lastModified", uint64_t(0));
}
return {std::move(storePath), input};
DBG_END
}
static void mergeAttrs(Attrs &dest, Attrs &&source)
{
while (true) {
auto next = source.begin();
if (next == source.end()) {
break;
}
auto handle = source.extract(next);
mergeOne(dest, std::move(handle.key()), std::move(handle.mapped()));
}
}
static void mergeOne(Attrs &dest, std::string key, Attr attr)
{
const auto &d = dest.find(key);
if (d != dest.end()) {
if (d->second != attr) {
throw Error("while merging attrs: value mismatch for %s", d->first);
}
} else {
dest.emplace(std::move(key), std::move(attr));
}
}
};
[[maybe_unused]] static auto rPijulInputScheme = OnStartup([] {
registerInputScheme(std::make_shared<PijulInputScheme>());
});
}
#endif
std::pair<ref<SourceAccessor>, Input> getAccessor(ref<Store> store, const Input &_input) const override
DBG_BEGIN