Compute the status of the working copy. Optimized for repositories with tens of thousands of files: 1. **Single TREE pass** — builds tracked_paths + inode_map together 2. **FILE_INDEX fast path** — stat-only check for unchanged files 3. **Clean files skipped** — only Modified/Added/Deleted/Untracked allocated 4. **Deferred walkdir** — filesystem walk only when untracked files requested # Perfor
(&self, options: StatusOptions)
| 22 | /// | 43,000 files | ~150s | <3s | |
| 23 | /// | 80,000 files | ~150s | <5s | |
| 24 | pub fn status(&self, options: StatusOptions) -> Result<RepositoryStatus, RepositoryError> { |
| 25 | use std::time::SystemTime; |
| 26 | |
| 27 | let overall_start = std::time::Instant::now(); |
| 28 | |
| 29 | let txn = self |
| 30 | .pristine |
| 31 | .read_txn() |
| 32 | .map_err(|e| RepositoryError::Database(e.to_string()))?; |
| 33 | |
| 34 | let view_state = txn |
| 35 | .get_view(&self.current_view) |
| 36 | .map_err(|e| RepositoryError::Database(e.to_string()))? |
| 37 | .map(|s| s.state); |
| 38 | |
| 39 | let mut status = RepositoryStatus::new(self.current_view.clone(), view_state); |
| 40 | |
| 41 | // ── View-aware filtering ─────────────────────────────────────── |
| 42 | // |
| 43 | // Always build the explicit change filter. The TREE table is |
| 44 | // global — it contains entries from ALL views, including child |
| 45 | // views. Without filtering, files recorded on child views leak |
| 46 | // into the parent's status as false "Deleted" entries. |
| 47 | // |
| 48 | // The previous "universal" fast-path (skip filter for |
| 49 | // is_shared() && parent.is_none()) was unsound: the dev view is |
| 50 | // shared with no parent, but child/sibling views may have unique |
| 51 | // changes. Skipping the filter caused TREE entries created by |
| 52 | // those changes to surface as phantom `Deleted` files in dev's |
| 53 | // status. |
| 54 | // |
| 55 | // The filter computation is O(C) where C is changes on the view — |
| 56 | // a single B-tree scan, fast even on large repos. |
| 57 | // |
| 58 | // None means "no current view" (a misconfigured repo); preserve |
| 59 | // the legacy "show everything" behavior in that case rather than |
| 60 | // producing an empty status. |
| 61 | let current_view_change_ids: Option<HashSet<NodeId>> = if let Some(ref view) = txn |
| 62 | .get_view(&self.current_view) |
| 63 | .map_err(|e| RepositoryError::Database(e.to_string()))? |
| 64 | { |
| 65 | Some(collect_visible_change_ids_with_deps(&txn, view)?) |
| 66 | } else { |
| 67 | None |
| 68 | }; |
| 69 | |
| 70 | let phase1_ms = overall_start.elapsed().as_millis(); |
| 71 | log::debug!("status: view filter setup took {}ms", phase1_ms); |
| 72 | |
| 73 | // ── Single-pass TREE scan ────────────────────────────────────── |
| 74 | let tree_start = std::time::Instant::now(); |
| 75 | // |
| 76 | // Build tracked_paths, inode_map, and directory_inodes in ONE |
| 77 | // iter_tree() call instead of three passes. |
| 78 | let mut tracked_paths: HashSet<PathBuf> = HashSet::new(); |
| 79 | let mut inode_map: HashMap<PathBuf, atomic_core::types::Inode> = HashMap::new(); |
| 80 | let mut directory_inodes: HashSet<atomic_core::types::Inode> = HashSet::new(); |
| 81 | // Cache inode → has_graph_content so we don't call inode_position twice |