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

baoyachi / shadow-rs / 17409696840

02 Sep 2025 04:21PM UTC coverage: 75.988% (-0.7%) from 76.672%
17409696840

Pull #243

github

web-flow
Merge 9ae895bcd into 30dd5927c
Pull Request #243: Expand `print_build_in` functionality

21 of 21 new or added lines in 1 file covered. (100.0%)

9 existing lines in 2 files now uncovered.

500 of 658 relevant lines covered (75.99%)

1.06 hits per line

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

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

10
const BRANCH_DOC: &str = r#"
11
The name of the Git branch that this project was built from.
12
This constant will be empty if the branch cannot be determined."#;
13
pub const BRANCH: ShadowConst = "BRANCH";
14

15
const TAG_DOC: &str = r#"
16
The name of the Git tag that this project was built from.
17
Note that this will be empty if there is no tag for the HEAD at the time of build."#;
18
pub const TAG: ShadowConst = "TAG";
19

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

27
pub const COMMITS_SINCE_TAG_DOC: &str = r#"
28
The number of commits since the last Git tag on the branch that this project was built from.
29
This value indicates how many commits have been made after the last tag and before the current commit.
30

31
If there are no additional commits after the last tag (i.e., the current commit is exactly at a tag),
32
this value will be `0`.
33

34
This constant will be empty or `0` if the last tag cannot be determined or if there are no commits after it.
35
"#;
36

37
pub const COMMITS_SINCE_TAG: &str = "COMMITS_SINCE_TAG";
38

