Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/libfetchers-tests/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ TEST_F(GitUtilsTest, sink_basic)
// sink->createHardlink("foo-1.1/links/foo-2", CanonPath("foo-1.1/hello"));

auto result = repo->dereferenceSingletonDirectory(sink->flush());
auto accessor = repo->getAccessor(result, false, getRepoName());
auto accessor = repo->getAccessor(result, {}, getRepoName());
auto entries = accessor->readDirectory(CanonPath::root);
ASSERT_EQ(entries.size(), 5u);
ASSERT_EQ(accessor->readFile(CanonPath("hello")), "hello world");
Expand Down
7 changes: 4 additions & 3 deletions src/libfetchers/fetchers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(const Settings
auto accessor = make_ref<SubstitutedSourceAccessor>(ref{store->getFSAccessor(*storePath)});

accessor->fingerprint = getFingerprint(store);
cachedFingerprint = accessor->fingerprint;

// Store a cache entry for the substituted tree so later fetches
// can reuse the existing nar instead of copying the unpacked
Expand Down Expand Up @@ -357,10 +358,10 @@ std::pair<ref<SourceAccessor>, Input> Input::getAccessorUnchecked(const Settings
try {
auto [accessor, result] = scheme->getAccessor(settings, store, *this);

if (!accessor->fingerprint)
accessor->fingerprint = result.getFingerprint(store);
if (auto fp = accessor->getFingerprint(CanonPath::root).second)
result.cachedFingerprint = *fp;
else
result.cachedFingerprint = accessor->fingerprint;
accessor->fingerprint = result.getFingerprint(store);

return {accessor, std::move(result)};
} catch (Error & e) {
Expand Down
69 changes: 50 additions & 19 deletions src/libfetchers/git-utils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -550,14 +550,15 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this<GitRepoImpl>
}

/**
* A 'GitSourceAccessor' with no regard for export-ignore or any other transformations.
* A 'GitSourceAccessor' with no regard for export-ignore.
*/
ref<GitSourceAccessor> getRawAccessor(const Hash & rev, bool smudgeLfs = false);
ref<GitSourceAccessor> getRawAccessor(const Hash & rev, const GitAccessorOptions & options);

ref<SourceAccessor>
getAccessor(const Hash & rev, bool exportIgnore, std::string displayPrefix, bool smudgeLfs = false) override;
getAccessor(const Hash & rev, const GitAccessorOptions & options, std::string displayPrefix) override;

ref<SourceAccessor> getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError e) override;
ref<SourceAccessor>
getAccessor(const WorkdirInfo & wd, const GitAccessorOptions & options, MakeNotAllowedError e) override;

ref<GitFileSystemObjectSink> getFileSystemObjectSink() override;

Expand Down Expand Up @@ -700,7 +701,7 @@ struct GitRepoImpl : GitRepo, std::enable_shared_from_this<GitRepoImpl>

Hash treeHashToNarHash(const fetchers::Settings & settings, const Hash & treeHash) override
{
auto accessor = getAccessor(treeHash, false, "");
auto accessor = getAccessor(treeHash, {}, "");

fetchers::Cache::Key cacheKey{"treeHashToNarHash", {{"treeHash", treeHash.gitRev()}}};

Expand Down Expand Up @@ -737,28 +738,37 @@ ref<GitRepo> GitRepo::openRepo(const std::filesystem::path & path, bool create,
return make_ref<GitRepoImpl>(path, create, bare);
}

std::string GitAccessorOptions::makeFingerprint(const Hash & rev) const
{
return rev.gitRev() + (exportIgnore ? ";e" : "") + (smudgeLfs ? ";l" : "") + (applyFilters ? ";f" : "");
}

/**
* Raw git tree input accessor.
*/

struct GitSourceAccessor : SourceAccessor
{
struct State
{
ref<GitRepoImpl> repo;
git_oid oid;
Object root;
std::optional<lfs::Fetch> lfsFetch = std::nullopt;
GitAccessorOptions options;
};

Sync<State> state_;

GitSourceAccessor(ref<GitRepoImpl> repo_, const Hash & rev, bool smudgeLfs)
GitSourceAccessor(ref<GitRepoImpl> repo_, const Hash & rev, const GitAccessorOptions & options)
: state_{State{
.repo = repo_,
.oid = hashToOID(rev),
.root = peelToTreeOrBlob(lookupObject(*repo_, hashToOID(rev)).get()),
.lfsFetch = smudgeLfs ? std::make_optional(lfs::Fetch(*repo_, hashToOID(rev))) : std::nullopt,
.lfsFetch = options.smudgeLfs ? std::make_optional(lfs::Fetch(*repo_, hashToOID(rev))) : std::nullopt,
.options = options,
}}
{
fingerprint = options.makeFingerprint(rev);
}

std::string readBlob(const CanonPath & path, bool symlink)
Expand All @@ -784,7 +794,28 @@ struct GitSourceAccessor : SourceAccessor
}
}

return std::string((const char *) git_blob_rawcontent(blob.get()), git_blob_rawsize(blob.get()));
if (!state->options.applyFilters)
return std::string((const char *) git_blob_rawcontent(blob.get()), git_blob_rawsize(blob.get()));
else {
// Apply git filters including potential CRLF conversion
git_buf filtered = GIT_BUF_INIT;
git_blob_filter_options opts = GIT_BLOB_FILTER_OPTIONS_INIT;

opts.attr_commit_id = state->oid;
opts.flags = GIT_BLOB_FILTER_ATTRIBUTES_FROM_COMMIT;

int error = git_blob_filter(&filtered, blob.get(), path.rel_c_str(), &opts);
if (error != 0) {
const git_error * e = git_error_last();
std::string errorMsg = e ? e->message : "Unknown error";
git_buf_dispose(&filtered);
throw Error("Failed to filter blob: " + errorMsg);
}
std::string result(filtered.ptr, filtered.size);
git_buf_dispose(&filtered);

return result;
}
}

