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

baoyachi / shadow-rs / 6440526697

07 Oct 2023 10:10AM UTC coverage: 24.1% (-0.003%) from 24.103%
6440526697

push

github

web-flow
Update Cargo.toml

596 of 2473 relevant lines covered (24.1%)

0.35 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

113
        let x = command_git_status_file();
1✔
114
        self.update_str(GIT_STATUS_FILE, x);
1✔
115

116
        self.init_git2(path)?;
2✔
117

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

314
    git.map
1✔
315
}
316

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

555
    #[test]
556
    fn test_command_last_tag() {
3✔
557
        let opt_last_tag = command_last_tag();
1✔
558
        assert!(opt_last_tag.is_some())
2✔
559
    }
560
}
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