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
11pub 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 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 assert!(root.join("dvs.toml").is_file());
67 assert!(root.join(".dvs").is_dir());
69 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 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 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 let config = Config::new_local(&storage, None).unwrap();
130 init(&root, config).unwrap();
131
132 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 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 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 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 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 assert!(
188 !root.join("dvs.toml").exists(),
189 "dvs.toml should be cleaned up"
190 );
191 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}