diff --git a/doc/manual/expressions/builtins.xml b/doc/manual/expressions/builtins.xml
index 394e1fc32c9..9a209c57de0 100644
--- a/doc/manual/expressions/builtins.xml
+++ b/doc/manual/expressions/builtins.xml
@@ -422,6 +422,28 @@ stdenv.mkDerivation { … }
+
+ fetchRevInsteadOfRef
+
+
+ Defaults to false.
+
+
+
+ When set to true the specified
+ rev is fetched directly
+ instead of fetching the ref (which defaults to
+ HEAD when not specified). This is useful in
+ case a ref is not available.
+
+
+
+ Do note that git servers by default don't allow fetching revisions directly
+ (GitHub does allow it).
+
+
+
diff --git a/src/libexpr/primops/fetchGit.cc b/src/libexpr/primops/fetchGit.cc
index 4aee1073eeb..b7ec6ae0f6e 100644
--- a/src/libexpr/primops/fetchGit.cc
+++ b/src/libexpr/primops/fetchGit.cc
@@ -26,9 +26,80 @@ struct GitInfo
std::regex revRegex("^[0-9a-fA-F]{40}$");
+void fetchRev(const Path cacheDir, const std::string & uri, std::string rev) {
+ Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching '%s' of Git repository '%s'", rev, uri));
+
+ // FIXME: git stderr messes up our progress indicator, so
+ // we're using --quiet for now. Should process its stderr.
+ runProgram("git", true, { "-C", cacheDir, "fetch", "--quiet", "--", uri, rev });
+}
+
+void fetchRef(const Path cacheDir, const std::string & uri, std::string ref, time_t now, Path localRefFile) {
+ Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching '%s' of Git repository '%s'", ref, uri));
+
+ // FIXME: git stderr messes up our progress indicator, so
+ // we're using --quiet for now. Should process its stderr.
+ runProgram("git", true, { "-C", cacheDir, "fetch", "--quiet", "--force", "--", uri, fmt("%s:%s", ref, ref) });
+
+ 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);
+}
+
+bool isAncestor(const Path cacheDir, std::string rev1, std::string rev2) {
+ try {
+ runProgram("git", true, { "-C", cacheDir, "merge-base", "--is-ancestor", rev1, rev2});
+ return true;
+ } catch (ExecError & e) {
+ if (WIFEXITED(e.status)) {
+ return false;
+ }
+ else {
+ throw;
+ }
+ }
+}
+
+void checkAncestor(const Path cacheDir, const std::string & uri, std::string rev, std::string ref) {
+ time_t now = time(0);
+
+ Path localRefFile;
+ if (ref.compare(0, 5, "refs/") == 0)
+ localRefFile = cacheDir + "/" + ref;
+ else
+ localRefFile = cacheDir + "/refs/heads/" + ref;
+
+ struct stat st;
+ if (stat(localRefFile.c_str(), &st) == 0 &&
+ isAncestor(cacheDir, rev, chomp(readFile(localRefFile)))) return;
+
+ fetchRef(cacheDir, uri, ref, now, localRefFile);
+ std::string refRev = chomp(readFile(localRefFile));
+ if (!isAncestor(cacheDir, rev, refRev)) {
+ throw Error("The specified rev '%s' is not an ancestor of the specified ref '%s' with revision '%s'", rev, ref, refRev);
+ }
+}
+
+bool revInCache(const Path cacheDir, std::string rev) {
+ try {
+ runProgram("git", true, { "-C", cacheDir, "cat-file", "-e", rev });
+ return true;
+ } catch (ExecError & e) {
+ if (WIFEXITED(e.status)) {
+ return false;
+ } else {
+ throw;
+ }
+ }
+}
+
GitInfo exportGit(ref store, const std::string & uri,
std::optional ref, std::string rev,
- const std::string & name)
+ const std::string & name, bool fetchRevInsteadOfRef)
{
if (evalSettings.pureEval && rev == "")
throw Error("in pure evaluation mode, 'fetchGit' requires a Git revision");
@@ -79,8 +150,6 @@ GitInfo exportGit(ref store, const std::string & uri,
ref = "HEAD"s;
}
- if (!ref) ref = "HEAD"s;
-
if (rev != "" && !std::regex_match(rev, revRegex))
throw Error("invalid Git revision '%s'", rev);
@@ -93,54 +162,66 @@ GitInfo exportGit(ref store, const std::string & uri,
runProgram("git", true, { "init", "--bare", cacheDir });
}
- Path localRefFile;
- if (ref->compare(0, 5, "refs/") == 0)
- localRefFile = cacheDir + "/" + *ref;
- else
- localRefFile = cacheDir + "/refs/heads/" + *ref;
+ if (rev.empty()) {
+ std::string fetchedRef = ref ? *ref : "HEAD";
- bool doFetch;
- time_t now = time(0);
- /* If a rev was specified, we need to fetch if it's not in the
- repo. */
- if (rev != "") {
- try {
- runProgram("git", true, { "-C", cacheDir, "cat-file", "-e", rev });
- doFetch = false;
- } catch (ExecError & e) {
- if (WIFEXITED(e.status)) {
- doFetch = true;
+ time_t now = time(0);
+
+ Path localRefFile;
+ if (fetchedRef.compare(0, 5, "refs/") == 0)
+ localRefFile = cacheDir + "/" + fetchedRef;
+ else
+ localRefFile = cacheDir + "/refs/heads/" + fetchedRef;
+
+ struct stat st;
+ if (stat(localRefFile.c_str(), &st) != 0 ||
+ (uint64_t) st.st_mtime + settings.tarballTtl <= (uint64_t) now) {
+ fetchRef(cacheDir, uri, fetchedRef, now, localRefFile);
+ }
+ rev = chomp(readFile(localRefFile));
+ } else {
+ if (revInCache(cacheDir, rev)) {
+ if (ref) { checkAncestor(cacheDir, uri, rev, *ref); }
+ } else {
+ if (fetchRevInsteadOfRef) {
+ fetchRev(cacheDir, uri, rev);
+ if (ref) { checkAncestor(cacheDir, uri, rev, *ref); }
} else {
- throw;
+ std::string fetchedRef = ref ? *ref : "HEAD";
+
+ time_t now = time(0);
+
+ Path localRefFile;
+ if (fetchedRef.compare(0, 5, "refs/") == 0)
+ localRefFile = cacheDir + "/" + fetchedRef;
+ else
+ localRefFile = cacheDir + "/refs/heads/" + fetchedRef;
+
+ fetchRef(cacheDir, uri, fetchedRef, now, localRefFile);
+ std::string refRev = chomp(readFile(localRefFile));
+
+ if (!isAncestor(cacheDir, rev, refRev)) {
+ throw Error(
+ "The specified rev '%s' "
+ "is not an ancestor of ref '%s' "
+ "with revision '%s'. %s",
+ rev, fetchedRef, refRev,
+ ref ? "" :
+ "Note that 'ref' defaults to 'HEAD' when not specified. "
+ "So either set a correct 'ref' "
+ "or set 'fetchRevInsteadOfRef = true;' "
+ "to directly fetch the specified 'rev' "
+ "(which might be denied by git servers "
+ "which have the config option "
+ "'uploadpack.allowAnySHA1InWant' set to 'false')."
+ );
+ }
}
}
- } else {
- /* If the local ref is older than ‘tarball-ttl’ seconds, do a
- 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;
- }
- if (doFetch)
- {
- Activity act(*logger, lvlTalkative, actUnknown, fmt("fetching Git repository '%s'", uri));
-
- // FIXME: git stderr messes up our progress indicator, so
- // we're using --quiet for now. Should process its stderr.
- runProgram("git", true, { "-C", cacheDir, "fetch", "--quiet", "--force", "--", uri, fmt("%s:%s", *ref, *ref) });
-
- 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);
}
- // FIXME: check whether rev is an ancestor of ref.
GitInfo gitInfo;
- gitInfo.rev = rev != "" ? rev : chomp(readFile(localRefFile));
+ gitInfo.rev = rev;
gitInfo.shortRev = std::string(gitInfo.rev, 0, 7);
printTalkative("using revision %s of repo '%s'", gitInfo.rev, uri);
@@ -198,6 +279,7 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
std::optional ref;
std::string rev;
std::string name = "source";
+ bool fetchRevInsteadOfRef = false;
PathSet context;
state.forceValue(*args[0]);
@@ -216,6 +298,8 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
rev = state.forceStringNoCtx(*attr.value, *attr.pos);
else if (n == "name")
name = state.forceStringNoCtx(*attr.value, *attr.pos);
+ else if (n == "fetchRevInsteadOfRef")
+ fetchRevInsteadOfRef = state.forceBool(*attr.value, *attr.pos);
else
throw EvalError("unsupported argument '%s' to 'fetchGit', at %s", attr.name, *attr.pos);
}
@@ -230,7 +314,7 @@ static void prim_fetchGit(EvalState & state, const Pos & pos, Value * * args, Va
// whitelist. Ah well.
state.checkURI(url);
- auto gitInfo = exportGit(state.store, url, ref, rev, name);
+ auto gitInfo = exportGit(state.store, url, ref, rev, name, fetchRevInsteadOfRef);
state.mkAttrs(v, 8);
mkString(*state.allocAttr(v, state.sOutPath), gitInfo.storePath, PathSet({gitInfo.storePath}));
diff --git a/tests/fetchGit.sh b/tests/fetchGit.sh
index 4c46bdf0465..c066ef2829e 100644
--- a/tests/fetchGit.sh
+++ b/tests/fetchGit.sh
@@ -25,6 +25,23 @@ echo world > $repo/hello
git -C $repo commit -m 'Bla2' -a
rev2=$(git -C $repo rev-parse HEAD)
+git -C $repo checkout -b my-branch
+echo universe > $repo/hello
+git -C $repo commit -m 'Bla3' -a
+rev3=$(git -C $repo rev-parse HEAD)
+
+git -C $repo checkout master
+
+# Fetch rev3 which is not an ancestor of HEAD without specifying a ref or setting `fetchRevInsteadOfRef = true` should fail.
+(! nix eval --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev3\"; }).outPath")
+
+# Fetch rev3 which is not an ancestor of HEAD without specifying a ref but setting `fetchRevInsteadOfRef = true`.
+path=$(nix eval --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev3\"; fetchRevInsteadOfRef = true; }).outPath")
+[[ $(cat $path/hello) = universe ]]
+
+# Fetching rev3 which is not an ancestor of HEAD should fail when specifying HEAD as ref.
+(! nix eval --raw "(builtins.fetchGit { url = file://$repo; rev = \"$rev3\"; ref = \"HEAD\"; })")
+
# Fetch the default branch.
path=$(nix eval --raw "(builtins.fetchGit file://$repo).outPath")
[[ $(cat $path/hello) = world ]]