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

baoyachi / shadow-rs / 4283566185

pending completion
4283566185

push

github

GitHub
Merge pull request #131 from baoyachi/LAST_TAG

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

598 of 2500 relevant lines covered (23.92%)

0.34 hits per line

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

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

11
pub const BRANCH: ShadowConst = "BRANCH";
12
pub const TAG: ShadowConst = "TAG";
13
pub const LAST_TAG: ShadowConst = "LAST_TAG";
14
pub const SHORT_COMMIT: ShadowConst = "SHORT_COMMIT";
15
pub const COMMIT_HASH: ShadowConst = "COMMIT_HASH";
16
pub const COMMIT_DATE: ShadowConst = "COMMIT_DATE";
17
pub const COMMIT_DATE_2822: ShadowConst = "COMMIT_DATE_2822";
18
pub const COMMIT_DATE_3339: ShadowConst = "COMMIT_DATE_3339";
19
pub const COMMIT_AUTHOR: ShadowConst = "COMMIT_AUTHOR";
20
pub const COMMIT_EMAIL: ShadowConst = "COMMIT_EMAIL";
21
pub const GIT_CLEAN: ShadowConst = "GIT_CLEAN";
22
pub const GIT_STATUS_FILE: ShadowConst = "GIT_STATUS_FILE";
23

24
#[derive(Default, Debug)]
25
pub struct Git {
26
    map: BTreeMap<ShadowConst, ConstVal>,
27
    ci_type: CiType,
28
}
29

