1use std::fs::File;
2use std::path::{Path, PathBuf};
3
4use nix::fcntl::{Flock, FlockArg};
5
6use crate::config::Config;
7use crate::error::{Error, IoResultExt, Result};
8use crate::namespace::{current_gid_map, current_uid_map, NsConfig};
9
10pub struct Repo {
12 path: PathBuf,
13 config: Config,
14}
15
16impl Repo {
17 pub fn init(path: &Path) -> Result<Self> {
19 let config_path = path.join("config.toml");
20 if config_path.exists() {
21 return Err(Error::RepoExists(path.to_path_buf()));
22 }
23
24 std::fs::create_dir_all(path.join("objects/blobs")).with_path(path)?;
26 std::fs::create_dir_all(path.join("objects/trees")).with_path(path)?;
27 std::fs::create_dir_all(path.join("objects/commits")).with_path(path)?;
28 std::fs::create_dir_all(path.join("refs/heads")).with_path(path)?;
29 std::fs::create_dir_all(path.join("refs/tags")).with_path(path)?;
30 std::fs::create_dir_all(path.join("tmp")).with_path(path)?;
31
32 let uid_map = current_uid_map()?;
34 let gid_map = current_gid_map()?;
35
36 let config = Config::new(NsConfig { uid_map, gid_map });
37 config.save(&config_path)?;
38
39 Ok(Self {
40 path: path.to_path_buf(),
41 config,
42 })
43 }
44
45 pub fn open(path: &Path) -> Result<Self> {
47 let config_path = path.join("config.toml");
48 if !config_path.exists() {
49 return Err(Error::NoRepo(path.to_path_buf()));
50 }
51
52 let config = Config::load(&config_path)?;
53
54 Ok(Self {
55 path: path.to_path_buf(),
56 config,
57 })
58 }
59
60 pub fn path(&self) -> &Path {
62 &self.path
63 }
64
65 pub fn config(&self) -> &Config {
67 &self.config
68 }
69
70 pub fn config_mut(&mut self) -> &mut Config {
72 &mut self.config
73 }
74
75 pub fn save_config(&self) -> Result<()> {
77 self.config.save(&self.config_path())
78 }
79
80 pub fn config_path(&self) -> PathBuf {
82 self.path.join("config.toml")
83 }
84
85 pub fn objects_path(&self) -> PathBuf {
87 self.path.join("objects")
88 }
89
90 pub fn blobs_path(&self) -> PathBuf {
92 self.objects_path().join("blobs")
93 }
94
95 pub fn trees_path(&self) -> PathBuf {
97 self.objects_path().join("trees")
98 }
99
100 pub fn commits_path(&self) -> PathBuf {
102 self.objects_path().join("commits")
103 }
104
105 pub fn refs_path(&self) -> PathBuf {
107 self.path.join("refs/heads")
108 }
109
110 pub fn tags_path(&self) -> PathBuf {
112 self.path.join("refs/tags")
113 }
114
115 pub fn tmp_path(&self) -> PathBuf {
117 self.path.join("tmp")
118 }
119
120 pub fn lock_path(&self) -> PathBuf {
122 self.path.join(".lock")
123 }
124
125 pub fn lock(&self) -> Result<RepoLock> {
128 let lock_path = self.lock_path();
129 let file = File::create(&lock_path).with_path(&lock_path)?;
130
131 let flock = Flock::lock(file, FlockArg::LockExclusiveNonblock)
132 .map_err(|_| Error::LockContention)?;
133
134 Ok(RepoLock { flock })
135 }
136
137 pub fn try_lock(&self) -> Result<Option<RepoLock>> {
139 let lock_path = self.lock_path();
140 let file = File::create(&lock_path).with_path(&lock_path)?;
141
142 match Flock::lock(file, FlockArg::LockExclusiveNonblock) {
143 Ok(flock) => Ok(Some(RepoLock { flock })),
144 Err((_, nix::errno::Errno::EWOULDBLOCK)) => Ok(None),
145 Err(_) => Err(Error::LockContention),
146 }
147 }
148}
149
150pub struct RepoLock {
152 #[allow(dead_code)]
153 flock: Flock<File>,
154}
155#[allow(dead_code)]
159pub fn with_lock<T, F>(repo: &Repo, f: F) -> Result<T>
160where
161 F: FnOnce() -> Result<T>,
162{
163 let _lock = repo.lock()?;
164 f()
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use tempfile::tempdir;
171
172 #[test]
173 fn test_repo_init() {
174 let dir = tempdir().unwrap();
175 let repo_path = dir.path().join("test-repo");
176
177 let repo = Repo::init(&repo_path).unwrap();
178
179 assert!(repo_path.join("objects/blobs").is_dir());
181 assert!(repo_path.join("objects/trees").is_dir());
182 assert!(repo_path.join("objects/commits").is_dir());
183 assert!(repo_path.join("refs/heads").is_dir());
184 assert!(repo_path.join("refs/tags").is_dir());
185 assert!(repo_path.join("tmp").is_dir());
186 assert!(repo_path.join("config.toml").is_file());
187
188 assert!(!repo.config().namespace.uid_map.is_empty());
190 }
191
192 #[test]
193 fn test_repo_init_already_exists() {
194 let dir = tempdir().unwrap();
195 let repo_path = dir.path().join("test-repo");
196
197 Repo::init(&repo_path).unwrap();
198 let result = Repo::init(&repo_path);
199
200 assert!(matches!(result, Err(Error::RepoExists(_))));
201 }
202
203 #[test]
204 fn test_repo_open() {
205 let dir = tempdir().unwrap();
206 let repo_path = dir.path().join("test-repo");
207
208 Repo::init(&repo_path).unwrap();
209 let repo = Repo::open(&repo_path).unwrap();
210
211 assert_eq!(repo.path(), repo_path);
212 }
213
214 #[test]
215 fn test_repo_open_not_found() {
216 let dir = tempdir().unwrap();
217 let repo_path = dir.path().join("nonexistent");
218
219 let result = Repo::open(&repo_path);
220 assert!(matches!(result, Err(Error::NoRepo(_))));
221 }
222
223 #[test]
224 fn test_repo_paths() {
225 let dir = tempdir().unwrap();
226 let repo_path = dir.path().join("test-repo");
227 let repo = Repo::init(&repo_path).unwrap();
228
229 assert_eq!(repo.blobs_path(), repo_path.join("objects/blobs"));
230 assert_eq!(repo.trees_path(), repo_path.join("objects/trees"));
231 assert_eq!(repo.commits_path(), repo_path.join("objects/commits"));
232 assert_eq!(repo.refs_path(), repo_path.join("refs/heads"));
233 assert_eq!(repo.tmp_path(), repo_path.join("tmp"));
234 }
235
236 #[test]
237 fn test_repo_lock() {
238 let dir = tempdir().unwrap();
239 let repo_path = dir.path().join("test-repo");
240 let repo = Repo::init(&repo_path).unwrap();
241
242 let lock = repo.lock().unwrap();
244
245 let result = repo.try_lock().unwrap();
247 assert!(result.is_none());
248
249 drop(lock);
251
252 let lock2 = repo.try_lock().unwrap();
254 assert!(lock2.is_some());
255 }
256
257 #[test]
258 fn test_config_modification() {
259 let dir = tempdir().unwrap();
260 let repo_path = dir.path().join("test-repo");
261 let mut repo = Repo::init(&repo_path).unwrap();
262
263 repo.config_mut()
264 .add_remote("origin", "ssh://server/repo")
265 .unwrap();
266 repo.save_config().unwrap();
267
268 let repo2 = Repo::open(&repo_path).unwrap();
270 assert_eq!(repo2.config().remotes.len(), 1);
271 assert_eq!(repo2.config().remotes[0].name, "origin");
272 }
273}