From 629af83b2d86d77305dc994b83f176a377106c3e Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Tue, 14 Jul 2020 20:59:24 +0200 Subject: [PATCH 1/4] Provide a more meaningful error-message for `builtins.fetchGit` if a revision can't be checked out A common pitfall when using e.g. `builtins.fetchGit` is the `fatal: not a tree object`-error when trying to fetch a revision of a git-repository that isn't on the `master` branch and no `ref` is specified. In order to make clear what's the problem, I added a simple check whether the revision in question exists and if it doesn't a more meaningful error-message is displayed: ``` nix-repl> builtins.fetchGit { url = "https://github.com/owner/myrepo"; rev = ""; } moderror: --- Error -------------------------------------------------------------------- nix Cannot find Git revision 'bf1cc5c648e6aed7360448a3745bb2fe4fbbf0e9' in ref 'master' of repository 'https://gitlab.com/Ma27/nvim.nix'! Please make sure that the rev exists on the ref you've specified or add allRefs = true; to fetchGit. ``` Closes #2431 --- src/libfetchers/git.cc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index e7712c5fd..1f298c2d6 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -392,6 +392,28 @@ struct GitInputScheme : InputScheme AutoDelete delTmpDir(tmpDir, true); PathFilter filter = defaultPathFilter; + RunOptions checkCommitOpts( + "git", + { "-C", repoDir, "cat-file", "commit", input.getRev()->gitRev() } + ); + checkCommitOpts.searchPath = true; + checkCommitOpts.mergeStderrToStdout = true; + + auto result = runProgram(checkCommitOpts); + if (WEXITSTATUS(result.first) == 128 + && result.second.find("bad file") != std::string::npos + ) { + throw Error( + "Cannot find Git revision '%s' in ref '%s' of repository '%s'! " + "Please make sure that the " ANSI_BOLD "rev" ANSI_NORMAL " exists on the " + ANSI_BOLD "ref" ANSI_NORMAL " you've specified or add " ANSI_BOLD + "allRefs = true;" ANSI_NORMAL " to " ANSI_BOLD "fetchGit" ANSI_NORMAL ".", + input.getRev()->gitRev(), + *input.getRef(), + actualUrl + ); + } + if (submodules) { Path tmpGitDir = createTempDir(); AutoDelete delTmpGitDir(tmpGitDir, true); From 2857b1baaf78bbadeec01adfae8a50fc0f2a254f Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Fri, 17 Jul 2020 20:34:57 +0200 Subject: [PATCH 2/4] Add explicit `allRefs = true;` argument to `fetchGit` Sometimes it's necessary to fetch a git repository at a revision and it's unknown which ref contains the revision in question. An example would be a Cargo.lock which only provides the URL and the revision when using a git repository as build input. However it's considered a bad practice to perform a full checkout of a repository since this may take a lot of time and can eat up a lot of disk space. This patch makes a full checkout explicit by adding an `allRefs` argument to `builtins.fetchGit` which fetches all refs if explicitly set to true. Closes #2409 --- src/libfetchers/git.cc | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/libfetchers/git.cc b/src/libfetchers/git.cc index 1f298c2d6..81c647f89 100644 --- a/src/libfetchers/git.cc +++ b/src/libfetchers/git.cc @@ -59,12 +59,13 @@ struct GitInputScheme : InputScheme if (maybeGetStrAttr(attrs, "type") != "git") return {}; for (auto & [name, value] : attrs) - if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "shallow" && name != "submodules" && name != "lastModified" && name != "revCount" && name != "narHash") + if (name != "type" && name != "url" && name != "ref" && name != "rev" && name != "shallow" && name != "submodules" && name != "lastModified" && name != "revCount" && name != "narHash" && name != "allRefs") throw Error("unsupported Git input attribute '%s'", name); parseURL(getStrAttr(attrs, "url")); maybeGetBoolAttr(attrs, "shallow"); maybeGetBoolAttr(attrs, "submodules"); + maybeGetBoolAttr(attrs, "allRefs"); if (auto ref = maybeGetStrAttr(attrs, "ref")) { if (std::regex_search(*ref, badGitRefRegex)) @@ -169,10 +170,12 @@ struct GitInputScheme : InputScheme bool shallow = maybeGetBoolAttr(input.attrs, "shallow").value_or(false); bool submodules = maybeGetBoolAttr(input.attrs, "submodules").value_or(false); + bool allRefs = maybeGetBoolAttr(input.attrs, "allRefs").value_or(false); std::string cacheType = "git"; if (shallow) cacheType += "-shallow"; if (submodules) cacheType += "-submodules"; + if (allRefs) cacheType += "-all-refs"; auto getImmutableAttrs = [&]() { @@ -338,11 +341,15 @@ struct GitInputScheme : InputScheme } } } 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 (allRefs) { + doFetch = true; + } 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) { @@ -352,9 +359,11 @@ struct GitInputScheme : InputScheme // we're using --quiet for now. Should process its stderr. try { auto ref = input.getRef(); - auto fetchRef = ref->compare(0, 5, "refs/") == 0 - ? *ref - : "refs/heads/" + *ref; + auto fetchRef = allRefs + ? "refs/*" + : ref->compare(0, 5, "refs/") == 0 + ? *ref + : "refs/heads/" + *ref; runProgram("git", true, { "-C", repoDir, "fetch", "--quiet", "--force", "--", actualUrl, fmt("%s:%s", fetchRef, fetchRef) }); } catch (Error & e) { if (!pathExists(localRefFile)) throw; From e54971d019821dd213db028a80cd5fca85ee2ed6 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Tue, 8 Sep 2020 19:45:28 +0200 Subject: [PATCH 3/4] Document `allRefs` argument of `builtins.fetchTree` --- src/libexpr/primops/fetchTree.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libexpr/primops/fetchTree.cc b/src/libexpr/primops/fetchTree.cc index 6e7ddde8e..133299030 100644 --- a/src/libexpr/primops/fetchTree.cc +++ b/src/libexpr/primops/fetchTree.cc @@ -324,6 +324,11 @@ static RegisterPrimOp primop_fetchGit({ A Boolean parameter that specifies whether submodules should be checked out. Defaults to `false`. + - allRefs + Whether to fetch all refs of the repository. With this argument being + true, it's possible to load a `rev` from *any* `ref` (by default only + `rev`s from the specified `ref` are supported). + Here are some examples of how to use `fetchGit`. - To fetch a private repository over SSH: From 897ae235fc2cef0ce711470a7b620241d82a1b09 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Tue, 22 Dec 2020 12:18:10 +0100 Subject: [PATCH 4/4] tests/fetchGit: test behavior of `allRefs = true;` --- tests/fetchGit.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/fetchGit.sh b/tests/fetchGit.sh index 76390fa59..1e8963d76 100644 --- a/tests/fetchGit.sh +++ b/tests/fetchGit.sh @@ -41,6 +41,19 @@ export _NIX_FORCE_HTTP=1 path=$(nix eval --impure --raw --expr "(builtins.fetchGit file://$repo).outPath") [[ $(cat $path/hello) = world ]] +# Fetch a rev from another branch +git -C $repo checkout -b devtest +echo "different file" >> $TEST_ROOT/git/differentbranch +git -C $repo add differentbranch +git -C $repo commit -m 'Test2' +git -C $repo checkout master +devrev=$(git -C $repo rev-parse devtest) +out=$(nix eval --impure --raw --expr "builtins.fetchGit { url = file://$repo; rev = \"$devrev\"; }" 2>&1) || status=$? +[[ $status == 1 ]] +[[ $out =~ 'Cannot find Git revision' ]] + +[[ $(nix eval --raw --expr "builtins.readFile (builtins.fetchGit { url = file://$repo; rev = \"$devrev\"; allRefs = true; } + \"/differentbranch\")") = 'different file' ]] + # In pure eval mode, fetchGit without a revision should fail. [[ $(nix eval --impure --raw --expr "builtins.readFile (fetchGit file://$repo + \"/hello\")") = world ]] (! nix eval --raw --expr "builtins.readFile (fetchGit file://$repo + \"/hello\")")