30
impl Git {
31
    fn update_str(&mut self, c: ShadowConst, v: String) {
1✔
32
        if let Some(val) = self.map.get_mut(c) {
3✔
33
            *val = ConstVal {
1✔
34
                desc: val.desc.clone(),
1✔
35
                v,
1✔
36
                t: ConstType::Str,
×
37
            }
38
        }
39
    }
40

41
    fn update_bool(&mut self, c: ShadowConst, v: bool) {
1✔
42
        if let Some(val) = self.map.get_mut(c) {
2✔
43
            *val = ConstVal {
1✔
44
                desc: val.desc.clone(),
1✔
45
                v: v.to_string(),
1✔
46
                t: ConstType::Bool,
×
47
            }
48
        }
49
    }
50

51
    fn init(&mut self, path: &Path, std_env: &BTreeMap<String, String>) -> SdResult<()> {
1✔
52
        // check git status
53
        let x = command_git_clean();
1✔
54
        self.update_bool(GIT_CLEAN, x);
1✔
55

56
        let x = command_git_status_file();
1✔
57
        self.update_str(GIT_STATUS_FILE, x);
1✔
58

59
        self.init_git2(path)?;
1✔
60

61
        // use command branch
62
        if let Some(x) = command_current_branch() {
2✔
63
            self.update_str(BRANCH, x)
1✔
64
        };
65

66
        // use command tag
67
        if let Some(x) = command_current_tag() {
2✔
68
            self.update_str(TAG, x)
1✔
69
        }
70

71
        // use command get last tag
72
        if let Some(x) = command_last_tag() {
2✔
73
            self.update_str(LAST_TAG, x)
1✔
74
        }
75

76
        // try use ci branch,tag
77
        self.ci_branch_tag(std_env);
1✔
78
        Ok(())
1✔
79
    }
80

81
    fn init_git2(&mut self, path: &Path) -> SdResult<()> {
2✔
82
        #[cfg(feature = "git2")]
83
        {
84
            use crate::git::git2_mod::git_repo;
85

86
            let repo = git_repo(path).map_err(ShadowError::new)?;
1✔
87
            let reference = repo.head().map_err(ShadowError::new)?;
2✔
88

89
            //get branch
90
            let branch = reference
2✔
91
                .shorthand()
92
                .map(|x| x.trim().to_string())
2✔
93
                .or_else(command_current_branch)
×
94
                .unwrap_or_default();
95

96
            //get HEAD branch
97
            let tag = command_current_tag().unwrap_or_default();
2✔
98
            let last_tag = command_last_tag().unwrap_or_default();
2✔
99
            self.update_str(BRANCH, branch);
1✔
100
            self.update_str(TAG, tag);
1✔
101
            self.update_str(LAST_TAG, last_tag);
1✔
102

103
            if let Some(v) = reference.target() {
1✔
104
                let commit = v.to_string();
1✔
105
                self.update_str(COMMIT_HASH, commit.clone());
2✔
106
                let mut short_commit = commit.as_str();
1✔
107

108
                if commit.len() > 8 {
2✔
109
                    short_commit = short_commit.get(0..8).unwrap();
1✔
110
                }
111
                self.update_str(SHORT_COMMIT, short_commit.to_string());
2✔
112
            }
113

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

116
            let time_stamp = commit.time().seconds().to_string().parse::<i64>()?;
2✔
117
            let date_time = DateTime::timestamp_2_utc(time_stamp);
1✔
118
            self.update_str(COMMIT_DATE, date_time.human_format());
1✔
119

120
            self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
1✔
121

122
            self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
1✔
123

124
            let author = commit.author();
1✔
125
            if let Some(v) = author.email() {
2✔
126
                self.update_str(COMMIT_EMAIL, v.to_string());
2✔
127
            }
128

129
            if let Some(v) = author.name() {
2✔
130
                self.update_str(COMMIT_AUTHOR, v.to_string());
2✔
131
            }
132
            let status_file = Self::git2_dirty_stage(&repo);
2✔
133
            if status_file.trim().is_empty() {
2✔
134
                self.update_bool(GIT_CLEAN, true);
1✔
135
            } else {
136
                self.update_bool(GIT_CLEAN, false);
×
137
            }
138
            self.update_str(GIT_STATUS_FILE, status_file);
1✔
139
        }
140
        Ok(())
1✔
141
    }
142

143
    //use git2 crates git repository 'dirty or stage' status files.
144
    #[cfg(feature = "git2")]
145
    pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
1✔
146
        let mut repo_opts = git2::StatusOptions::new();
1✔
147
        repo_opts.include_ignored(false);
1✔
148
        if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
2✔
149
            let mut dirty_files = Vec::new();
1✔
150
            let mut staged_files = Vec::new();
1✔
151

152
            for status in statue.iter() {
2✔
153
                if let Some(path) = status.path() {
×
154
                    match status.status() {
×
155
                        git2::Status::CURRENT => (),
×
156
                        git2::Status::INDEX_NEW
×
157
                        | git2::Status::INDEX_MODIFIED
×
158
                        | git2::Status::INDEX_DELETED
×
159
                        | git2::Status::INDEX_RENAMED
×
160
                        | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
×
161
                        _ => dirty_files.push(path.to_string()),
×
162
                    };
163
                }
164
            }
165
            filter_git_dirty_stage(dirty_files, staged_files)
1✔
166
        } else {
167
            "".into()
×
168
        }
169
    }
170

171
    #[allow(clippy::manual_strip)]