std::string readFile(const CanonPath & path) override
Expand Down Expand Up @@ -1307,26 +1338,26 @@ struct GitFileSystemObjectSinkImpl : GitFileSystemObjectSink
}
};

ref<GitSourceAccessor> GitRepoImpl::getRawAccessor(const Hash & rev, bool smudgeLfs)
ref<GitSourceAccessor> GitRepoImpl::getRawAccessor(const Hash & rev, const GitAccessorOptions & options)
{
auto self = ref<GitRepoImpl>(shared_from_this());
return make_ref<GitSourceAccessor>(self, rev, smudgeLfs);
return make_ref<GitSourceAccessor>(self, rev, options);
}

ref<SourceAccessor>
GitRepoImpl::getAccessor(const Hash & rev, bool exportIgnore, std::string displayPrefix, bool smudgeLfs)
GitRepoImpl::getAccessor(const Hash & rev, const GitAccessorOptions & options, std::string displayPrefix)
{
auto self = ref<GitRepoImpl>(shared_from_this());
ref<GitSourceAccessor> rawGitAccessor = getRawAccessor(rev, smudgeLfs);
ref<GitSourceAccessor> rawGitAccessor = getRawAccessor(rev, options);
rawGitAccessor->setPathDisplay(std::move(displayPrefix));
if (exportIgnore)
if (options.exportIgnore)
return make_ref<GitExportIgnoreSourceAccessor>(self, rawGitAccessor, rev);
else
return rawGitAccessor;
}

ref<SourceAccessor>
GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError)
ref<SourceAccessor> GitRepoImpl::getAccessor(
const WorkdirInfo & wd, const GitAccessorOptions & options, MakeNotAllowedError makeNotAllowedError)
{
auto self = ref<GitRepoImpl>(shared_from_this());
ref<SourceAccessor> fileAccessor = AllowListSourceAccessor::create(
Expand All @@ -1336,7 +1367,7 @@ GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllow
boost::unordered_flat_set<CanonPath>{CanonPath::root},
std::move(makeNotAllowedError))
.cast<SourceAccessor>();
if (exportIgnore)
if (options.exportIgnore)
fileAccessor = make_ref<GitExportIgnoreSourceAccessor>(self, fileAccessor, std::nullopt);
return fileAccessor;
}
Expand All @@ -1351,7 +1382,7 @@ std::vector<std::tuple<GitRepoImpl::Submodule, Hash>> GitRepoImpl::getSubmodules
/* Read the .gitmodules files from this revision. */
CanonPath modulesFile(".gitmodules");

