Merge pull request #10044 from edolstra/empty-git-repos

Handle empty Git repositories / workdirs
This commit is contained in:
Théophane Hufschmitt 2024-02-20 14:01:23 +01:00 committed by GitHub
commit d2c6a93bd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 24 deletions

View file

@ -467,13 +467,13 @@ EvalState::~EvalState()
void EvalState::allowPath(const Path & path) void EvalState::allowPath(const Path & path)
{ {
if (auto rootFS2 = rootFS.dynamic_pointer_cast<AllowListInputAccessor>()) if (auto rootFS2 = rootFS.dynamic_pointer_cast<AllowListInputAccessor>())
rootFS2->allowPath(CanonPath(path)); rootFS2->allowPrefix(CanonPath(path));
} }
void EvalState::allowPath(const StorePath & storePath) void EvalState::allowPath(const StorePath & storePath)
{ {
if (auto rootFS2 = rootFS.dynamic_pointer_cast<AllowListInputAccessor>()) if (auto rootFS2 = rootFS.dynamic_pointer_cast<AllowListInputAccessor>())
rootFS2->allowPath(CanonPath(store->toRealPath(storePath))); rootFS2->allowPrefix(CanonPath(store->toRealPath(storePath)));
} }
void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & v) void EvalState::allowAndSetStorePathString(const StorePath & storePath, Value & v)

View file

@ -51,33 +51,33 @@ void FilteringInputAccessor::checkAccess(const CanonPath & path)
struct AllowListInputAccessorImpl : AllowListInputAccessor struct AllowListInputAccessorImpl : AllowListInputAccessor
{ {
std::set<CanonPath> allowedPaths; std::set<CanonPath> allowedPrefixes;
AllowListInputAccessorImpl( AllowListInputAccessorImpl(
ref<InputAccessor> next, ref<InputAccessor> next,
std::set<CanonPath> && allowedPaths, std::set<CanonPath> && allowedPrefixes,
MakeNotAllowedError && makeNotAllowedError) MakeNotAllowedError && makeNotAllowedError)
: AllowListInputAccessor(SourcePath(next), std::move(makeNotAllowedError)) : AllowListInputAccessor(SourcePath(next), std::move(makeNotAllowedError))
, allowedPaths(std::move(allowedPaths)) , allowedPrefixes(std::move(allowedPrefixes))
{ } { }
bool isAllowed(const CanonPath & path) override bool isAllowed(const CanonPath & path) override
{ {
return path.isAllowed(allowedPaths); return path.isAllowed(allowedPrefixes);
} }
void allowPath(CanonPath path) override void allowPrefix(CanonPath prefix) override
{ {
allowedPaths.insert(std::move(path)); allowedPrefixes.insert(std::move(prefix));
} }
}; };
ref<AllowListInputAccessor> AllowListInputAccessor::create( ref<AllowListInputAccessor> AllowListInputAccessor::create(
ref<InputAccessor> next, ref<InputAccessor> next,
std::set<CanonPath> && allowedPaths, std::set<CanonPath> && allowedPrefixes,
MakeNotAllowedError && makeNotAllowedError) MakeNotAllowedError && makeNotAllowedError)
{ {
return make_ref<AllowListInputAccessorImpl>(next, std::move(allowedPaths), std::move(makeNotAllowedError)); return make_ref<AllowListInputAccessorImpl>(next, std::move(allowedPrefixes), std::move(makeNotAllowedError));
} }
bool CachingFilteringInputAccessor::isAllowed(const CanonPath & path) bool CachingFilteringInputAccessor::isAllowed(const CanonPath & path)

View file