172
    fn ci_branch_tag(&mut self, std_env: &BTreeMap<String, String>) {
1✔
173
        let mut branch: Option<String> = None;
1✔
174
        let mut tag: Option<String> = None;
1✔
175
        match self.ci_type {
1✔
176
            CiType::Gitlab => {
×
177
                if let Some(v) = std_env.get("CI_COMMIT_TAG") {
×
178
                    tag = Some(v.to_string());
×
179
                } else if let Some(v) = std_env.get("CI_COMMIT_REF_NAME") {
×
180
                    branch = Some(v.to_string());
×
181
                }
182
            }
183
            CiType::Github => {
×
184
                if let Some(v) = std_env.get("GITHUB_REF") {
2✔
185
                    let ref_branch_prefix: &str = "refs/heads/";
1✔
186
                    let ref_tag_prefix: &str = "refs/tags/";
1✔
187

188
                    if v.starts_with(ref_branch_prefix) {
2✔
189
                        branch = Some(
1✔
190
                            v.get(ref_branch_prefix.len()..)
1✔
191
                                .unwrap_or_default()
×
192
                                .to_string(),
×
193
                        )
194
                    } else if v.starts_with(ref_tag_prefix) {
×
195
                        tag = Some(
×
196
                            v.get(ref_tag_prefix.len()..)
×
197
                                .unwrap_or_default()
×
198
                                .to_string(),
×
199
                        )
200
                    }
201
                }
202
            }
203
            _ => {}
×
204
        }
205
        if let Some(x) = branch {
2✔
206
            self.update_str(BRANCH, x);
2✔
207
        }
208

209
        if let Some(x) = tag {
1✔
210
            self.update_str(TAG, x.clone());
×
211
            self.update_str(LAST_TAG, x);
×
212
        }
213
    }
214
}
215

216
pub fn new_git(
1✔
217
    path: &Path,
218
    ci: CiType,
219
    std_env: &BTreeMap<String, String>,
220
) -> BTreeMap<ShadowConst, ConstVal> {
221
    let mut git = Git {
222
        map: Default::default(),
1✔
223
        ci_type: ci,
224
    };
225
    git.map
1✔
226
        .insert(BRANCH, ConstVal::new("display current branch"));
2✔
227

228
    git.map.insert(TAG, ConstVal::new("display current tag"));
1✔
229

230
    git.map.insert(LAST_TAG, ConstVal::new("display last tag"));
1✔
231

232
    git.map
1✔
233
        .insert(COMMIT_HASH, ConstVal::new("display current commit_id"));
2✔
234

235
    git.map.insert(
1✔
236
        SHORT_COMMIT,
237
        ConstVal::new("display current short commit_id"),
1✔
238
    );
239

240
    git.map.insert(
1✔
241
        COMMIT_AUTHOR,
242
        ConstVal::new("display current commit author"),
1✔
243
    );
244
    git.map
1✔
245
        .insert(COMMIT_EMAIL, ConstVal::new("display current commit email"));
2✔
246
    git.map
1✔
247
        .insert(COMMIT_DATE, ConstVal::new("display current commit date"));
2✔
248

249
    git.map.insert(
1✔
250
        COMMIT_DATE_2822,
251
        ConstVal::new("display current commit date by rfc2822"),
1✔
252
    );
253

254
    git.map.insert(
1✔
255
        COMMIT_DATE_3339,
256
        ConstVal::new("display current commit date by rfc3339"),
1✔
257
    );
258

259
    git.map.insert(
1✔
260
        GIT_CLEAN,
261
        ConstVal::new_bool("display current git repository status clean:'true or false'"),
1✔
262
    );
263

264
    git.map.insert(
1✔
265
        GIT_STATUS_FILE,
266
        ConstVal::new("display current git repository status files:'dirty or stage'"),
1✔
267
    );
268

269
    if let Err(e) = git.init(path, std_env) {
1✔
270
        println!("{e}");
×
271
    }
272

273
    git.map
1✔
274
}
275

276
#[cfg(feature = "git2")]
277
pub mod git2_mod {
278
    use git2::Error as git2Error;
279
    use git2::Repository;
280
    use std::path::Path;
281

282
    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
1✔
283
        git2::Repository::discover(path)
1✔
284
    }
285

286
    pub fn git2_current_branch(repo: &Repository) -> Option<String> {
×
287
        repo.head()
×
288
            .map(|x| x.shorthand().map(|x| x.to_string()))
×
289
            .unwrap_or(None)
×
290
    }
291
}
292

