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                },
86            );
87        }
88    }
89
90    // Step 2: Walk working tree
91    let void_dir_name = void_dir
92        .file_name()
93        .and_then(|s| s.to_str())
94        .unwrap_or(".void")
95        .to_string();
96
97    let mut working_tree_files: HashMap<String, ()> = HashMap::new();
98    let mut builder = WalkBuilder::new(workspace);
99    let walker = configure_walker(&mut builder)
100        .filter_entry({
101            let void_dir_name = void_dir_name.clone();
102            move |entry| {
103                let name = entry.file_name().to_string_lossy();
104                if name == void_dir_name
105                    || name == ".git"
106                    || name == "node_modules"
107                    || name == ".DS_Store"
108                {
109                    return false;
110                }
111                true
112            }
113        })
114        .build();
115
116    for entry in walker.flatten() {
117        if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
118            continue;
119        }
120        let path = entry.path().to_path_buf();
121        let rel = match path.strip_prefix(workspace) {
122            Ok(r) => r.to_string_lossy().replace('\\', "/"),
123            Err(_) => continue,
124        };
125        working_tree_files.insert(rel, ());
126    }
127
128    // Step 3: Build index entries
129    let mut entries = Vec::new();
130    let mut from_head = 0usize;
131    let mut from_working_tree = 0usize;
132
133    for (rel_path, _) in &working_tree_files {
134        if let Some(head_entry) = head_entries.get(rel_path) {
135            // File exists in HEAD, check if it matches current disk state
136            let current_entry = match index_entry_from_file(&workspace_utf8, rel_path) {
137                Ok(entry) => entry,
138                Err(_) => continue, // Skip if can't read file
139            };
140
141            if current_entry.content_hash == head_entry.content_hash {
142                // File matches HEAD, use HEAD entry with current metadata
143                entries.push(IndexEntry {
144                    path: rel_path.clone(),
145                    content_hash: head_entry.content_hash,
146                    mtime_secs: current_entry.mtime_secs,
147                    mtime_nanos: current_entry.mtime_nanos,
148                    size: current_entry.size,
149                });
150                from_head += 1;
151            } else {
152                // File has been modified, use current state (staged)
153                entries.push(current_entry);
154                from_working_tree += 1;
155            }
156        } else {
157            // File is new (not in HEAD), add to index as staged
158            match index_entry_from_file(&workspace_utf8, rel_path) {
159                Ok(entry) => {
160                    entries.push(entry);
161                    from_working_tree += 1;
162                }
163                Err(_) => continue, // Skip if can't read file
164            }
165        }
166    }
167
168    // Step 4: Write index
169    let entries_rebuilt = entries.len();
170    write_index(void_dir, vault.index_key()?, commit_cid_typed, entries)?;
171
172    Ok(RebuildResult {
173        entries_rebuilt,
174        from_head,
175        from_working_tree,
176    })
177}