1use crate::{
2 parser::{GitReceivePackArgs, GitUploadPackArgs},
3 user_info::{get_gid, get_user_groups, get_username},
4 ShackleError,
5};
6use git2::{ErrorCode, Repository, RepositoryInitMode, RepositoryInitOptions};
7use std::{
8 fs,
9 os::unix::fs::PermissionsExt,
10 path::{Path, PathBuf},
11 process::Command,
12};
13
14pub struct GitInitResult {
15 pub path: PathBuf,
16}
17
18fn git_dir_prefix() -> PathBuf {
19 PathBuf::from("git")
20}
21
22fn personal_git_dir() -> Result<PathBuf, ShackleError> {
23 let username = get_username().ok_or(ShackleError::UserReadError)?;
24 Ok(git_dir_prefix().join(username))
25}
26
27fn verify_user_is_in_group(group: &str) -> bool {
28 let user_groups = get_user_groups();
29 user_groups.iter().any(|g| g == group)
30}
31
32fn group_git_dir(group: &str) -> PathBuf {
33 git_dir_prefix().join(group)
34}
35
36fn is_valid_git_repo_path(path: &Path) -> Result<bool, ShackleError> {
37 let prefix = git_dir_prefix();
38 let relative_path = match path.strip_prefix(&prefix) {
39 Ok(relative_path) => relative_path,
40 Err(_) => {
41 return Ok(false);
42 }
43 };
44
45 let mut it = relative_path.iter();
46 let group = it.next();
47 let repo_name = it.next();
48 let end = it.next();
49
50 match (group, repo_name, end) {
51 (_, _, Some(_)) | (None, _, _) | (_, None, _) => Ok(false),
52 (Some(group_name), Some(_repo_name), _) => {
53 if relative_path.extension().map(|ext| ext == "git") != Some(true) {
54 Ok(false)
55 } else {
56 let group_name = group_name.to_string_lossy();
57
58 let user_name = get_username();
59 let is_valid_personal_repo_path = user_name
60 .map(|user_name| user_name == group_name)
61 .unwrap_or(false);
62
63 let user_groups = get_user_groups();
64 let is_valid_shared_repo_path =
65 user_groups.iter().any(|group| group.as_ref() == group_name);
66
67 Ok(is_valid_personal_repo_path || is_valid_shared_repo_path)
68 }
69 }
70 }
71}
72
73pub fn init(
74 repo_name: &str,
75 group: &Option<String>,
76 description: &Option<String>,
77 branch: &str,
78) -> Result<GitInitResult, ShackleError> {
79 if let Some(group) = &group {
80 if !verify_user_is_in_group(group) {
81 return Err(ShackleError::InvalidGroup);
82 }
83 }
84
85 let git_prefix = git_dir_prefix();
86 let collection_dir = match group {
87 Some(group) => group_git_dir(group),
88 None => personal_git_dir()?,
89 };
90 let path = collection_dir.join(repo_name).with_extension("git");
91
92 if !git_prefix.is_dir() {
93 fs::create_dir(&git_prefix)?;
94 }
95
96 if !collection_dir.is_dir() {
97 fs::create_dir(&collection_dir)?;
98
99 if let Some(group) = group {
100 let gid = get_gid(&group).expect("User is in group but no group ID?");
101 nix::unistd::chown(&collection_dir, None, Some(gid))?;
102 }
103
104 let mut perms = collection_dir.metadata()?.permissions();
105 perms.set_mode(match group {
106 Some(_) => 0o2770,
107 None => 0o700,
108 });
109 fs::set_permissions(&collection_dir, perms)?;
110 }
111
112 let mut init_opts = RepositoryInitOptions::new();
113 init_opts
114 .bare(true)
115 .mkdir(false)
116 .no_reinit(true)
117 .initial_head(branch);
118 if group.is_some() {
119 init_opts.mode(RepositoryInitMode::SHARED_GROUP);
120 }
121
122 Repository::init_opts(&path, &init_opts)?;
123 if let Some(description) = description {
124 set_description(&path, description)?;
127 }
128
129 Ok(GitInitResult { path })
130}
131
132pub struct RepoMetadata {
133 pub path: PathBuf,
134 pub description: String,
135}
136
137pub struct VerboseRepoMetadata {
138 pub path: PathBuf,
139 pub description: String,
140 pub size: u64,
141}
142
143fn get_size(path: impl AsRef<Path>) -> Result<u64, ShackleError> {
144 let path_metadata = path.as_ref().symlink_metadata()?;
145
146 if path_metadata.is_dir() {
147 let mut size_in_bytes = path_metadata.len();
148 for entry in path.as_ref().read_dir()? {
149 let entry = entry?;
150 let entry_metadata = entry.metadata()?;
151
152 if entry_metadata.is_dir() {
153 size_in_bytes += get_size(entry.path())?;
154 } else {
155 size_in_bytes += entry_metadata.len();
156 }
157 }
158 Ok(size_in_bytes)
159 } else {
160 Ok(path_metadata.len())
161 }
162}
163
164pub fn list() -> Result<Vec<RepoMetadata>, ShackleError> {
165 fn add_from_dir(
166 collection_dir: &Path,
167 is_checking_group: bool,
168 ) -> Result<Vec<RepoMetadata>, ShackleError> {
169 let mut results = Vec::new();
170 if !collection_dir.is_dir() {
171 return Ok(results);
172 }
173
174 for dir in collection_dir.read_dir()? {
175 let path = dir?.path();
176 let description_path = path.join("description");
177 let has_git_ext = path.extension().map_or(false, |ext| ext == "git");
178
179 if has_git_ext {
180 if let Ok(repo) = Repository::open_bare(&path) {
181 let config = repo.config()?.snapshot()?;
182 let shared_config = config.get_str("core.sharedRepository").or_else(|e| {
183 if e.code() == ErrorCode::NotFound {
184 Ok("")
185 } else {
186 Err(e)
187 }
188 })?;
189 let is_group_shared =
190 [Some("group"), Some("1"), Some("true")].contains(&Some(shared_config));
191
192 if is_group_shared == is_checking_group {
193 let description = if description_path.is_file() {
194 fs::read_to_string(description_path)?
195 } else {
196 String::new()
197 };
198
199 results.push(RepoMetadata { path, description });
200 }
201 }
202 }
203 }
204 Ok(results)
205 }
206
207 let mut results = Vec::new();
208
209 results.append(&mut add_from_dir(&personal_git_dir()?, false)?);
210 let groups = get_user_groups();
211 for group in &groups {
212 results.append(&mut add_from_dir(&group_git_dir(group), true)?);
213 }
214
215 Ok(results)
216}
217
218pub fn list_verbose() -> Result<Vec<VerboseRepoMetadata>, ShackleError> {
219 list()?
220 .into_iter()
221 .map(|meta| {
222 get_size(&meta.path).map(|size| VerboseRepoMetadata {
223 path: meta.path,
224 description: meta.description,
225 size,
226 })
227 })
228 .collect()
229}
230
231pub fn set_description(directory: &Path, description: &str) -> Result<(), ShackleError> {
232 if !is_valid_git_repo_path(directory)? {
233 return Err(ShackleError::InvalidDirectory);
234 }
235
236 let description_path = directory.join("description");
237 if description_path.is_file() {
238 fs::write(description_path, description).map_err(|e| e.into())
239 } else {
240 Err(ShackleError::InvalidDirectory)
241 }
242}
243
244pub fn set_branch(directory: &Path, branch: &str) -> Result<(), ShackleError> {
245 if !is_valid_git_repo_path(directory)? {
246 return Err(ShackleError::InvalidDirectory);
247 }
248
249 if let Ok(repo) = Repository::open_bare(directory) {
250 repo.reference_symbolic(
251 "HEAD",
252 &format!("refs/heads/{branch}"),
253 true,
254 "shackle set-branch",
255 )?;
256 Ok(())
257 } else {
258 Err(ShackleError::InvalidDirectory)
259 }
260}
261
262pub fn housekeeping(directory: &Path) -> Result<(), ShackleError> {
263 if !is_valid_git_repo_path(directory)? {
264 return Err(ShackleError::InvalidDirectory);
265 }
266
267 Command::new("git")
268 .arg("gc")
269 .arg("--prune=now")
270 .current_dir(directory)
271 .spawn()?
272 .wait()?;
273
274 Ok(())
275}
276
277pub fn delete(directory: &Path) -> Result<(), ShackleError> {
278 if !is_valid_git_repo_path(directory)? {
279 return Err(ShackleError::InvalidDirectory);
280 }
281
282 if Repository::open_bare(directory).is_ok() {
283 fs::remove_dir_all(directory)?;
284 Ok(())
285 } else {
286 Err(ShackleError::InvalidDirectory)
287 }
288}
289
290pub fn upload_pack(upload_pack_args: &GitUploadPackArgs) -> Result<(), ShackleError> {
291 if !is_valid_git_repo_path(&upload_pack_args.directory)? {
292 return Err(ShackleError::InvalidDirectory);
293 }
294
295 let mut command = Command::new("git-upload-pack");
296 command.arg("--strict");
297
298 if let Some(timeout) = upload_pack_args.timeout {
299 command.args(["--timeout", &timeout.to_string()]);
300 }
301 if upload_pack_args.stateless_rpc {
302 command.arg("--stateless-rpc");
303 }
304 if upload_pack_args.advertise_refs {
305 command.arg("--advertise-refs");
306 }
307 command.arg(&upload_pack_args.directory);
308
309 command.spawn()?.wait()?;
310 Ok(())
311}
312
313pub fn receive_pack(receive_pack_args: &GitReceivePackArgs) -> Result<(), ShackleError> {
314 if !is_valid_git_repo_path(&receive_pack_args.directory)? {
315 return Err(ShackleError::InvalidDirectory);
316 }
317
318 let mut command = Command::new("git-receive-pack");
319 command.arg(&receive_pack_args.directory);
320
321 command.spawn()?.wait()?;
322 Ok(())
323}