@@ -485,6 +485,44 @@ bool EvalState::isTectonixSourceAvailable() const
485485 return !settings.tectonixCheckoutPath .get ().empty ();
486486}
487487
488+ bool EvalState::isTectonixFullCheckout () const
489+ {
490+ std::call_once (tectonixFullCheckoutFlag, [this ]() {
491+ tectonixFullCheckout = false ;
492+ if (!isTectonixSourceAvailable ())
493+ return ;
494+
495+ StringMap gitEnvironment = getEnv ();
496+ gitEnvironment.erase (" GIT_DIR" );
497+ gitEnvironment.erase (" GIT_WORK_TREE" );
498+ gitEnvironment.erase (" GIT_COMMON_DIR" );
499+
500+ auto checkoutPath = settings.tectonixCheckoutPath .get ();
501+ auto [gitConfigCode, gitConfigOutput] = runProgram (
502+ {.program = " git" ,
503+ .args = {" -C" , checkoutPath, " config" , " --bool" , " --get" , " core.sparseCheckout" },
504+ .environment = gitEnvironment});
505+
506+ // If core.sparseCheckout is unset, git config exits non-zero. That is a
507+ // normal full-checkout state. Other failures will be caught later by the
508+ // git-status path and treated as clean.
509+ if (!statusOk (gitConfigCode)) {
510+ tectonixFullCheckout = true ;
511+ debug (" source checkout at '%s' has no sparse-checkout config; treating as full checkout" , checkoutPath);
512+ return ;
513+ }
514+
515+ auto value = trim (gitConfigOutput);
516+ tectonixFullCheckout = value != " true" ;
517+ debug (
518+ " source checkout at '%s' core.sparseCheckout=%s; fullCheckout=%s" ,
519+ checkoutPath,
520+ value,
521+ tectonixFullCheckout ? " true" : " false" );
522+ });
523+ return tectonixFullCheckout;
524+ }
525+
488526// Helper to normalize zone paths: strip leading // prefix
489527// Zone paths in manifest have // prefix (e.g., //areas/tools/dev)
490528// Filesystem operations need paths without // (e.g., areas/tools/dev)
@@ -496,6 +534,22 @@ static std::string normalizeZonePath(std::string_view zonePath)
496534 return path;
497535}
498536
537+ static std::optional<std::string> findZonePathForRepoPath (const nlohmann::json & manifest, std::string_view repoPath)
538+ {
539+ std::string candidate (repoPath);
540+ while (!candidate.empty ()) {
541+ auto zonePath = " //" + candidate;
542+ if (manifest.contains (zonePath))
543+ return zonePath;
544+
545+ auto slash = candidate.rfind (' /' );
546+ if (slash == std::string::npos)
547+ break ;
548+ candidate.resize (slash);
549+ }
550+ return std::nullopt ;
551+ }
552+
499553// Helper to sanitize zone path for use in store path names.
500554// Store paths only allow: a-zA-Z0-9 and +-._?=
501555// Replaces / with - and any other invalid chars with _
@@ -627,10 +681,18 @@ const std::map<std::string, EvalState::ZoneDirtyInfo> & EvalState::getTectonixDi
627681 if (!isTectonixSourceAvailable ())
628682 return ;
629683
630- // Get sparse checkout roots (zone IDs)
631- auto & sparseRoots = getTectonixSparseCheckoutRoots ();
632- if (sparseRoots.empty ())
633- return ;
684+ bool fullCheckout = isTectonixFullCheckout ();
685+
686+ // In sparse checkout mode, use the explicit root set. In full checkout
687+ // mode, avoid expanding every zone eagerly; dirty files will be mapped
688+ // to zones directly from their repo-relative paths.
689+ const std::set<std::string> * sparseRoots = nullptr ;
690+ if (!fullCheckout) {
691+ auto & roots = getTectonixSparseCheckoutRoots ();
692+ if (roots.empty ())
693+ return ;
694+ sparseRoots = &roots;
695+ }
634696
635697 // Get manifest (uses cached parsed JSON)
636698 const nlohmann::json * manifest;
@@ -644,21 +706,26 @@ const std::map<std::string, EvalState::ZoneDirtyInfo> & EvalState::getTectonixDi
644706 return ;
645707 }
646708
647- // Build map of zone ID -> zone path for sparse roots only
709+ // Build map of zone ID -> zone path for sparse roots only. Full
710+ // checkouts do not need this map because every zone is physically
711+ // available and clean zones can stay implicit.
648712 std::map<std::string, std::string> zoneIdToPath;
649- for (auto & [path, value] : manifest->items ()) {
650- if (!value.contains (" id" ) || !value.at (" id" ).is_string ()) {
651- warn (" zone '%s' in manifest has missing or non-string 'id' field" , path);
652- continue ;
713+ if (!fullCheckout) {
714+ for (auto & [path, value] : manifest->items ()) {
715+ if (!value.contains (" id" ) || !value.at (" id" ).is_string ()) {
716+ warn (" zone '%s' in manifest has missing or non-string 'id' field" , path);
717+ continue ;
718+ }
719+ auto & id = value.at (" id" ).get_ref <const std::string &>();
720+ if (sparseRoots->count (id))
721+ zoneIdToPath[id] = path;
653722 }
654- auto & id = value.at (" id" ).get_ref <const std::string &>();
655- if (sparseRoots.count (id))
656- zoneIdToPath[id] = path;
657- }
658723
659- // Initialize all sparse-checked-out zones as not dirty
660- for (auto & [zoneId, zonePath] : zoneIdToPath) {
661- tectonixDirtyZones[zonePath] = {};
724+ // Initialize all sparse-checked-out zones as not dirty. Full
725+ // checkouts only materialize dirty entries.
726+ for (auto & [zoneId, zonePath] : zoneIdToPath) {
727+ tectonixDirtyZones[zonePath] = {};
728+ }
662729 }
663730
664731 // Get dirty files via git status with -z for NUL-separated output
@@ -710,11 +777,22 @@ const std::map<std::string, EvalState::ZoneDirtyInfo> & EvalState::getTectonixDi
710777 }
711778
712779 for (const auto & filePath : pathsToCheck) {
780+ auto repoPath = filePath.substr (1 );
781+
782+ if (fullCheckout) {
783+ if (auto zonePath = findZonePathForRepoPath (*manifest, repoPath)) {
784+ auto & info = tectonixDirtyZones[*zonePath];
785+ info.dirty = true ;
786+ info.dirtyFiles .insert (repoPath);
787+ }
788+ continue ;
789+ }
790+
713791 for (auto & [zonePath, info] : tectonixDirtyZones) {
714792 auto normalized = " /" + normalizeZonePath (zonePath);
715793 if (hasPrefix (filePath, normalized + " /" ) || filePath == normalized) {
716794 info.dirty = true ;
717- info.dirtyFiles .insert (filePath. substr ( 1 ) );
795+ info.dirtyFiles .insert (repoPath );
718796 break ;
719797 }
720798 }
@@ -724,11 +802,21 @@ const std::map<std::string, EvalState::ZoneDirtyInfo> & EvalState::getTectonixDi
724802 size_t dirtyCount = 0 ;
725803 for (const auto & [_, info] : tectonixDirtyZones)
726804 if (info.dirty ) dirtyCount++;
727- debug (" computed dirty zones: %d of %d zones are dirty" , dirtyCount, tectonixDirtyZones.size ());
805+ debug (" computed dirty zones: %d of %d materialized zones are dirty" , dirtyCount, tectonixDirtyZones.size ());
728806 });
729807 return tectonixDirtyZones;
730808}
731809
810+ bool EvalState::isTectonixZoneDirty (std::string_view zonePath) const
811+ {
812+ if (!isTectonixSourceAvailable ())
813+ return false ;
814+
815+ auto & dirtyZones = getTectonixDirtyZones ();
816+ auto it = dirtyZones.find (std::string (zonePath));
817+ return it != dirtyZones.end () && it->second .dirty ;
818+ }
819+
732820// Path to the tectonix manifest file within the world repository
733821static constexpr std::string_view TECTONIX_MANIFEST_PATH = " /.meta/manifest.json" ;
734822
0 commit comments