293
/// get current repository git branch.
294
///
295
/// When current repository exists git folder.
296
///
297
/// It's use default feature.This function try use [git2] crates get current branch.
298
/// If not use git2 feature,then try use [Command] to get.
299
pub fn branch() -> String {
×
300
    #[cfg(feature = "git2")]
301
    {
302
        use crate::git::git2_mod::{git2_current_branch, git_repo};
303
        git_repo(".")
304
            .map(|x| git2_current_branch(&x))
×
305
            .unwrap_or_else(|_| command_current_branch())
×
306
            .unwrap_or_default()
307
    }
308
    #[cfg(not(feature = "git2"))]
309
    {
310
        command_current_branch().unwrap_or_default()
311
    }
312
}
313

314
/// get current repository git tag.
315
///
316
/// When current repository exists git folder.
317
/// I's use [Command] to get.
318
pub fn tag() -> String {
×
319
    command_current_tag().unwrap_or_default()
×
320
}
321

322
/// Check current git Repository status without nothing(dirty or stage)
323
///
324
/// if nothing,It means clean:true. On the contrary, it is 'dirty':false
325
pub fn git_clean() -> bool {
×
326
    #[cfg(feature = "git2")]
327
    {
328
        use crate::git::git2_mod::git_repo;
329
        git_repo(".")
330
            .map(|x| Git::git2_dirty_stage(&x))
×
331
            .map(|x| x.trim().is_empty())
×
332
            .unwrap_or(true)
333
    }
334
    #[cfg(not(feature = "git2"))]
335
    {
336
        command_git_clean()
337
    }
338
}
339

340
/// List current git Repository statue(dirty or stage) contain file changed
341
///
342
/// Refer to the 'cargo fix' result output when git statue(dirty or stage) changed.
343
///
344
/// Example output:`   * examples/builtin_fn.rs (dirty)`
345
pub fn git_status_file() -> String {
×
346
    #[cfg(feature = "git2")]
347
    {
348
        use crate::git::git2_mod::git_repo;
349
        git_repo(".")
350
            .map(|x| Git::git2_dirty_stage(&x))
×
351
            .unwrap_or_default()
352
    }
353
    #[cfg(not(feature = "git2"))]
354
    {
355
        command_git_status_file()
356
    }
357
}
358

359
/// Command exec git current tag
360
fn command_current_tag() -> Option<String> {
1✔
361
    Command::new("git")
362
        .args(["tag", "-l", "--contains", "HEAD"])
1✔
363
        .output()
364
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
365
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
366
        .unwrap_or(None)
1✔
367
}
368

369
/// git describe --tags --abbrev=0 HEAD
370
/// Command exec git last tag
371
fn command_last_tag() -> Option<String> {
1✔
372
    Command::new("git")
373
        .args(["describe", "--tags", "--abbrev=0", "HEAD"])
1✔
374
        .output()
375
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
376
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
377
        .unwrap_or(None)
1✔
378
}
379

380
/// git clean:git status --porcelain
381
/// check repository git status is clean
382
fn command_git_clean() -> bool {
1✔
383
    Command::new("git")
384
        .args(["status", "--porcelain"])
1✔
385
        .output()
386
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
387
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
388
        .map(|x| x.is_none() || x.map(|y| y.is_empty()).unwrap_or_default())
4✔
389
        .unwrap_or(true)
390
}
391

392
/// check git repository 'dirty or stage' status files.
393
/// git dirty:git status  --porcelain | grep '^\sM.' |awk '{print $2}'
394
/// git stage:git status --porcelain --untracked-files=all | grep '^[A|M|D|R]'|awk '{print $2}'
395
fn command_git_status_file() -> String {
1✔
396
    let git_status_files =
2✔
397
        move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
398
            let git_shell = Command::new("git")
3✔
399
                .args(args)
400
                .stdin(Stdio::piped())
1✔
401
                .stdout(Stdio::piped())
1✔
402
                .spawn()?;
1✔
403
            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
1✔
404

405
            let grep_shell = Command::new("grep")
4✔
406
                .args(grep)
407
                .stdin(Stdio::from(git_out))
1✔
408
                .stdout(Stdio::piped())
1✔
409
                .spawn()?;
1✔
410
            let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
1✔
411

412
            let mut awk_shell = Command::new("awk")
4✔
413
                .args(awk)
414
                .stdin(Stdio::from(grep_out))
1✔
415
                .stdout(Stdio::piped())
1✔
416
                .spawn()?;
1✔
417
            let mut awk_out = BufReader::new(
418
                awk_shell
1✔
419
                    .stdout
420
                    .as_mut()
421
                    .ok_or("Failed to exec awk stdout")?,
×
422
            );
423
            let mut line = String::new();
1✔
424
            awk_out.read_to_string(&mut line)?;
2✔
425
            Ok(line.lines().map(|x| x.into()).collect())
2✔
426
        };
