aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/manual/src/release-notes/rl-next.md5
-rw-r--r--flake.nix2
-rw-r--r--misc/zsh/local.mk1
-rw-r--r--misc/zsh/run-help-nix42
-rw-r--r--src/libfetchers/git-utils.cc27
-rw-r--r--src/libfetchers/git-utils.hh23
-rw-r--r--src/libfetchers/git.cc371
-rw-r--r--src/libfetchers/github.cc28
-rw-r--r--src/libstore/build/derivation-goal.cc5
-rw-r--r--src/libstore/s3.hh1
-rw-r--r--src/nix/flake.md7
-rw-r--r--src/nix/fmt.cc3
-rw-r--r--tests/build-remote.sh3
-rw-r--r--tests/common.sh.in11
-rw-r--r--tests/fetchClosure.sh2
-rw-r--r--tests/fetchGit.sh8
-rw-r--r--tests/flakes.sh9
-rw-r--r--tests/lang.sh11
-rw-r--r--tests/post-hook.sh6
-rwxr-xr-xtests/push-to-store.sh4
20 files changed, 410 insertions, 159 deletions
diff --git a/doc/manual/src/release-notes/rl-next.md b/doc/manual/src/release-notes/rl-next.md
index 06c2583c9..7b3ad4e58 100644
--- a/doc/manual/src/release-notes/rl-next.md
+++ b/doc/manual/src/release-notes/rl-next.md
@@ -1,5 +1,10 @@
# Release X.Y (202?-??-??)
+* Nix now provides better integration with zsh's run-help feature. It is now
+ included in the Nix installation in the form of an autoloadable shell
+ function, run-help-nix. It picks up Nix subcommands from the currently typed
+ in command and directs the user to the associated man pages.
+
* `nix repl` has a new build-'n-link (`:bl`) command that builds a derivation
while creating GC root symlinks.
diff --git a/flake.nix b/flake.nix
index 87b00edf4..dd3a25e9e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -23,7 +23,7 @@
crossSystems = [ "armv6l-linux" "armv7l-linux" ];
- stdenvs = [ "gccStdenv" "clangStdenv" "clang11Stdenv" "stdenv" ];
+ stdenvs = [ "gccStdenv" "clangStdenv" "clang11Stdenv" "stdenv" "libcxxStdenv" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
forAllSystemsAndStdenvs = f: forAllSystems (system:
diff --git a/misc/zsh/local.mk b/misc/zsh/local.mk
index 418fb1377..0b4e294fb 100644
--- a/misc/zsh/local.mk
+++ b/misc/zsh/local.mk
@@ -1 +1,2 @@
$(eval $(call install-file-as, $(d)/completion.zsh, $(datarootdir)/zsh/site-functions/_nix, 0644))
+$(eval $(call install-file-as, $(d)/run-help-nix, $(datarootdir)/zsh/site-functions/run-help-nix, 0644))
diff --git a/misc/zsh/run-help-nix b/misc/zsh/run-help-nix
new file mode 100644
index 000000000..f91a304eb
--- /dev/null
+++ b/misc/zsh/run-help-nix
@@ -0,0 +1,42 @@
+emulate -L zsh
+
+# run-help is a zsh widget that can be bound to a key. It mainly looks up the
+# man page for the currently typed in command.
+#
+# Although run-help works for any command without requiring special support,
+# it can only deduce the right man page based solely on the name of the
+# command. Programs like Nix provide better integration with run-help by
+# helping zsh identify Nix subcommands and their corresponding man pages. This
+# is what this function does.
+#
+# To actually use run-help on zsh, place the following lines in your .zshrc:
+#
+# (( $+aliases[run-help] )) && unalias run-help
+# autoload -Uz run-help run-help-nix
+#
+# Then also assign run-help to any key of choice:
+#
+# bindkey '^[h' run-help
+
+while [[ "$#" != 0 && "$1" == -* ]]; do
+ shift
+done
+
+local -a subcommands; subcommands=( nix3 )
+
+local arg
+for arg in "$@"; do
+ if man -w "${(j:-:)subcommands}-$arg" >/dev/null 2>&1; then
+ subcommands+="$arg"
+ else
+ break
+ fi
+done
+
+if (( $#subcommands > 1 )); then
+ man "${(j:-:)subcommands}"
+else
+ man nix
+fi
+
+return $?
diff --git a/src/libfetchers/git-utils.cc b/src/libfetchers/git-utils.cc
new file mode 100644
index 000000000..b2d6b7893
--- /dev/null
+++ b/src/libfetchers/git-utils.cc
@@ -0,0 +1,27 @@
+#include "git-utils.hh"
+
+#include <regex>
+
+std::optional<std::string> parseListReferenceHeadRef(std::string_view line)
+{
+ const static std::regex head_ref_regex("^ref: ([^\\s]+)\\t+HEAD$");
+ std::match_results<std::string_view::const_iterator> match;
+ if (std::regex_match(line.cbegin(), line.cend(), match, head_ref_regex)) {
+ return match[1];
+ } else {
+ return std::nullopt;
+ }
+}
+
+std::optional<std::string> parseListReferenceForRev(std::string_view rev, std::string_view line)
+{
+ const static std::regex rev_regex("^([^\\t]+)\\t+(.*)$");
+ std::match_results<std::string_view::const_iterator> match;
+ if (!std::regex_match(line.cbegin(), line.cend(), match, rev_regex)) {
+ return std::nullopt;
+ }
+ if (rev != match[2].str()) {
+ return std::nullopt;
+ }
+ return match[1];
+}
diff --git a/src/libfetchers/git-utils.hh b/src/libfetchers/git-utils.hh
new file mode 100644
index 000000000..946a68a9e
--- /dev/null
+++ b/src/libfetchers/git-utils.hh
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <optional>
+
+// Parses the HEAD ref as reported by `git ls-remote --symref`
+//
+// Returns the head branch name as reported by `git ls-remote --symref`, e.g., if
+// ls-remote returns the output below, "main" is returned based on the ref line.
+//
+// ref: refs/heads/main HEAD
+//
+// If the repository is in 'detached head' state (HEAD is pointing to a rev
+// instead of a branch), parseListReferenceForRev("HEAD") may be used instead.
+std::optional<std::string> parseListReferenceHeadRef(std::string_view line);
+
+// Parses a reference line from `git ls-remote --symref`, e.g.,
+// parseListReferenceForRev("refs/heads/master", line) will return 6926...
+// given the line below.
+//
+// 6926beab444c33fb57b21819b6642d032016bb1e refs/heads/master
+std::optional<std::string> parseListReferenceForRev(std::string_view rev, std::string_view line);
diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc
index af40990e5..266246fe9 100644
--- a/src/libfetchers/git.cc
+++ b/src/libfetchers/git.cc
@@ -5,15 +5,20 @@
#include "store-api.hh"
#include "url-parts.hh"
#include "pathlocks.hh"
+#include "util.hh"
+#include "git-utils.hh"
#include "fetch-settings.hh"
+#include <regex>
+#include <string.h>
#include <sys/time.h>
#include <sys/wait.h>
using namespace std::string_literals;
namespace nix::fetchers {
+namespace {
// Explicit initial branch of our bare repo to suppress warnings from new version of git.
// The value itself does not matter, since we always fetch a specific revision or branch.
@@ -21,25 +26,228 @@ namespace nix::fetchers {
// old version of git, which will ignore unrecognized `-c` options.
const std::string gitInitialBranch = "__nix_dummy_branch";
-static std::string getGitDir()
+std::string getGitDir()
{
- auto gitDir = getEnv("GIT_DIR");
- if (!gitDir) {
- return ".git";
+ return getEnv("GIT_DIR").value_or(".git");
+}
+
+bool isCacheFileWithinTtl(const time_t now, const struct stat & st)
+{
+ return st.st_mtime + settings.tarballTtl > now;
+}
+
+bool touchCacheFile(const Path& path, const time_t& touch_time)
+{
+ struct timeval times[2];
+ times[0].tv_sec = touch_time;
+ times[0].tv_usec = 0;
+ times[1].tv_sec = touch_time;
+ times[1].tv_usec = 0;
+
+ return lutimes(path.c_str(), times) == 0;
+}
+
+Path getCachePath(std::string key)
+{
+ return getCacheDir() + "/nix/gitv3/" +
+ hashString(htSHA256, key).to_string(Base32, false);
+}
+
+// Returns the name of the HEAD branch.
+//
+// Returns the head branch name as reported by git ls-remote --symref, e.g., if
+// ls-remote returns the output below, "main" is returned based on the ref line.
+//
+// ref: refs/heads/main HEAD
+// ...
+std::optional<std::string> readHead(const Path & path)
+{
+ auto [exit_code, output] = runProgram(RunOptions {
+ .program = "git",
+ .args = {"ls-remote", "--symref", path},
+ });
+ if (exit_code != 0) {
+ return std::nullopt;
+ }
+
+ std::string_view line = output;
+ line = line.substr(0, line.find("\n"));
+ if (const auto ref = parseListReferenceHeadRef(line); ref) {
+ debug("resolved HEAD ref '%s' for repo '%s'", *ref, path);
+ return *ref;
+ }
+ if (const auto rev = parseListReferenceForRev("HEAD", line); rev) {
+ debug("resolved HEAD rev '%s' for repo '%s'", *rev, path);
+ return *rev;
}
- return *gitDir;
+ return std::nullopt;
}
-static std::string readHead(const Path & path)
+// Persist the HEAD ref from the remote repo in the local cached repo.
+bool storeCachedHead(const std::string& actualUrl, const std::string& headRef)
{
- return chomp(runProgram("git", true, { "-C", path, "--git-dir", ".git", "rev-parse", "--abbrev-ref", "HEAD" }));
+ Path cacheDir = getCachePath(actualUrl);
+ try {
+ runProgram("git", true, { "-C", cacheDir, "symbolic-ref", "--", "HEAD", headRef });
+ } catch (ExecError &e) {
+ if (!WIFEXITED(e.status)) throw;
+ return false;
+ }
+ /* No need to touch refs/HEAD, because `git symbolic-ref` updates the mtime. */
+ return true;
}
-static bool isNotDotGitDirectory(const Path & path)
+std::optional<std::string> readHeadCached(const std::string& actualUrl)
+{
+ // Create a cache path to store the branch of the HEAD ref. Append something
+ // in front of the URL to prevent collision with the repository itself.
+ Path cacheDir = getCachePath(actualUrl);
+ Path headRefFile = cacheDir + "/HEAD";
+
+ time_t now = time(0);
+ struct stat st;
+ std::optional<std::string> cachedRef;
+ if (stat(headRefFile.c_str(), &st) == 0) {
+ cachedRef = readHead(cacheDir);
+ if (cachedRef != std::nullopt &&
+ *cachedRef != gitInitialBranch &&
+ isCacheFileWithinTtl(now, st)) {
+ debug("using cached HEAD ref '%s' for repo '%s'", *cachedRef, actualUrl);
+ return cachedRef;
+ }
+ }
+
+ auto ref = readHead(actualUrl);
+ if (ref) {
+ return ref;
+ }
+
+ if (cachedRef) {
+ // If the cached git ref is expired in fetch() below, and the 'git fetch'
+ // fails, it falls back to continuing with the most recent version.
+ // This function must behave the same way, so we return the expired
+ // cached ref here.
+ warn("could not get HEAD ref for repository '%s'; using expired cached ref '%s'", actualUrl, *cachedRef);
+ return *cachedRef;
+ }
+
+ return std::nullopt;
+}
+
+bool isNotDotGitDirectory(const Path & path)
{
return baseNameOf(path) != ".git";
}
+struct WorkdirInfo
+{
+ bool clean = false;
+ bool hasHead = false;
+};
+
+// Returns whether a git workdir is clean and has commits.
+WorkdirInfo getWorkdirInfo(const Input & input, const Path & workdir)
+{
+ const bool submodules = maybeGetBoolAttr(input.attrs, "submodules").value_or(false);
+ auto gitDir = getGitDir();
+
+ auto env = getEnv();
+ // Set LC_ALL to C: because we rely on the error messages from git rev-parse to determine what went wrong
+ // that way unknown errors can lead to a failure instead of continuing through the wrong code path
+ env["LC_ALL"] = "C";
+
+ /* Check whether HEAD points to something that looks like a commit,
+ since that is the refrence we want to use later on. */
+ auto result = runProgram(RunOptions {
+ .program = "git",
+ .args = { "-C", workdir, "--git-dir", gitDir, "rev-parse", "--verify", "--no-revs", "HEAD^{commit}" },
+ .environment = env,
+ .mergeStderrToStdout = true
+ });
+ auto exitCode = WEXITSTATUS(result.first);
+ auto errorMessage = result.second;
+
+ if (errorMessage.find("fatal: not a git repository") != std::string::npos) {
+ throw Error("'%s' is not a Git repository", workdir);
+ } else if (errorMessage.find("fatal: Needed a single revision") != std::string::npos) {
+ // indicates that the repo does not have any commits
+ // we want to proceed and will consider it dirty later
+ } else if (exitCode != 0) {
+ // any other errors should lead to a failure
+ throw Error("getting the HEAD of the Git tree '%s' failed with exit code %d:\n%s", workdir, exitCode, errorMessage);
+ }
+
+ bool clean = false;
+ bool hasHead = exitCode == 0;
+
+ try {
+ if (hasHead) {
+ // Using git diff is preferrable over lower-level operations here,
+ // because its conceptually simpler and we only need the exit code anyways.
+ auto gitDiffOpts = Strings({ "-C", workdir, "diff", "HEAD", "--quiet"});
+ if (!submodules) {
+ // Changes in submodules should only make the tree dirty
+ // when those submodules will be copied as well.
+ gitDiffOpts.emplace_back("--ignore-submodules");
+ }
+ gitDiffOpts.emplace_back("--");
+ runProgram("git", true, gitDiffOpts);
+
+ clean = true;
+ }
+ } catch (ExecError & e) {
+ if (!WIFEXITED(e.status) || WEXITSTATUS(e.status) != 1) throw;
+ }
+
+ return WorkdirInfo { .clean = clean, .hasHead = hasHead };
+}
+
+std::pair<StorePath, Input> fetchFromWorkdir(ref<Store> store, Input & input, const Path & workdir, const WorkdirInfo & workdirInfo)
+{
+ const bool submodules = maybeGetBoolAttr(input.attrs, "submodules").value_or(false);
+
+ if (!fetchSettings.allowDirty)
+ throw Error("Git tree '%s' is dirty", workdir);
+
+ if (fetchSettings.warnDirty)
+ warn("Git tree '%s' is dirty", workdir);
+
+ auto gitOpts = Strings({ "-C", workdir, "ls-files", "-z" });
+ if (submodules)
+ gitOpts.emplace_back("--recurse-submodules");
+
+ auto files = tokenizeString<std::set<std::string>>(
+ runProgram("git", true, gitOpts), "\0"s);
+
+ Path actualPath(absPath(workdir));
+
+ PathFilter filter = [&](const Path & p) -> bool {
+ assert(hasPrefix(p, 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() && hasPrefix(*i, prefix);
+ }
+
+ return files.count(file);
+ };
+
+ auto storePath = store->addToStore(input.getName(), actualPath, FileIngestionMethod::Recursive, htSHA256, filter);
+
+ // FIXME: maybe we should use the timestamp of the last
+ // modified dirty file?
+ input.attrs.insert_or_assign(
+ "lastModified",
+ workdirInfo.hasHead ? std::stoull(runProgram("git", true, { "-C", actualPath, "log", "-1", "--format=%ct", "--no-show-signature", "HEAD" })) : 0);
+
+ return {std::move(storePath), input};
+}
+} // end namespace
+
struct GitInputScheme : InputScheme
{
std::optional<Input> inputFromURL(const ParsedURL & url) override
@@ -234,106 +442,16 @@ struct GitInputScheme : InputScheme
auto [isLocal, actualUrl_] = getActualUrl(input);
auto actualUrl = actualUrl_; // work around clang bug
- // If this is a local directory and no ref or revision is
- // given, then allow the use of an unclean working tree.
+ /* If this is a local directory and no ref or revision is given,
+ allow fetching directly from a dirty workdir. */
if (!input.getRef() && !input.getRev() && isLocal) {
- bool clean = false;
-
- auto env = getEnv();
- // Set LC_ALL to C: because we rely on the error messages from git rev-parse to determine what went wrong
- // that way unknown errors can lead to a failure instead of continuing through the wrong code path
- env["LC_ALL"] = "C";
-
- /* Check whether HEAD points to something that looks like a commit,
- since that is the refrence we want to use later on. */
- auto result = runProgram(RunOptions {
- .program = "git",
- .args = { "-C", actualUrl, "--git-dir", gitDir, "rev-parse", "--verify", "--no-revs", "HEAD^{commit}" },
- .environment = env,
- .mergeStderrToStdout = true
- });
- auto exitCode = WEXITSTATUS(result.first);
- auto errorMessage = result.second;
-
- if (errorMessage.find("fatal: not a git repository") != std::string::npos) {
- throw Error("'%s' is not a Git repository", actualUrl);
- } else if (errorMessage.find("fatal: Needed a single revision") != std::string::npos) {
- // indicates that the repo does not have any commits
- // we want to proceed and will consider it dirty later
- } else if (exitCode != 0) {
- // any other errors should lead to a failure
- throw Error("getting the HEAD of the Git tree '%s' failed with exit code %d:\n%s", actualUrl, exitCode, errorMessage);
- }
-
- bool hasHead = exitCode == 0;
- try {
- if (hasHead) {
- // Using git diff is preferrable over lower-level operations here,
- // because its conceptually simpler and we only need the exit code anyways.
- auto gitDiffOpts = Strings({ "-C", actualUrl, "--git-dir", gitDir, "diff", "HEAD", "--quiet"});
- if (!submodules) {
- // Changes in submodules should only make the tree dirty
- // when those submodules will be copied as well.
- gitDiffOpts.emplace_back("--ignore-submodules");
- }
- gitDiffOpts.emplace_back("--");
- runProgram("git", true, gitDiffOpts);
-
- clean = true;
- }
- } catch (ExecError & e) {
- if (!WIFEXITED(e.status) || WEXITSTATUS(e.status) != 1) throw;
- }
-
- if (!clean) {
-
- /* This is an unclean working tree. So copy all tracked files. */
-
- if (!fetchSettings.allowDirty)
- throw Error("Git tree '%s' is dirty", actualUrl);
-
- if (fetchSettings.warnDirty)
- warn("Git tree '%s' is dirty", actualUrl);
-
- auto gitOpts = Strings({ "-C", actualUrl, "--git-dir", gitDir, "ls-files", "-z" });
- if (submodules)
- gitOpts.emplace_back("--recurse-submodules");
-
- auto files = tokenizeString<std::set<std::string>>(
- runProgram("git", true, gitOpts), "\0"s);
-
- Path actualPath(absPath(actualUrl));
-
- PathFilter filter = [&](const Path & p) -> bool {
- assert(hasPrefix(p, 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() && hasPrefix(*i, prefix);
- }
-
- return files.count(file);
- };
-
- auto storePath = store->addToStore(input.getName(), actualPath, FileIngestionMethod::Recursive, htSHA256, filter);
-
- // FIXME: maybe we should use the timestamp of the last
- // modified dirty file?
- input.attrs.insert_or_assign(
- "lastModified",
- hasHead ? std::stoull(runProgram("git", true, { "-C", actualPath, "--git-dir", gitDir, "log", "-1", "--format=%ct", "--no-show-signature", "HEAD" })) : 0);
-
- return {std::move(storePath), input};
+ auto workdirInfo = getWorkdirInfo(input, actualUrl);
+ if (!workdirInfo.clean) {
+ return fetchFromWorkdir(store, input, actualUrl, workdirInfo);
}
}
- if (!input.getRef()) input.attrs.insert_or_assign("ref", isLocal ? readHead(actualUrl) : "master");
-
- Attrs unlockedAttrs({
+ const Attrs unlockedAttrs({
{"type", cacheType},
{"name", name},
{"url", actualUrl},
@@ -343,14 +461,30 @@ struct GitInputScheme : InputScheme
Path repoDir;
if (isLocal) {
+ if (!input.getRef()) {
+ auto head = readHead(actualUrl);
+ if (!head) {
+ warn("could not read HEAD ref from repo at '%s', using 'master'", actualUrl);
+ head = "master";
+ }
+ input.attrs.insert_or_assign("ref", *head);
+ }
if (!input.getRev())
input.attrs.insert_or_assign("rev",
Hash::parseAny(chomp(runProgram("git", true, { "-C", actualUrl, "--git-dir", gitDir, "rev-parse", *input.getRef() })), htSHA1).gitRev());
repoDir = actualUrl;
-
} else {
+ const bool useHeadRef = !input.getRef();
+ if (useHeadRef) {
+ auto head = readHeadCached(actualUrl);
+ if (!head) {
+ warn("could not read HEAD ref from repo at '%s', using 'master'", actualUrl);
+ head = "master";
+ }
+ input.attrs.insert_or_assign("ref", *head);
+ }
if (auto res = getCache()->lookup(store, unlockedAttrs)) {
auto rev2 = Hash::parseAny(getStrAttr(res->first, "rev"), htSHA1);
@@ -360,7 +494,7 @@ struct GitInputScheme : InputScheme
}
}
- Path cacheDir = getCacheDir() + "/nix/gitv3/" + hashString(htSHA256, actualUrl).to_string(Base32, false);
+ Path cacheDir = getCachePath(actualUrl);
repoDir = cacheDir;
gitDir = ".";
@@ -383,7 +517,7 @@ struct GitInputScheme : InputScheme
repo. */
if (input.getRev()) {
try {
- runProgram("git", true, { "-C", repoDir, "cat-file", "-e", input.getRev()->gitRev() });
+ runProgram("git", true, { "-C", repoDir, "--git-dir", gitDir, "cat-file", "-e", input.getRev()->gitRev() });
doFetch = false;
} catch (ExecError & e) {
if (WIFEXITED(e.status)) {
@@ -400,7 +534,7 @@ struct GitInputScheme : InputScheme
git fetch to update the local ref to the remote ref. */
struct stat st;
doFetch = stat(localRefFile.c_str(), &st) != 0 ||
- (uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now;
+ !isCacheFileWithinTtl(now, st);
}
}
@@ -418,19 +552,16 @@ struct GitInputScheme : InputScheme
: ref == "HEAD"
? *ref
: "refs/heads/" + *ref;
- runProgram("git", true, { "-C", repoDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", fetchRef, fetchRef) });
+ runProgram("git", true, { "-C", repoDir, "--git-dir", gitDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", fetchRef, fetchRef) });
} catch (Error & e) {
if (!pathExists(localRefFile)) throw;
warn("could not update local clone of Git repository '%s'; continuing with the most recent version", actualUrl);
}
- struct timeval times[2];
- times[0].tv_sec = now;
- times[0].tv_usec = 0;
- times[1].tv_sec = now;
- times[1].tv_usec = 0;
-
- utimes(localRefFile.c_str(), times);
+ if (!touchCacheFile(localRefFile, now))
+ warn("could not update mtime for file '%s': %s", localRefFile, strerror(errno));
+ if (useHeadRef && !storeCachedHead(actualUrl, *input.getRef()))
+ warn("could not update cached head '%s' for '%s'", *input.getRef(), actualUrl);
}
if (!input.getRev())
@@ -459,7 +590,7 @@ struct GitInputScheme : InputScheme
auto result = runProgram(RunOptions {
.program = "git",
- .args = { "-C", repoDir, "cat-file", "commit", input.getRev()->gitRev() },
+ .args = { "-C", repoDir, "--git-dir", gitDir, "cat-file", "commit", input.getRev()->gitRev() },
.mergeStderrToStdout = true
});
if (WEXITSTATUS(result.first) == 128
@@ -498,7 +629,7 @@ struct GitInputScheme : InputScheme
auto source = sinkToSource([&](Sink & sink) {
runProgram2({
.program = "git",
- .args = { "-C", repoDir, "archive", input.getRev()->gitRev() },
+ .args = { "-C", repoDir, "--git-dir", gitDir, "archive", input.getRev()->gitRev() },
.standardOut = &sink
});
});
@@ -508,7 +639,7 @@ struct GitInputScheme : InputScheme
auto storePath = store->addToStore(name, tmpDir, FileIngestionMethod::Recursive, htSHA256, filter);
- auto lastModified = std::stoull(runProgram("git", true, { "-C", repoDir, "log", "-1", "--format=%ct", "--no-show-signature", input.getRev()->gitRev() }));
+ auto lastModified = std::stoull(runProgram("git", true, { "-C", repoDir, "--git-dir", gitDir, "log", "-1", "--format=%ct", "--no-show-signature", input.getRev()->gitRev() }));
Attrs infoAttrs({
{"rev", input.getRev()->gitRev()},
@@ -517,7 +648,7 @@ struct GitInputScheme : InputScheme
if (!shallow)
infoAttrs.insert_or_assign("revCount",
- std::stoull(runProgram("git", true, { "-C", repoDir, "rev-list", "--count", input.getRev()->gitRev() })));
+ std::stoull(runProgram("git", true, { "-C", repoDir, "--git-dir", gitDir, "rev-list", "--count", input.getRev()->gitRev() })));
if (!_input.getRev())
getCache()->add(
diff --git a/src/libfetchers/github.cc b/src/libfetchers/github.cc
index 58b6e7c04..1bdf2759f 100644
--- a/src/libfetchers/github.cc
+++ b/src/libfetchers/github.cc
@@ -4,7 +4,7 @@
#include "store-api.hh"
#include "types.hh"
#include "url-parts.hh"
-
+#include "git-utils.hh"
#include "fetchers.hh"
#include "fetch-settings.hh"
@@ -383,35 +383,29 @@ struct SourceHutInputScheme : GitArchiveInputScheme
std::string line;
getline(is, line);
- auto ref_index = line.find("ref: ");
- if (ref_index == std::string::npos) {
+ auto r = parseListReferenceHeadRef(line);
+ if (!r) {
throw BadURL("in '%d', couldn't resolve HEAD ref '%d'", input.to_string(), ref);
}
-
- ref_uri = line.substr(ref_index+5, line.length()-1);
- } else
+ ref_uri = *r;
+ } else {
ref_uri = fmt("refs/(heads|tags)/%s", ref);
+ }
auto file = store->toRealPath(
downloadFile(store, fmt("%s/info/refs", base_url), "source", false, headers).storePath);
std::ifstream is(file);
std::string line;
- std::string id;
- while(getline(is, line)) {
- // Append $ to avoid partial name matches
- std::regex pattern(fmt("%s$", ref_uri));
-
- if (std::regex_search(line, pattern)) {
- id = line.substr(0, line.find('\t'));
- break;
- }
+ std::optional<std::string> id;
+ while(!id && getline(is, line)) {
+ id = parseListReferenceForRev(ref_uri, line);
}
- if(id.empty())
+ if(!id)
throw BadURL("in '%d', couldn't find ref '%d'", input.to_string(), ref);
- auto rev = Hash::parseAny(id, htSHA1);
+ auto rev = Hash::parseAny(*id, htSHA1);
debug("HEAD revision for '%s' is %s", fmt("%s/%s", base_url, ref), rev.gitRev());
return rev;
}
diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc
index 53f212c1d..d095a0f02 100644
--- a/src/libstore/build/derivation-goal.cc
+++ b/src/libstore/build/derivation-goal.cc
@@ -786,8 +786,7 @@ void runPostBuildHook(
Store & store,
Logger & logger,
const StorePath & drvPath,
- StorePathSet outputPaths
-)
+ const StorePathSet & outputPaths)
{
auto hook = settings.postBuildHook;
if (hook == "")
@@ -906,7 +905,7 @@ void DerivationGoal::buildDone()
auto builtOutputs = registerOutputs();
StorePathSet outputPaths;
- for (auto & [_, output] : buildResult.builtOutputs)
+ for (auto & [_, output] : builtOutputs)
outputPaths.insert(output.outPath);
runPostBuildHook(
worker.store,
diff --git a/src/libstore/s3.hh b/src/libstore/s3.hh
index 3f55c74db..cdb3e5908 100644
--- a/src/libstore/s3.hh
+++ b/src/libstore/s3.hh
@@ -5,6 +5,7 @@
#include "ref.hh"
#include <optional>
+#include <string>
namespace Aws { namespace Client { class ClientConfiguration; } }
namespace Aws { namespace S3 { class S3Client; } }
diff --git a/src/nix/flake.md b/src/nix/flake.md
index 7d179a6c4..c8251eb74 100644
--- a/src/nix/flake.md
+++ b/src/nix/flake.md
@@ -153,7 +153,7 @@ Currently the `type` attribute can be one of the following:
git(+http|+https|+ssh|+git|+file|):(//<server>)?<path>(\?<params>)?
```
- The `ref` attribute defaults to `master`.
+ The `ref` attribute defaults to resolving the `HEAD` reference.
The `rev` attribute must denote a commit that exists in the branch
or tag specified by the `ref` attribute, since Nix doesn't do a full
@@ -161,6 +161,11 @@ Currently the `type` attribute can be one of the following:
doesn't allow fetching a `rev` without a known `ref`). The default
is the commit currently pointed to by `ref`.
+ When `git+file` is used without specifying `ref` or `rev`, files are
+ fetched directly from the local `path` as long as they have been added
+ to the Git repository. If there are uncommitted changes, the reference
+ is treated as dirty and a warning is printed.
+
For example, the following are valid Git flake references:
* `git+https://example.org/my/repo`
diff --git a/src/nix/fmt.cc b/src/nix/fmt.cc
index e5d44bd38..6f6a4a632 100644
--- a/src/nix/fmt.cc
+++ b/src/nix/fmt.cc
@@ -26,7 +26,8 @@ struct CmdFmt : SourceExprCommand {
Strings getDefaultFlakeAttrPathPrefixes() override { return Strings{}; }
- void run(ref<Store> store) {
+ void run(ref<Store> store) override
+ {
auto evalState = getEvalState();
auto evalStore = getEvalStore();
diff --git a/tests/build-remote.sh b/tests/build-remote.sh
index d1da134dc..e73c37ea4 100644
--- a/tests/build-remote.sh
+++ b/tests/build-remote.sh
@@ -34,7 +34,7 @@ outPath=$(readlink -f $TEST_ROOT/result)
grep 'FOO BAR BAZ' $TEST_ROOT/machine0/$outPath
-testPrintOutPath=$(nix build -L -v -f $file --print-out-paths --max-jobs 0 \
+testPrintOutPath=$(nix build -L -v -f $file --no-link --print-out-paths --max-jobs 0 \
--arg busybox $busybox \
--store $TEST_ROOT/machine0 \
--builders "$(join_by '; ' "${builders[@]}")"
@@ -72,6 +72,7 @@ fi
# Behavior of keep-failed
out="$(nix-build 2>&1 failing.nix \
+ --no-out-link \
--builders "$(join_by '; ' "${builders[@]}")" \
--keep-failed \
--store $TEST_ROOT/machine0 \
diff --git a/tests/common.sh.in b/tests/common.sh.in
index 8ce28d318..6cb579e0d 100644
--- a/tests/common.sh.in
+++ b/tests/common.sh.in
@@ -157,11 +157,12 @@ expect() {
local expected res
expected="$1"
shift
- set +e
- "$@"
- res="$?"
- set -e
- [[ $res -eq $expected ]]
+ "$@" || res="$?"
+ if [[ $res -ne $expected ]]; then
+ echo "Expected '$expected' but got '$res' while running '$*'"
+ return 1
+ fi
+ return 0
}
needLocalStore() {
diff --git a/tests/fetchClosure.sh b/tests/fetchClosure.sh
index 96e4bb741..44050c878 100644
--- a/tests/fetchClosure.sh
+++ b/tests/fetchClosure.sh
@@ -7,7 +7,7 @@ clearStore
clearCacheCache
# Initialize binary cache.
-nonCaPath=$(nix build --json --file ./dependencies.nix | jq -r .[].outputs.out)
+nonCaPath=$(nix build --json --file ./dependencies.nix --no-link | jq -r .[].outputs.out)
caPath=$(nix store make-content-addressed --json $nonCaPath | jq -r '.rewrites | map(.) | .[]')
nix copy --to file://$cacheDir $nonCaPath
diff --git a/tests/fetchGit.sh b/tests/fetchGit.sh
index 9179e2071..166bccfc7 100644
--- a/tests/fetchGit.sh
+++ b/tests/fetchGit.sh
@@ -161,6 +161,14 @@ path4=$(nix eval --impure --raw --expr "(builtins.fetchGit $repo).outPath")
[[ $(cat $path4/hello) = dev ]]
[[ $path3 = $path4 ]]
+# Using remote path with branch other than 'master' should fetch the HEAD revision.
+# (--tarball-ttl 0 to prevent using the cached repo above)
+export _NIX_FORCE_HTTP=1
+path4=$(nix eval --tarball-ttl 0 --impure --raw --expr "(builtins.fetchGit $repo).outPath")
+[[ $(cat $path4/hello) = dev ]]
+[[ $path3 = $path4 ]]
+unset _NIX_FORCE_HTTP
+
# Confirm same as 'dev' branch
path5=$(nix eval --impure --raw --expr "(builtins.fetchGit { url = $repo; ref = \"dev\"; }).outPath")
[[ $path3 = $path5 ]]
diff --git a/tests/flakes.sh b/tests/flakes.sh
index 46e6a7982..24601784f 100644
--- a/tests/flakes.sh
+++ b/tests/flakes.sh
@@ -31,7 +31,14 @@ flakeFollowsE=$TEST_ROOT/follows/flakeA/flakeE
for repo in $flake1Dir $flake2Dir $flake3Dir $flake7Dir $templatesDir $nonFlakeDir $flakeA $flakeB $flakeFollowsA; do
rm -rf $repo $repo.tmp
mkdir -p $repo
- git -C $repo init
+
+ # Give one repo a non-master initial branch.
+ extraArgs=
+ if [[ $repo == $flake2Dir ]]; then
+ extraArgs="--initial-branch=main"
+ fi
+
+ git -C $repo init $extraArgs
git -C $repo config user.email "foobar@example.com"
git -C $repo config user.name "Foobar"
done
diff --git a/tests/lang.sh b/tests/lang.sh
index 61bb444ba..f09eaeb31 100644
--- a/tests/lang.sh
+++ b/tests/lang.sh
@@ -4,6 +4,7 @@ export TEST_VAR=foo # for eval-okay-getenv.nix
export NIX_REMOTE=dummy://
nix-instantiate --eval -E 'builtins.trace "Hello" 123' 2>&1 | grep -q Hello
+nix-instantiate --eval -E 'builtins.addErrorContext "Hello" 123' 2>&1
(! nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello" 123' 2>&1 | grep -q Hello)
nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello" (throw "Foo")' 2>&1 | grep -q Hello
@@ -14,7 +15,7 @@ fail=0
for i in lang/parse-fail-*.nix; do
echo "parsing $i (should fail)";
i=$(basename $i .nix)
- if nix-instantiate --parse - < lang/$i.nix; then
+ if ! expect 1 nix-instantiate --parse - < lang/$i.nix; then
echo "FAIL: $i shouldn't parse"
fail=1
fi
@@ -23,7 +24,7 @@ done
for i in lang/parse-okay-*.nix; do
echo "parsing $i (should succeed)";
i=$(basename $i .nix)
- if ! nix-instantiate --parse - < lang/$i.nix > lang/$i.out; then
+ if ! expect 0 nix-instantiate --parse - < lang/$i.nix > lang/$i.out; then
echo "FAIL: $i should parse"
fail=1
fi
@@ -32,7 +33,7 @@ done
for i in lang/eval-fail-*.nix; do
echo "evaluating $i (should fail)";
i=$(basename $i .nix)
- if nix-instantiate --eval lang/$i.nix; then
+ if ! expect 1 nix-instantiate --eval lang/$i.nix; then
echo "FAIL: $i shouldn't evaluate"
fail=1
fi
@@ -47,7 +48,7 @@ for i in lang/eval-okay-*.nix; do
if test -e lang/$i.flags; then
flags=$(cat lang/$i.flags)
fi
- if ! NIX_PATH=lang/dir3:lang/dir4 nix-instantiate $flags --eval --strict lang/$i.nix > lang/$i.out; then
+ if ! expect 0 env NIX_PATH=lang/dir3:lang/dir4 nix-instantiate $flags --eval --strict lang/$i.nix > lang/$i.out; then
echo "FAIL: $i should evaluate"
fail=1
elif ! diff lang/$i.out lang/$i.exp; then
@@ -57,7 +58,7 @@ for i in lang/eval-okay-*.nix; do
fi
if test -e lang/$i.exp.xml; then
- if ! nix-instantiate --eval --xml --no-location --strict \
+ if ! expect 0 nix-instantiate --eval --xml --no-location --strict \
lang/$i.nix > lang/$i.out.xml; then
echo "FAIL: $i should evaluate"
fail=1
diff --git a/tests/post-hook.sh b/tests/post-hook.sh
index 049e40749..4eff5f511 100644
--- a/tests/post-hook.sh
+++ b/tests/post-hook.sh
@@ -9,12 +9,12 @@ echo 'require-sigs = false' >> $NIX_CONF_DIR/nix.conf
restartDaemon
-# Build the dependencies and push them to the remote store
+# Build the dependencies and push them to the remote store.
nix-build -o $TEST_ROOT/result dependencies.nix --post-build-hook $PWD/push-to-store.sh
clearStore
-# Ensure that we the remote store contains both the runtime and buildtime
-# closure of what we've just built
+# Ensure that the remote store contains both the runtime and build-time
+# closure of what we've just built.
nix copy --from "$REMOTE_STORE" --no-require-sigs -f dependencies.nix
nix copy --from "$REMOTE_STORE" --no-require-sigs -f dependencies.nix input1_drv
diff --git a/tests/push-to-store.sh b/tests/push-to-store.sh
index 25352c751..b1495c9e2 100755
--- a/tests/push-to-store.sh
+++ b/tests/push-to-store.sh
@@ -1,6 +1,10 @@
#!/bin/sh
set -x
+set -e
+
+[ -n "$OUT_PATHS" ]
+[ -n "$DRV_PATH" ]
echo Pushing "$OUT_PATHS" to "$REMOTE_STORE"
printf "%s" "$DRV_PATH" | xargs nix copy --to "$REMOTE_STORE" --no-require-sigs