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