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
197
198pub mod git_info {
199use std::env;
200use std::process::Command;
201
202 const BRANCH_ENV_VAR_NAMES: &[&str] = &[
203 "GITHUB_HEAD_REF",
204 "GITHUB_REF",
205 "BUILDKITE_BRANCH",
206 "CIRCLE_BRANCH",
207 "TRAVIS_BRANCH",
208 "GIT_BRANCH",
209 "GIT_LOCAL_BRANCH",
210 "APPVEYOR_REPO_BRANCH",
211 "CI_COMMIT_REF_NAME",
212 "BITBUCKET_BRANCH",
213 "BUILD_SOURCEBRANCHNAME",
214 "CIRRUS_BRANCH",
215 ];
216
217 const COMMIT_ENV_VAR_NAMES: &[&str] = &[
218 "GITHUB_SHA",
219 "BUILDKITE_COMMIT",
220 "CIRCLE_SHA1",
221 "TRAVIS_COMMIT",
222 "GIT_COMMIT",
223 "APPVEYOR_REPO_COMMIT",
224 "CI_COMMIT_ID",
225 "BITBUCKET_COMMIT",
226 "BUILD_SOURCEVERSION",
227 "CIRRUS_CHANGE_IN_REPO",
228 ];
229
230 const BUILD_URL_ENV_VAR_NAMES: &[&str] = &[
231 "BUILDKITE_BUILD_URL",
232 "CIRCLE_BUILD_URL",
233 "TRAVIS_BUILD_WEB_URL",
234 "BUILD_URL",
235 ];
236
237 pub fn commit(raise_error: bool) -> Option<String> {
238 find_commit_from_env_vars().or_else(|| commit_from_git_command(raise_error))
239 }
240
241 pub fn branch(raise_error: bool) -> Option<String> {
242 find_branch_from_known_env_vars()
243 .or_else(find_branch_from_env_var_ending_with_branch)
244 .or_else(|| branch_from_git_command(raise_error))
245 }
246
247 pub fn build_url() -> Option<String> {
248 github_build_url().or_else(|| {
249 BUILD_URL_ENV_VAR_NAMES
250 .iter()
251 .filter_map(|&name| value_from_env_var(name))
252 .next()
253 })
254 }
255
256 fn find_commit_from_env_vars() -> Option<String> {
257 COMMIT_ENV_VAR_NAMES
258 .iter()
259 .filter_map(|&name| value_from_env_var(name))
260 .next()
261 }
262
263 fn find_branch_from_known_env_vars() -> Option<String> {
264 BRANCH_ENV_VAR_NAMES
265 .iter()
266 .filter_map(|&name| value_from_env_var(name))
267 .next()
268 .map(|val| val.trim_start_matches("refs/heads/").to_string())
269 }
270
271 fn find_branch_from_env_var_ending_with_branch() -> Option<String> {
272 let values: Vec<String> = env::vars()
273 .filter(|(k, _)| k.ends_with("_BRANCH"))
274 .filter_map(|(_, v)| {
275 let v = v.trim();
276 if !v.is_empty() {
277 Some(v.to_string())
278 } else {
279 None
280 }
281 })
282 .collect();
283 if values.len() == 1 {
284 Some(values[0].clone())
285 } else {
286 None
287 }
288 }
289
290 fn value_from_env_var(name: &str) -> Option<String> {
291 env::var(name).ok().and_then(|v| {
292 let v = v.trim();
293 if !v.is_empty() {
294 Some(v.to_string())
295 } else {
296 None
297 }
298 })
299 }
300
301 fn branch_from_git_command(raise_error: bool) -> Option<String> {
302 let branch_names = execute_and_parse_command(raise_error);
303 if raise_error {
304 validate_branch_names(&branch_names);
305 }
306 if branch_names.len() == 1 {
307 Some(branch_names[0].clone())
308 } else {
309 None
310 }
311 }
312
313 fn commit_from_git_command(raise_error: bool) -> Option<String> {
314 match execute_git_commit_command() {
315 Ok(s) => {
316 let trimmed = s.trim();
317 if trimmed.is_empty() {
318 None
319 } else {
320 Some(trimmed.to_string())
321 }
322 }
323 Err(e) => {
324 if raise_error {
325 panic!(
326 "Could not determine current git commit using command `git rev-parse HEAD`. {}",
327 e
328 );
329 }
330 None
331 }
332 }
333 }
334
335 fn validate_branch_names(branch_names: &[String]) {
336 if branch_names.is_empty() {
337 panic!(
338 "Command `git rev-parse --abbrev-ref HEAD` didn't return anything that could be identified as the current branch."
339 );
340 }
341 if branch_names.len() > 1 {
342 panic!(
343 "Command `git rev-parse --abbrev-ref HEAD` returned multiple branches: {}. You will need to get the branch name another way.",
344 branch_names.join(", ")
345 );
346 }
347 }
348
349 fn execute_git_command() -> Result<String, String> {
350 Command::new("git")
351 .args(&["rev-parse", "--abbrev-ref", "HEAD"])
352 .output()
353 .map_err(|e| e.to_string())
354 .and_then(|output| {
355 if output.status.success() {
356 Ok(String::from_utf8_lossy(&output.stdout).to_string())
357 } else {
358 Err(String::from_utf8_lossy(&output.stderr).to_string())
359 }
360 })
361 }
362
363 fn execute_git_commit_command() -> Result<String, String> {
364 Command::new("git")
365 .args(&["rev-parse", "HEAD"])
366 .output()
367 .map_err(|e| e.to_string())
368 .and_then(|output| {
369 if output.status.success() {
370 Ok(String::from_utf8_lossy(&output.stdout).to_string())
371 } else {
372 Err(String::from_utf8_lossy(&output.stderr).to_string())
373 }
374 })
375 }
376
377 fn execute_and_parse_command(raise_error: bool) -> Vec<String> {
378 match execute_git_command() {
379 Ok(output) => output
380 .lines()
381 .map(str::trim)
382 .filter(|l| !l.is_empty())
383 .map(|l| l.split_whitespace().next().unwrap_or("").to_string())
384 .map(|l| l.trim_start_matches("origin/").to_string())
385 .filter(|l| l != "HEAD")
386 .collect(),
387 Err(e) => {
388 if raise_error {
389 panic!(
390 "Could not determine current git branch using command `git rev-parse --abbrev-ref HEAD`. {}",
391 e
392 );
393 }
394 vec![]
395 }
396 }
397 }
398
399 fn github_build_url() -> Option<String> {
400 let parts: Vec<String> = ["GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_RUN_ID"]
401 .iter()
402 .filter_map(|&name| value_from_env_var(name))
403 .collect();
404 if parts.len() == 3 {
405 Some(format!(
406 "{}/{}/actions/runs/{}",
407 parts[0], parts[1], parts[2]
408 ))
409 } else {
410 None
411 }
412 }
413}