1use anyhow::{Context, Result};
2use base64::Engine;
3use std::time::Duration;
4use tokio::process::Command;
5
6use super::types::{CredentialKind, Credentials, LsRemoteResult};
7
8use super::aws;
9use crate::clone::https::embed_credentials_in_url;
10use crate::clone::ssh::{normalize_ssh_url, setup_ssh_env};
11
12pub async fn ls_remote(
13 url: &str,
14 branch: &str,
15 credentials: Option<&Credentials>,
16 follow_redirects: bool,
17) -> Result<LsRemoteResult> {
18 match credentials.map(|c| &c.kind) {
19 Some(CredentialKind::Ssh { key }) => {
20 ls_remote_ssh(url, branch, key, follow_redirects).await
21 }
22 Some(CredentialKind::AwsRole {
23 arn,
24 external_id,
25 via,
26 }) => ls_remote_codecommit(url, branch, arn, external_id, via.as_deref()).await,
27 Some(CredentialKind::Https { user, password }) => {
28 let repo_url = embed_credentials_in_url(url, user, password)?;
29 ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
30 }
31 Some(CredentialKind::Token {
32 token,
33 is_pat,
34 oauth_type: _,
35 }) => {
36 if *is_pat {
37 let encoded = base64::engine::general_purpose::STANDARD.encode(format!(":{token}"));
38 let extra_config = vec![
39 "-c".to_owned(),
40 format!("http.extraHeader=Authorization: Basic {encoded}"),
41 ];
42 ls_remote_https(url, branch, follow_redirects, &extra_config).await
43 } else {
44 let user = if url.to_lowercase().contains("bitbucket") {
45 "x-token-auth"
46 } else {
47 "oauth2"
48 };
49 let repo_url = embed_credentials_in_url(url, user, token)?;
50 ls_remote_https(&repo_url, branch, follow_redirects, &[]).await
51 }
52 }
53 None => ls_remote_https(url, branch, follow_redirects, &[]).await,
54 }
55}
56
57async fn ls_remote_ssh(
58 url: &str,
59 branch: &str,
60 key_b64: &str,
61 follow_redirects: bool,
62) -> Result<LsRemoteResult> {
63 let key_data = base64::engine::general_purpose::STANDARD
64 .decode(key_b64)
65 .context("decoding SSH key")?;
66
67 let key_file = tempfile::Builder::new()
68 .prefix("melts-ssh-key-")
69 .tempfile()
70 .context("creating SSH key file")?;
71
72 std::fs::write(key_file.path(), &key_data).context("writing SSH key")?;
73
74 #[cfg(unix)]
75 {
76 use std::os::unix::fs::PermissionsExt;
77 std::fs::set_permissions(key_file.path(), std::fs::Permissions::from_mode(0o400))
78 .context("setting SSH key permissions")?;
79 }
80
81 let git_ssh_command = setup_ssh_env(key_file.path());
82 let normalized_url = normalize_ssh_url(url);
83
84 let mut args: Vec<String> = Vec::new();
85 if follow_redirects {
86 args.extend(["-c".to_owned(), "http.followRedirects=true".to_owned()]);
87 }
88 args.extend([
89 "ls-remote".to_owned(),
90 "--".to_owned(),
91 normalized_url,
92 branch.to_owned(),
93 ]);
94
95 let output = tokio::time::timeout(
96 Duration::from_secs(20),
97 Command::new("git")
98 .args(&args)
99 .env("GIT_SSH_COMMAND", &git_ssh_command)
100 .kill_on_drop(true)
101 .output(),
102 )
103 .await
104 .context("git ls-remote timed out")?
105 .context("running git ls-remote (SSH)")?;
106
107 Ok(parse_ls_remote_output(&output))
108}
109
110async fn ls_remote_https(
111 url: &str,
112 branch: &str,
113 follow_redirects: bool,
114 extra_config: &[String],
115) -> Result<LsRemoteResult> {
116 let mut args = vec!["-c".to_owned(), "http.sslVerify=false".to_owned()];
117
118 if follow_redirects {
119 args.extend(["-c".to_owned(), "http.followRedirects=true".to_owned()]);
120 }
121
122 args.extend(extra_config.iter().cloned());
123 args.extend([
124 "ls-remote".to_owned(),
125 "--".to_owned(),
126 url.to_owned(),
127 branch.to_owned(),
128 ]);
129
130 let output = tokio::time::timeout(
131 Duration::from_secs(20),
132 Command::new("git").args(&args).kill_on_drop(true).output(),
133 )
134 .await
135 .context("git ls-remote timed out")?
136 .context("running git ls-remote (HTTPS)")?;
137
138 Ok(parse_ls_remote_output(&output))
139}
140
141async fn ls_remote_codecommit(
142 url: &str,
143 branch: &str,
144 arn: &str,
145 external_id: &str,
146 via: Option<&str>,
147) -> Result<LsRemoteResult> {
148 let creds = aws::assume_role(arn, external_id, url, via).await?;
149
150 let args = vec![
151 "-c".to_owned(),
152 "http.sslVerify=false".to_owned(),
153 "ls-remote".to_owned(),
154 "--".to_owned(),
155 url.to_owned(),
156 branch.to_owned(),
157 ];
158
159 let output = tokio::time::timeout(
160 Duration::from_secs(20),
161 Command::new("git")
162 .args(&args)
163 .env("AWS_ACCESS_KEY_ID", &creds.access_key)
164 .env("AWS_SECRET_ACCESS_KEY", &creds.secret_key)
165 .env("AWS_SESSION_TOKEN", &creds.session_token)
166 .env("AWS_DEFAULT_REGION", &creds.region)
167 .kill_on_drop(true)
168 .output(),
169 )
170 .await
171 .context("git ls-remote timed out")?
172 .context("running git ls-remote (CodeCommit)")?;
173
174 Ok(parse_ls_remote_output(&output))
175}
176
177fn parse_ls_remote_output(output: &std::process::Output) -> LsRemoteResult {
178 if !output.status.success() {
179 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
180 return LsRemoteResult {
181 commit: None,
182 error: Some(stderr),
183 };
184 }
185
186 let stdout = String::from_utf8_lossy(&output.stdout);
187 let commit = stdout
188 .lines()
189 .next()
190 .and_then(|line| line.split('\t').next())
191 .map(|s| s.trim().to_owned())
192 .filter(|s| !s.is_empty());
193
194 LsRemoteResult {
195 commit,
196 error: None,
197 }
198}