Skip to content

Commit e788e3d

Browse files
authored
Adding git stage support (#65)
* adding git stage support * testing for skip false * functions are used in tests
1 parent 886804b commit e788e3d

File tree

4 files changed

+176
-13
lines changed

4 files changed

+176
-13
lines changed

src/cli.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ enum Command {
2626
about = "Generate the CODEOWNERS file and save it to '--codeowners-file-path'.",
2727
visible_alias = "g"
2828
)]
29-
Generate,
29+
Generate {
30+
#[arg(long, short, default_value = "false", help = "Skip staging the CODEOWNERS file")]
31+
skip_stage: bool,
32+
},
3033

3134
#[clap(
3235
about = "Validate the validity of the CODEOWNERS file. A validation failure will exit with a failure code and a detailed output of the validation errors.",
@@ -35,7 +38,10 @@ enum Command {
3538
Validate,
3639

3740
#[clap(about = "Chains both `generate` and `validate` commands.", visible_alias = "gv")]
38-
GenerateAndValidate,
41+
GenerateAndValidate {
42+
#[arg(long, short, default_value = "false", help = "Skip staging the CODEOWNERS file")]
43+
skip_stage: bool,
44+
},
3945

4046
#[clap(about = "Delete the cache file.", visible_alias = "d")]
4147
DeleteCache,
@@ -101,8 +107,8 @@ pub fn cli() -> Result<RunResult, RunnerError> {
101107

102108
let runner_result = match args.command {
103109
Command::Validate => runner::validate(&run_config, vec![]),
104-
Command::Generate => runner::generate(&run_config),
105-
Command::GenerateAndValidate => runner::generate_and_validate(&run_config, vec![]),
110+
Command::Generate { skip_stage } => runner::generate(&run_config, !skip_stage),
111+
Command::GenerateAndValidate { skip_stage } => runner::generate_and_validate(&run_config, vec![], !skip_stage),
106112
Command::ForFile { name, fast } => runner::for_file(&run_config, &name, fast),
107113
Command::ForTeam { name } => runner::for_team(&run_config, &name),
108114
Command::DeleteCache => runner::delete_cache(&run_config),

src/runner.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use core::fmt;
22
use std::{
33
fs::File,
44
path::{Path, PathBuf},
5+
process::Command,
56
};
67

78
use error_stack::{Context, Result, ResultExt};
@@ -91,12 +92,12 @@ pub fn validate(run_config: &RunConfig, _file_paths: Vec<String>) -> RunResult {
9192
run_with_runner(run_config, |runner| runner.validate())
9293
}
9394

94-
pub fn generate(run_config: &RunConfig) -> RunResult {
95-
run_with_runner(run_config, |runner| runner.generate())
95+
pub fn generate(run_config: &RunConfig, git_stage: bool) -> RunResult {
96+
run_with_runner(run_config, |runner| runner.generate(git_stage))
9697
}
9798

98-
pub fn generate_and_validate(run_config: &RunConfig, _file_paths: Vec<String>) -> RunResult {
99-
run_with_runner(run_config, |runner| runner.generate_and_validate())
99+
pub fn generate_and_validate(run_config: &RunConfig, _file_paths: Vec<String>, git_stage: bool) -> RunResult {
100+
run_with_runner(run_config, |runner| runner.generate_and_validate(git_stage))
100101
}
101102

102103
pub fn delete_cache(run_config: &RunConfig) -> RunResult {
@@ -199,29 +200,41 @@ impl Runner {
199200
},
200201
}
201202
}
202-
203-
pub fn generate(&self) -> RunResult {
203+
pub fn generate(&self, git_stage: bool) -> RunResult {
204204
let content = self.ownership.generate_file();
205205
if let Some(parent) = &self.run_config.codeowners_file_path.parent() {
206206
let _ = std::fs::create_dir_all(parent);
207207
}
208208
match std::fs::write(&self.run_config.codeowners_file_path, content) {
209-
Ok(_) => RunResult::default(),
209+
Ok(_) => {
210+
if git_stage {
211+
self.git_stage();
212+
}
213+
RunResult::default()
214+
}
210215
Err(err) => RunResult {
211216
io_errors: vec![err.to_string()],
212217
..Default::default()
213218
},
214219
}
215220
}
216221

217-
pub fn generate_and_validate(&self) -> RunResult {
218-
let run_result = self.generate();
222+
pub fn generate_and_validate(&self, git_stage: bool) -> RunResult {
223+
let run_result = self.generate(git_stage);
219224
if run_result.has_errors() {
220225
return run_result;
221226
}
222227
self.validate()
223228
}
224229

230+
fn git_stage(&self) {
231+
let _ = Command::new("git")
232+
.arg("add")
233+
.arg(&self.run_config.codeowners_file_path)
234+
.current_dir(&self.run_config.project_root)
235+
.output();
236+
}
237+
225238
pub fn for_file(&self, file_path: &str) -> RunResult {
226239
let relative_file_path = Path::new(file_path)
227240
.strip_prefix(&self.run_config.project_root)

tests/common/mod.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
use std::fs;
2+
use std::path::Path;
3+
use std::process::Command;
4+
5+
use codeowners::runner::{self, RunConfig};
6+
use tempfile::TempDir;
27

38
#[allow(dead_code)]
49
pub fn teardown() {
@@ -11,3 +16,98 @@ pub fn teardown() {
1116
}
1217
});
1318
}
19+
20+
#[allow(dead_code)]
21+
pub fn copy_dir_recursive(from: &Path, to: &Path) {
22+
fs::create_dir_all(to).expect("failed to create destination root");
23+
for entry in fs::read_dir(from).expect("failed to read source dir") {
24+
let entry = entry.expect("failed to read dir entry");
25+
let file_type = entry.file_type().expect("failed to read file type");
26+
let src_path = entry.path();
27+
let dest_path = to.join(entry.file_name());
28+
if file_type.is_dir() {
29+
copy_dir_recursive(&src_path, &dest_path);
30+
} else if file_type.is_file() {
31+
if let Some(parent) = dest_path.parent() {
32+
fs::create_dir_all(parent).expect("failed to create parent dir");
33+
}
34+
fs::copy(&src_path, &dest_path).expect("failed to copy file");
35+
}
36+
}
37+
}
38+
39+
#[allow(dead_code)]
40+
pub fn init_git_repo(path: &Path) {
41+
let status = Command::new("git")
42+
.arg("init")
43+
.current_dir(path)
44+
.output()
45+
.expect("failed to run git init");
46+
assert!(
47+
status.status.success(),
48+
"git init failed: {}",
49+
String::from_utf8_lossy(&status.stderr)
50+
);
51+
52+
let _ = Command::new("git")
53+
.arg("config")
54+
.arg("user.email")
55+
56+
.current_dir(path)
57+
.output();
58+
let _ = Command::new("git")
59+
.arg("config")
60+
.arg("user.name")
61+
.arg("Test User")
62+
.current_dir(path)
63+
.output();
64+
}
65+
66+
#[allow(dead_code)]
67+
pub fn is_file_staged(repo_root: &Path, rel_path: &str) -> bool {
68+
let output = Command::new("git")
69+
.arg("diff")
70+
.arg("--name-only")
71+
.arg("--cached")
72+
.current_dir(repo_root)
73+
.output()
74+
.expect("failed to run git diff --cached");
75+
assert!(
76+
output.status.success(),
77+
"git diff failed: {}",
78+
String::from_utf8_lossy(&output.stderr)
79+
);
80+
let stdout = String::from_utf8_lossy(&output.stdout);
81+
stdout.lines().any(|line| line.trim() == rel_path)
82+
}
83+
84+
#[allow(dead_code)]
85+
pub fn build_run_config(project_root: &Path, codeowners_rel_path: &str) -> RunConfig {
86+
let project_root = project_root.canonicalize().expect("failed to canonicalize project root");
87+
let codeowners_file_path = project_root.join(codeowners_rel_path);
88+
let config_path = project_root.join("config/code_ownership.yml");
89+
RunConfig {
90+
project_root,
91+
codeowners_file_path,
92+
config_path,
93+
no_cache: true,
94+
}
95+
}
96+
97+
#[allow(dead_code)]
98+
pub fn setup_fixture_repo(fixture_root: &Path) -> TempDir {
99+
let temp_dir = tempfile::tempdir().expect("failed to create tempdir");
100+
copy_dir_recursive(fixture_root, temp_dir.path());
101+
init_git_repo(temp_dir.path());
102+
temp_dir
103+
}
104+
105+
#[allow(dead_code)]
106+
pub fn assert_no_run_errors(result: &runner::RunResult) {
107+
assert!(result.io_errors.is_empty(), "io_errors: {:?}", result.io_errors);
108+
assert!(
109+
result.validation_errors.is_empty(),
110+
"validation_errors: {:?}",
111+
result.validation_errors
112+
);
113+
}

tests/git_stage_test.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use std::path::Path;
2+
3+
use codeowners::runner::{self, RunConfig};
4+
5+
mod common;
6+
use common::{assert_no_run_errors, build_run_config, is_file_staged, setup_fixture_repo};
7+
8+
#[test]
9+
fn test_generate_stages_codeowners() {
10+
run_and_check(runner::generate, true, true);
11+
}
12+
13+
#[test]
14+
fn test_generate_and_validate_stages_codeowners() {
15+
run_and_check(|rc, s| runner::generate_and_validate(rc, vec![], s), true, true);
16+
}
17+
18+
#[test]
19+
fn test_generate_does_not_stage_codeowners() {
20+
run_and_check(runner::generate, false, false);
21+
}
22+
23+
#[test]
24+
fn test_generate_and_validate_does_not_stage_codeowners() {
25+
run_and_check(|rc, s| runner::generate_and_validate(rc, vec![], s), false, false);
26+
}
27+
28+
const FIXTURE: &str = "tests/fixtures/valid_project";
29+
const CODEOWNERS_REL: &str = ".github/CODEOWNERS";
30+
31+
fn run_and_check<F>(func: F, stage: bool, expected_staged: bool)
32+
where
33+
F: FnOnce(&RunConfig, bool) -> runner::RunResult,
34+
{
35+
let temp_dir = setup_fixture_repo(Path::new(FIXTURE));
36+
let run_config = build_run_config(temp_dir.path(), CODEOWNERS_REL);
37+
38+
let result = func(&run_config, stage);
39+
assert_no_run_errors(&result);
40+
41+
assert!(run_config.codeowners_file_path.exists(), "CODEOWNERS file was not created");
42+
let staged = is_file_staged(&run_config.project_root, CODEOWNERS_REL);
43+
assert_eq!(staged, expected_staged, "unexpected staged state for CODEOWNERS");
44+
}

0 commit comments

Comments
 (0)