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