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

baoyachi / shadow-rs / 4189880162

pending completion
4189880162

Pull #129

github

GitHub
Merge 8cc63ae9f into 6179d114a
Pull Request #129: Fix clippy

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

547 of 2046 relevant lines covered (26.74%)

0.38 hits per line

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

71.03
/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 SHORT_COMMIT: ShadowConst = "SHORT_COMMIT";
14
pub const COMMIT_HASH: ShadowConst = "COMMIT_HASH";
15
pub const COMMIT_DATE: ShadowConst = "COMMIT_DATE";
16
pub const COMMIT_DATE_2822: ShadowConst = "COMMIT_DATE_2822";
17
pub const COMMIT_DATE_3339: ShadowConst = "COMMIT_DATE_3339";
18
pub const COMMIT_AUTHOR: ShadowConst = "COMMIT_AUTHOR";
19
pub const COMMIT_EMAIL: ShadowConst = "COMMIT_EMAIL";
20
pub const GIT_CLEAN: ShadowConst = "GIT_CLEAN";
21
pub const GIT_STATUS_FILE: ShadowConst = "GIT_STATUS_FILE";
22

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

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

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

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

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

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

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

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

70
        // try use ci branch,tag
71
        self.ci_branch_tag(std_env);
1✔
72
        Ok(())
1✔
73
    }
74

75
    fn init_git2(&mut self, path: &Path) -> SdResult<()> {
1✔
76
        #[cfg(feature = "git2")]
77
        {
78
            use crate::git::git2_mod::git_repo;
79

80
            let repo = git_repo(path).map_err(ShadowError::new)?;
1✔
81
            let reference = repo.head().map_err(ShadowError::new)?;
2✔
82

83
            //get branch
84
            let branch = reference
2✔
85
                .shorthand()
86
                .map(|x| x.trim().to_string())
2✔
87
                .or_else(command_current_branch)
×
88
                .unwrap_or_default();
89

90
            //get HEAD branch
91
            let tag = command_current_tag().unwrap_or_default();
2✔
92

93
            self.update_str(BRANCH, branch);
1✔
94
            self.update_str(TAG, tag);
1✔
95
            if let Some(v) = reference.target() {
1✔
96
                let commit = v.to_string();
1✔
97
                self.update_str(COMMIT_HASH, commit.clone());
2✔
98
                let mut short_commit = commit.as_str();
1✔
99

100
                if commit.len() > 8 {
2✔
101
                    short_commit = short_commit.get(0..8).unwrap();
1✔
102
                }
103
                self.update_str(SHORT_COMMIT, short_commit.to_string());
2✔
104
            }
105

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

108
            let time_stamp = commit.time().seconds().to_string().parse::<i64>()?;
2✔
109
            let date_time = DateTime::timestamp_2_utc(time_stamp);
1✔
110
            self.update_str(COMMIT_DATE, date_time.human_format());
1✔
111

112
            self.update_str(COMMIT_DATE_2822, date_time.to_rfc2822());
1✔
113

114
            self.update_str(COMMIT_DATE_3339, date_time.to_rfc3339());
1✔
115

116
            let author = commit.author();
1✔
117
            if let Some(v) = author.email() {
2✔
118
                self.update_str(COMMIT_EMAIL, v.to_string());
2✔
119
            }
120

121
            if let Some(v) = author.name() {
2✔
122
                self.update_str(COMMIT_AUTHOR, v.to_string());
2✔
123
            }
124
            let status_file = Self::git2_dirty_stage(&repo);
2✔
125
            if status_file.trim().is_empty() {
2✔
126
                self.update_bool(GIT_CLEAN, true);
1✔
127
            } else {
128
                self.update_bool(GIT_CLEAN, false);
×
129
            }
130
            self.update_str(GIT_STATUS_FILE, status_file);
1✔
131
        }
132
        Ok(())
1✔
133
    }
134

135
    //use git2 crates git repository 'dirty or stage' status files.
136
    #[cfg(feature = "git2")]
