• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

baoyachi / shadow-rs / 7138398452

08 Dec 2023 06:50AM UTC coverage: 23.2% (+0.08%) from 23.122%
7138398452

push

github

web-flow
Update Cargo.toml

596 of 2569 relevant lines covered (23.2%)

0.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.22
/src/git.rs
1
use crate::build::{ConstType, ConstVal, ShadowConst};
2
use crate::ci::CiType;
3
use crate::err::*;
4
use std::collections::BTreeMap;
5
use std::io::{BufReader, Read};
6
use std::path::Path;
7
use std::process::{Command, Stdio};
8

9
const BRANCH_DOC: &str = r#"
10
The name of the Git branch that this project was built from.
11

12
This constant will be empty if the branch cannot be determined."#;
13
pub const BRANCH: ShadowConst = "BRANCH";
14
const TAG_DOC: &str = r#"
15
The name of the Git tag that this project was built from.
16
Note that this will be empty if there is no tag for the HEAD at the time of build."#;
17
pub const TAG: ShadowConst = "TAG";
18
const LAST_TAG_DOC: &str = r#"
19
The name of the last Git tag on the branch that this project was built from.
20
As opposed to [`TAG`], this does not require the current commit to be tagged, just one of its parents.
21

22
This constant will be empty if the last tag cannot be determined."#;
23
pub const LAST_TAG: ShadowConst = "LAST_TAG";
24
const SHORT_COMMIT_DOC: &str = r#"
25
The short hash of the Git commit that this project was built from.
26
Note that this will always truncate [`COMMIT_HASH`] to 8 characters if necessary.
27
Depending on the amount of commits in your project, this may not yield a unique Git identifier
28
([see here for more details on hash abbreviation](https://git-scm.com/docs/git-describe#_examples)).
29

30
This constant will be empty if the last commit cannot be determined."#;
31
pub const SHORT_COMMIT: ShadowConst = "SHORT_COMMIT";
32
const COMMIT_HASH_DOC: &str = r#"
33
The full commit hash of the Git commit that this project was built from.
34
An abbreviated, but not necessarily unique, version of this is [`SHORT_COMMIT`].
35

36
This constant will be empty if the last commit cannot be determined."#;
37
pub const COMMIT_HASH: ShadowConst = "COMMIT_HASH";
38
const COMMIT_DATE_DOC: &str = r#"The time of the Git commit that this project was built from.
39
The time is formatted in modified ISO 8601 format (`YYYY-MM-DD HH-MM ±hh-mm` where hh-mm is the offset from UTC).
40

41
This constant will be empty if the last commit cannot be determined."#;
42
pub const COMMIT_DATE: ShadowConst = "COMMIT_DATE";
43
const COMMIT_DATE_2822_DOC: &str = r#"
44
The name of the Git branch that this project was built from.
45
The time is formatted according to [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.3) (e.g. HTTP Headers).
46

47
This constant will be empty if the last commit cannot be determined."#;
48
pub const COMMIT_DATE_2822: ShadowConst = "COMMIT_DATE_2822";
49
const COMMIT_DATE_3339_DOC: &str = r#"
50
The name of the Git branch that this project was built from.
51
The time is formatted according to [RFC 3339 and ISO 8601](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6).
52

53
This constant will be empty if the last commit cannot be determined."#;
54
pub const COMMIT_DATE_3339: ShadowConst = "COMMIT_DATE_3339";
55
const COMMIT_AUTHOR_DOC: &str = r#"
56
The author of the Git commit that this project was built from.
57

58
This constant will be empty if the last commit cannot be determined."#;
59
pub const COMMIT_AUTHOR: ShadowConst = "COMMIT_AUTHOR";
60
const COMMIT_EMAIL_DOC: &str = r#"
61
The e-mail address of the author of the Git commit that this project was built from.
62

63
This constant will be empty if the last commit cannot be determined."#;
64
pub const COMMIT_EMAIL: ShadowConst = "COMMIT_EMAIL";
65
const GIT_CLEAN_DOC: &str = r#"
66
Whether the Git working tree was clean at the time of project build (`true`), or not (`false`).
67

68
This constant will be `false` if the last commit cannot be determined."#;
69
pub const GIT_CLEAN: ShadowConst = "GIT_CLEAN";
70
const GIT_STATUS_FILE_DOC: &str = r#"
71
The Git working tree status as a list of files with their status, similar to `git status`.
72
Each line of the list is preceded with `  * `, followed by the file name.
73
Files marked `(dirty)` have unstaged changes.
74
Files marked `(staged)` have staged changes.
75

76
This constant will be empty if the working tree status cannot be determined."#;
77
pub const GIT_STATUS_FILE: ShadowConst = "GIT_STATUS_FILE";
78

79
#[derive(Default, Debug)]
80
pub struct Git {
81
    map: BTreeMap<ShadowConst, ConstVal>,
82
    ci_type: CiType,
83
}
84

85
impl Git {
86
    fn update_str(&mut self, c: ShadowConst, v: String) {
2✔
87
        if let Some(val) = self.map.get_mut(c) {
3✔
88
            *val = ConstVal {
1✔
89
                desc: val.desc.clone(),
1✔
90
                v,
1✔
91
                t: ConstType::Str,
1✔
92
            }
93
        }
94
    }
95

96
    fn update_bool(&mut self, c: ShadowConst, v: bool) {
1✔
97
        if let Some(val) = self.map.get_mut(c) {
2✔
98
            *val = ConstVal {
1✔
99
                desc: val.desc.clone(),
1✔
100
                v: v.to_string(),
1✔
101
                t: ConstType::Bool,
1✔
102
            }
103
        }
104
    }
105

106
    fn init(&mut self, path: &Path, std_env: &BTreeMap<String, String>) -> SdResult<()> {
1✔
107
        // check git status
108
        let x = command_git_clean();
1✔
109
        self.update_bool(GIT_CLEAN, x);
1✔
110

111
        let x = command_git_status_file();
1✔
112
        self.update_str(GIT_STATUS_FILE, x);
1✔
113

114
        self.init_git2(path)?;
2✔
115

116
        // use command branch
117
        if let Some(x) = command_current_branch() {
2✔
118
            self.update_str(BRANCH, x)
1✔
119
        };
120

121
        // use command tag
122
        if let Some(x) = command_current_tag() {
2✔
123
            self.update_str(TAG, x)
1✔
124
        }
125

126
        // use command get last tag
127
        if let Some(x) = command_last_tag() {
2✔
128
            self.update_str(LAST_TAG, x)
1✔
129
        }
130

131
        // try use ci branch,tag
132
        self.ci_branch_tag(std_env);
1✔
133
        Ok(())
1✔
134
    }
135

136
    #[allow(unused_variables)]
137
    fn init_git2(&mut self, path: &Path) -> SdResult<()> {
1✔
138
        #[cfg(feature = "git2")]
139
        {
140
            use crate::date_time::DateTime;
141
            use crate::git::git2_mod::git_repo;
142
            use crate::Format;
143

144
            let repo = git_repo(path).map_err(ShadowError::new)?;
1✔
145
            let reference = repo.head().map_err(ShadowError::new)?;
2✔
146

147
            //get branch
148
            let branch = reference
2✔
149
                .shorthand()
150
                .map(|x| x.trim().to_string())
2✔
151
                .or_else(command_current_branch)
×
152
                .unwrap_or_default();
153

154
            //get HEAD branch
155
            let tag = command_current_tag().unwrap_or_default();
2✔
156
            let last_tag = command_last_tag().unwrap_or_default();
2✔
157
            self.update_str(BRANCH, branch);
1✔
158
            self.update_str(TAG, tag);
1✔
159
            self.update_str(LAST_TAG, last_tag);
1✔
160

161
            if let Some(v) = reference.target() {
1✔
162
                let commit = v.to_string();
1✔
163
                self.update_str(COMMIT_HASH, commit.clone());
2✔
164
                let mut short_commit = commit.as_str();
1✔
165

166
                if commit.len() > 8 {
2✔
167
                    short_commit = short_commit.get(0..8).unwrap();
1✔
168
                }
169
                self.update_str(SHORT_COMMIT, short_commit.to_string());
2✔
170
            }
171

172
            let commit = reference.peel_to_commit().map_err(ShadowError::new)?;
2✔
173

174
            let time_stamp = commit.time().seconds().to_string().parse::<i64>()?;
2✔
175
            let date_time = DateTime::timestamp_2_utc(time_stamp);
1✔
176
            self.update_str(COMMIT_DATE, date_time.human_format());
1✔
177

178
            self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
1✔
179

180
            self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
1✔
181

182
            let author = commit.author();
1✔
183
            if let Some(v) = author.email() {
2✔
184
                self.update_str(COMMIT_EMAIL, v.to_string());
2✔
185
            }
186

187
            if let Some(v) = author.name() {
2✔
188
                self.update_str(COMMIT_AUTHOR, v.to_string());
2✔
189
            }
190
            let status_file = Self::git2_dirty_stage(&repo);
2✔
191
            if status_file.trim().is_empty() {
2✔
192
                self.update_bool(GIT_CLEAN, true);
2✔
193
            } else {
194
                self.update_bool(GIT_CLEAN, false);
×
195
            }
196
            self.update_str(GIT_STATUS_FILE, status_file);
1✔
197
        }
198
        Ok(())
1✔
199
    }
200

201
    //use git2 crates git repository 'dirty or stage' status files.
202
    #[cfg(feature = "git2")]
203
    pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
1✔
204
        let mut repo_opts = git2::StatusOptions::new();
1✔
205
        repo_opts.include_ignored(false);
1✔
206
        if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
1✔
207
            let mut dirty_files = Vec::new();
1✔
208
            let mut staged_files = Vec::new();
1✔
209

210
            for status in statue.iter() {
2✔
211
                if let Some(path) = status.path() {
×
212
                    match status.status() {
×
213
                        git2::Status::CURRENT => (),
×
214
                        git2::Status::INDEX_NEW
×
215
                        | git2::Status::INDEX_MODIFIED
×
216
                        | git2::Status::INDEX_DELETED
×
217
                        | git2::Status::INDEX_RENAMED
×
218
                        | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
×
219
                        _ => dirty_files.push(path.to_string()),
×
220
                    };
221
                }
222
            }
223
            filter_git_dirty_stage(dirty_files, staged_files)
1✔
224
        } else {
225
            "".into()
×
226
        }
227
    }
228

229
    #[allow(clippy::manual_strip)]
230
    fn ci_branch_tag(&mut self, std_env: &BTreeMap<String, String>) {
1✔
231
        let mut branch: Option<String> = None;
1✔
232
        let mut tag: Option<String> = None;
1✔
233
        match self.ci_type {
1✔
234
            CiType::Gitlab => {
×
235
                if let Some(v) = std_env.get("CI_COMMIT_TAG") {
×
236
                    tag = Some(v.to_string());
×
237
                } else if let Some(v) = std_env.get("CI_COMMIT_REF_NAME") {
×
238
                    branch = Some(v.to_string());
×
239
                }
240
            }
241
            CiType::Github => {
×
242
                if let Some(v) = std_env.get("GITHUB_REF") {
2✔
243
                    let ref_branch_prefix: &str = "refs/heads/";
1✔
244
                    let ref_tag_prefix: &str = "refs/tags/";
1✔
245

246
                    if v.starts_with(ref_branch_prefix) {
1✔
247
                        branch = Some(
×
248
                            v.get(ref_branch_prefix.len()..)
×
249
                                .unwrap_or_default()
×
250
                                .to_string(),
×
251
                        )
252
                    } else if v.starts_with(ref_tag_prefix) {
3✔
253
                        tag = Some(
1✔
254
                            v.get(ref_tag_prefix.len()..)
1✔
255
                                .unwrap_or_default()
×
256
                                .to_string(),
×
257
                        )
258
                    }
259
                }
260
            }
261
            _ => {}
×
262
        }
263
        if let Some(x) = branch {
1✔
264
            self.update_str(BRANCH, x);
×
265
        }
266

267
        if let Some(x) = tag {
3✔
268
            self.update_str(TAG, x.clone());
2✔
269
            self.update_str(LAST_TAG, x);
1✔
270
        }
271
    }
272
}
273

