repolith_actions/
git_clone.rs1use crate::util::{check_status, run_with_cancel};
12use async_trait::async_trait;
13use repolith_core::action::Action;
14use repolith_core::types::{ActionId, BuildError, BuildOutput, Ctx, Sha256};
15use sha2::{Digest, Sha256 as ShaHasher};
16use std::path::PathBuf;
17use tokio::process::Command;
18
19pub struct GitClone {
26 pub id: ActionId,
28 pub repo_url: String,
30 pub path: PathBuf,
32 pub deps: Vec<ActionId>,
34}
35
36#[async_trait]
37impl Action for GitClone {
38 fn id(&self) -> ActionId {
39 self.id.clone()
40 }
41
42 fn deps(&self) -> Vec<ActionId> {
43 self.deps.clone()
44 }
45
46 async fn input_hash(&self, ctx: &Ctx) -> Result<Sha256, BuildError> {
47 let mut cmd = Command::new("git");
49 cmd.args(ls_remote_args(&self.repo_url));
50 let out = run_with_cancel(cmd, &ctx.cancel).await?;
51 if !out.status.success() {
52 return Err(BuildError::UpstreamUnreachable(format!(
53 "git ls-remote {}: {}",
54 self.repo_url,
55 String::from_utf8_lossy(&out.stderr).trim()
56 )));
57 }
58 let sha1 = String::from_utf8_lossy(&out.stdout)
63 .split_whitespace()
64 .next()
65 .unwrap_or_default()
66 .to_string();
67 if sha1.is_empty() {
68 return Err(BuildError::UpstreamUnreachable(format!(
69 "git ls-remote {} returned no HEAD",
70 self.repo_url
71 )));
72 }
73 Ok(hash_url_and_sha(&self.repo_url, &sha1))
74 }
75
76 async fn execute(&self, ctx: &Ctx) -> Result<BuildOutput, BuildError> {
77 let path_str = self
78 .path
79 .to_str()
80 .ok_or_else(|| BuildError::Io(format!("non-utf8 path: {:?}", self.path)))?;
81
82 if self.path.join(".git").is_dir() {
83 let mut fetch = Command::new("git");
85 fetch.args(["-C", path_str, "fetch", "origin", "--quiet"]);
86 check_status(&run_with_cancel(fetch, &ctx.cancel).await?)?;
87
88 let mut reset = Command::new("git");
89 reset.args(["-C", path_str, "reset", "--hard", "FETCH_HEAD", "--quiet"]);
90 check_status(&run_with_cancel(reset, &ctx.cancel).await?)?;
91 } else {
92 if let Some(parent) = self.path.parent()
94 && !parent.as_os_str().is_empty()
95 {
96 std::fs::create_dir_all(parent)
97 .map_err(|e| BuildError::Io(format!("create_dir_all: {e}")))?;
98 }
99 let mut clone = Command::new("git");
100 clone.args(clone_args(&self.repo_url, path_str));
101 check_status(&run_with_cancel(clone, &ctx.cancel).await?)?;
102 }
103
104 let mut head = Command::new("git");
106 head.args(["-C", path_str, "rev-parse", "HEAD"]);
107 let head_out = run_with_cancel(head, &ctx.cancel).await?;
108 if !head_out.status.success() {
109 return Err(BuildError::CommandFailed {
110 exit_code: head_out.status.code().unwrap_or(-1),
111 stderr: String::from_utf8_lossy(&head_out.stderr).trim().to_string(),
112 });
113 }
114 let sha1 = String::from_utf8_lossy(&head_out.stdout).trim().to_string();
115 let mut h = ShaHasher::new();
116 h.update(sha1.as_bytes());
117 Ok(BuildOutput {
118 output_hash: Sha256(h.finalize().into()),
119 stdout: format!("HEAD {sha1}"),
120 })
121 }
122}
123
124fn hash_url_and_sha(url: &str, sha1: &str) -> Sha256 {
128 let mut h = ShaHasher::new();
129 h.update(url.as_bytes());
130 h.update(b":");
131 h.update(sha1.as_bytes());
132 Sha256(h.finalize().into())
133}
134
135fn ls_remote_args(url: &str) -> [&str; 4] {
140 ["ls-remote", "--", url, "HEAD"]
141}
142
143fn clone_args<'a>(url: &'a str, path: &'a str) -> [&'a str; 5] {
144 ["clone", "--quiet", "--", url, path]
145}
146
147#[cfg(test)]
148mod argv_tests {
149 use super::{clone_args, ls_remote_args};
150
151 fn assert_separator_before(args: &[&str], url: &str) {
152 let dash = args
153 .iter()
154 .position(|a| *a == "--")
155 .unwrap_or_else(|| panic!("`--` argv separator missing from {args:?}"));
156 let url_pos = args
157 .iter()
158 .position(|a| *a == url)
159 .unwrap_or_else(|| panic!("url `{url}` missing from {args:?}"));
160 assert!(
161 dash < url_pos,
162 "`--` (idx {dash}) must precede url (idx {url_pos}) in {args:?}"
163 );
164 }
165
166 #[test]
167 fn ls_remote_argv_has_separator() {
168 let url = "https://example.com/r.git";
169 assert_separator_before(&ls_remote_args(url), url);
170 }
171
172 #[test]
173 fn clone_argv_has_separator() {
174 let url = "https://example.com/r.git";
175 let path = "/tmp/r";
176 assert_separator_before(&clone_args(url, path), url);
177 }
178}