39
const SHORT_COMMIT_DOC: &str = r#"
40
The short hash of the Git commit that this project was built from.
41
Note that this will always truncate [`COMMIT_HASH`] to 8 characters if necessary.
42
Depending on the amount of commits in your project, this may not yield a unique Git identifier
43
([see here for more details on hash abbreviation](https://git-scm.com/docs/git-describe#_examples)).
44

45
This constant will be empty if the last commit cannot be determined."#;
46
pub const SHORT_COMMIT: ShadowConst = "SHORT_COMMIT";
47

48
const COMMIT_HASH_DOC: &str = r#"
49
The full commit hash of the Git commit that this project was built from.
50
An abbreviated, but not necessarily unique, version of this is [`SHORT_COMMIT`].
51

52
This constant will be empty if the last commit cannot be determined."#;
53
pub const COMMIT_HASH: ShadowConst = "COMMIT_HASH";
54

55
const COMMIT_DATE_DOC: &str = r#"The time of the Git commit that this project was built from.
56
The time is formatted in modified ISO 8601 format (`YYYY-MM-DD HH-MM ±hh-mm` where hh-mm is the offset from UTC).
57

58
This constant will be empty if the last commit cannot be determined."#;
59
pub const COMMIT_DATE: ShadowConst = "COMMIT_DATE";
60

61
const COMMIT_DATE_2822_DOC: &str = r#"
62
The name of the Git branch that this project was built from.
63
The time is formatted according to [RFC 2822](https://datatracker.ietf.org/doc/html/rfc2822#section-3.3) (e.g. HTTP Headers).
64

65
This constant will be empty if the last commit cannot be determined."#;
66
pub const COMMIT_DATE_2822: ShadowConst = "COMMIT_DATE_2822";
67

68
const COMMIT_DATE_3339_DOC: &str = r#"
69
The name of the Git branch that this project was built from.
70
The time is formatted according to [RFC 3339 and ISO 8601](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6).
71

72
This constant will be empty if the last commit cannot be determined."#;
73
pub const COMMIT_DATE_3339: ShadowConst = "COMMIT_DATE_3339";
74

75
const COMMIT_AUTHOR_DOC: &str = r#"
76
The author of the Git commit that this project was built from.
77

78
This constant will be empty if the last commit cannot be determined."#;
79
pub const COMMIT_AUTHOR: ShadowConst = "COMMIT_AUTHOR";
80

81
const COMMIT_EMAIL_DOC: &str = r#"
82
The e-mail address of the author of the Git commit that this project was built from.
83

84
This constant will be empty if the last commit cannot be determined."#;
85
pub const COMMIT_EMAIL: ShadowConst = "COMMIT_EMAIL";
86

87
const GIT_CLEAN_DOC: &str = r#"
88
Whether the Git working tree was clean at the time of project build (`true`), or not (`false`).
89

90
This constant will be `false` if the last commit cannot be determined."#;
91
pub const GIT_CLEAN: ShadowConst = "GIT_CLEAN";
92

93
const GIT_STATUS_FILE_DOC: &str = r#"
94
The Git working tree status as a list of files with their status, similar to `git status`.
95
Each line of the list is preceded with `  * `, followed by the file name.
96
Files marked `(dirty)` have unstaged changes.
97
Files marked `(staged)` have staged changes.
98

99
This constant will be empty if the working tree status cannot be determined."#;
100
pub const GIT_STATUS_FILE: ShadowConst = "GIT_STATUS_FILE";
101

102
#[derive(Default, Debug)]
103
pub struct Git {
104
    map: BTreeMap<ShadowConst, ConstVal>,
105
    ci_type: CiType,
106
}
107

108
impl Git {
109
    fn update_str(&mut self, c: ShadowConst, v: String) {
1✔
110
        if let Some(val) = self.map.get_mut(c) {
3✔
111
            *val = ConstVal {
1✔
112
                desc: val.desc.clone(),
1✔
113
                v,
1✔
114
                t: ConstType::Str,
1✔
115
            }
116
        }
117
    }
118

119
    fn update_bool(&mut self, c: ShadowConst, v: bool) {
1✔
120
        if let Some(val) = self.map.get_mut(c) {
2✔
121
            *val = ConstVal {
1✔
122
                desc: val.desc.clone(),
1✔
123
                v: v.to_string(),
1✔
124
                t: ConstType::Bool,
1✔
125
            }
126
        }
127
    }
128

129
    fn update_usize(&mut self, c: ShadowConst, v: usize) {
×
130
        if let Some(val) = self.map.get_mut(c) {
×
131
            *val = ConstVal {
×
132
                desc: val.desc.clone(),
×
133
                v: v.to_string(),
×
134
                t: ConstType::Usize,
×
135
            }
136
        }
137
    }
138

139
    fn init(&mut self, path: &Path, std_env: &BTreeMap<String, String>) -> SdResult<()> {
1✔
140
        // First, try executing using the git command.
141
        if let Err(err) = self.init_git() {
1✔
142
            println!("{err}");
×
143
        }
144

145
        // If the git2 feature is enabled, then replace the corresponding values with git2.
146
        self.init_git2(path)?;
1✔
147

148
        // use command branch
149
        if let Some(x) = find_branch_in(path) {
2✔
150
            self.update_str(BRANCH, x)
1✔
151
        };
152

153
        // use command tag
154
        if let Some(x) = command_current_tag() {
2✔
155
            self.update_str(TAG, x)
1✔
156
        }
157

158
        // use command get last tag
159
        let describe = command_git_describe();
1✔
160
        if let Some(x) = describe.0 {
2✔
161
            self.update_str(LAST_TAG, x)
2✔
162
        }
163

164
        if let Some(x) = describe.1 {
1✔
165
            self.update_usize(COMMITS_SINCE_TAG, x)
×
166
        }
167

168
        // try use ci branch,tag
169
        self.ci_branch_tag(std_env);
2✔
170
        Ok(())
2✔
171
    }
172

173
    fn init_git(&mut self) -> SdResult<()> {
1✔
174
        // check git status
175
        let x = command_git_clean();
1✔
176
        self.update_bool(GIT_CLEAN, x);
1✔
177

178
        let x = command_git_status_file();
1✔
179
        self.update_str(GIT_STATUS_FILE, x);
1✔
180

181
        let git_info = command_git_head();
1✔
182

183
        self.update_str(COMMIT_EMAIL, git_info.email);
1✔
184
        self.update_str(COMMIT_AUTHOR, git_info.author);
1✔
185
        self.update_str(SHORT_COMMIT, git_info.short_commit);
1✔
186
        self.update_str(COMMIT_HASH, git_info.commit);
1✔
187

188
        let time_stamp = git_info.date.parse::<i64>()?;
1✔
189
        if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
2✔
190
            self.update_str(COMMIT_DATE, date_time.human_format());
2✔
191
            self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
1✔
192
            self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
1✔
193
        }
194

195
        Ok(())
1✔
196
    }
197

198
    #[allow(unused_variables)]
199
    fn init_git2(&mut self, path: &Path) -> SdResult<()> {
1✔
200
        #[cfg(feature = "git2")]
201
        {
202
            use crate::date_time::DateTime;
203
            use crate::git::git2_mod::git_repo;
204
            use crate::Format;
205

206
            let repo = git_repo(path).map_err(ShadowError::new)?;
1✔
207
            let reference = repo.head().map_err(ShadowError::new)?;
2✔
208

209
            //get branch
210
            let branch = reference
211
                .shorthand()
212
                .map(|x| x.trim().to_string())
3✔
213
                .or_else(command_current_branch)
1✔
214
                .unwrap_or_default();
215

216
            //get HEAD branch
217
            let tag = command_current_tag().unwrap_or_default();
2✔
218
            self.update_str(BRANCH, branch);
2✔
219
            self.update_str(TAG, tag);
2✔
220

221
            // use command get last tag
222
            let describe = command_git_describe();
2✔
223
            if let Some(x) = describe.0 {
1✔
224
                self.update_str(LAST_TAG, x)
2✔
225
            }
226

227
            if let Some(x) = describe.1 {
1✔
228
                self.update_usize(COMMITS_SINCE_TAG, x)
×
229
            }
230

231
            if let Some(v) = reference.target() {
2✔
232
                let commit = v.to_string();
1✔
233
                self.update_str(COMMIT_HASH, commit.clone());
2✔
234
                let mut short_commit = commit.as_str();
1✔
235

236
                if commit.len() > 8 {
2✔
237
                    short_commit = short_commit.get(0..8).unwrap();
1✔
238
                }
239
                self.update_str(SHORT_COMMIT, short_commit.to_string());
2✔
240
            }
241

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

244
            let author = commit.author();
2✔
245
            if let Some(v) = author.email() {
2✔
246
                self.update_str(COMMIT_EMAIL, v.to_string());
2✔
247
            }
248

249
            if let Some(v) = author.name() {
2✔
250
                self.update_str(COMMIT_AUTHOR, v.to_string());
2✔
251
            }
252
            let status_file = Self::git2_dirty_stage(&repo);
2✔
253
            if status_file.trim().is_empty() {
2✔
254
                self.update_bool(GIT_CLEAN, true);
2✔
255
            } else {
256
                self.update_bool(GIT_CLEAN, false);
×
257
            }
258
            self.update_str(GIT_STATUS_FILE, status_file);
1✔
259

260
            let time_stamp = commit.time().seconds().to_string().parse::<i64>()?;
1✔
261
            if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
2✔
262
                self.update_str(COMMIT_DATE, date_time.human_format());
2✔
263

264
                self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
1✔
265

266
                self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
1✔
267
            }
268
        }
269
        Ok(())
1✔
270
    }
271

272
    //use git2 crates git repository 'dirty or stage' status files.
273
    #[cfg(feature = "git2")]
274
    pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
1✔
275
        let mut repo_opts = git2::StatusOptions::new();
1✔
276
        repo_opts.include_ignored(false);
1✔
277
        if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
2✔
278
            let mut dirty_files = Vec::new();
1✔
279
            let mut staged_files = Vec::new();
1✔
280

281
            for status in statue.iter() {
2✔
282
                if let Some(path) = status.path() {
×
283
                    match status.status() {
×
284
                        git2::Status::CURRENT => (),
285
                        git2::Status::INDEX_NEW
×
286
                        | git2::Status::INDEX_MODIFIED
287
                        | git2::Status::INDEX_DELETED
288
                        | git2::Status::INDEX_RENAMED
289
                        | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
290
                        _ => dirty_files.push(path.to_string()),
×
291
                    };
292
                }
293
            }
294
            filter_git_dirty_stage(dirty_files, staged_files)
2✔
295
        } else {
296
            "".into()
×
297
        }
298
    }
299

300
    #[allow(clippy::manual_strip)]
301
    fn ci_branch_tag(&mut self, std_env: &BTreeMap<String, String>) {
2✔
302
        let mut branch: Option<String> = None;
2✔
303
        let mut tag: Option<String> = None;
2✔
304
        match self.ci_type {
2✔
305
            CiType::Gitlab => {
306
                if let Some(v) = std_env.get("CI_COMMIT_TAG") {
×
307
                    tag = Some(v.to_string());
×
308
                } else if let Some(v) = std_env.get("CI_COMMIT_REF_NAME") {
×
309
                    branch = Some(v.to_string());
×
310
                }
311
            }
312
            CiType::Github => {
313
                if let Some(v) = std_env.get("GITHUB_REF") {
4✔
314
                    let ref_branch_prefix: &str = "refs/heads/";
2✔
315
                    let ref_tag_prefix: &str = "refs/tags/";
2✔
316

317
                    if v.starts_with(ref_branch_prefix) {
2✔
UNCOV
318
                        branch = Some(
×
UNCOV
319
                            v.get(ref_branch_prefix.len()..)
×
UNCOV
320
                                .unwrap_or_default()
×
UNCOV
321
                                .to_string(),
×
322
                        )
323
                    } else if v.starts_with(ref_tag_prefix) {
4✔
324
                        tag = Some(
×
325
                            v.get(ref_tag_prefix.len()..)
×
326
                                .unwrap_or_default()
×
327
                                .to_string(),
×
328
                        )
329
                    }
330
                }
331
            }
332
            _ => {}
333
        }
334
        if let Some(x) = branch {
2✔
UNCOV
335
            self.update_str(BRANCH, x);
×
336
        }
337

338
        if let Some(x) = tag {
2✔
339
            self.update_str(TAG, x.clone());
×
340
            self.update_str(LAST_TAG, x);
×
341
        }
342
    }
343
}
344

345
pub(crate) fn new_git(
1✔
346
    path: &Path,
347
    ci: CiType,
348
    std_env: &BTreeMap<String, String>,
349
) -> BTreeMap<ShadowConst, ConstVal> {
350
    let mut git = Git {
351
        map: Default::default(),
1✔
352
        ci_type: ci,
353
    };
354
    git.map.insert(BRANCH, ConstVal::new(BRANCH_DOC));
2✔
355

356
    git.map.insert(TAG, ConstVal::new(TAG_DOC));
1✔
357

358
    git.map.insert(LAST_TAG, ConstVal::new(LAST_TAG_DOC));
1✔
359

360
    git.map
361
        .insert(COMMITS_SINCE_TAG, ConstVal::new(COMMITS_SINCE_TAG_DOC));
1✔
362

363
    git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
1✔
364

365
    git.map
366
        .insert(SHORT_COMMIT, ConstVal::new(SHORT_COMMIT_DOC));
1✔
367

368
    git.map
369
        .insert(COMMIT_AUTHOR, ConstVal::new(COMMIT_AUTHOR_DOC));
1✔
370
    git.map
371
        .insert(COMMIT_EMAIL, ConstVal::new(COMMIT_EMAIL_DOC));
1✔
372
    git.map.insert(COMMIT_DATE, ConstVal::new(COMMIT_DATE_DOC));
1✔
373

374
    git.map
375
        .insert(COMMIT_DATE_2822, ConstVal::new(COMMIT_DATE_2822_DOC));
1✔
376

377
    git.map
378
        .insert(COMMIT_DATE_3339, ConstVal::new(COMMIT_DATE_3339_DOC));
1✔
379

380
    git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
1✔
381

382
    git.map
383
        .insert(GIT_STATUS_FILE, ConstVal::new(GIT_STATUS_FILE_DOC));
1✔
384

385
    if let Err(e) = git.init(path, std_env) {
1✔
386
        println!("{e}");
×
387
    }
388

389
    git.map
2✔
390
}
391

392
#[cfg(feature = "git2")]
393
pub mod git2_mod {
394
    use git2::Error as git2Error;
395
    use git2::Repository;
396
    use std::path::Path;
397

398
    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
1✔
399
        Repository::discover(path)
1✔
400
    }
401

402
    pub fn git2_current_branch(repo: &Repository) -> Option<String> {
×
403
        repo.head()
×
404
            .map(|x| x.shorthand().map(|x| x.to_string()))
×
405
            .unwrap_or(None)
×
406
    }
407
}
408

409
/// get current repository git branch.
410
///
411
/// When current repository exists git folder.
412
///
413
/// It's use default feature.This function try use [git2] crates get current branch.
414
/// If not use git2 feature,then try use [Command] to get.
415
pub fn branch() -> String {
×
416
    #[cfg(feature = "git2")]
417
    {
418
        use crate::git::git2_mod::{git2_current_branch, git_repo};
419
        git_repo(".")
×
420
            .map(|x| git2_current_branch(&x))
×
421
            .unwrap_or_else(|_| command_current_branch())
×
422
            .unwrap_or_default()
423
    }
424
    #[cfg(not(feature = "git2"))]
425
    {
426
        command_current_branch().unwrap_or_default()
427
    }
428
}
429

430
/// get current repository git tag.
431
///
432
/// When current repository exists git folder.
433
/// I's use [Command] to get.
434
pub fn tag() -> String {
×
435
    command_current_tag().unwrap_or_default()
×
436
}
437

438
/// Check current git Repository status without nothing(dirty or stage)
439
///
440
/// if nothing,It means clean:true. On the contrary, it is 'dirty':false
441
pub fn git_clean() -> bool {
×
442
    #[cfg(feature = "git2")]
443
    {
444
        use crate::git::git2_mod::git_repo;
445
        git_repo(".")
×
446
            .map(|x| Git::git2_dirty_stage(&x))
×
447
            .map(|x| x.trim().is_empty())
×
448
            .unwrap_or(true)
449
    }
450
    #[cfg(not(feature = "git2"))]
451
    {
452
        command_git_clean()
453
    }
454
}
455

456
/// List current git Repository statue(dirty or stage) contain file changed
457
///
458
/// Refer to the 'cargo fix' result output when git statue(dirty or stage) changed.
459
///
460
/// Example output:`   * examples/builtin_fn.rs (dirty)`
461
pub fn git_status_file() -> String {
×
462
    #[cfg(feature = "git2")]
463
    {
464
        use crate::git::git2_mod::git_repo;
465
        git_repo(".")
×
466
            .map(|x| Git::git2_dirty_stage(&x))
×
467
            .unwrap_or_default()
468
    }
469
    #[cfg(not(feature = "git2"))]
470
    {
471
        command_git_status_file()
472
    }
473
}
474

475
struct GitHeadInfo {
476
    commit: String,
477
    short_commit: String,
478
    email: String,
479
    author: String,
480
    date: String,
481
}
482

483
struct GitCommandExecutor<'a> {
484
    path: &'a Path,
485
}
486

487
impl Default for GitCommandExecutor<'_> {
488
    fn default() -> Self {
1✔
489
        Self::new(Path::new("."))
1✔
490
    }
491
}
492

493
impl<'a> GitCommandExecutor<'a> {
494
    fn new(path: &'a Path) -> Self {
1✔
495
        GitCommandExecutor { path }
496
    }
497

498
    fn exec(&self, args: &[&str]) -> Option<String> {
1✔
499
        Command::new("git")
1✔
500
            .env("GIT_OPTIONAL_LOCKS", "0")
501
            .current_dir(self.path)
1✔
502
            .args(args)
1✔
503
            .output()
504
            .map(|x| {
2✔
505
                String::from_utf8(x.stdout)
1✔
506
                    .map(|x| x.trim().to_string())
3✔
507
                    .ok()
1✔
508
            })
509
            .unwrap_or(None)
1✔
510
    }
511
}
512

513
fn command_git_head() -> GitHeadInfo {
1✔
514
    let cli = |args: &[&str]| GitCommandExecutor::default().exec(args).unwrap_or_default();
2✔
515
    GitHeadInfo {
516
        commit: cli(&["rev-parse", "HEAD"]),
1✔
517
        short_commit: cli(&["rev-parse", "--short", "HEAD"]),
1✔
518
        author: cli(&["log", "-1", "--pretty=format:%an"]),
2✔
519
        email: cli(&["log", "-1", "--pretty=format:%ae"]),
1✔
520
        date: cli(&["show", "--pretty=format:%ct", "--date=raw", "-s"]),
2✔
521
    }
522
}
523

524
/// Command exec git current tag
525
fn command_current_tag() -> Option<String> {
1✔
526
    GitCommandExecutor::default().exec(&["tag", "-l", "--contains", "HEAD"])
1✔
527
}
528

529
/// git describe --tags HEAD
530
/// Command exec git describe
531
fn command_git_describe() -> (Option<String>, Option<usize>, Option<String>) {
2✔
532
    let last_tag =
2✔
533
        GitCommandExecutor::default().exec(&["describe", "--tags", "--abbrev=0", "HEAD"]);
534
    if last_tag.is_none() {
2✔
535
        return (None, None, None);
×
536
    }
537

538
    let tag = last_tag.unwrap();
2✔
539

540
    let describe = GitCommandExecutor::default().exec(&["describe", "--tags", "HEAD"]);
2✔
541
    if let Some(desc) = describe {
1✔
542
        match parse_git_describe(&tag, &desc) {
2✔
543
            Ok((tag, commits, hash)) => {
1✔
544
                return (Some(tag), commits, hash);
1✔
545
            }
546
            Err(_) => {
547
                return (Some(tag), None, None);
×
548
            }
549
        }
550
    }
551
    (Some(tag), None, None)
×
552
}
553

554
fn parse_git_describe(
1✔
555
    last_tag: &str,
556
    describe: &str,
557
) -> SdResult<(String, Option<usize>, Option<String>)> {
558
    if !describe.starts_with(last_tag) {
1✔
559
        return Err(ShadowError::String("git describe result error".to_string()));
×
560
    }
561

562
    if last_tag == describe {
1✔
563
        return Ok((describe.to_string(), None, None));
1✔
564
    }
565

566
    let parts: Vec<&str> = describe.rsplit('-').collect();
1✔
567

568
    if parts.is_empty() || parts.len() == 2 {
3✔
569
        return Err(ShadowError::String(
×
570
            "git describe result error,expect:<tag>-<num_commits>-g<hash>".to_string(),
×
571
        ));
572
    }
573

574
    if parts.len() > 2 {
1✔
575
        let short_hash = parts[0]; // last part
2✔
576

577
        if !short_hash.starts_with('g') {
1✔
578
            return Err(ShadowError::String(
1✔
579
                "git describe result error,expect commit hash end with:-g<hash>".to_string(),
1✔
580
            ));
581
        }
582
        let short_hash = short_hash.trim_start_matches('g');
2✔
583

584
        // Full example:v1.0.0-alpha0-5-ga1b2c3d
585
        let num_commits_str = parts[1];
1✔
586
        let num_commits = num_commits_str
3✔
587
            .parse::<usize>()
588
            .map_err(|e| ShadowError::String(e.to_string()))?;
4✔
589
        let last_tag = parts[2..]
2✔
590
            .iter()
591
            .rev()
592
            .copied()
593
            .collect::<Vec<_>>()
594
            .join("-");
595
        return Ok((last_tag, Some(num_commits), Some(short_hash.to_string())));
1✔
596
    }
597
    Ok((describe.to_string(), None, None))
×
598
}
599

600
/// git clean:git status --porcelain
601
/// check repository git status is clean
602
fn command_git_clean() -> bool {
1✔
603
    GitCommandExecutor::default()
1✔
604
        .exec(&["status", "--porcelain"])
1✔
605
        .map(|x| x.is_empty())
3✔
606
        .unwrap_or(true)
607
}
608

609
/// check git repository 'dirty or stage' status files.
610
/// git dirty:git status  --porcelain | grep '^\sM.' |awk '{print $2}'
611
/// git stage:git status --porcelain --untracked-files=all | grep '^[A|M|D|R]'|awk '{print $2}'
612
fn command_git_status_file() -> String {
1✔
613
    let git_status_files =
2✔
614
        move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
615
            let git_shell = Command::new("git")
2✔
616
                .env("GIT_OPTIONAL_LOCKS", "0")
617
                .args(args)
1✔
618
                .stdin(Stdio::piped())
1✔
619
                .stdout(Stdio::piped())
1✔
620
                .spawn()?;
621
            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
1✔
622

623
            let grep_shell = Command::new("grep")
2✔
624
                .args(grep)
1✔
625
                .stdin(Stdio::from(git_out))
1✔
626
                .stdout(Stdio::piped())
1✔
627
                .spawn()?;
628
            let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
1✔
629

630
            let mut awk_shell = Command::new("awk")
2✔
631
                .args(awk)
1✔
632
                .stdin(Stdio::from(grep_out))
1✔
633
                .stdout(Stdio::piped())
1✔
634
                .spawn()?;
635
            let mut awk_out = BufReader::new(
636
                awk_shell
2✔
637
                    .stdout
638
                    .as_mut()
1✔
639
                    .ok_or("Failed to exec awk stdout")?,
1✔
640
            );
641
            let mut line = String::new();
1✔
642
            awk_out.read_to_string(&mut line)?;
2✔
643
            Ok(line.lines().map(|x| x.into()).collect())
1✔
644
        };
645

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

649
    let stage = git_status_files(
650
        &["status", "--porcelain", "--untracked-files=all"],
651
        &[r#"^[A|M|D|R]"#],
652
        &["{print $2}"],
653
    )
654
    .unwrap_or_default();
655
    filter_git_dirty_stage(dirty, stage)
1✔
656
}
657

658
/// Command exec git current branch
659
fn command_current_branch() -> Option<String> {
×
660
    find_branch_in(Path::new("."))
×
661
}
662

663
fn find_branch_in(path: &Path) -> Option<String> {
1✔
664
    GitCommandExecutor::new(path).exec(&["symbolic-ref", "--short", "HEAD"])
1✔
665
}
666

667
fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
1✔
668
    let mut concat_file = String::new();
1✔
669
    for file in dirty_files {
3✔
670
        concat_file.push_str("  * ");
×
671
        concat_file.push_str(&file);
×
672
        concat_file.push_str(" (dirty)\n");
×
673
    }
674
    for file in staged_files {
2✔
675
        concat_file.push_str("  * ");
×
676
        concat_file.push_str(&file);
×
677
        concat_file.push_str(" (staged)\n");
×
678
    }
679
    concat_file
1✔
680
}
681

682
#[cfg(test)]
683
mod tests {
684
    use super::*;
685
    use crate::get_std_env;
686

687
    #[test]
688
    fn test_git() {
689
        let env_map = get_std_env();
690
        let map = new_git(Path::new("./"), CiType::Github, &env_map);
691
        for (k, v) in map {
692
            assert!(!v.desc.is_empty());
693
            if !k.eq(TAG)
694
                && !k.eq(LAST_TAG)
695
                && !k.eq(COMMITS_SINCE_TAG)
696
                && !k.eq(BRANCH)
697
                && !k.eq(GIT_STATUS_FILE)
698
            {
699
                assert!(!v.v.is_empty());
700
                continue;
701
            }
702

703
            //assert github tag always exist value
704
            if let Some(github_ref) = env_map.get("GITHUB_REF") {
705
                if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
706
                    assert!(!v.v.is_empty(), "not empty");
707
                } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
708
                    assert!(!v.v.is_empty());
709
                }
710
            }
711
        }
712
    }
713

714
    #[test]
715
    fn test_current_branch() {
716
        if get_std_env().contains_key("GITHUB_REF") {
717
            return;
718
        }
719
        #[cfg(feature = "git2")]
720
        {
721
            use crate::git::git2_mod::{git2_current_branch, git_repo};
722
            let git2_branch = git_repo(".")
723
                .map(|x| git2_current_branch(&x))
724
                .unwrap_or(None);
725
            let command_branch = command_current_branch();
726
            assert!(git2_branch.is_some());
727
            assert!(command_branch.is_some());
728
            assert_eq!(command_branch, git2_branch);
729
        }
730

731
        assert_eq!(Some(branch()), command_current_branch());
732
    }
733

734
    #[test]
735
    fn test_parse_git_describe() {
736
        let commit_hash = "24skp4489";
737
        let describe = "v1.0.0";
738
        assert_eq!(
739
            parse_git_describe("v1.0.0", describe).unwrap(),
740
            (describe.into(), None, None)
741
        );
742

743
        let describe = "v1.0.0-0-g24skp4489";
744
        assert_eq!(
745
            parse_git_describe("v1.0.0", describe).unwrap(),
746
            ("v1.0.0".into(), Some(0), Some(commit_hash.into()))
747
        );
748

749
        let describe = "v1.0.0-1-g24skp4489";
750
        assert_eq!(
751
            parse_git_describe("v1.0.0", describe).unwrap(),
752
            ("v1.0.0".into(), Some(1), Some(commit_hash.into()))
753
        );
754

755
        let describe = "v1.0.0-alpha-0-g24skp4489";
756
        assert_eq!(
757
            parse_git_describe("v1.0.0-alpha", describe).unwrap(),
758
            ("v1.0.0-alpha".into(), Some(0), Some(commit_hash.into()))
759
        );
760

761
        let describe = "v1.0.0.alpha-0-g24skp4489";
762
        assert_eq!(
763
            parse_git_describe("v1.0.0.alpha", describe).unwrap(),
764
            ("v1.0.0.alpha".into(), Some(0), Some(commit_hash.into()))
765
        );
766

767
        let describe = "v1.0.0-alpha";
768
        assert_eq!(
769
            parse_git_describe("v1.0.0-alpha", describe).unwrap(),
770
            ("v1.0.0-alpha".into(), None, None)
771
        );
772

773
        let describe = "v1.0.0-alpha-99-0-g24skp4489";
774
        assert_eq!(
775
            parse_git_describe("v1.0.0-alpha-99", describe).unwrap(),
776
            ("v1.0.0-alpha-99".into(), Some(0), Some(commit_hash.into()))
777
        );
778

779
        let describe = "v1.0.0-alpha-99-024skp4489";
780
        assert!(parse_git_describe("v1.0.0-alpha-99", describe).is_err());
781

782
        let describe = "v1.0.0-alpha-024skp4489";
783
        assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
784

785
        let describe = "v1.0.0-alpha-024skp4489";
786
        assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
787

788
        let describe = "v1.0.0-alpha-g024skp4489";
789
        assert!(parse_git_describe("v1.0.0-alpha", describe).is_err());
790

791
        let describe = "v1.0.0----alpha-g024skp4489";
792
        assert!(parse_git_describe("v1.0.0----alpha", describe).is_err());
793
    }
794
}
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