Skip to content

Commit c1baf62

Browse files
Add pre-push subcommand and update dependencies
- Implement `pre-push` subcommand that runs targeted checks on modified crates - Detects changed files since merge-base with origin/main - Identifies affected crates from file paths - Runs clippy, tests, and doc-tests only for affected crates - Provides clear, colorized output for check results - Update dependencies: - fs-err: 3.1.0 -> 3.2.0 - log: 0.4.27 -> 0.4.29 - owo-colors: 4.2.1 -> 4.2.3 - Update GitHub Actions: - actions/checkout: v5 -> v6 - github/codeql-action: v3 -> v4 Resolves facet-rs/facet#1247
1 parent 2fbbd68 commit c1baf62

File tree

4 files changed

+222
-16
lines changed

4 files changed

+222
-16
lines changed

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ rust-version = "1.87"
99
license = "MIT OR Apache-2.0"
1010

1111
[dependencies]
12-
fs-err = "3.1.0"
13-
log = { version = "0.4.27", features = ["std"] }
14-
owo-colors = "4.2.1"
12+
fs-err = "3.2.0"
13+
log = { version = "0.4.29", features = ["std"] }
14+
owo-colors = "4.2.3"
1515

1616
[dev-dependencies]
1717
cargo-husky = { version = "1.5.0", default-features = false, features = [

src/.github/workflows/test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
container:
1717
image: ghcr.io/facet-rs/facet-ci:latest-amd64
1818
steps:
19-
- uses: actions/checkout@v5
19+
- uses: actions/checkout@v6
2020

2121
- uses: Swatinem/rust-cache@v2
2222

@@ -47,7 +47,7 @@ jobs:
4747
container:
4848
image: ghcr.io/facet-rs/facet-ci:latest-amd64
4949
steps:
50-
- uses: actions/checkout@v5
50+
- uses: actions/checkout@v6
5151

5252
- uses: Swatinem/rust-cache@v2
5353

@@ -68,7 +68,7 @@ jobs:
6868
container:
6969
image: ghcr.io/facet-rs/facet-ci:latest-amd64
7070
steps:
71-
- uses: actions/checkout@v5
71+
- uses: actions/checkout@v6
7272

7373
- uses: Swatinem/rust-cache@v2
7474

@@ -92,7 +92,7 @@ jobs:
9292
container:
9393
image: ghcr.io/facet-rs/facet-ci:latest-amd64
9494
steps:
95-
- uses: actions/checkout@v5
95+
- uses: actions/checkout@v6
9696

9797
- uses: Swatinem/rust-cache@v2
9898

@@ -111,7 +111,7 @@ jobs:
111111
container:
112112
image: ghcr.io/facet-rs/facet-ci:latest-amd64
113113
steps:
114-
- uses: actions/checkout@v5
114+
- uses: actions/checkout@v6
115115

116116
- uses: Swatinem/rust-cache@v2
117117

@@ -128,7 +128,7 @@ jobs:
128128
permissions:
129129
security-events: write # to upload sarif results
130130
steps:
131-
- uses: actions/checkout@v5
131+
- uses: actions/checkout@v6
132132

133133
- uses: Swatinem/rust-cache@v2
134134

@@ -145,7 +145,7 @@ jobs:
145145
continue-on-error: true
146146

147147
- name: Upload SARIF results
148-
uses: github/codeql-action/upload-sarif@v3
148+
uses: github/codeql-action/upload-sarif@v4
149149
with:
150150
sarif_file: clippy-results.sarif
151151
wait-for-processing: true

src/main.rs

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,205 @@ fn enqueue_cargo_husky_precommit_hook_jobs(sender: std::sync::mpsc::Sender<Job>)
380380
}
381381
}
382382