137
    pub fn git2_dirty_stage(repo: &git2::Repository) -> String {
1✔
138
        let mut repo_opts = git2::StatusOptions::new();
1✔
139
        repo_opts.include_ignored(false);
1✔
140
        if let Ok(statue) = repo.statuses(Some(&mut repo_opts)) {
2✔
141
            let mut dirty_files = Vec::new();
1✔
142
            let mut staged_files = Vec::new();
1✔
143

144
            for status in statue.iter() {
2✔
145
                if let Some(path) = status.path() {
×
146
                    match status.status() {
×
147
                        git2::Status::CURRENT => (),
×
148
                        git2::Status::INDEX_NEW
×
149
                        | git2::Status::INDEX_MODIFIED
×
150
                        | git2::Status::INDEX_DELETED
×
151
                        | git2::Status::INDEX_RENAMED
×
152
                        | git2::Status::INDEX_TYPECHANGE => staged_files.push(path.to_string()),
×
153
                        _ => dirty_files.push(path.to_string()),
×
154
                    };
155
                }
156
            }
157
            filter_git_dirty_stage(dirty_files, staged_files)
1✔
158
        } else {
159
            "".into()
×
160
        }
161
    }
162

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

180
                    if v.starts_with(ref_branch_prefix) {
1✔
181
                        branch = Some(
×
182
                            v.get(ref_branch_prefix.len()..)
×
183
                                .unwrap_or_default()
×
184
                                .to_string(),
×
185
                        )
186
                    } else if v.starts_with(ref_tag_prefix) {
2✔
187
                        tag = Some(
×
188
                            v.get(ref_tag_prefix.len()..)
×
189
                                .unwrap_or_default()
×
190
                                .to_string(),
×
191
                        )
192
                    }
193
                }
194
            }
195
            _ => {}
×
196
        }
197
        if let Some(x) = branch {
1✔
198
            self.update_str(BRANCH, x);
×
199
        }
200

201
        if let Some(x) = tag {
1✔
202
            self.update_str(TAG, x);
×
203
        }
204
    }
205
}
206

207
pub fn new_git(
1✔
208
    path: &Path,
209
    ci: CiType,
210
    std_env: &BTreeMap<String, String>,
211
) -> BTreeMap<ShadowConst, ConstVal> {
212
    let mut git = Git {
213
        map: Default::default(),
1✔
214
        ci_type: ci,
215
    };
216
    git.map
1✔
217
        .insert(BRANCH, ConstVal::new("display current branch"));
2✔
218

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

221
    git.map
1✔
222
        .insert(COMMIT_HASH, ConstVal::new("display current commit_id"));
2✔
223

224
    git.map.insert(
1✔
225
        SHORT_COMMIT,
226
        ConstVal::new("display current short commit_id"),
1✔
227
    );
228

229
    git.map.insert(
1✔
230
        COMMIT_AUTHOR,
231
        ConstVal::new("display current commit author"),
1✔
232
    );
233
    git.map
1✔
234
        .insert(COMMIT_EMAIL, ConstVal::new("display current commit email"));
2✔
235
    git.map
1✔
236
        .insert(COMMIT_DATE, ConstVal::new("display current commit date"));
2✔
237

238
    git.map.insert(
1✔
239
        COMMIT_DATE_2822,
240
        ConstVal::new("display current commit date by rfc2822"),
1✔
241
    );
242

243
    git.map.insert(
1✔
244
        COMMIT_DATE_3339,
245
        ConstVal::new("display current commit date by rfc3339"),
1✔
246
    );
247

248
    git.map.insert(
1✔
249
        GIT_CLEAN,
250
        ConstVal::new_bool("display current git repository status clean:'true or false'"),
1✔
251
    );
252

253
    git.map.insert(
1✔
254
        GIT_STATUS_FILE,
255
        ConstVal::new("display current git repository status files:'dirty or stage'"),
1✔
256
    );
257

258
    if let Err(e) = git.init(path, std_env) {
1✔
259
        println!("{e}");
×
260
    }
261

262
    git.map
1✔
263
}
264

265
#[cfg(feature = "git2")]
266
pub mod git2_mod {
267
    use git2::Error as git2Error;
268
    use git2::Repository;
269
    use std::path::Path;
270

271
    pub fn git_repo<P: AsRef<Path>>(path: P) -> Result<Repository, git2Error> {
1✔
272
        git2::Repository::discover(path)
1✔
273
    }
274

275
    pub fn git2_current_branch(repo: &Repository) -> Option<String> {
×
276
        repo.head()
×
277
            .map(|x| x.shorthand().map(|x| x.to_string()))
×
278
            .unwrap_or(None)
×
279
    }
280
}
281

