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

baoyachi / shadow-rs / 13919813785

18 Mar 2025 09:30AM UTC coverage: 76.024% (-1.5%) from 77.541%
13919813785

push

github

web-flow
Merge pull request #223 from baoyachi/last_tag_verbose

Last tag verbose

35 of 54 new or added lines in 3 files covered. (64.81%)

7 existing lines in 3 files now uncovered.

501 of 659 relevant lines covered (76.02%)

1.48 hits per line

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

76.65
/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) {
3✔
120
        if let Some(val) = self.map.get_mut(c) {
6✔
121
            *val = ConstVal {
3✔
122
                desc: val.desc.clone(),
3✔
123
                v: v.to_string(),
3✔
124
                t: ConstType::Bool,
3✔
125
            }
126
        }
127
    }
128

NEW
129
    fn update_usize(&mut self, c: ShadowConst, v: usize) {
×
NEW
130
        if let Some(val) = self.map.get_mut(c) {
×
NEW
131
            *val = ConstVal {
×
NEW
132
                desc: val.desc.clone(),
×
NEW
133
                v: v.to_string(),
×
NEW
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) {
4✔
150
            self.update_str(BRANCH, x)
2✔
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();
2✔
160
        if let Some(x) = describe.0 {
2✔
161
            self.update_str(LAST_TAG, x)
3✔
162
        }
163

164
        if let Some(x) = describe.1 {
1✔
NEW
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(())
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);
3✔
177

178
        let x = command_git_status_file();
3✔
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);
2✔
185
        self.update_str(SHORT_COMMIT, git_info.short_commit);
1✔
186
        self.update_str(COMMIT_HASH, git_info.commit);
2✔
187

188
        let time_stamp = git_info.date.parse::<i64>()?;
1✔
189
        if let Ok(date_time) = DateTime::timestamp_2_utc(time_stamp) {
3✔
190
            self.update_str(COMMIT_DATE, date_time.human_format());
3✔
191
            self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
2✔
192
            self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
1✔
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)?;
1✔
207
            let reference = repo.head().map_err(ShadowError::new)?;
3✔
208

209
            //get branch
210
            let branch = reference
3✔
211
                .shorthand()
212
                .map(|x| x.trim().to_string())
3✔
213
                .or_else(command_current_branch)
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 {
3✔
224
                self.update_str(LAST_TAG, x)
6✔
225
            }
226

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

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

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

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

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

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

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

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

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

281
            for status in statue.iter() {
6✔
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)
3✔
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") {
4✔
314
                    let ref_branch_prefix: &str = "refs/heads/";
3✔
315
                    let ref_tag_prefix: &str = "refs/tags/";
2✔
316

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

338
        if let Some(x) = tag {
1✔
UNCOV
339
            self.update_str(TAG, x.clone());
×
UNCOV
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
1✔
361
        .insert(COMMITS_SINCE_TAG, ConstVal::new(COMMITS_SINCE_TAG_DOC));
2✔
362

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

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

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

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

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

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

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

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

389
    git.map
1✔
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> {
2✔
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")
7✔
500
            .current_dir(self.path)
1✔
501
            .args(args)
×
502
            .output()
503
            .map(|x| {
1✔
504
                String::from_utf8(x.stdout)
6✔
505
                    .map(|x| x.trim().to_string())
6✔
506
                    .ok()
×
507
            })
508
            .unwrap_or(None)
3✔
509
    }
510
}
511

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

781
        let describe = "v1.0.0-alpha-024skp4489";
782
        assert!(parse_git_describe("v1.0.0-alpha", 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-g024skp4489";
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
}
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