274
pub fn new_git(
1✔
275
    path: &Path,
276
    ci: CiType,
277
    std_env: &BTreeMap<String, String>,
278
) -> BTreeMap<ShadowConst, ConstVal> {
279
    let mut git = Git {
280
        map: Default::default(),
1✔
281
        ci_type: ci,
282
    };
283
    git.map.insert(BRANCH, ConstVal::new(BRANCH_DOC));
2✔
284

285
    git.map.insert(TAG, ConstVal::new(TAG_DOC));
1✔
286

287
    git.map.insert(LAST_TAG, ConstVal::new(LAST_TAG_DOC));
1✔
288

289
    git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
1✔
290

291
    git.map
1✔
292
        .insert(SHORT_COMMIT, ConstVal::new(SHORT_COMMIT_DOC));
2✔
293

294
    git.map
1✔
295
        .insert(COMMIT_AUTHOR, ConstVal::new(COMMIT_AUTHOR_DOC));
2✔
296
    git.map
1✔
297
        .insert(COMMIT_EMAIL, ConstVal::new(COMMIT_EMAIL_DOC));
2✔
298
    git.map.insert(COMMIT_DATE, ConstVal::new(COMMIT_DATE_DOC));
1✔
299

300
    git.map
1✔
301
        .insert(COMMIT_DATE_2822, ConstVal::new(COMMIT_DATE_2822_DOC));
2✔
302

303
    git.map
1✔
304
        .insert(COMMIT_DATE_3339, ConstVal::new(COMMIT_DATE_3339_DOC));
2✔
305

306
    git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
1✔
307

308
    git.map
1✔
309
        .insert(GIT_STATUS_FILE, ConstVal::new(GIT_STATUS_FILE_DOC));
2✔
310

311
    if let Err(e) = git.init(path, std_env) {
1✔
312
        println!("{e}");
×
313
    }
314

315
    git.map
1✔
316
}
317