282
/// get current repository git branch.
283
///
284
/// When current repository exists git folder.
285
///
286
/// It's use default feature.This function try use [git2] crates get current branch.
287
/// If not use git2 feature,then try use [Command] to get.
288
pub fn branch() -> String {
×
289
    #[cfg(feature = "git2")]
290
    {
291
        use crate::git::git2_mod::{git2_current_branch, git_repo};
292
        git_repo(".")
293
            .map(|x| git2_current_branch(&x))
×
294
            .unwrap_or_else(|_| command_current_branch())
×
295
            .unwrap_or_default()
296
    }
297
    #[cfg(not(feature = "git2"))]
298
    {
299
        command_current_branch().unwrap_or_default()
300
    }
301
}
302

303
/// get current repository git tag.
304
///
305
/// When current repository exists git folder.
306
/// I's use [Command] to get.
307
pub fn tag() -> String {
×
308
    command_current_tag().unwrap_or_default()
×
309
}
310

311
/// Check current git Repository status without nothing(dirty or stage)
312
///
313
/// if nothing,It means clean:true. On the contrary, it is 'dirty':false
314
pub fn git_clean() -> bool {
×
315
    #[cfg(feature = "git2")]
316
    {
317
        use crate::git::git2_mod::git_repo;
318
        git_repo(".")
319
            .map(|x| Git::git2_dirty_stage(&x))
×
320
            .map(|x| x.trim().is_empty())
×
321
            .unwrap_or(true)
322
    }
323
    #[cfg(not(feature = "git2"))]
324
    {
325
        command_git_clean()
326
    }
327
}
328

329
/// List current git Repository statue(dirty or stage) contain file changed
330
///
331
/// Refer to the 'cargo fix' result output when git statue(dirty or stage) changed.
332
///
333
/// Example output:`   * examples/builtin_fn.rs (dirty)`
334
pub fn git_status_file() -> String {
×
335
    #[cfg(feature = "git2")]
336
    {
337
        use crate::git::git2_mod::git_repo;
338
        git_repo(".")
339
            .map(|x| Git::git2_dirty_stage(&x))
×
340
            .unwrap_or_default()
341
    }
342
    #[cfg(not(feature = "git2"))]
343
    {
344
        command_git_status_file()
345
    }
346
}
347

348
/// Command exec git current tag
349
fn command_current_tag() -> Option<String> {
1✔
350
    Command::new("git")
351
        .args(["tag", "-l", "--contains", "HEAD"])
1✔
352
        .output()
353
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
354
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
355
        .unwrap_or(None)
1✔
356
}
357

358
/// git clean:git status --porcelain
359
/// check repository git status is clean
360
fn command_git_clean() -> bool {
1✔
361
    Command::new("git")
362
        .args(["status", "--porcelain"])
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
        .map(|x| x.is_none() || x.map(|y| y.is_empty()).unwrap_or_default())
4✔
367
        .unwrap_or(true)
368
}
369

