Skip to main content
← dvs documentation Rust API reference

dvs/
init.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, bail};
4use fs_err as fs;
5use uuid::Uuid;
6
7use crate::audit::AuditEntry;
8use crate::config::Config;
9use crate::paths;
10
11/// Starts a new dvs project.
12/// We need a ready to use Config object + the current directory the user is in
13pub fn init(root_dir: impl AsRef<Path>, config: Config) -> Result<PathBuf> {
14    let root_dir = root_dir.as_ref();
15    if root_dir.join(paths::CONFIG_FILE_NAME).exists() {
16        bail!("dvs is already initialized (dvs.toml exists)");
17    }
18    if config.backend().is_initialized()? {
19        bail!("dvs is already initialized (backend storage exists)");
20    }
21    config.save(root_dir)?;
22
23    let metadata_dir = root_dir.join(config.metadata_folder_name());
24    let config_path = root_dir.join(paths::CONFIG_FILE_NAME);
25    let metadata_existed = metadata_dir.exists();
26
27    let result = (|| {
28        log::debug!("Creating metadata folder: {}", metadata_dir.display());
29        fs::create_dir(&metadata_dir)?;
30        log::debug!("Initializing backend");
31        config.backend().init()
32    })();
33
34    if let Err(e) = result {
35        // Best-effort cleanup of local artifacts we created
36        if !metadata_existed {
37            let _ = fs::remove_dir_all(&metadata_dir);
38        }
39        let _ = fs::remove_file(&config_path);
40        return Err(e);
41    }
42
43    let audit_entry = AuditEntry::new_init(Uuid::new_v4(), config.clone(), root_dir.to_path_buf());
44    if let Err(e) = config.backend().log_audit(&audit_entry) {
45        log::error!("Failed to write init audit log {audit_entry:?}: {e}");
46    }
47
48    log::info!("DVS repository initialized successfully");
49    Ok(root_dir.to_path_buf())
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::testutil::create_temp_git_repo;
56
57    #[test]
58    fn init_creates_config_and_directories() {
59        let (_tmp, root) = create_temp_git_repo();
60        let storage = root.join(".storage");
61
62        let config = Config::new_local(&storage, None).unwrap();
63        init(&root, config).unwrap();
64
65        // Config file should exist
66        assert!(root.join("dvs.toml").is_file());
67        // Metadata folder should exist
68        assert!(root.join(".dvs").is_dir());
69        // Storage folder should exist
70        assert!(storage.is_dir());
71    }
72
73    #[test]
74    fn init_logs_audit_entry() {
75        let (_tmp, root) = create_temp_git_repo();
76        let storage = root.join(".storage");
77
78        let config = Config::new_local(&storage, None).unwrap();
79        init(&root, config.clone()).unwrap();
80
81        let entries = config.backend().read_audit_file(&[]).unwrap();
82        assert_eq!(entries.len(), 1);
83        assert!(matches!(
84            entries[0].action,
85            crate::audit::Action::Init { .. }
86        ));
87    }
88
89    #[test]
90    fn init_fails_if_already_initialized() {
91        let (_tmp, root) = create_temp_git_repo();
92        let storage = root.join(".storage");
93
94        let config = Config::new_local(&storage, None).unwrap();
95        init(&root, config.clone()).unwrap();
96
97        // Second init should fail because dvs.toml exists
98        let result = init(&root, config.clone());
99        assert!(result.is_err(), "second init should fail");
100    }
101
102    #[test]
103    fn init_fails_if_backend_already_initialized() {
104        let (_tmp, root) = create_temp_git_repo();
105        let storage = root.join(".storage");
106
107        let config = Config::new_local(&storage, None).unwrap();
108        assert!(!config.backend().is_initialized().unwrap());
109        init(&root, config.clone()).unwrap();
110        assert!(config.backend().is_initialized().unwrap());
111
112        // Remove dvs.toml and .dvs but leave backend storage intact
113        fs::remove_file(root.join("dvs.toml")).unwrap();
114        fs::remove_dir_all(root.join(".dvs")).unwrap();
115
116        let result = init(&root, config);
117        assert!(
118            result.is_err(),
119            "should detect backend is already initialized"
120        );
121    }
122
123    #[test]
124    fn init_succeeds_in_subdirectory_of_initialized_project() {
125        let (_tmp, root) = create_temp_git_repo();
126        let storage = root.join(".storage");
127
128        // Initialize the parent project
129        let config = Config::new_local(&storage, None).unwrap();
130        init(&root, config).unwrap();
131
132        // Create a subdirectory and initialize a nested project there
133        let subdir = root.join("nested");
134        fs::create_dir(&subdir).unwrap();
135        let nested_storage = subdir.join(".storage");
136        let nested_config = Config::new_local(&nested_storage, None).unwrap();
137        let result = init(&subdir, nested_config);
138        assert!(
139            result.is_ok(),
140            "init in subdirectory should succeed: {result:?}"
141        );
142        assert!(subdir.join("dvs.toml").is_file());
143        assert!(subdir.join(".dvs").is_dir());
144    }
145
146    #[cfg(unix)]
147    #[test]
148    fn init_cleans_up_on_backend_failure() {
149        let (_tmp, root) = create_temp_git_repo();
150        // Point storage at an impossible path so backend init fails
151        let storage = Path::new("/dev/null/impossible");
152
153        let config = Config::new_local(storage, None).unwrap();
154        let result = init(&root, config);
155        assert!(result.is_err());
156
157        // Local artifacts should have been cleaned up
158        assert!(
159            !root.join("dvs.toml").exists(),
160            "dvs.toml should be cleaned up"
161        );
162        assert!(!root.join(".dvs").exists(), ".dvs should be cleaned up");
163
164        // A retry with a valid storage path should now succeed
165        let valid_storage = root.join(".storage");
166        let config = Config::new_local(&valid_storage, None).unwrap();
167        init(&root, config).unwrap();
168        assert!(root.join("dvs.toml").is_file());
169        assert!(root.join(".dvs").is_dir());
170    }
171
172    #[test]
173    fn init_preserves_preexisting_metadata_dir_on_failure() {
174        let (_tmp, root) = create_temp_git_repo();
175        // Create a .dvs directory before init runs
176        let metadata_dir = root.join(".dvs");
177        fs::create_dir(&metadata_dir).unwrap();
178        let marker = metadata_dir.join("marker.txt");
179        fs::write(&marker, "keep me").unwrap();
180
181        let storage = Path::new("/dev/null/impossible");
182        let config = Config::new_local(storage, None).unwrap();
183        let result = init(&root, config);
184        assert!(result.is_err());
185
186        // dvs.toml should be cleaned up
187        assert!(
188            !root.join("dvs.toml").exists(),
189            "dvs.toml should be cleaned up"
190        );
191        // Pre-existing .dvs directory and its contents should be preserved
192        assert!(metadata_dir.is_dir(), ".dvs should be preserved");
193        assert!(
194            marker.is_file(),
195            "marker file inside .dvs should be preserved"
196        );
197    }
198}