318
#[cfg(feature = "git2")]
319
pub mod git2_mod {
320
    use git2::Error as git2Error;
321
    use git2::Repository;
322
    use std::path::Path;
323

324
    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
1✔
325
        git2::Repository::discover(path)
1✔
326
    }
327

328
    pub fn git2_current_branch(repo: &Repository) -> Option<String> {
×
329
        repo.head()
×
330
            .map(|x| x.shorthand().map(|x| x.to_string()))
×
331
            .unwrap_or(None)
×
332
    }
333
}
334

335
/// get current repository git branch.
336
///
337
/// When current repository exists git folder.
338
///
339
/// It's use default feature.This function try use [git2] crates get current branch.
340
/// If not use git2 feature,then try use [Command] to get.
341
pub fn branch() -> String {
×
342
    #[cfg(feature = "git2")]
343
    {
344
        use crate::git::git2_mod::{git2_current_branch, git_repo};
345
        git_repo(".")
346
            .map(|x| git2_current_branch(&x))
×
347
            .unwrap_or_else(|_| command_current_branch())
×
348
            .unwrap_or_default()
349
    }
350
    #[cfg(not(feature = "git2"))]
351
    {
352
        command_current_branch().unwrap_or_default()
353
    }
354
}
355

356
/// get current repository git tag.
357
///
358
/// When current repository exists git folder.
359
/// I's use [Command] to get.
360
pub fn tag() -> String {
×
361
    command_current_tag().unwrap_or_default()
×
362
}
363