auto accessor = getAccessor(rev, exportIgnore, "");
auto accessor = getAccessor(rev, {.exportIgnore = exportIgnore}, "");
if (!accessor->pathExists(modulesFile))
return {};

Expand All @@ -1368,7 +1399,7 @@ std::vector<std::tuple<GitRepoImpl::Submodule, Hash>> GitRepoImpl::getSubmodules

std::vector<std::tuple<Submodule, Hash>> result;

auto rawAccessor = getRawAccessor(rev);
auto rawAccessor = getRawAccessor(rev, {});

for (auto & submodule : parseSubmodules(pathTemp)) {
/* Filter out .gitmodules entries that don't exist or are not
Expand Down
62 changes: 48 additions & 14 deletions src/libfetchers/git.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "nix/util/json-utils.hh"
#include "nix/util/archive.hh"
#include "nix/util/mounted-source-accessor.hh"
#include "nix/fetchers/fetch-to-store.hh"

#include <regex>
#include <string.h>
Expand Down Expand Up @@ -637,6 +638,15 @@ struct GitInputScheme : InputScheme
return shallow || input.getRevCount().has_value();
}

std::string makeFingerprint(const Input & input, const Hash & rev) const
{
auto options = GitAccessorOptions{
.exportIgnore = getExportIgnoreAttr(input),
.smudgeLfs = getLfsAttr(input),
};
return options.makeFingerprint(rev) + (getSubmodulesAttr(input) ? ";s" : "");
}

std::pair<ref<SourceAccessor>, Input>
getAccessorFromCommit(const Settings & settings, ref<Store> store, RepoInfo & repoInfo, Input && input) const
{
Expand Down Expand Up @@ -779,17 +789,46 @@ struct GitInputScheme : InputScheme

verifyCommit(input, repo);

bool exportIgnore = getExportIgnoreAttr(input);
bool smudgeLfs = getLfsAttr(input);
auto accessor = repo->getAccessor(rev, exportIgnore, "«" + input.to_string(true) + "»", smudgeLfs);
GitAccessorOptions options{
.exportIgnore = getExportIgnoreAttr(input),
.smudgeLfs = getLfsAttr(input),
};
auto accessor = repo->getAccessor(rev, options, "«" + input.to_string(true) + "»");

/* Backward compatibility hack for locks produced by Nix < 2.20 that depend on Nix applying Git filters or
* `export-ignore`. Nix >= 2.20 doesn't do those, so we may get a NAR hash mismatch. If that happens, try again
* with filters and export-ignore enabled. */
if (auto expectedNarHash = input.getNarHash()) {
if (accessor->pathExists(CanonPath(".gitattributes"))) {
auto narHashNew =
fetchToStore2(settings, *store, {accessor}, FetchMode::DryRun, input.getName()).second;
if (expectedNarHash != narHashNew) {
GitAccessorOptions options2{.exportIgnore = true, .applyFilters = true};
auto accessor2 = repo->getAccessor(rev, options2, "«" + input.to_string(true) + "»");
auto narHashOld =
fetchToStore2(settings, *store, {accessor2}, FetchMode::DryRun, input.getName()).second;
if (expectedNarHash == narHashOld) {
warn(
"Git input '%s' specifies a NAR hash '%s' that was created by Nix < 2.20.\n"
"Nix >= 2.20 does not apply Git filters and `export-ignore` by default, which changes the NAR hash.\n"
"Please update the NAR hash to '%s'.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: update this to recommend re-locking with a newer Nix version, and include a link to docs.determinate.systems on other approaches for extending backwards compatibility

input.to_string(),
expectedNarHash->to_string(HashFormat::SRI, true),
narHashNew.to_string(HashFormat::SRI, true));
accessor = accessor2;
options = options2;
}
}
}
}

/* If the repo has submodules, fetch them and return a mounted
input accessor consisting of the accessor for the top-level
repo and the accessors for the submodules. */
if (getSubmodulesAttr(input)) {
std::map<CanonPath, nix::ref<SourceAccessor>> mounts;

for (auto & [submodule, submoduleRev] : repo->getSubmodules(rev, exportIgnore)) {
for (auto & [submodule, submoduleRev] : repo->getSubmodules(rev, options.exportIgnore)) {
auto resolved = repo->resolveSubmoduleUrl(submodule.url);
debug(
"Git submodule %s: %s %s %s -> %s",
Expand All @@ -812,9 +851,9 @@ struct GitInputScheme : InputScheme
}
}
attrs.insert_or_assign("rev", submoduleRev.gitRev());
attrs.insert_or_assign("exportIgnore", Explicit<bool>{exportIgnore});
attrs.insert_or_assign("exportIgnore", Explicit<bool>{options.exportIgnore});
attrs.insert_or_assign("submodules", Explicit<bool>{true});
attrs.insert_or_assign("lfs", Explicit<bool>{smudgeLfs});
attrs.insert_or_assign("lfs", Explicit<bool>{options.smudgeLfs});
attrs.insert_or_assign("allRefs", Explicit<bool>{true});
auto submoduleInput = fetchers::Input::fromAttrs(settings, std::move(attrs));
auto [submoduleAccessor, submoduleInput2] = submoduleInput.getAccessor(settings, store);
Expand Down Expand Up @@ -848,7 +887,7 @@ struct GitInputScheme : InputScheme
auto exportIgnore = getExportIgnoreAttr(input);

ref<SourceAccessor> accessor =
repo->getAccessor(repoInfo.workdirInfo, exportIgnore, makeNotAllowedError(repoPath));
repo->getAccessor(repoInfo.workdirInfo, {.exportIgnore = exportIgnore}, makeNotAllowedError(repoPath));

/* If the repo has submodules, return a mounted input accessor
consisting of the accessor for the top-level repo and the
Expand Down Expand Up @@ -942,13 +981,8 @@ struct GitInputScheme : InputScheme

std::optional<std::string> getFingerprint(ref<Store> store, const Input & input) const override
{
auto makeFingerprint = [&](const Hash & rev) {
return rev.gitRev() + (getSubmodulesAttr(input) ? ";s" : "") + (getExportIgnoreAttr(input) ? ";e" : "")
+ (getLfsAttr(input) ? ";l" : "");
};

if (auto rev = input.getRev())
return makeFingerprint(*rev);
return makeFingerprint(input, *rev);
else {
auto repoInfo = getRepoInfo(input);
if (auto repoPath = repoInfo.getPath(); repoPath && repoInfo.workdirInfo.submodules.empty()) {
Expand All @@ -964,7 +998,7 @@ struct GitInputScheme : InputScheme
writeString("deleted:", hashSink);
writeString(file.abs(), hashSink);
}
return makeFingerprint(repoInfo.workdirInfo.headRev.value_or(nullRev))
return makeFingerprint(input, repoInfo.workdirInfo.headRev.value_or(nullRev))
+ ";d=" + hashSink.finish().hash.to_string(HashFormat::Base16, false);
}
return std::nullopt;
Expand Down
2 changes: 1 addition & 1 deletion src/libfetchers/github.cc
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ struct GitArchiveInputScheme : InputScheme
input.attrs.insert_or_assign("lastModified", uint64_t(tarballInfo.lastModified));

auto accessor =
settings.getTarballCache()->getAccessor(tarballInfo.treeHash, false, "«" + input.to_string(true) + "»");
settings.getTarballCache()->getAccessor(tarballInfo.treeHash, {}, "«" + input.to_string(true) + "»");

if (!settings.trustTarballsFromGitForges)
// FIXME: computing the NAR hash here is wasteful if
Expand Down
15 changes: 12 additions & 3 deletions src/libfetchers/include/nix/fetchers/git-utils.hh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ struct GitFileSystemObjectSink : ExtendedFileSystemObjectSink
virtual Hash flush() = 0;
};

struct GitAccessorOptions
{
bool exportIgnore = false;
bool smudgeLfs = false;
bool applyFilters = false;

std::string makeFingerprint(const Hash & rev) const;
};

struct GitRepo
{
virtual ~GitRepo() {}
Expand Down Expand Up @@ -89,10 +98,10 @@ struct GitRepo
virtual bool hasObject(const Hash & oid) = 0;

virtual ref<SourceAccessor>
getAccessor(const Hash & rev, bool exportIgnore, std::string displayPrefix, bool smudgeLfs = false) = 0;
getAccessor(const Hash & rev, const GitAccessorOptions & options, std::string displayPrefix) = 0;

virtual ref<SourceAccessor>
getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) = 0;
virtual ref<SourceAccessor> getAccessor(
const WorkdirInfo & wd, const GitAccessorOptions & options, MakeNotAllowedError makeNotAllowedError) = 0;

virtual ref<GitFileSystemObjectSink> getFileSystemObjectSink() = 0;

Expand Down
2 changes: 1 addition & 1 deletion src/libfetchers/tarball.cc
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ static DownloadTarballResult downloadTarball_(
.treeHash = treeHash,
.lastModified = (time_t) getIntAttr(infoAttrs, "lastModified"),
.immutableUrl = maybeGetStrAttr(infoAttrs, "immutableUrl"),
.accessor = settings.getTarballCache()->getAccessor(treeHash, false, displayPrefix),
.accessor = settings.getTarballCache()->getAccessor(treeHash, {}, displayPrefix),
};
};

Expand Down
32 changes: 32 additions & 0 deletions tests/functional/fetchGit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,35 @@ git -C "$empty" config user.name "Foobar"
git -C "$empty" commit --allow-empty --allow-empty-message --message ""

nix eval --impure --expr "let attrs = builtins.fetchGit $empty; in assert attrs.lastModified != 0; assert attrs.rev != \"0000000000000000000000000000000000000000\"; assert attrs.revCount == 1; true"

# Test backward compatibility hack for Nix < 2.20 locks / fetchTree calls that expect Git filters to be applied.
eol="$TEST_ROOT/git-eol"
mkdir -p "$eol"
git init "$eol"
git -C "$eol" config user.email "foobar@example.com"
git -C "$eol" config user.name "Foobar"
printf "Hello\nWorld\n" > "$eol/crlf"
printf "ignore me" > "$eol/ignored"
git -C "$eol" add crlf ignored
git -C "$eol" commit -a -m Initial
printf "crlf text eol=crlf\nignored export-ignore\n" > "$eol/.gitattributes"
git -C "$eol" add .gitattributes
git -C "$eol" commit -a -m 'Apply gitattributes'

rev="$(git -C "$eol" rev-parse HEAD)"

export _NIX_TEST_BARF_ON_UNCACHEABLE=1

oldHash="sha256-OizkFa+lIZbi+DCfbH8AYzsJ+BRmW0u/a7KKuzXADEU="
newHash="sha256-hKHmbBsoVwqPNkL0MXL0RAtbOCqK6tP4xpX2tKR6GPI="

expectStderr 0 nix eval --expr \
"let tree = builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"$oldHash\"; }; in assert builtins.readFile \"\${tree}/crlf\" == \"Hello\r\nWorld\r\n\"; assert !builtins.pathExists \"\${tree}/ignored\"; true" \
| grepQuiet "Please update the NAR hash to '$newHash'"

nix eval --expr \
"let tree = builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"$newHash\"; }; in assert builtins.readFile \"\${tree}/crlf\" == \"Hello\nWorld\n\"; assert builtins.pathExists \"\${tree}/ignored\"; true"

expectStderr 102 nix eval --expr \
"builtins.fetchTree { type = \"git\"; url = \"file://$eol\"; rev = \"$rev\"; narHash = \"sha256-DLDvcwdcwCxnuPTxSQ6gLAyopB20lD0bOQoQB3i2hsA=\"; }" \
| grepQuiet "NAR hash mismatch"