Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 16 additions & 30 deletions crates/git-tidy-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use std::path::PathBuf;

use serde::Serialize;

use crate::counts::Counts;
use crate::output::IntoJsonItem;
use crate::scan::Classified;

/// Shared interface for classification enums across all git-tidy tools.
pub trait ClassificationLabel {
Expand Down Expand Up @@ -187,15 +188,10 @@ pub struct WorktreeInfo {
pub meaningful_dirty_files: Vec<String>,
}

/// A group of worktrees sharing the same parent repo.
#[derive(Debug, Clone, Serialize)]
pub struct RepoGroup {
/// Path to the parent repo.
pub repo_path: PathBuf,
/// Display name (directory basename).
pub name: String,
/// Worktrees belonging to this repo, sorted by classification priority.
pub worktrees: Vec<WorktreeInfo>,
impl Classified for WorktreeInfo {
fn classification_label(&self) -> &str {
self.classification.label()
}
}

/// Flat JSON representation of a worktree matching the spec.
Expand Down Expand Up @@ -244,31 +240,21 @@ impl From<&WorktreeInfo> for JsonWorktree {
}
}

/// Result of a full scan operation.
#[derive(Debug, Clone, Serialize)]
pub struct ScanResult {
/// Worktrees grouped by parent repo.
pub repos: Vec<RepoGroup>,
/// Total worktrees scanned.
pub total_scanned: usize,
/// Summary counts by classification.
pub counts: Counts,
/// Repos that were skipped (e.g. no default branch).
pub warnings: Vec<String>,
}

impl crate::output::FlatJsonItems for ScanResult {
impl IntoJsonItem for WorktreeInfo {
type JsonItem = JsonWorktree;

fn to_json_items(&self) -> Vec<JsonWorktree> {
self.repos
.iter()
.flat_map(|g| g.worktrees.iter())
.map(JsonWorktree::from)
.collect()
fn to_json_item(&self) -> JsonWorktree {
JsonWorktree::from(self)
}
}

/// Result of a full worktree scan.
///
/// Worktrees are grouped per parent repo in `repos` as `RepoGroup<WorktreeInfo>`
/// (the generic core group type); each group's `items` are sorted by
/// classification priority.
pub type WorktreeScanResult = crate::scan::ScanResult<WorktreeInfo>;

#[cfg(test)]
mod tests {
use super::*;
Expand Down
17 changes: 10 additions & 7 deletions crates/git-worktree-tidy/src/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::path::PathBuf;

use git_tidy_core::error::Error;
use git_tidy_core::git::GitOps;
use git_tidy_core::types::{Classification, CleanResult, FailedItem, ScanResult, WorktreeInfo};
use git_tidy_core::types::{
Classification, CleanResult, FailedItem, WorktreeInfo, WorktreeScanResult,
};

/// Options controlling worktree cleanup behavior.
pub struct CleanOptions {
Expand Down Expand Up @@ -32,7 +34,7 @@ pub struct RemovedWorktree {
/// Run the clean operation on a scan result.
pub fn run_clean(
git: &dyn GitOps,
scan_result: &ScanResult,
scan_result: &WorktreeScanResult,
options: &CleanOptions,
out: &mut dyn Write,
) -> Result<CleanResult<RemovedWorktree>, Error> {
Expand All @@ -41,7 +43,7 @@ pub fn run_clean(
let mut skipped = 0;

for group in &scan_result.repos {
for wt in &group.worktrees {
for wt in &group.items {
// Filter by classification
if !should_clean(&wt.classification, options) {
skipped += 1;
Expand Down Expand Up @@ -230,8 +232,9 @@ mod tests {
use std::path::PathBuf;

use git_tidy_core::counts::Counts;
use git_tidy_core::scan::RepoGroup;
use git_tidy_core::testutil::MockGitBuilder;
use git_tidy_core::types::{Annotations, ClassificationLabel, RepoGroup, WorktreeInfo};
use git_tidy_core::types::{Annotations, ClassificationLabel, WorktreeInfo};

use super::*;

Expand All @@ -255,17 +258,17 @@ mod tests {
}
}

fn make_scan(worktrees: Vec<WorktreeInfo>) -> ScanResult {
fn make_scan(worktrees: Vec<WorktreeInfo>) -> WorktreeScanResult {
let mut counts = Counts::default();
for wt in &worktrees {
counts.increment(wt.classification.label());
}
let total = worktrees.len();
ScanResult {
WorktreeScanResult {
repos: vec![RepoGroup {
repo_path: repo(),
name: "test".to_string(),
worktrees,
items: worktrees,
}],
total_scanned: total,
counts,
Expand Down
32 changes: 14 additions & 18 deletions crates/git-worktree-tidy/src/output.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
use std::io::Write;

use git_tidy_core::output as shared;
use git_tidy_core::types::ScanResult;
use git_tidy_core::types::WorktreeScanResult;

/// Write human-readable scan output.
///
/// Per-group: heading + `format_table` over the group's `WorktreeInfo`s, which
/// reads `TidyItem for WorktreeInfo` defined in `git_tidy_core::output`.
pub fn write_human(out: &mut dyn Write, result: &ScanResult) -> std::io::Result<()> {
pub fn write_human(out: &mut dyn Write, result: &WorktreeScanResult) -> std::io::Result<()> {
shared::write_warnings(out, &result.warnings)?;

for group in &result.repos {
writeln!(
out,
"\n{} ({} worktrees)",
group.name,
group.worktrees.len()
)?;
shared::format_table(out, &group.worktrees)?;
writeln!(out, "\n{} ({} worktrees)", group.name, group.items.len())?;
shared::format_table(out, &group.items)?;
}

shared::write_summary_line(
Expand All @@ -33,14 +28,14 @@ pub fn write_human(out: &mut dyn Write, result: &ScanResult) -> std::io::Result<
}

/// Write JSON scan output using the flat spec format.
pub fn write_json(out: &mut dyn Write, result: &ScanResult) -> std::io::Result<()> {
pub fn write_json(out: &mut dyn Write, result: &WorktreeScanResult) -> std::io::Result<()> {
shared::write_json_flat(out, result)
}

/// Write porcelain (machine-readable, tab-delimited) scan output.
pub fn write_porcelain(out: &mut dyn Write, result: &ScanResult) -> std::io::Result<()> {
pub fn write_porcelain(out: &mut dyn Write, result: &WorktreeScanResult) -> std::io::Result<()> {
for group in &result.repos {
shared::format_porcelain(out, &group.worktrees)?;
shared::format_porcelain(out, &group.items)?;
}
Ok(())
}
Expand All @@ -51,14 +46,15 @@ mod tests {

use super::*;
use git_tidy_core::counts::Counts;
use git_tidy_core::scan::RepoGroup;
use git_tidy_core::types::*;

fn make_scan_result() -> ScanResult {
ScanResult {
fn make_scan_result() -> WorktreeScanResult {
WorktreeScanResult {
repos: vec![RepoGroup {
repo_path: PathBuf::from("/repos/Backend"),
name: "Backend".to_string(),
worktrees: vec![
items: vec![
WorktreeInfo {
path: PathBuf::from("/dev/Backend-parallel"),
parent_repo: PathBuf::from("/repos/Backend"),
Expand Down Expand Up @@ -113,11 +109,11 @@ mod tests {

#[test]
fn write_human_with_partial_includes_unmatched_extras() {
let result = ScanResult {
let result = WorktreeScanResult {
repos: vec![RepoGroup {
repo_path: PathBuf::from("/repos/App"),
name: "App".to_string(),
worktrees: vec![WorktreeInfo {
items: vec![WorktreeInfo {
path: PathBuf::from("/dev/App-theme"),
parent_repo: PathBuf::from("/repos/App"),
branch: Some("alternate-icons".to_string()),
Expand Down Expand Up @@ -166,7 +162,7 @@ mod tests {

#[test]
fn write_human_with_warnings() {
let result = ScanResult {
let result = WorktreeScanResult {
repos: vec![],
total_scanned: 0,
counts: Counts::default(),
Expand Down
88 changes: 44 additions & 44 deletions crates/git-worktree-tidy/src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use git_tidy_core::classification;
use git_tidy_core::counts::Counts;
use git_tidy_core::discovery;
use git_tidy_core::error::Error;
use git_tidy_core::filter::{NameFilter, filter_paths};
use git_tidy_core::git::GitOps;
use git_tidy_core::output::repo_display_name;
use git_tidy_core::progress::Progress;
use git_tidy_core::scan::parallel_classify;
use git_tidy_core::types::{ClassificationLabel, RepoGroup, ScanResult};
use git_tidy_core::scan::{ScanOptions, run_pipeline};
use git_tidy_core::types::{ClassificationLabel, WorktreeScanResult};

use crate::discovery::{self as wt_discovery, DiscoveredWorktree};

Expand Down Expand Up @@ -62,7 +61,7 @@ pub fn run_scan(
entity_filter: &NameFilter,
repo_filter: &NameFilter,
progress: &Progress,
) -> Result<ScanResult, Error> {
) -> Result<WorktreeScanResult, Error> {
let repo_paths = discovery::discover_repos(directory)?;
let repo_paths = filter_paths(repo_paths, repo_filter);
let groups = wt_discovery::discover_worktrees(git, &repo_paths);
Expand All @@ -81,28 +80,33 @@ pub fn run_scan(
/// Classify pre-discovered worktree groups.
///
/// Accepts a `BTreeMap` of parent-repo to worktrees (as returned by
/// [`discovery::discover_worktrees`] and optional filtering), fetches each
/// repo, classifies every worktree, and returns a [`ScanResult`].
/// [`discovery::discover_worktrees`] and optional filtering) and runs the shared
/// [`run_pipeline`] seam over the parent repos with fetch enabled. The per-repo
/// closure looks each repo's worktrees up from the captured map and classifies
/// them; the seam fetches every repo first and aggregates the
/// [`WorktreeScanResult`].
pub fn run_scan_repos(
git: &dyn GitOps,
groups: BTreeMap<PathBuf, Vec<DiscoveredWorktree>>,
behind_threshold: usize,
verbose: bool,
noise_patterns: &[String],
progress: &Progress,
) -> Result<ScanResult, Error> {
) -> Result<WorktreeScanResult, Error> {
let repo_paths: Vec<PathBuf> = groups.keys().cloned().collect();
let fetch_paths: Vec<&Path> = repo_paths.iter().map(|p| p.as_path()).collect();
let mut warnings = git_tidy_core::fetch::parallel_fetch(git, &fetch_paths, progress);

let (repos, scan_warnings) = parallel_classify(
let result = run_pipeline(
git,
&repo_paths,
&ScanOptions { fetch: true },
"Scanning worktrees",
progress,
|repo_path| {
let mut local_warnings = Vec::new();

let worktrees = match groups.get(repo_path) {
Some(wts) => wts,
None => return (None, vec![]),
None => return (Vec::new(), vec![]),
};

let default_branch = match classification::detect_default_branch(git, repo_path) {
Expand All @@ -112,15 +116,14 @@ pub fn run_scan_repos(
"could not determine default branch for {} -- skipping",
repo_path.display()
));
return (None, local_warnings);
return (Vec::new(), local_warnings);
}
};

let repo_name = repo_display_name(repo_path);

if verbose {
eprintln!(
"{repo_name}: {} worktrees (default_branch={default_branch})",
"{}: {} worktrees (default_branch={default_branch})",
repo_display_name(repo_path),
worktrees.len(),
);
}
Expand Down Expand Up @@ -159,38 +162,11 @@ pub fn run_scan_repos(

classified.sort_by_key(|wt| wt.classification.priority());

let group = if classified.is_empty() {
None
} else {
Some(RepoGroup {
repo_path: repo_path.to_path_buf(),
name: repo_name,
worktrees: classified,
})
};

(group, local_warnings)
(classified, local_warnings)
},
"Scanning worktrees",
progress,
);
warnings.extend(scan_warnings);

let mut counts = Counts::default();
let mut total_scanned = 0;
for g in &repos {
for wt in &g.worktrees {
counts.increment(wt.classification.label());
}
total_scanned += g.worktrees.len();
}

Ok(ScanResult {
repos,
total_scanned,
counts,
warnings,
})
Ok(result)
}

#[cfg(test)]
Expand Down Expand Up @@ -334,4 +310,28 @@ mod tests {
assert!(result.repos.is_empty());
assert!(result.warnings.is_empty());
}

#[test]
fn run_scan_repos_drops_repo_when_default_branch_errors() {
// Confirms preserved behavior across the run_pipeline migration: a repo
// present in the map with a worktree but whose default-branch detection
// fails yields no group. The closure returns (Vec::new(), warning) and the
// seam drops the now-empty group, exactly as the old manual is_empty check did.
let groups = make_groups(&[("/dev/RepoA", vec![make_worktree("RepoA-feature", "RepoA")])]);
// No with_symbolic_ref / with_rev_parse_verify stubs, so detect_default_branch fails.
let git = MockGitBuilder::new().build();
let progress = Progress::disabled();
let result = run_scan_repos(&git, groups, 100, false, &[], &progress).unwrap();

assert!(result.repos.is_empty());
assert_eq!(result.total_scanned, 0);
assert!(
result
.warnings
.iter()
.any(|w| w.contains("could not determine default branch")),
"expected default-branch warning, got: {:?}",
result.warnings,
);
}
}
2 changes: 1 addition & 1 deletion crates/git-worktree-tidy/tests/integration_git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ fn worktree_with_deleted_branch_classifies_as_landed_stale() {

assert_eq!(result.total_scanned, 1);
assert_eq!(result.repos.len(), 1);
let wt_info = &result.repos[0].worktrees[0];
let wt_info = &result.repos[0].items[0];
assert_eq!(
wt_info.classification,
Classification::LandedStale,
Expand Down