synwire_agent/vfs/
clone.rs1use std::path::Path;
4use std::process::Command;
5
6#[derive(Debug, Clone)]
8#[non_exhaustive]
9pub struct CloneOptions {
10 pub url: String,
12 pub depth: Option<u32>,
14 pub r#ref: Option<String>,
16 pub index: bool,
18}
19
20impl CloneOptions {
21 #[must_use]
23 pub fn new(url: impl Into<String>) -> Self {
24 Self {
25 url: url.into(),
26 depth: None,
27 r#ref: None,
28 index: false,
29 }
30 }
31}
32
33#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum CloneError {
37 #[error("git error: {0}")]
39 Git(String),
40 #[error("I/O error: {0}")]
42 Io(String),
43}
44
45fn run_git(args: &[&str]) -> Result<(), CloneError> {
47 let output = Command::new("git")
48 .args(args)
49 .output()
50 .map_err(|e| CloneError::Io(e.to_string()))?;
51
52 if output.status.success() {
53 Ok(())
54 } else {
55 let stderr = String::from_utf8_lossy(&output.stderr);
56 Err(CloneError::Git(stderr.trim().to_owned()))
57 }
58}
59
60pub fn clone_or_update(options: &CloneOptions, dest: &Path) -> Result<(), CloneError> {
73 if dest.join(".git").is_dir() {
74 let dest_str = dest
76 .to_str()
77 .ok_or_else(|| CloneError::Io("destination path is not valid UTF-8".to_owned()))?;
78
79 run_git(&["-C", dest_str, "fetch", "origin"])?;
80
81 if let Some(git_ref) = &options.r#ref {
82 run_git(&["-C", dest_str, "checkout", git_ref])?;
83 }
84 } else {
85 let dest_str = dest
87 .to_str()
88 .ok_or_else(|| CloneError::Io("destination path is not valid UTF-8".to_owned()))?;
89
90 let mut args: Vec<&str> = vec!["clone"];
91
92 let depth_str;
95 if let Some(depth) = options.depth {
96 depth_str = depth.to_string();
97 args.extend_from_slice(&["--depth", &depth_str]);
98 }
99
100 if let Some(git_ref) = &options.r#ref {
101 args.extend_from_slice(&["--branch", git_ref]);
102 }
103
104 args.push(&options.url);
105 args.push(dest_str);
106
107 run_git(&args)?;
108 }
109
110 Ok(())
111}
112
113#[cfg(test)]
114#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
115mod tests {
116 use super::*;
117 use tempfile::tempdir;
118
119 #[test]
120 fn clone_invalid_url_returns_git_error() {
121 let dir = tempdir().expect("tempdir");
122 let dest = dir.path().join("target");
125 let opts = CloneOptions::new("https://invalid.example.test/no-such-repo.git");
126 let err = clone_or_update(&opts, &dest);
127 assert!(err.is_err());
130 let msg = err.unwrap_err().to_string();
131 assert!(
132 !msg.contains("not yet implemented"),
133 "should no longer return stub error, got: {msg}"
134 );
135 }
136}