1use anyhow::{bail, Context, Result};
12use rust_i18n::t;
13use sha1::{Digest, Sha1};
14use std::path::PathBuf;
15use std::sync::atomic::AtomicBool;
16
17pub struct DynamicRepoOptions {
19 pub url: String,
20 pub branch: Option<String>,
21 pub username: Option<String>,
22 pub password: Option<String>,
23 pub commit: Option<String>,
24 pub location: Option<PathBuf>,
25}
26
27pub fn prepare(opts: &DynamicRepoOptions) -> Result<PathBuf> {
30 if opts.branch.is_none() {
31 bail!("{}", t!("remote.branch_required"));
32 }
33 let branch = opts.branch.as_deref().unwrap();
34
35 let base = opts.location.clone().unwrap_or_else(std::env::temp_dir);
37 let mut hasher = Sha1::new();
38 hasher.update(opts.url.as_bytes());
39 let hash: String = hasher
40 .finalize()
41 .iter()
42 .map(|b| format!("{b:02x}"))
43 .collect();
44 let dest = base.join(format!("gitversion-dynamic-{hash}"));
45
46 if dest.exists() {
48 std::fs::remove_dir_all(&dest)
49 .with_context(|| t!("remote.remove_failed", path = dest.display()))?;
50 }
51 std::fs::create_dir_all(&dest)?;
52
53 let url = inject_credentials(
55 &opts.url,
56 opts.username.as_deref(),
57 opts.password.as_deref(),
58 );
59
60 log::info!(
61 "{}",
62 t!(
63 "remote.cloning",
64 url = opts.url,
65 branch = branch,
66 dest = dest.display()
67 )
68 );
69
70 let should_interrupt = AtomicBool::new(false);
71 let mut prepare = gix::prepare_clone(url.as_str(), &dest)
72 .with_context(|| t!("remote.clone_prepare_failed", url = opts.url))?
73 .with_ref_name(Some(branch))
74 .with_context(|| t!("remote.set_ref_failed").to_string())?;
75
76 let (mut checkout, _) = prepare
77 .fetch_then_checkout(gix::progress::Discard, &should_interrupt)
78 .with_context(|| t!("remote.fetch_failed").to_string())?;
79 let (_repo, _) = checkout
80 .main_worktree(gix::progress::Discard, &should_interrupt)
81 .with_context(|| t!("remote.checkout_failed").to_string())?;
82
83 if let Some(commit) = &opts.commit {
85 detach_head_to_commit(&dest, commit)?;
86 }
87
88 Ok(dest)
89}
90
91fn inject_credentials(url: &str, user: Option<&str>, pass: Option<&str>) -> String {
96 let Some(user) = user.filter(|u| !u.is_empty()) else {
97 return url.to_string();
98 };
99 if let Some(rest) = url.strip_prefix("https://") {
100 let cred = match pass.filter(|p| !p.is_empty()) {
101 Some(p) => format!("{user}:{p}"),
102 None => user.to_string(),
103 };
104 return format!("https://{cred}@{rest}");
105 }
106 if let Some(rest) = url.strip_prefix("ssh://") {
107 let host_part = rest.split('/').next().unwrap_or(rest);
109 if !host_part.contains('@') {
110 return format!("ssh://{user}@{rest}");
111 }
112 }
113 url.to_string()
114}
115
116fn detach_head_to_commit(dest: &std::path::Path, commit: &str) -> Result<()> {
118 let repo = gix::open(dest).with_context(|| t!("remote.open_failed").to_string())?;
119 let id = repo
120 .rev_parse_single(commit)
121 .with_context(|| t!("git.commit_not_found", commit = commit))?;
122 let full_sha = id.detach().to_string();
123 let head_path = repo.git_dir().join("HEAD");
124 std::fs::write(&head_path, format!("{full_sha}\n"))
125 .with_context(|| t!("remote.head_write_failed", path = head_path.display()))?;
126 log::info!("{}", t!("remote.head_set", sha = full_sha));
127 Ok(())
128}
129
130#[cfg(test)]
131mod tests {
132 use super::inject_credentials;
133
134 #[test]
135 fn https_injects_user_and_pass() {
136 assert_eq!(
137 inject_credentials("https://host/r.git", Some("u"), Some("p")),
138 "https://u:p@host/r.git"
139 );
140 assert_eq!(
141 inject_credentials("https://host/r.git", Some("u"), None),
142 "https://u@host/r.git"
143 );
144 }
145
146 #[test]
147 fn ssh_injects_user_when_absent() {
148 assert_eq!(
149 inject_credentials("ssh://host/r.git", Some("git"), None),
150 "ssh://git@host/r.git"
151 );
152 assert_eq!(
154 inject_credentials("ssh://git@host/r.git", Some("other"), None),
155 "ssh://git@host/r.git"
156 );
157 }
158
159 #[test]
160 fn scp_like_and_no_user_unchanged() {
161 assert_eq!(
162 inject_credentials("git@host:r.git", Some("u"), None),
163 "git@host:r.git"
164 );
165 assert_eq!(
166 inject_credentials("https://host/r.git", None, None),
167 "https://host/r.git"
168 );
169 }
170}