Skip to content

Commit 8689278

Browse files
committed
adding git stage support
1 parent 886804b commit 8689278

File tree

3 files changed

+166
-13
lines changed

3 files changed

+166
-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/git_stage_test.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use std::fs;
2+
use std::path::{Path, PathBuf};
3+
use std::process::Command;
4+
5+
use codeowners::runner::{self, RunConfig};
6+
7+
fn copy_dir_recursive(from: &Path, to: &Path) {
8+
fs::create_dir_all(to).expect("failed to create destination root");
9+
for entry in fs::read_dir(from).expect("failed to read source dir") {
10+
let entry = entry.expect("failed to read dir entry");
11+
let file_type = entry.file_type().expect("failed to read file type");
12+
let src_path = entry.path();
13+
let dest_path = to.join(entry.file_name());
14+
if file_type.is_dir() {
15+
copy_dir_recursive(&src_path, &dest_path);
16+
} else if file_type.is_file() {
17+
// Ensure parent exists then copy
18+
if let Some(parent) = dest_path.parent() {
19+
fs::create_dir_all(parent).expect("failed to create parent dir");
20+
}
21+
fs::copy(&src_path, &dest_path).expect("failed to copy file");
22+
}
23+
}
24+
}
25+
26+
fn init_git_repo(path: &Path) {
27+
// Initialize a new git repository in the temp project
28+
let status = Command::new("git")
29+
.arg("init")
30+
.current_dir(path)
31+
.output()
32+
.expect("failed to run git init");
33+
assert!(
34+
status.status.success(),
35+
"git init failed: {}",
36+
String::from_utf8_lossy(&status.stderr)
37+
);
38+
39+
// Configure a dummy identity to appease git if commits ever happen; not strictly needed for staging
40+
let _ = Command::new("git")
41+
.arg("config")
42+
.arg("user.email")
43+
44+
.current_dir(path)
45+
.output();
46+
let _ = Command::new("git")
47+
.arg("config")
48+
.arg("user.name")
49+
.arg("Test User")
50+
.current_dir(path)
51+
.output();
52+
}
53+
54+
fn is_file_staged(repo_root: &Path, rel_path: &str) -> bool {
55+
// Use git diff --name-only --cached to list staged files
56+
let output = Command::new("git")
57+
.arg("diff")
58+
.arg("--name-only")
59+
.arg("--cached")
60+
.current_dir(repo_root)
61+
.output()
62+
.expect("failed to run git diff --cached");
63+
assert!(
64+
output.status.success(),
65+
"git diff failed: {}",
66+
String::from_utf8_lossy(&output.stderr)
67+
);
68+
let stdout = String::from_utf8_lossy(&output.stdout);
69+
stdout.lines().any(|line| line.trim() == rel_path)
70+
}
71+
72+
fn build_run_config(project_root: &Path, codeowners_rel_path: &str) -> RunConfig {
73+
let project_root = project_root.canonicalize().expect("failed to canonicalize project root");
74+
let codeowners_file_path = project_root.join(codeowners_rel_path);
75+
let config_path = project_root.join("config/code_ownership.yml");
76+
RunConfig {
77+
project_root,
78+
codeowners_file_path,
79+
config_path,
80+
no_cache: true,
81+
}
82+
}
83+
84+
#[test]
85+
fn test_generate_stages_codeowners() {
86+
let temp_dir = tempfile::tempdir().expect("failed to create tempdir");
87+
let temp_root = temp_dir.path().to_path_buf();
88+
89+
// Copy the valid project fixture into a temporary git repo
90+
let fixture_root = PathBuf::from("tests/fixtures/valid_project");
91+
copy_dir_recursive(&fixture_root, &temp_root);
92+
init_git_repo(&temp_root);
93+
94+
// Run generate with staging enabled, targeting the standard CODEOWNERS path
95+
let run_config = build_run_config(&temp_root, ".github/CODEOWNERS");
96+
let result = runner::generate(&run_config, true);
97+
assert!(result.io_errors.is_empty(), "io_errors: {:?}", result.io_errors);
98+
assert!(
99+
result.validation_errors.is_empty(),
100+
"validation_errors: {:?}",
101+
result.validation_errors
102+
);
103+
104+
// Assert CODEOWNERS file exists and is staged
105+
let rel_path = ".github/CODEOWNERS";
106+
assert!(run_config.codeowners_file_path.exists(), "CODEOWNERS file was not created");
107+
assert!(is_file_staged(&run_config.project_root, rel_path), "CODEOWNERS file was not staged");
108+
}
109+
110+
#[test]
111+
fn test_generate_and_validate_stages_codeowners() {
112+
let temp_dir = tempfile::tempdir().expect("failed to create tempdir");
113+
let temp_root = temp_dir.path().to_path_buf();
114+
115+
// Copy the valid project fixture into a temporary git repo
116+
let fixture_root = PathBuf::from("tests/fixtures/valid_project");
117+
copy_dir_recursive(&fixture_root, &temp_root);
118+
init_git_repo(&temp_root);
119+
120+
// Run generate_and_validate with staging enabled
121+
let run_config = build_run_config(&temp_root, ".github/CODEOWNERS");
122+
let result = runner::generate_and_validate(&run_config, vec![], true);
123+
assert!(result.io_errors.is_empty(), "io_errors: {:?}", result.io_errors);
124+
assert!(
125+
result.validation_errors.is_empty(),
126+
"validation_errors: {:?}",
127+
result.validation_errors
128+
);
129+
130+
// Assert CODEOWNERS file exists and is staged
131+
let rel_path = ".github/CODEOWNERS";
132+
assert!(run_config.codeowners_file_path.exists(), "CODEOWNERS file was not created");
133+
assert!(is_file_staged(&run_config.project_root, rel_path), "CODEOWNERS file was not staged");
134+
}

0 commit comments

Comments
 (0)