Skip to main content

git_spawn/command/
clone.rs

1//! `git clone` — clone a repository into a new directory.
2
3use crate::command::{CommandExecutor, GitCommand};
4use crate::error::Result;
5use crate::repo::Repository;
6use async_trait::async_trait;
7use std::path::PathBuf;
8
9/// Builder for `git clone`.
10#[derive(Debug, Clone)]
11pub struct CloneCommand {
12    /// Shared executor.
13    pub executor: CommandExecutor,
14    /// Remote URL (required).
15    pub url: String,
16    /// Local destination directory.
17    pub directory: Option<PathBuf>,
18    /// `--bare`.
19    pub bare: bool,
20    /// `--mirror`.
21    pub mirror: bool,
22    /// `--depth`.
23    pub depth: Option<u32>,
24    /// `--branch`.
25    pub branch: Option<String>,
26    /// `--single-branch`.
27    pub single_branch: bool,
28    /// `--recurse-submodules`.
29    pub recurse_submodules: bool,
30    /// `--origin`.
31    pub origin: Option<String>,
32    /// `--quiet`.
33    pub quiet: bool,
34}
35
36impl CloneCommand {
37    /// Create a new clone command for `url`.
38    pub fn new(url: impl Into<String>) -> Self {
39        Self {
40            executor: CommandExecutor::default(),
41            url: url.into(),
42            directory: None,
43            bare: false,
44            mirror: false,
45            depth: None,
46            branch: None,
47            single_branch: false,
48            recurse_submodules: false,
49            origin: None,
50            quiet: false,
51        }
52    }
53
54    /// Target directory.
55    pub fn directory(&mut self, path: impl Into<PathBuf>) -> &mut Self {
56        self.directory = Some(path.into());
57        self
58    }
59
60    /// Clone as a bare repository.
61    pub fn bare(&mut self) -> &mut Self {
62        self.bare = true;
63        self
64    }
65
66    /// Mirror all refs.
67    pub fn mirror(&mut self) -> &mut Self {
68        self.mirror = true;
69        self
70    }
71
72    /// Shallow clone with the given depth.
73    pub fn depth(&mut self, depth: u32) -> &mut Self {
74        self.depth = Some(depth);
75        self
76    }
77
78    /// Check out the named branch instead of the remote HEAD.
79    pub fn branch(&mut self, name: impl Into<String>) -> &mut Self {
80        self.branch = Some(name.into());
81        self
82    }
83
84    /// Clone only a single branch.
85    pub fn single_branch(&mut self) -> &mut Self {
86        self.single_branch = true;
87        self
88    }
89
90    /// Recursively clone submodules.
91    pub fn recurse_submodules(&mut self) -> &mut Self {
92        self.recurse_submodules = true;
93        self
94    }
95
96    /// Set the remote name (default `origin`).
97    pub fn origin(&mut self, name: impl Into<String>) -> &mut Self {
98        self.origin = Some(name.into());
99        self
100    }
101
102    /// Suppress output.
103    pub fn quiet(&mut self) -> &mut Self {
104        self.quiet = true;
105        self
106    }
107}
108
109#[async_trait]
110impl GitCommand for CloneCommand {
111    type Output = Repository;
112
113    fn get_executor(&self) -> &CommandExecutor {
114        &self.executor
115    }
116
117    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
118        &mut self.executor
119    }
120
121    fn build_command_args(&self) -> Vec<String> {
122        let mut args = vec!["clone".to_string()];
123        if self.bare {
124            args.push("--bare".into());
125        }
126        if self.mirror {
127            args.push("--mirror".into());
128        }
129        if let Some(d) = self.depth {
130            args.push(format!("--depth={d}"));
131        }
132        if let Some(b) = &self.branch {
133            args.push("--branch".into());
134            args.push(b.clone());
135        }
136        if self.single_branch {
137            args.push("--single-branch".into());
138        }
139        if self.recurse_submodules {
140            args.push("--recurse-submodules".into());
141        }
142        if let Some(o) = &self.origin {
143            args.push("--origin".into());
144            args.push(o.clone());
145        }
146        if self.quiet {
147            args.push("--quiet".into());
148        }
149        args.push(self.url.clone());
150        if let Some(d) = &self.directory {
151            args.push(d.display().to_string());
152        }
153        args
154    }
155
156    async fn execute(&self) -> Result<Repository> {
157        self.execute_raw().await?;
158        let dir = self
159            .directory
160            .clone()
161            .unwrap_or_else(|| PathBuf::from(infer_dest_dir(&self.url)));
162        let full = if dir.is_absolute() {
163            dir
164        } else {
165            self.executor
166                .cwd
167                .clone()
168                .map_or_else(|| dir.clone(), |c| c.join(&dir))
169        };
170        Ok(Repository::new_unchecked(full))
171    }
172}
173
174fn infer_dest_dir(url: &str) -> String {
175    let last = url.trim_end_matches('/').rsplit('/').next().unwrap_or(url);
176    let last = last.split(':').next_back().unwrap_or(last);
177    last.trim_end_matches(".git").to_string()
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn infer_https_url() {
186        assert_eq!(infer_dest_dir("https://github.com/foo/bar.git"), "bar");
187        assert_eq!(infer_dest_dir("https://github.com/foo/bar"), "bar");
188    }
189
190    #[test]
191    fn infer_ssh_url() {
192        assert_eq!(infer_dest_dir("git@github.com:foo/bar.git"), "bar");
193    }
194}