364
/// Check current git Repository status without nothing(dirty or stage)
365
///
366
/// if nothing,It means clean:true. On the contrary, it is 'dirty':false
367
pub fn git_clean() -> bool {
×
368
    #[cfg(feature = "git2")]
369
    {
370
        use crate::git::git2_mod::git_repo;
371
        git_repo(".")
372
            .map(|x| Git::git2_dirty_stage(&x))
×
373
            .map(|x| x.trim().is_empty())
×
374
            .unwrap_or(true)
375
    }
376
    #[cfg(not(feature = "git2"))]
377
    {
378
        command_git_clean()
379
    }
380
}
381

382
/// List current git Repository statue(dirty or stage) contain file changed
383
///
384
/// Refer to the 'cargo fix' result output when git statue(dirty or stage) changed.
385
///
386
/// Example output:`   * examples/builtin_fn.rs (dirty)`
387
pub fn git_status_file() -> String {
×
388
    #[cfg(feature = "git2")]
389
    {
390
        use crate::git::git2_mod::git_repo;
391
        git_repo(".")
392
            .map(|x| Git::git2_dirty_stage(&x))
×
393
            .unwrap_or_default()
394
    }
395
    #[cfg(not(feature = "git2"))]
396
    {
397
        command_git_status_file()
398
    }
399
}
400

401
/// Command exec git current tag
402
fn command_current_tag() -> Option<String> {
1✔
403
    Command::new("git")
404
        .args(["tag", "-l", "--contains", "HEAD"])
1✔
405
        .output()
406
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
407
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
408
        .unwrap_or(None)
1✔
409
}
410

411
/// git describe --tags --abbrev=0 HEAD
412
/// Command exec git last tag
413
fn command_last_tag() -> Option<String> {
1✔
414
    Command::new("git")
415
        .args(["describe", "--tags", "--abbrev=0", "HEAD"])
1✔
416
        .output()
417
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
418
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
419
        .unwrap_or(None)
1✔
420
}
421

422
/// git clean:git status --porcelain
423
/// check repository git status is clean
424
fn command_git_clean() -> bool {
1✔
425
    Command::new("git")
426
        .args(["status", "--porcelain"])
1✔
427
        .output()
428
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
429
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
430
        .map(|x| x.is_none() || x.map(|y| y.is_empty()).unwrap_or_default())
4✔
431
        .unwrap_or(true)
432
}
433

