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::{Backend, Config};
9use crate::paths;
10
11pub fn init(root_dir: impl AsRef<Path>, mut config: Config) -> Result<PathBuf> {
14 let root_dir = root_dir.as_ref();
15
16 match &mut config.backend {
19 Backend::Local(b) => {
20 let storage_path = &b.path;
21 let abs_storage = if storage_path.exists() {
22 fs::canonicalize(storage_path)?
23 } else if let Some(parent) = storage_path.parent().filter(|p| p.exists()) {
24 fs::canonicalize(parent)?.join(storage_path.file_name().unwrap())
25 } else {
26 std::path::absolute(storage_path)?
27 };
28 let abs_root = fs::canonicalize(root_dir)?;
29 if abs_storage.starts_with(&abs_root) {
30 bail!("The given storage path is within the repository.");
31 }
32 b.path = abs_storage;
33 }
34 }
35
36 if root_dir.join(paths::CONFIG_FILE_NAME).exists() {
37 bail!("dvs is already initialized (dvs.toml exists)");
38 }
39 if config.backend().is_initialized()? {
40 bail!("dvs is already initialized (backend storage exists)");
41 }
42 config.save(root_dir)?;
43
44 let metadata_dir = root_dir.join(config.metadata_folder_name());
45 let config_path = root_dir.join(paths::CONFIG_FILE_NAME);
46 let metadata_existed = metadata_dir.exists();
47
48 let result = (|| {
49 log::debug!("Creating metadata folder: {}", metadata_dir.display());
50 fs::create_dir(&metadata_dir)?;
51 log::debug!("Initializing backend");
52 config.backend().init()
53 })();
54
55 if let Err(e) = result {
56 if !metadata_existed {
58 let _ = fs::remove_dir_all(&metadata_dir);
59 }
60 let _ = fs::remove_file(&config_path);
61 return Err(e);
62 }
63
64 let audit_entry = AuditEntry::new_init(Uuid::new_v4(), config.clone(), root_dir.to_path_buf());
65 if let Err(e) = config.backend().log_audit(&audit_entry) {
66 log::error!("Failed to write init audit log {audit_entry:?}: {e}");
67 }
68
69 log::info!("DVS repository initialized successfully");
70 Ok(root_dir.to_path_buf())
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use crate::testutil::create_temp_git_repo;
77
78 #[test]
79 fn init_creates_config_and_directories() {
80 let (_tmp, root) = create_temp_git_repo();
81 let storage = root.parent().unwrap().join(".storage");
82
83 let config = Config::new_local(&storage, None).unwrap();
84 init(&root, config).unwrap();
85
86 assert!(root.join("dvs.toml").is_file());
88 assert!(root.join(".dvs").is_dir());
90 assert!(storage.is_dir());
92 }
93
94 #[test]
95 fn init_logs_audit_entry() {
96 let (_tmp, root) = create_temp_git_repo();
97 let storage = root.parent().unwrap().join(".storage");
98
99 let config = Config::new_local(&storage, None).unwrap();
100 init(&root, config.clone()).unwrap();
101
102 let entries = config.backend().read_audit_file(&[]).unwrap();
103 assert_eq!(entries.len(), 1);
104 assert!(matches!(
105 entries[0].action,
106 crate::audit::Action::Init { .. }
107 ));
108 }
109
110 #[test]
111 fn init_fails_if_already_initialized() {
112 let (_tmp, root) = create_temp_git_repo();
113 let storage = root.parent().unwrap().join(".storage");
114
115 let config = Config::new_local(&storage, None).unwrap();
116 init(&root, config.clone()).unwrap();
117
118 let result = init(&root, config.clone());
120 assert!(result.is_err(), "second init should fail");
121 }
122
123 #[test]
124 fn init_fails_if_backend_already_initialized() {
125 let (_tmp, root) = create_temp_git_repo();
126 let storage = root.parent().unwrap().join(".storage");
127
128 let config = Config::new_local(&storage, None).unwrap();
129 assert!(!config.backend().is_initialized().unwrap());
130 init(&root, config.clone()).unwrap();
131 assert!(config.backend().is_initialized().unwrap());
132
133 fs::remove_file(root.join("dvs.toml")).unwrap();
135 fs::remove_dir_all(root.join(".dvs")).unwrap();
136
137 let result = init(&root, config);
138 assert!(
139 result.is_err(),
140 "should detect backend is already initialized"
141 );
142 }
143
144 #[test]
145 fn init_rejects_storage_inside_repo() {
146 let (_tmp, root) = create_temp_git_repo();
147 let storage = root.join("inside").join(".storage");
149
150 let config = Config::new_local(&storage, None).unwrap();
151 let result = init(&root, config);
152 assert!(
153 result.is_err(),
154 "init should reject storage inside the repository"
155 );
156 assert!(
157 result
158 .unwrap_err()
159 .to_string()
160 .contains("within the repository"),
161 "error should explain the storage path is inside the repo"
162 );
163 assert!(!root.join("dvs.toml").exists());
165 }
166
167 #[test]
168 fn init_succeeds_in_subdirectory_of_initialized_project() {
169 let (_tmp, root) = create_temp_git_repo();
170 let storage = root.parent().unwrap().join(".storage");
171
172 let config = Config::new_local(&storage, None).unwrap();
174 init(&root, config).unwrap();
175
176 let subdir = root.join("nested");
178 fs::create_dir(&subdir).unwrap();
179 let nested_storage = root.parent().unwrap().join(".nested-storage");
180 let nested_config = Config::new_local(&nested_storage, None).unwrap();
181 let result = init(&subdir, nested_config);
182 assert!(
183 result.is_ok(),
184 "init in subdirectory should succeed: {result:?}"
185 );
186 assert!(subdir.join("dvs.toml").is_file());
187 assert!(subdir.join(".dvs").is_dir());
188 }
189
190 #[cfg(unix)]
191 #[test]
192 fn init_cleans_up_on_backend_failure() {
193 let (_tmp, root) = create_temp_git_repo();
194 let storage = Path::new("/dev/null/impossible");
196
197 let config = Config::new_local(storage, None).unwrap();
198 let result = init(&root, config);
199 assert!(result.is_err());
200
201 assert!(
203 !root.join("dvs.toml").exists(),
204 "dvs.toml should be cleaned up"
205 );
206 assert!(!root.join(".dvs").exists(), ".dvs should be cleaned up");
207
208 let valid_storage = root.parent().unwrap().join(".storage");
210 let config = Config::new_local(&valid_storage, None).unwrap();
211 init(&root, config).unwrap();
212 assert!(root.join("dvs.toml").is_file());
213 assert!(root.join(".dvs").is_dir());
214 }
215
216 #[test]
217 fn init_preserves_preexisting_metadata_dir_on_failure() {
218 let (_tmp, root) = create_temp_git_repo();
219 let metadata_dir = root.join(".dvs");
221 fs::create_dir(&metadata_dir).unwrap();
222 let marker = metadata_dir.join("marker.txt");
223 fs::write(&marker, "keep me").unwrap();
224
225 let storage = Path::new("/dev/null/impossible");
226 let config = Config::new_local(storage, None).unwrap();
227 let result = init(&root, config);
228 assert!(result.is_err());
229
230 assert!(
232 !root.join("dvs.toml").exists(),
233 "dvs.toml should be cleaned up"
234 );
235 assert!(metadata_dir.is_dir(), ".dvs should be preserved");
237 assert!(
238 marker.is_file(),
239 "marker file inside .dvs should be preserved"
240 );
241 }
242}