1616#include " nix/util/json-utils.hh"
1717#include " nix/util/archive.hh"
1818#include " nix/util/mounted-source-accessor.hh"
19+ #include " nix/fetchers/fetch-to-store.hh"
1920
2021#include < regex>
2122#include < string.h>
@@ -637,8 +638,72 @@ struct GitInputScheme : InputScheme
637638 return shallow || input.getRevCount ().has_value ();
638639 }
639640
641+ GitAccessorOptions getGitAccessorOptions (const Input & input) const
642+ {
643+ return GitAccessorOptions{
644+ .exportIgnore = getExportIgnoreAttr (input),
645+ .smudgeLfs = getLfsAttr (input),
646+ .submodules = getSubmodulesAttr (input),
647+ };
648+ }
649+
650+ /* *
651+ * Get a `SourceAccessor` for the given Git revision using Nix < 2.20 semantics, i.e. using `git archive` or `git
652+ * checkout`.
653+ */
654+ ref<SourceAccessor> getLegacyGitAccessor (
655+ Store & store,
656+ RepoInfo & repoInfo,
657+ const std::filesystem::path & repoDir,
658+ const Hash & rev,
659+ GitAccessorOptions & options) const
660+ {
661+ auto tmpDir = createTempDir ();
662+ AutoDelete delTmpDir (tmpDir, true );
663+
664+ auto storePath =
665+ options.submodules
666+ ? [&]() {
667+ // Nix < 2.20 used `git checkout` for repos with submodules.
668+ runProgram2 ({.program = " git" , .args = {" init" , tmpDir}});
669+ runProgram2 ({.program = " git" , .args = {" -C" , tmpDir, " remote" , " add" , " origin" , repoDir}});
670+ runProgram2 ({.program = " git" , .args = {" -C" , tmpDir, " fetch" , " origin" , rev.gitRev ()}});
671+ runProgram2 ({.program = " git" , .args = {" -C" , tmpDir, " checkout" , rev.gitRev ()}});
672+ PathFilter filter = [&](const Path & path) { return baseNameOf (path) != " .git" ; };
673+ return store.addToStore (
674+ " source" ,
675+ {getFSSourceAccessor (), CanonPath (tmpDir)},
676+ ContentAddressMethod::Raw::NixArchive,
677+ HashAlgorithm::SHA256,
678+ {},
679+ filter);
680+ }()
681+ : [&]() {
682+ // Nix < 2.20 used `git archive` for repos without submodules.
683+ options.exportIgnore = true ;
684+
685+ auto source = sinkToSource ([&](Sink & sink) {
686+ runProgram2 (
687+ {.program = " git" ,
688+ .args = {" -C" , repoDir, " --git-dir" , repoInfo.gitDir , " archive" , rev.gitRev ()},
689+ .standardOut = &sink});
690+ });
691+
692+ unpackTarfile (*source, tmpDir);
693+
694+ return store.addToStore (" source" , {getFSSourceAccessor (), CanonPath (tmpDir)});
695+ }();
696+
697+ auto accessor = store.getFSAccessor (storePath);
698+
699+ accessor->fingerprint = options.makeFingerprint (rev) + " ;legacy" ;
700+
701+ return ref{accessor};
702+ }
703+
640704 std::pair<ref<SourceAccessor>, Input>
641705 getAccessorFromCommit (const Settings & settings, ref<Store> store, RepoInfo & repoInfo, Input && input) const
706+
642707 {
643708 assert (!repoInfo.workdirInfo .isDirty );
644709
@@ -779,17 +844,59 @@ struct GitInputScheme : InputScheme
779844
780845 verifyCommit (input, repo);
781846
782- bool exportIgnore = getExportIgnoreAttr (input);
783- bool smudgeLfs = getLfsAttr (input);
784- auto accessor = repo->getAccessor (rev, exportIgnore, " «" + input.to_string (true ) + " »" , smudgeLfs);
847+ auto options = getGitAccessorOptions (input);
848+
849+ auto expectedNarHash = input.getNarHash ();
850+
851+ auto accessor = repo->getAccessor (rev, options, " «" + input.to_string (true ) + " »" );
852+
853+ if (settings.nix219Compat && !options.smudgeLfs && accessor->pathExists (CanonPath (" .gitattributes" ))) {
854+ /* Use Nix 2.19 semantics to generate locks, but if a NAR hash is specified, support Nix >= 2.20 semantics
855+ * as well. */
856+ warn (" Using Nix 2.19 semantics to export Git repository '%s'." , input.to_string ());
857+ auto accessorModern = accessor;
858+ accessor = getLegacyGitAccessor (*store, repoInfo, repoDir, rev, options);
859+ if (expectedNarHash) {
860+ auto narHashLegacy =
861+ fetchToStore2 (settings, *store, {accessor}, FetchMode::DryRun, input.getName ()).second ;
862+ if (expectedNarHash != narHashLegacy) {
863+ auto narHashModern =
864+ fetchToStore2 (settings, *store, {accessorModern}, FetchMode::DryRun, input.getName ()).second ;
865+ if (expectedNarHash == narHashModern)
866+ accessor = accessorModern;
867+ }
868+ }
869+ } else {
870+ /* Backward compatibility hack for locks produced by Nix < 2.20 that depend on Nix applying Git filters,
871+ * `export-ignore` or `export-subst`. Nix >= 2.20 doesn't do those, so we may get a NAR hash mismatch. If
872+ * that happens, try again using `git archive`. */
873+ auto narHashNew = fetchToStore2 (settings, *store, {accessor}, FetchMode::DryRun, input.getName ()).second ;
874+ if (expectedNarHash && accessor->pathExists (CanonPath (" .gitattributes" ))) {
875+ if (expectedNarHash != narHashNew) {
876+ auto accessorLegacy = getLegacyGitAccessor (*store, repoInfo, repoDir, rev, options);
877+ auto narHashLegacy =
878+ fetchToStore2 (settings, *store, {accessorLegacy}, FetchMode::DryRun, input.getName ()).second ;
879+ if (expectedNarHash == narHashLegacy) {
880+ warn (
881+ " Git input '%s' specifies a NAR hash '%s' that was created by Nix < 2.20.\n "
882+ " Nix >= 2.20 does not apply Git filters, `export-ignore` and `export-subst` by default, which changes the NAR hash.\n "
883+ " Please update the NAR hash to '%s'." ,
884+ input.to_string (),
885+ expectedNarHash->to_string (HashFormat::SRI, true ),
886+ narHashNew.to_string (HashFormat::SRI, true ));
887+ accessor = accessorLegacy;
888+ }
889+ }
890+ }
891+ }
785892
786893 /* If the repo has submodules, fetch them and return a mounted
787894 input accessor consisting of the accessor for the top-level
788895 repo and the accessors for the submodules. */
789- if (getSubmodulesAttr (input) ) {
896+ if (options. submodules ) {
790897 std::map<CanonPath, nix::ref<SourceAccessor>> mounts;
791898
792- for (auto & [submodule, submoduleRev] : repo->getSubmodules (rev, exportIgnore)) {
899+ for (auto & [submodule, submoduleRev] : repo->getSubmodules (rev, options. exportIgnore )) {
793900 auto resolved = repo->resolveSubmoduleUrl (submodule.url );
794901 debug (
795902 " Git submodule %s: %s %s %s -> %s" ,
@@ -812,9 +919,9 @@ struct GitInputScheme : InputScheme
812919 }
813920 }
814921 attrs.insert_or_assign (" rev" , submoduleRev.gitRev ());
815- attrs.insert_or_assign (" exportIgnore" , Explicit<bool >{exportIgnore});
922+ attrs.insert_or_assign (" exportIgnore" , Explicit<bool >{options. exportIgnore });
816923 attrs.insert_or_assign (" submodules" , Explicit<bool >{true });
817- attrs.insert_or_assign (" lfs" , Explicit<bool >{smudgeLfs});
924+ attrs.insert_or_assign (" lfs" , Explicit<bool >{options. smudgeLfs });
818925 attrs.insert_or_assign (" allRefs" , Explicit<bool >{true });
819926 auto submoduleInput = fetchers::Input::fromAttrs (settings, std::move (attrs));
820927 auto [submoduleAccessor, submoduleInput2] = submoduleInput.getAccessor (settings, store);
@@ -823,8 +930,10 @@ struct GitInputScheme : InputScheme
823930 }
824931
825932 if (!mounts.empty ()) {
933+ auto newFingerprint = accessor->getFingerprint (CanonPath::root).second ->append (" ;s" );
826934 mounts.insert_or_assign (CanonPath::root, accessor);
827935 accessor = makeMountedSourceAccessor (std::move (mounts));
936+ accessor->fingerprint = newFingerprint;
828937 }
829938 }
830939
@@ -848,7 +957,7 @@ struct GitInputScheme : InputScheme
848957 auto exportIgnore = getExportIgnoreAttr (input);
849958
850959 ref<SourceAccessor> accessor =
851- repo->getAccessor (repoInfo.workdirInfo , exportIgnore, makeNotAllowedError (repoPath));
960+ repo->getAccessor (repoInfo.workdirInfo , {. exportIgnore = exportIgnore} , makeNotAllowedError (repoPath));
852961
853962 /* If the repo has submodules, return a mounted input accessor
854963 consisting of the accessor for the top-level repo and the
@@ -942,13 +1051,12 @@ struct GitInputScheme : InputScheme
9421051
9431052 std::optional<std::string> getFingerprint (ref<Store> store, const Input & input) const override
9441053 {
945- auto makeFingerprint = [&](const Hash & rev) {
946- return rev.gitRev () + (getSubmodulesAttr (input) ? " ;s" : " " ) + (getExportIgnoreAttr (input) ? " ;e" : " " )
947- + (getLfsAttr (input) ? " ;l" : " " );
948- };
1054+ auto options = getGitAccessorOptions (input);
9491055
9501056 if (auto rev = input.getRev ())
951- return makeFingerprint (*rev);
1057+ // FIXME: this can return a wrong fingerprint for the legacy (`git archive`) case, since we don't know here
1058+ // whether to append the `;legacy` suffix or not.
1059+ return options.makeFingerprint (*rev);
9521060 else {
9531061 auto repoInfo = getRepoInfo (input);
9541062 if (auto repoPath = repoInfo.getPath (); repoPath && repoInfo.workdirInfo .submodules .empty ()) {
@@ -964,7 +1072,7 @@ struct GitInputScheme : InputScheme
9641072 writeString (" deleted:" , hashSink);
9651073 writeString (file.abs (), hashSink);
9661074 }
967- return makeFingerprint (repoInfo.workdirInfo .headRev .value_or (nullRev))
1075+ return options. makeFingerprint (repoInfo.workdirInfo .headRev .value_or (nullRev))
9681076 + " ;d=" + hashSink.finish ().hash .to_string (HashFormat::Base16, false );
9691077 }
9701078 return std::nullopt ;
0 commit comments