370
/// check git repository 'dirty or stage' status files.
371
/// git dirty:git status  --porcelain | grep '^\sM.' |awk '{print $2}'
372
/// git stage:git status --porcelain --untracked-files=all | grep '^[A|M|D|R]'|awk '{print $2}'
373
fn command_git_status_file() -> String {
1✔
374
    let git_status_files =
2✔
375
        move |args: &[&str], grep: &[&str], awk: &[&str]| -> SdResult<Vec<String>> {
376
            let git_shell = Command::new("git")
3✔
377
                .args(args)
378
                .stdin(Stdio::piped())
1✔
379
                .stdout(Stdio::piped())
1✔
380
                .spawn()?;
1✔
381
            let git_out = git_shell.stdout.ok_or("Failed to exec git stdout")?;
1✔
382

383
            let grep_shell = Command::new("grep")
4✔
384
                .args(grep)
385
                .stdin(Stdio::from(git_out))
1✔
386
                .stdout(Stdio::piped())
1✔
387
                .spawn()?;
1✔
388
            let grep_out = grep_shell.stdout.ok_or("Failed to exec grep stdout")?;
1✔
389

390
            let mut awk_shell = Command::new("awk")
4✔
391
                .args(awk)
392
                .stdin(Stdio::from(grep_out))
1✔
393
                .stdout(Stdio::piped())
1✔
394
                .spawn()?;
1✔
395
            let mut awk_out = BufReader::new(
396
                awk_shell
1✔
397
                    .stdout
398
                    .as_mut()
399
                    .ok_or("Failed to exec awk stdout")?,
×
400
            );
401
            let mut line = String::new();
1✔
402
            awk_out.read_to_string(&mut line)?;
2✔
403
            Ok(line.lines().map(|x| x.into()).collect())
2✔
404
        };
405

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

409
    let stage = git_status_files(
410
        &["status", "--porcelain", "--untracked-files=all"],
411
        &[r#"^[A|M|D|R]"#],
412
        &["{print $2}"],
413
    )
414
    .unwrap_or_default();
415
    filter_git_dirty_stage(dirty, stage)
1✔
416
}
417

418
/// Command exec git current branch
419
fn command_current_branch() -> Option<String> {
1✔
420
    Command::new("git")
421
        .args(["symbolic-ref", "--short", "HEAD"])
1✔
422
        .output()
423
        .map(|x| String::from_utf8(x.stdout).ok())
2✔
424
        .map(|x| x.map(|x| x.trim().to_string()))
4✔
425
        .unwrap_or(None)
1✔
426
}
427

428
fn filter_git_dirty_stage(dirty_files: Vec<String>, staged_files: Vec<String>) -> String {
1✔
429
    let mut concat_file = String::new();
1✔
430
    for file in dirty_files {
2✔
431
        concat_file.push_str("  * ");
×
432
        concat_file.push_str(&file);
×
433
        concat_file.push_str(" (dirty)\n");
×
434
    }
435
    for file in staged_files {
1✔
436
        concat_file.push_str("  * ");
×
437
        concat_file.push_str(&file);
×
438
        concat_file.push_str(" (staged)\n");
×
439
    }
440
    concat_file
441
}
442

443
#[cfg(test)]
444
mod tests {
445
    use super::*;
446
    use crate::get_std_env;
447
    use std::path::Path;
448

449
    #[test]
450
    fn test_git() {
3✔
451
        let env_map = get_std_env();
1✔
452
        let map = new_git(Path::new("./"), CiType::Github, &env_map);
2✔
453
        for (k, v) in map {
2✔
454
            assert!(!v.desc.is_empty());
2✔
455
            if !k.eq(TAG) && !k.eq(BRANCH) && !k.eq(GIT_STATUS_FILE) {
2✔
456
                assert!(!v.v.is_empty());
1✔
457
                continue;
458
            }
459

460
            //assert github tag always exist value
461
            if let Some(github_ref) = env_map.get("GITHUB_REF") {
2✔
462
                if github_ref.starts_with("refs/tags/") && k.eq(TAG) {
2✔
463
                    assert!(!v.v.is_empty(), "not empty");
×
464
                } else if github_ref.starts_with("refs/heads/") && k.eq(BRANCH) {
2✔
465
                    assert!(!v.v.is_empty());
×
466
                }
467
            }
468
        }
469
    }
470

471
    #[test]
472
    fn test_current_branch() {
3✔
473
        if get_std_env().get("GITHUB_REF").is_some() {
1✔
474
            return;
475
        }
476
        #[cfg(feature = "git2")]
477
        {
478
            use crate::git::git2_mod::{git2_current_branch, git_repo};
479
            let git2_branch = git_repo(".")
480
                .map(|x| git2_current_branch(&x))
×
481
                .unwrap_or(None);
×
482
            let command_branch = command_current_branch();
×
483
            assert!(git2_branch.is_some());
×
484
            assert!(command_branch.is_some());
×
485
            assert_eq!(command_branch, git2_branch);
×
486
        }
487

488
        assert_eq!(Some(branch()), command_current_branch());
×
489
    }
490
}
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