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