1use console::Style;
2
3pub fn glob_value(v: String) -> Result<String, String> {
4 match glob::Pattern::new(&v) {
5 Ok(res) => Ok(res.to_string()),
6 Err(err) => Err(format!("'{}' is not a valid glob pattern - {}", v, err)),
7 }
8}
9
10pub const RED: Style = Style::new().red();
11pub const GREEN: Style = Style::new().green();
12pub const YELLOW: Style = Style::new().yellow();
13pub const CYAN: Style = Style::new().cyan();
14
15#[macro_export]
18macro_rules! dbg_as_curl {
19 ($req:expr) => {
20 match $req {
21 tmp => {
22 match tmp.try_clone().map(|b| b.build()) {
23 Some(Ok(req)) => tracing::debug!("{}", crate::cli::utils::AsCurl::new(&req)),
24 Some(Err(err)) => tracing::debug!("*Error*: {}", err),
25 None => tracing::debug!("*Error*: request not cloneable",),
26 }
27 tmp
28 }
29 }
30 };
31}
32
33pub struct AsCurl<'a> {
35 req: &'a reqwest::Request,
36}
37
38impl<'a> AsCurl<'a> {
39 pub fn new(req: &'a reqwest::Request) -> AsCurl<'a> {
41 Self { req }
42 }
43}
44
45impl<'a> std::fmt::Debug for AsCurl<'a> {
46 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
47 <Self as std::fmt::Display>::fmt(self, f)
48 }
49}
50
51impl<'a> std::fmt::Display for AsCurl<'a> {
52 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
53 let AsCurl { req } = *self;
54
55 write!(f, "curl ")?;
56
57 let method = req.method();
58 if method != "GET" {
59 write!(f, "-X {} ", method)?;
60 }
61
62 for (name, value) in req.headers() {
63 let value = value
64 .to_str()
65 .expect("Headers must contain only visible ASCII characters")
66 .replace("'", r"'\''");
67
68 write!(f, "--header '{}: {}' ", name, value)?;
69 }
70
71 if let Some(body) = req.body() {
73 if let Some(bytes) = body.as_bytes() {
75 let s = String::from_utf8_lossy(bytes).replace("'", r"'\''");
76 write!(f, "--data-raw '{}' ", s)?;
77 } else {
78 write!(
79 f,
80 "# NOTE: Body present but not shown (stream or unknown type) "
81 )?;
82 }
83 }
84
85 write!(f, "'{}'", req.url().to_string().replace("'", "%27"))?;
87
88 Ok(())
89 }
90}
91
92#[cfg(test)]
93mod debug_as_curl_tests {
94
95 use crate::dbg_as_curl;
96
97 fn compare(req: reqwest::RequestBuilder, result: &str) {
98 let req = dbg_as_curl!(req);
99
100 let req = req.build().unwrap();
101 assert_eq!(format!("{}", super::AsCurl::new(&req)), result);
102 }
103
104 #[test]
105 fn basic() {
106 let client = reqwest::Client::new();
107
108 compare(
109 client.get("http://example.org"),
110 "curl 'http://example.org/'",
111 );
112 compare(
113 client.get("https://example.org"),
114 "curl 'https://example.org/'",
115 );
116 }
117
118 #[test]
119 fn escape_url() {
120 let client = reqwest::Client::new();
121
122 compare(
123 client.get("https://example.org/search?q='"),
124 "curl 'https://example.org/search?q=%27'",
125 );
126 }
127
128 #[test]
129 fn bearer() {
130 let client = reqwest::Client::new();
131
132 compare(
133 client.get("https://example.org").bearer_auth("foo"),
134 "curl --header 'authorization: Bearer foo' 'https://example.org/'",
135 );
136 }
137
138 #[test]
139 fn escape_headers() {
140 let client = reqwest::Client::new();
141
142 compare(
143 client.get("https://example.org").bearer_auth("test's"),
144 r"curl --header 'authorization: Bearer test'\''s' 'https://example.org/'",
145 );
146 }
147
148 #[test]
150 fn body() {
151 let client = reqwest::Client::new();
152
153 compare(
154 client.get("https://example.org").body("test's"),
155 r"curl --data-raw 'test'\''s' 'https://example.org/'",
156 );
157 }
158}
159
160pub mod git_info {
161 use std::env;
162 use std::process::Command;
163
164 const BRANCH_ENV_VAR_NAMES: &[&str] = &[
165 "GITHUB_HEAD_REF",
166 "GITHUB_REF",
167 "BUILDKITE_BRANCH",
168 "CIRCLE_BRANCH",
169 "TRAVIS_BRANCH",
170 "GIT_BRANCH",
171 "GIT_LOCAL_BRANCH",
172 "APPVEYOR_REPO_BRANCH",
173 "CI_COMMIT_REF_NAME",
174 "BITBUCKET_BRANCH",
175 "BUILD_SOURCEBRANCHNAME",
176 "CIRRUS_BRANCH",
177 ];
178
179 const COMMIT_ENV_VAR_NAMES: &[&str] = &[
180 "GITHUB_SHA",
181 "BUILDKITE_COMMIT",
182 "CIRCLE_SHA1",
183 "TRAVIS_COMMIT",
184 "GIT_COMMIT",
185 "APPVEYOR_REPO_COMMIT",
186 "CI_COMMIT_ID",
187 "BITBUCKET_COMMIT",
188 "BUILD_SOURCEVERSION",
189 "CIRRUS_CHANGE_IN_REPO",
190 ];
191
192 const BUILD_URL_ENV_VAR_NAMES: &[&str] = &[
193 "BUILDKITE_BUILD_URL",
194 "CIRCLE_BUILD_URL",
195 "TRAVIS_BUILD_WEB_URL",
196 "BUILD_URL",
197 ];
198
199 pub fn commit(raise_error: bool) -> Option<String> {
200 find_commit_from_env_vars().or_else(|| commit_from_git_command(raise_error))
201 }
202
203 pub fn branch(raise_error: bool) -> Option<String> {
204 find_branch_from_known_env_vars()
205 .or_else(find_branch_from_env_var_ending_with_branch)
206 .or_else(|| branch_from_git_command(raise_error))
207 }
208
209 pub fn build_url() -> Option<String> {
210 github_build_url().or_else(|| {
211 BUILD_URL_ENV_VAR_NAMES
212 .iter()
213 .filter_map(|&name| value_from_env_var(name))
214 .next()
215 })
216 }
217
218 fn find_commit_from_env_vars() -> Option<String> {
219 COMMIT_ENV_VAR_NAMES
220 .iter()
221 .filter_map(|&name| value_from_env_var(name))
222 .next()
223 }
224
225 fn find_branch_from_known_env_vars() -> Option<String> {
226 BRANCH_ENV_VAR_NAMES
227 .iter()
228 .filter_map(|&name| value_from_env_var(name))
229 .next()
230 .map(|val| val.trim_start_matches("refs/heads/").to_string())
231 }
232
233 fn find_branch_from_env_var_ending_with_branch() -> Option<String> {
234 let values: Vec<String> = env::vars()
235 .filter(|(k, _)| k.ends_with("_BRANCH"))
236 .filter_map(|(_, v)| {
237 let v = v.trim();
238 if !v.is_empty() {
239 Some(v.to_string())
240 } else {
241 None
242 }
243 })
244 .collect();
245 if values.len() == 1 {
246 Some(values[0].clone())
247 } else {
248 None
249 }
250 }
251
252 fn value_from_env_var(name: &str) -> Option<String> {
253 env::var(name).ok().and_then(|v| {
254 let v = v.trim();
255 if !v.is_empty() {
256 Some(v.to_string())
257 } else {
258 None
259 }
260 })
261 }
262
263 fn branch_from_git_command(raise_error: bool) -> Option<String> {
264 let branch_names = execute_and_parse_command(raise_error);
265 if raise_error {
266 validate_branch_names(&branch_names);
267 }
268 if branch_names.len() == 1 {
269 Some(branch_names[0].clone())
270 } else {
271 None
272 }
273 }
274
275 fn commit_from_git_command(raise_error: bool) -> Option<String> {
276 match execute_git_commit_command() {
277 Ok(s) => {
278 let trimmed = s.trim();
279 if trimmed.is_empty() {
280 None
281 } else {
282 Some(trimmed.to_string())
283 }
284 }
285 Err(e) => {
286 if raise_error {
287 panic!(
288 "Could not determine current git commit using command `git rev-parse HEAD`. {}",
289 e
290 );
291 }
292 None
293 }
294 }
295 }
296
297 fn validate_branch_names(branch_names: &[String]) {
298 if branch_names.is_empty() {
299 panic!(
300 "Command `git rev-parse --abbrev-ref HEAD` didn't return anything that could be identified as the current branch."
301 );
302 }
303 if branch_names.len() > 1 {
304 panic!(
305 "Command `git rev-parse --abbrev-ref HEAD` returned multiple branches: {}. You will need to get the branch name another way.",
306 branch_names.join(", ")
307 );
308 }
309 }
310
311 fn execute_git_command() -> Result<String, String> {
312 Command::new("git")
313 .args(&["rev-parse", "--abbrev-ref", "HEAD"])
314 .output()
315 .map_err(|e| e.to_string())
316 .and_then(|output| {
317 if output.status.success() {
318 Ok(String::from_utf8_lossy(&output.stdout).to_string())
319 } else {
320 Err(String::from_utf8_lossy(&output.stderr).to_string())
321 }
322 })
323 }
324
325 fn execute_git_commit_command() -> Result<String, String> {
326 Command::new("git")
327 .args(&["rev-parse", "HEAD"])
328 .output()
329 .map_err(|e| e.to_string())
330 .and_then(|output| {
331 if output.status.success() {
332 Ok(String::from_utf8_lossy(&output.stdout).to_string())
333 } else {
334 Err(String::from_utf8_lossy(&output.stderr).to_string())
335 }
336 })
337 }
338
339 fn execute_and_parse_command(raise_error: bool) -> Vec<String> {
340 match execute_git_command() {
341 Ok(output) => output
342 .lines()
343 .map(str::trim)
344 .filter(|l| !l.is_empty())
345 .map(|l| l.split_whitespace().next().unwrap_or("").to_string())
346 .map(|l| l.trim_start_matches("origin/").to_string())
347 .filter(|l| l != "HEAD")
348 .collect(),
349 Err(e) => {
350 if raise_error {
351 panic!(
352 "Could not determine current git branch using command `git rev-parse --abbrev-ref HEAD`. {}",
353 e
354 );
355 }
356 vec![]
357 }
358 }
359 }
360
361 fn github_build_url() -> Option<String> {
362 let parts: Vec<String> = ["GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_RUN_ID"]
363 .iter()
364 .filter_map(|&name| value_from_env_var(name))
365 .collect();
366 if parts.len() == 3 {
367 Some(format!(
368 "{}/{}/actions/runs/{}",
369 parts[0], parts[1], parts[2]
370 ))
371 } else {
372 None
373 }
374 }
375}