@ -54,18 +54,19 @@ struct FilteringInputAccessor : InputAccessor
}; };
/** /**
* A wrapping `InputAccessor` that checks paths against an allow-list. * A wrapping `InputAccessor` that checks paths against a set of
* allowed prefixes.
*/ */
struct AllowListInputAccessor : public FilteringInputAccessor struct AllowListInputAccessor : public FilteringInputAccessor
{ {
/** /**
* Grant access to the specified path. * Grant access to the specified prefix.
*/ */
virtual void allowPath(CanonPath path) = 0; virtual void allowPrefix(CanonPath prefix) = 0;
static ref<AllowListInputAccessor> create( static ref<AllowListInputAccessor> create(
ref<InputAccessor> next, ref<InputAccessor> next,
std::set<CanonPath> && allowedPaths, std::set<CanonPath> && allowedPrefixes,
MakeNotAllowedError && makeNotAllowedError); MakeNotAllowedError && makeNotAllowedError);
using FilteringInputAccessor::FilteringInputAccessor; using FilteringInputAccessor::FilteringInputAccessor;

View file

@ -2,6 +2,7 @@
#include "fs-input-accessor.hh" #include "fs-input-accessor.hh"
#include "input-accessor.hh" #include "input-accessor.hh"
#include "filtering-input-accessor.hh" #include "filtering-input-accessor.hh"
#include "memory-input-accessor.hh"
#include "cache.hh" #include "cache.hh"
#include "finally.hh" #include "finally.hh"
#include "processes.hh" #include "processes.hh"
@ -942,17 +943,21 @@ ref<InputAccessor> GitRepoImpl::getAccessor(const Hash & rev, bool exportIgnore)
ref<InputAccessor> GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError) ref<InputAccessor> GitRepoImpl::getAccessor(const WorkdirInfo & wd, bool exportIgnore, MakeNotAllowedError makeNotAllowedError)
{ {
auto self = ref<GitRepoImpl>(shared_from_this()); auto self = ref<GitRepoImpl>(shared_from_this());
/* In case of an empty workdir, return an empty in-memory tree. We
cannot use AllowListInputAccessor because it would return an
error for the root (and we can't add the root to the allow-list
since that would allow access to all its children). */
ref<InputAccessor> fileAccessor = ref<InputAccessor> fileAccessor =
AllowListInputAccessor::create( wd.files.empty()
makeFSInputAccessor(path), ? makeEmptyInputAccessor()
std::set<CanonPath> { wd.files }, : AllowListInputAccessor::create(
std::move(makeNotAllowedError)); makeFSInputAccessor(path),
if (exportIgnore) { std::set<CanonPath> { wd.files },
std::move(makeNotAllowedError)).cast<InputAccessor>();
if (exportIgnore)
return make_ref<GitExportIgnoreInputAccessor>(self, fileAccessor, std::nullopt); return make_ref<GitExportIgnoreInputAccessor>(self, fileAccessor, std::nullopt);
} else
else {
return fileAccessor; return fileAccessor;
}
} }
ref<GitFileSystemObjectSink> GitRepoImpl::getFileSystemObjectSink() ref<GitFileSystemObjectSink> GitRepoImpl::getFileSystemObjectSink()

View file

@ -158,6 +158,8 @@ std::vector<PublicKey> getPublicKeys(const Attrs & attrs)
} // end namespace } // end namespace
static const Hash nullRev{HashAlgorithm::SHA1};
struct GitInputScheme : InputScheme struct GitInputScheme : InputScheme
{ {
std::optional<Input> inputFromURL(const ParsedURL & url, bool requireTree) const override std::optional<Input> inputFromURL(const ParsedURL & url, bool requireTree) const override
@ -708,10 +710,12 @@ struct GitInputScheme : InputScheme
if (auto ref = repo->getWorkdirRef()) if (auto ref = repo->getWorkdirRef())
input.attrs.insert_or_assign("ref", *ref); input.attrs.insert_or_assign("ref", *ref);
auto rev = repoInfo.workdirInfo.headRev.value(); /* Return a rev of 000... if there are no commits yet. */
auto rev = repoInfo.workdirInfo.headRev.value_or(nullRev);
input.attrs.insert_or_assign("rev", rev.gitRev()); input.attrs.insert_or_assign("rev", rev.gitRev());
input.attrs.insert_or_assign("revCount", getRevCount(repoInfo, repoInfo.url, rev)); input.attrs.insert_or_assign("revCount",
rev == nullRev ? 0 : getRevCount(repoInfo, repoInfo.url, rev));
verifyCommit(input, repo); verifyCommit(input, repo);
} else { } else {

View file

@ -20,4 +20,10 @@ ref<MemoryInputAccessor> makeMemoryInputAccessor()
return make_ref<MemoryInputAccessorImpl>(); return make_ref<MemoryInputAccessorImpl>();
} }
ref<InputAccessor> makeEmptyInputAccessor()
{
static auto empty = makeMemoryInputAccessor().cast<InputAccessor>();
return empty;
}
} }

View file

@ -13,4 +13,6 @@ struct MemoryInputAccessor : InputAccessor
ref<MemoryInputAccessor> makeMemoryInputAccessor(); ref<MemoryInputAccessor> makeMemoryInputAccessor();
ref<InputAccessor> makeEmptyInputAccessor();
} }

View file

@ -271,3 +271,28 @@ git -C "$repo" add hello .gitignore
git -C "$repo" commit -m 'Bla1' git -C "$repo" commit -m 'Bla1'
cd "$repo" cd "$repo"
path11=$(nix eval --impure --raw --expr "(builtins.fetchGit ./.).outPath") path11=$(nix eval --impure --raw --expr "(builtins.fetchGit ./.).outPath")
# Test a workdir with no commits.
empty="$TEST_ROOT/empty"
git init "$empty"
emptyAttrs='{ lastModified = 0; lastModifiedDate = "19700101000000"; narHash = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo="; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }'
[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = $emptyAttrs ]]
echo foo > "$empty/x"
[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = $emptyAttrs ]]
git -C "$empty" add x
[[ $(nix eval --impure --expr "builtins.removeAttrs (builtins.fetchGit $empty) [\"outPath\"]") = '{ lastModified = 0; lastModifiedDate = "19700101000000"; narHash = "sha256-wzlAGjxKxpaWdqVhlq55q5Gxo4Bf860+kLeEa/v02As="; rev = "0000000000000000000000000000000000000000"; revCount = 0; shortRev = "0000000"; submodules = false; }' ]]
# Test a repo with an empty commit.
git -C "$empty" rm -f x
git -C "$empty" config user.email "foobar@example.com"
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"