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

baoyachi / shadow-rs / 16680866786

01 Aug 2025 05:04PM UTC coverage: 76.723% (+0.3%) from 76.417%
16680866786

push

github

web-flow
Update Cargo.toml

501 of 653 relevant lines covered (76.72%)

1.12 hits per line

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

77.43
/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) {
1✔
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);
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);
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
2✔
211
                .shorthand()
212
                .map(|x| x.trim().to_string())
2✔
213
                .or_else(command_current_branch)
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);
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);
3✔
253
            if status_file.trim().is_empty() {
2✔
254
                self.update_bool(GIT_CLEAN, true);
3✔
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 {
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();
1✔
279
            let mut staged_files = Vec::new();
1✔
280

281
            for status in statue.iter() {
3✔
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)
1✔
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) {
3✔
324
                        tag = Some(
1✔
325
                            v.get(ref_tag_prefix.len()..)
1✔
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 {
3✔
339
            self.update_str(TAG, x.clone());
2✔
340
            self.update_str(LAST_TAG, x);
1✔
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> {
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")
3✔
500
            .env("GIT_OPTIONAL_LOCKS", "0")
501
            .current_dir(self.path)
1✔
502
            .args(args)
×
503
            .output()
504
            .map(|x| {
1✔
505
                String::from_utf8(x.stdout)
2✔
506
                    .map(|x| x.trim().to_string())
2✔
507
                    .ok()
×
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"]),
1✔
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>) {
1✔
532
    let last_tag =
1✔
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()))?;
3✔
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"])
605
        .map(|x| x.is_empty())
2✔
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")
3✔
616
                .args(args)
617
                .stdin(Stdio::piped())
1✔
618
                .stdout(Stdio::piped())
1✔
619
                .spawn()?;
620
            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
1✔
621

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

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

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

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

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

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

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

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

686
    #[test]
687
    fn test_git() {
688
        let env_map = get_std_env();
689
        let map = new_git(Path::new("./"), CiType::Github, &env_map);
690
        for (k, v) in map {
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

© 2025 Coveralls, Inc