434
/// check git repository 'dirty or stage' status files.
435
/// git dirty:git status  --porcelain | grep '^\sM.' |awk '{print $2}'
436
/// git stage:git status --porcelain --untracked-files=all | grep '^[A|M|D|R]'|awk '{print $2}'
437
fn command_git_status_file() -> String {
1✔
438
    let git_status_files =
2✔
439
        move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
440
            let git_shell = Command::new("git")
3✔
441
                .args(args)
442
                .stdin(Stdio::piped())
1✔
443
                .stdout(Stdio::piped())
1✔
444
                .spawn()?;
1✔
445
            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
1✔
446

447
            let grep_shell = Command::new("grep")
4✔
448
                .args(grep)
449
                .stdin(Stdio::from(git_out))
1✔
450
                .stdout(Stdio::piped())
1✔
451
                .spawn()?;
1✔
452
            let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
1✔
453

454
            let mut awk_shell = Command::new("awk")
4✔
455
                .args(awk)
456
                .stdin(Stdio::from(grep_out))
1✔
457
                .stdout(Stdio::piped())
1✔
458
                .spawn()?;
1✔
459
            let mut awk_out = BufReader::new(
460
                awk_shell
1✔
461
                    .stdout
462
                    .as_mut()
463
                    .ok_or("Failed to exec awk stdout")?,
×
464
            );
465
            let mut line = String::new();
1✔
466
            awk_out.read_to_string(&mut line)?;
2✔
467
            Ok(line.lines().map(|x| x.into()).collect())
2✔
468
        };
469

470
    let dirty = git_status_files(&["status", "--porcelain"], &[r"^\sM."], &["{print $2}"])
1✔
471
        .unwrap_or_default();
472

473
    let stage = git_status_files(
474
        &["status", "--porcelain", "--untracked-files=all"],
475
        &[r#"^[A|M|D|R]"#],
476
        &["{print $2}"],
477
    )
478
    .unwrap_or_default();
479
    filter_git_dirty_stage(dirty, stage)
1✔
480
}
481

482
/// Command exec git current branch
483
fn command_current_branch() -> Option<String> {
1✔
484
    Command::new("git")
485
        .args(["symbolic-ref", "--short", "HEAD"])
1✔
486
        .output()
487
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
488
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
489
        .unwrap_or(None)
1✔
490
}
491

492
fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
1✔
493
    let mut concat_file = String::new();
1✔
494
    for file in dirty_files {
2✔
495
        concat_file.push_str("  * ");
×
496
        concat_file.push_str(&file);
×
497
        concat_file.push_str(" (dirty)\n");
×
498
    }
499
    for file in staged_files {
1✔
500
        concat_file.push_str("  * ");
×
501
        concat_file.push_str(&file);
×
502
        concat_file.push_str(" (staged)\n");
×
503
    }
504
    concat_file
1✔
505
}
506

507
#[cfg(test)]
508
mod tests {
509
    use super::*;
510
    use crate::get_std_env;
511
    use std::path::Path;
512

513
    #[test]
514
    fn test_git() {
3✔
515
        let env_map = get_std_env();
1✔
516
        let map = new_git(Path::new("./"), CiType::Github, &env_map);
2✔
517
        for (k, v) in map {
1✔
518
            println!("k:{},v:{:?}", k, v);
1✔
519
            assert!(!v.desc.is_empty());
1✔
520
            if !k.eq(TAG) && !k.eq(LAST_TAG) && !k.eq(BRANCH) && !k.eq(GIT_STATUS_FILE) {
3✔
521
                assert!(!v.v.is_empty());
1✔
522
                continue;
523
            }
524

525
            //assert github tag always exist value
526
            if let Some(github_ref) = env_map.get("GITHUB_REF") {
2✔
527
                if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
3✔
528
                    assert!(!v.v.is_empty(), "not empty");
2✔
529
                } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
2✔
530
                    assert!(!v.v.is_empty());
×
531
                }
532
            }
533
        }
534
    }
535

536
    #[test]
537
    fn test_current_branch() {
3✔
538
        if get_std_env().get("GITHUB_REF").is_some() {
2✔
539
            return;
540
        }
541
        #[cfg(feature = "git2")]
542
        {
543
            use crate::git::git2_mod::{git2_current_branch, git_repo};
544
            let git2_branch = git_repo(".")
545
                .map(|x| git2_current_branch(&x))
×
546
                .unwrap_or(None);
×
547
            let command_branch = command_current_branch();
×
548
            assert!(git2_branch.is_some());
×
549
            assert!(command_branch.is_some());
×
550
            assert_eq!(command_branch, git2_branch);
×
551
        }
552

553
        assert_eq!(Some(branch()), command_current_branch());
×
554
    }
555

556
    #[test]
557
    fn test_command_last_tag() {
3✔
558
        let opt_last_tag = command_last_tag();
1✔
559
        assert!(opt_last_tag.is_some())
2✔
560
    }
561
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc