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

baoyachi / shadow-rs / 19549757540

20 Nov 2025 08:03PM UTC coverage: 76.57% (-0.8%) from 77.335%
19549757540

Pull #248

github

web-flow
Merge 26c2ca9d7 into 3536ce9ac
Pull Request #248: Bump actions/checkout from 5 to 6

500 of 653 relevant lines covered (76.57%)

1.19 hits per line

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

75.88
/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) {
2✔
110
        if let Some(val) = self.map.get_mut(c) {
6✔
111
            *val = ConstVal {
2✔
112
                desc: val.desc.clone(),
2✔
113
                v,
2✔
114
                t: ConstType::Str,
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,
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)?;
2✔
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() {
1✔
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 {
1✔
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);
1✔
170
        Ok(())
1✔
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);
2✔
180

181
        let git_info = command_git_head();
2✔
182

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

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

195
        Ok(())
2✔
196
    }
197

198
    #[allow(unused_variables)]
199
    fn init_git2(&mut self, path: &Path) -> SdResult<()> {
2✔
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)?;
2✔
207
            let reference = repo.head().map_err(ShadowError::new)?;
4✔
208

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

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

221
            // use command get last tag
222
            let describe = command_git_describe();
1✔
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 {
4✔
237
                    short_commit = short_commit.get(0..8).unwrap();
2✔
238
                }
239
                self.update_str(SHORT_COMMIT, short_commit.to_string());
4✔
240
            }
241

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

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

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

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

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

266
                self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
2✔
267
            }
268
        }
269
        Ok(())
2✔
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 {
2✔
275
        let mut repo_opts = git2::StatusOptions::new();
2✔
276
        repo_opts.include_ignored(false);
2✔
277
        if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
4✔
278
            let mut dirty_files = Vec::new();
2✔
279
            let mut staged_files = Vec::new();
2✔
280

281
            for status in statue.iter() {
4✔
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>) {
1✔
302
        let mut branch: Option<String> = None;
1✔
303
        let mut tag: Option<String> = None;
1✔
304
        match self.ci_type {
1✔
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") {
2✔
314
                    let ref_branch_prefix: &str = "refs/heads/";
1✔
315
                    let ref_tag_prefix: &str = "refs/tags/";
1✔
316

317
                    if v.starts_with(ref_branch_prefix) {
1✔
318
                        branch = Some(
×
319
                            v.get(ref_branch_prefix.len()..)
×
320
                                .unwrap_or_default()
×
321
                                .to_string(),
×
322
                        )
323
                    } else if v.starts_with(ref_tag_prefix) {
2✔
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 {
1✔
335
            self.update_str(BRANCH, x);
×
336
        }
337

338
        if let Some(x) = tag {
1✔
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.insert(
1✔
361
        COMMITS_SINCE_TAG,
362
        ConstVal::new_usize(COMMITS_SINCE_TAG_DOC),
1✔
363
    );
364

365
    git.map.insert(COMMIT_HASH, ConstVal::new(COMMIT_HASH_DOC));
1✔
366

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

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

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

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

382
    git.map.insert(GIT_CLEAN, ConstVal::new_bool(GIT_CLEAN_DOC));
1✔
383

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

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

391
    git.map
1✔
392
}
393

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

400
    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
2✔
401
        Repository::discover(path)
2✔
402
    }
403

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

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

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

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

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

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

485
struct GitCommandExecutor<'a> {
486
    path: &'a Path,
487
}
488

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

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

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

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

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

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

540
    let tag = last_tag.unwrap();
2✔
541

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

733
        assert_eq!(Some(branch()), command_current_branch());
734
    }
735

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

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

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

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

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

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

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

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

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

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

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

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