383+
fn run_pre_push() {
384+
use std::collections::{BTreeSet, HashSet};
385+
386+
println!("{}", "Running pre-push checks...".cyan().bold());
387+
388+
// Find the merge base with origin/main
389+
let merge_base_output = Command::new("git")
390+
.args(["merge-base", "HEAD", "origin/main"])
391+
.output();
392+
393+
let merge_base = match merge_base_output {
394+
Ok(output) if output.status.success() => {
395+
String::from_utf8_lossy(&output.stdout).trim().to_string()
396+
}
397+
_ => {
398+
warn!("Failed to find merge base with origin/main, using HEAD");
399+
"HEAD".to_string()
400+
}
401+
};
402+
403+
// Get the list of changed files
404+
let diff_output = Command::new("git")
405+
.args(["diff", "--name-only", &format!("{}...HEAD", merge_base)])
406+
.output();
407+
408+
let changed_files = match diff_output {
409+
Ok(output) if output.status.success() => String::from_utf8_lossy(&output.stdout)
410+
.lines()
411+
.map(|s| s.to_string())
412+
.collect::<Vec<_>>(),
413+
Err(e) => {
414+
error!("Failed to get changed files: {}", e);
415+
std::process::exit(1);
416+
}
417+
Ok(output) => {
418+
error!(
419+
"git diff failed: {}",
420+
String::from_utf8_lossy(&output.stderr)
421+
);
422+
std::process::exit(1);
423+
}
424+
};
425+
426+
if changed_files.is_empty() {
427+
println!("{}", "No changes detected".green().bold());
428+
std::process::exit(0);
429+
}
430+
431+
// Find which crates are affected
432+
let mut affected_crates = HashSet::new();
433+
434+
for file in &changed_files {
435+
let path = Path::new(file);
436+
437+
// Find the crate directory by looking for Cargo.toml
438+
let mut current = path;
439+
while let Some(parent) = current.parent() {
440+
let cargo_toml = if parent.as_os_str().is_empty() {
441+
PathBuf::from("Cargo.toml")
442+
} else {
443+
parent.join("Cargo.toml")
444+
};
445+
446+
if cargo_toml.exists() {
447+
// Read Cargo.toml to get the package name
448+
if let Ok(content) = fs::read_to_string(&cargo_toml) {
449+
// Simple parsing: look for [package] section and name field
450+
let mut in_package = false;
451+
for line in content.lines() {
452+
let trimmed = line.trim();
453+
if trimmed == "[package]" {
454+
in_package = true;
455+
} else if trimmed.starts_with('[') {
456+
in_package = false;
457+
} else if in_package && trimmed.starts_with("name") {
458+
if let Some(name_part) = trimmed.split('=').nth(1) {
459+
let name = name_part.trim().trim_matches('"').trim_matches('\'');
460+
affected_crates.insert(name.to_string());
461+
break;
462+
}
463+
}
464+
}
465+
}
466+
break;
467+
}
468+
469+
if parent.as_os_str().is_empty() {
470+
break;
471+
}
472+
current = parent;
473+
}
474+
}
475+
476+
if affected_crates.is_empty() {
477+
println!("{}", "No crates affected by changes".yellow());
478+
std::process::exit(0);
479+
}
480+
481+
// Sort for consistent output
482+
let affected_crates: BTreeSet<_> = affected_crates.into_iter().collect();
483+
484+
println!(
485+
"{} Affected crates: {}",
486+
"🔍".cyan(),
487+
affected_crates
488+
.iter()
489+
.map(|s| s.as_str())
490+
.collect::<Vec<_>>()
491+
.join(", ")
492+
.yellow()
493+
);
494+
495+
let mut all_passed = true;
496+
497+
for crate_name in &affected_crates {
498+
println!(
499+
"\n{} Checking crate: {}",
500+
"📦".cyan(),
501+
crate_name.yellow().bold()
502+
);
503+
504+
// Run clippy
505+
print!(" {} Running clippy... ", "🔍".cyan());
506+
io::stdout().flush().unwrap();
507+
let clippy_status = Command::new("cargo")
508+
.args(["clippy", "-p", crate_name, "--", "-D", "warnings"])
509+
.status();
510+
511+
match clippy_status {
512+
Ok(status) if status.success() => {
513+
println!("{}", "passed".green());
514+
}
515+
_ => {
516+
println!("{}", "failed".red());
517+
all_passed = false;
518+
}
519+
}
520+
521+
// Run tests
522+
print!(" {} Running tests... ", "🧪".cyan());
523+
io::stdout().flush().unwrap();
524+
let test_status = Command::new("cargo")
525+
.args(["test", "-p", crate_name])
526+
.stdout(Stdio::null())
527+
.stderr(Stdio::null())
528+
.status();
529+
530+
match test_status {
531+
Ok(status) if status.success() => {
532+
println!("{}", "passed".green());
533+
}
534+
_ => {
535+
println!("{}", "failed".red());
536+
all_passed = false;
537+
}
538+
}
539+
540+
// Run doc tests
541+
print!(" {} Running doc tests... ", "📚".cyan());
542+
io::stdout().flush().unwrap();
543+
let doctest_status = Command::new("cargo")
544+
.args(["test", "--doc", "-p", crate_name])
545+
.stdout(Stdio::null())
546+
.stderr(Stdio::null())
547+
.status();
548+
549+
match doctest_status {
550+
Ok(status) if status.success() => {
551+
println!("{}", "passed".green());
552+
}
553+
Ok(status) if status.code() == Some(101) => {
554+
// Exit code 101 often means "no tests to run"
555+
println!("{}", "skipped (no lib)".yellow());
556+
}
557+
_ => {
558+
println!("{}", "failed".red());
559+
all_passed = false;
560+
}
561+
}
562+
}
563+
564+
println!();
565+
if all_passed {
566+
println!(
567+
"{} {}",
568+
"✅".green(),
569+
"All pre-push checks passed!".green().bold()
570+
);
571+
std::process::exit(0);
572+
} else {
573+
println!(
574+
"{} {}",
575+
"❌".red(),
576+
"Some pre-push checks failed".red().bold()
577+
);
578+
std::process::exit(1);
579+
}
580+
}
581+
383582
fn show_and_apply_jobs(jobs: &mut [Job]) {
384583
use std::io::{self, Write};
385584

@@ -456,6 +655,13 @@ fn main() {
456655
}
457656
}
458657

658+
// Parse CLI arguments
659+
let args: Vec<String> = std::env::args().collect();
660+
if args.len() > 1 && args[1] == "pre-push" {
661+
run_pre_push();
662+
return;
663+
}
664+
459665
let staged_files = match collect_staged_files() {
460666
Ok(sf) => sf,
461667
Err(e) => {

0 commit comments

Comments
 (0)