427

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

431
    let stage = git_status_files(
432
        &["status", "--porcelain", "--untracked-files=all"],
433
        &[r#"^[A|M|D|R]"#],
434
        &["{print $2}"],
435
    )
436
    .unwrap_or_default();
437
    filter_git_dirty_stage(dirty, stage)
1✔
438
}
439

440
/// Command exec git current branch
441
fn command_current_branch() -> Option<String> {
1✔
442
    Command::new("git")
443
        .args(["symbolic-ref", "--short", "HEAD"])
1✔
444
        .output()
445
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
446
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
447
        .unwrap_or(None)
1✔
448
}
449

450
fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
1✔
451
    let mut concat_file = String::new();
1✔
452
    for file in dirty_files {
2✔
453
        concat_file.push_str("  * ");
×
454
        concat_file.push_str(&file);
×
455
        concat_file.push_str(" (dirty)\n");
×
456
    }
457
    for file in staged_files {
1✔
458
        concat_file.push_str("  * ");
×
459
        concat_file.push_str(&file);
×
460
        concat_file.push_str(" (staged)\n");
×
461
    }
462
    concat_file
463
}
464

465
#[cfg(test)]
466
mod tests {
467
    use super::*;
468
    use crate::get_std_env;
469
    use std::path::Path;
470

471
    #[test]
472
    fn test_git() {
4✔
473
        let env_map = get_std_env();
1✔
474
        let map = new_git(Path::new("./"), CiType::Github, &env_map);
2✔
475
        for (k, v) in map {
2✔
476
            println!("k:{},v:{:?}", k, v);
2✔
477
            assert!(!v.desc.is_empty());
1✔
478
            if !k.eq(TAG) && !k.eq(LAST_TAG) && !k.eq(BRANCH) && !k.eq(GIT_STATUS_FILE) {
2✔
479
                assert!(!v.v.is_empty());
1✔
480
                continue;
481
            }
482

483
            //assert github tag always exist value
484
            if let Some(github_ref) = env_map.get("GITHUB_REF") {
2✔
485
                if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
2✔
486
                    assert!(!v.v.is_empty(), "not empty");
×
487
                } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
2✔
488
                    assert!(!v.v.is_empty());
1✔
489
                }
490
            }
491
        }
492
    }
493

494
    #[test]
495
    fn test_current_branch() {
3✔
496
        if get_std_env().get("GITHUB_REF").is_some() {
1✔
497
            return;
498
        }
499
        #[cfg(feature = "git2")]
500
        {
501
            use crate::git::git2_mod::{git2_current_branch, git_repo};
502
            let git2_branch = git_repo(".")
503
                .map(|x| git2_current_branch(&x))
×
504
                .unwrap_or(None);
×
505
            let command_branch = command_current_branch();
×
506
            assert!(git2_branch.is_some());
×
507
            assert!(command_branch.is_some());
×
508
            assert_eq!(command_branch, git2_branch);
×
509
        }
510

511
        assert_eq!(Some(branch()), command_current_branch());
×
512
    }
513

514
    #[test]
515
    fn test_command_last_tag() {
3✔
516
        let opt_last_tag = command_last_tag();
1✔
517
        assert!(opt_last_tag.is_some())
2✔
518
    }
519
}
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