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 fn compare(req: reqwest::RequestBuilder, result: &str) {
96 let req = dbg_as_curl!(req);
97
98 let req = req.build().unwrap();
99 assert_eq!(format!("{}", super::AsCurl::new(&req)), result);
100 }
101
102 #[test]
103 fn basic() {
104 let client = reqwest::Client::new();
105
106 compare(
107 client.get("http://example.org"),
108 "curl 'http://example.org/'",
109 );
110 compare(
111 client.get("https://example.org"),
112 "curl 'https://example.org/'",
113 );
114 }
115
116 #[test]
117 fn escape_url() {
118 let client = reqwest::Client::new();
119
120 compare(
121 client.get("https://example.org/search?q='"),
122 "curl 'https://example.org/search?q=%27'",
123 );
124 }
125
126 #[test]
127 fn bearer() {
128 let client = reqwest::Client::new();
129
130 compare(
131 client.get("https://example.org").bearer_auth("foo"),
132 "curl --header 'authorization: Bearer foo' 'https://example.org/'",
133 );
134 }
135
136 #[test]
137 fn escape_headers() {
138 let client = reqwest::Client::new();
139
140 compare(
141 client.get("https://example.org").bearer_auth("test's"),
142 r"curl --header 'authorization: Bearer test'\''s' 'https://example.org/'",
143 );
144 }
145
146 #[test]
148 fn body() {
149 let client = reqwest::Client::new();
150
151 compare(
152 client.get("https://example.org").body("test's"),
153 r"curl --data-raw 'test'\''s' 'https://example.org/'",
154 );
155 }
156}
157
158pub mod git_info {
159 use std::env;
160 use std::process::Command;
161
162 const BRANCH_ENV_VAR_NAMES: &[&str] = &[
163 "GITHUB_HEAD_REF",
164 "GITHUB_REF",
165 "BUILDKITE_BRANCH",
166 "CIRCLE_BRANCH",
167 "TRAVIS_BRANCH",
168 "GIT_BRANCH",
169 "GIT_LOCAL_BRANCH",
170 "APPVEYOR_REPO_BRANCH",
171 "CI_COMMIT_REF_NAME",
172 "BITBUCKET_BRANCH",
173 "BUILD_SOURCEBRANCHNAME",
174 "CIRRUS_BRANCH",
175 ];
176
177 const COMMIT_ENV_VAR_NAMES: &[&str] = &[
178 "GITHUB_SHA",
179 "BUILDKITE_COMMIT",
180 "CIRCLE_SHA1",
181 "TRAVIS_COMMIT",
182 "GIT_COMMIT",
183 "APPVEYOR_REPO_COMMIT",
184 "CI_COMMIT_ID",
185 "BITBUCKET_COMMIT",
186 "BUILD_SOURCEVERSION",
187 "CIRRUS_CHANGE_IN_REPO",
188 ];
189
190 const BUILD_URL_ENV_VAR_NAMES: &[&str] = &[
191 "BUILDKITE_BUILD_URL",
192 "CIRCLE_BUILD_URL",
193 "TRAVIS_BUILD_WEB_URL",
194 "BUILD_URL",
195 ];
196
197 pub fn commit(raise_error: bool) -> Option<String> {
198 find_commit_from_env_vars().or_else(|| commit_from_git_command(raise_error))
199 }
200
201 pub fn branch(raise_error: bool) -> Option<String> {
202 find_branch_from_known_env_vars()
203 .or_else(find_branch_from_env_var_ending_with_branch)
204 .or_else(|| branch_from_git_command(raise_error))
205 }
206
207 pub fn build_url() -> Option<String> {
208 github_build_url().or_else(|| {
209 BUILD_URL_ENV_VAR_NAMES
210 .iter()
211 .filter_map(|&name| value_from_env_var(name))
212 .next()
213 })
214 }
215
216 fn find_commit_from_env_vars() -> Option<String> {
217 COMMIT_ENV_VAR_NAMES
218 .iter()
219 .filter_map(|&name| value_from_env_var(name))
220 .next()
221 }
222
223 fn find_branch_from_known_env_vars() -> Option<String> {
224 BRANCH_ENV_VAR_NAMES
225 .iter()
226 .filter_map(|&name| value_from_env_var(name))
227 .next()
228 .map(|val| val.trim_start_matches("refs/heads/").to_string())
229 }
230
231 fn find_branch_from_env_var_ending_with_branch() -> Option<String> {
232 let values: Vec<String> = env::vars()
233 .filter(|(k, _)| k.ends_with("_BRANCH"))
234 .filter_map(|(_, v)| {
235 let v = v.trim();
236 if !v.is_empty() {
237 Some(v.to_string())
238 } else {
239 None
240 }
241 })
242 .collect();
243 if values.len() == 1 {
244 Some(values[0].clone())
245 } else {
246 None
247 }
248 }
249
250 fn value_from_env_var(name: &str) -> Option<String> {
251 env::var(name).ok().and_then(|v| {
252 let v = v.trim();
253 if !v.is_empty() {
254 Some(v.to_string())
255 } else {
256 None
257 }
258 })
259 }
260
261 fn branch_from_git_command(raise_error: bool) -> Option<String> {
262 let branch_names = execute_and_parse_command(raise_error);
263 if raise_error {
264 validate_branch_names(&branch_names);
265 }
266 if branch_names.len() == 1 {
267 Some(branch_names[0].clone())
268 } else {
269 None
270 }
271 }
272
273 fn commit_from_git_command(raise_error: bool) -> Option<String> {
274 match execute_git_commit_command() {
275 Ok(s) => {
276 let trimmed = s.trim();
277 if trimmed.is_empty() {
278 None
279 } else {
280 Some(trimmed.to_string())
281 }
282 }
283 Err(e) => {
284 if raise_error {
285 panic!(
286 "Could not determine current git commit using command `git rev-parse HEAD`. {}",
287 e
288 );
289 }
290 None
291 }
292 }
293 }
294
295 fn validate_branch_names(branch_names: &[String]) {
296 if branch_names.is_empty() {
297 panic!(
298 "Command `git rev-parse --abbrev-ref HEAD` didn't return anything that could be identified as the current branch."
299 );
300 }
301 if branch_names.len() > 1 {
302 panic!(
303 "Command `git rev-parse --abbrev-ref HEAD` returned multiple branches: {}. You will need to get the branch name another way.",
304 branch_names.join(", ")
305 );
306 }
307 }
308
309 fn execute_git_command() -> Result<String, String> {
310 Command::new("git")
311 .args(["rev-parse", "--abbrev-ref", "HEAD"])
312 .output()
313 .map_err(|e| e.to_string())
314 .and_then(|output| {
315 if output.status.success() {
316 Ok(String::from_utf8_lossy(&output.stdout).to_string())
317 } else {
318 Err(String::from_utf8_lossy(&output.stderr).to_string())
319 }
320 })
321 }
322
323 fn execute_git_commit_command() -> Result<String, String> {
324 Command::new("git")
325 .args(["rev-parse", "HEAD"])
326 .output()
327 .map_err(|e| e.to_string())
328 .and_then(|output| {
329 if output.status.success() {
330 Ok(String::from_utf8_lossy(&output.stdout).to_string())
331 } else {
332 Err(String::from_utf8_lossy(&output.stderr).to_string())
333 }
334 })
335 }
336
337 fn execute_and_parse_command(raise_error: bool) -> Vec<String> {
338 match execute_git_command() {
339 Ok(output) => output
340 .lines()
341 .map(str::trim)
342 .filter(|l| !l.is_empty())
343 .map(|l| l.split_whitespace().next().unwrap_or("").to_string())
344 .map(|l| l.trim_start_matches("origin/").to_string())
345 .filter(|l| l != "HEAD")
346 .collect(),
347 Err(e) => {
348 if raise_error {
349 panic!(
350 "Could not determine current git branch using command `git rev-parse --abbrev-ref HEAD`. {}",
351 e
352 );
353 }
354 vec![]
355 }
356 }
357 }
358
359 fn github_build_url() -> Option<String> {
360 let parts: Vec<String> = ["GITHUB_SERVER_URL", "GITHUB_REPOSITORY", "GITHUB_RUN_ID"]
361 .iter()
362 .filter_map(|&name| value_from_env_var(name))
363 .collect();
364 if parts.len() == 3 {
365 Some(format!(
366 "{}/{}/actions/runs/{}",
367 parts[0], parts[1], parts[2]
368 ))
369 } else {
370 None
371 }
372 }
373}