Skip to main content

void_core/index/
rebuild.rs

1//! Rebuild index from HEAD commit state + working tree scan.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use camino::Utf8PathBuf;
7use ignore::WalkBuilder;
8use rayon::prelude::*;
9
10use crate::crypto::KeyVault;
11use crate::metadata::manifest_tree::TreeManifest;
12use crate::refs;
13
14use crate::store::{FsStore, ObjectStoreExt};
15use void_crypto::EncryptedCommit;
16use crate::support::configure_walker;
17use crate::{Result, VoidError};
18
19use super::entry::index_entry_from_file;
20use super::io::write_index;
21use super::types::IndexEntry;
22
23/// Result of rebuilding the index.
24#[derive(Debug, Clone)]
25pub struct RebuildResult {
26    /// Total number of entries in the rebuilt index.
27    pub entries_rebuilt: usize,
28    /// Number of entries sourced from HEAD commit.
29    pub from_head: usize,
30    /// Number of entries sourced from working tree scan.
31    pub from_working_tree: usize,
32}
33
34
35
36/// Rebuild index from HEAD commit state + working tree scan.
37///
38/// Recovers from corrupted index by:
39/// 1. Resolving HEAD to get current commit CID
40/// 2. Loading file list from HEAD commit (walking shards, collecting paths + content hashes)
41/// 3. Scanning working tree for current files
42/// 4. Building index entries by comparing HEAD vs working tree
43/// 5. Writing new index using atomic write
44///
45/// Files that exist in HEAD but not on disk are excluded from the rebuilt index.
46/// Files that exist on disk but not in HEAD use their current disk state.
47pub fn rebuild_index(void_dir: &Path, workspace: &Path, vault: &KeyVault) -> Result<RebuildResult> {
48    let void_dir_utf8 = Utf8PathBuf::try_from(void_dir.to_path_buf())
49        .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
50    let workspace_utf8 = Utf8PathBuf::try_from(workspace.to_path_buf())
51        .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
52
53    // Step 1: Load HEAD entries
54    let head_commit_cid = refs::resolve_head(&void_dir_utf8)?;
55    let mut head_entries: HashMap<String, IndexEntry> = HashMap::new();
56    let mut commit_cid_typed: Option<void_crypto::CommitCid> = None;
57
58    if let Some(ref cid_typed) = head_commit_cid {
59        commit_cid_typed = Some(cid_typed.clone());
60
61        let objects_dir = Utf8PathBuf::try_from(void_dir.join("objects"))
62            .map_err(|e| VoidError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
63        let store = FsStore::new(objects_dir)?;
64
65        let commit_cid = crate::cid::from_bytes(cid_typed.as_bytes())?;
66        let commit_encrypted: EncryptedCommit = store.get_blob(&commit_cid)?;
67        let (commit_bytes, reader) = crate::crypto::CommitReader::open_with_vault(vault, &commit_encrypted)?;
68        let commit = commit_bytes.parse()?;
69
70        let manifest = TreeManifest::from_commit(&store, &commit, &reader)?
71            .ok_or_else(|| VoidError::IntegrityError {
72                expected: "manifest_cid present on commit".into(),
73                actual: "None".into(),
74            })?;
75
76        for me in manifest.iter() {
77            let me = me?;
78            head_entries.insert(
79                me.path.clone(),
80                IndexEntry {
81                    path: me.path.clone(),
82                    content_hash: me.content_hash,
83                    mtime_secs: 0,
84                    mtime_nanos: 0,
85                    size: me.length,
86                    materialized: true,
87                },
88            );
89        }
90    }
91
92    // Step 2: Walk working tree and collect paths
93    let void_dir_name = void_dir
94        .file_name()
95        .and_then(|s| s.to_str())
96        .unwrap_or(".void")
97        .to_string();
98
99    let mut builder = WalkBuilder::new(workspace);
100    let walker = configure_walker(&mut builder)
101        .filter_entry({
102            let void_dir_name = void_dir_name.clone();
103            move |entry| {
104                let name = entry.file_name().to_string_lossy();
105                name != void_dir_name
106                    && name != ".git"
107                    && name != "node_modules"
108                    && name != ".DS_Store"
109            }
110        })
111        .build();
112
113    let file_paths: Vec<String> = walker
114        .flatten()
115        .filter(|entry| entry.file_type().map(|t| t.is_file()).unwrap_or(false))
116        .filter_map(|entry| {
117            let path = entry.path().to_path_buf();
118            let rel = path.strip_prefix(workspace).ok()?;
119            Some(rel.to_string_lossy().replace('\\', "/"))
120        })
121        .collect();
122
123    // Step 3: Build index entries in parallel (read + hash per file)
124    let results: Vec<Option<(IndexEntry, bool)>> = file_paths
125        .par_iter()
126        .map(|rel_path| {
127            let current_entry = match index_entry_from_file(&workspace_utf8, rel_path) {
128                Ok(entry) => entry,
129                Err(_) => return Ok(None),
130            };
131
132            if let Some(head_entry) = head_entries.get(rel_path) {
133                if current_entry.content_hash == head_entry.content_hash {
134                    // File matches HEAD, use HEAD hash with current metadata
135                    Ok(Some((IndexEntry {
136                        path: rel_path.clone(),
137                        content_hash: head_entry.content_hash,
138                        mtime_secs: current_entry.mtime_secs,
139                        mtime_nanos: current_entry.mtime_nanos,
140                        size: current_entry.size,
141                        materialized: true,
142                    }, true)))
143                } else {
144                    Ok(Some((current_entry, false)))
145                }
146            } else {
147                Ok(Some((current_entry, false)))
148            }
149        })
150        .collect::<Result<Vec<_>>>()?;
151
152    let mut entries = Vec::new();
153    let mut from_head = 0usize;
154    let mut from_working_tree = 0usize;
155
156    for result in results {
157        let Some((entry, is_from_head)) = result else { continue };
158        if is_from_head {
159            from_head += 1;
160        } else {
161            from_working_tree += 1;
162        }
163        entries.push(entry);
164    }
165
166    // Step 4: Write index
167    let entries_rebuilt = entries.len();
168    write_index(void_dir, vault.index_key()?, commit_cid_typed, entries)?;
169
170    Ok(RebuildResult {
171        entries_rebuilt,
172        from_head,
173        from_working_tree,
174    })
175}