Skip to content
Closed
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
26 changes: 26 additions & 0 deletions ast/src/lang/queries/skips/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,32 @@ pub const TEST_FILE_PATTERNS: &[&str] = &[
"test_.py",
];

pub const PRIORITY_SOURCE_DIRS: &[&str] = &[
"src", "app", "lib", "api", "routes", "routers", "controllers", "services",
"components", "pages", "pkg", "cmd", "internal", "crates", "server", "client",
"frontend", "backend", "mcp", "ast", "lsp", "shared", "cli", "web",
];

pub const ALWAYS_EXPAND_DIRS: &[&str] = &[
"src", "app", "lib", "api", "server", "client", "backend", "frontend",
];

pub const COLLAPSE_DIRS: &[&str] = &[
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add dist, build, anything like that

"migrations", "seeds", "fixtures", "tests", "test", "spec", "__tests__", "e2e",
"integration", "docs", "doc", "public", "assets", "static", ".github", ".ai",
".cursorrules", ".husky",
"dist", "build", "out", ".next", "coverage", "node_modules", "vendor", "target",
"__pycache__", "obj", ".turbo", ".cache", "storybook-static",
];

pub const PRIORITY_ROOT_FILES: &[&str] = &[
"package.json", "Cargo.toml", "README.md", "AGENTS.md", "CLAUDE.md", "Dockerfile",
"docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml",
"next.config.ts", "next.config.js", "tsconfig.json", "schema.prisma", "go.mod",
"pyproject.toml", "requirements.txt", "Gemfile", "composer.json", "pom.xml",
"build.gradle", "build.gradle.kts",
];

pub fn should_skip_dir(name: &str) -> bool {
name.starts_with('.') || JUNK_DIRS.contains(&name) || TEST_DIRS.contains(&name)
}
Expand Down
27 changes: 25 additions & 2 deletions cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ pub enum Commands {
Deps(DepsArgs),
/// Show what is affected if a node changes (reverse dependency tree)
Impact(ImpactArgs),
/// Show a de-noised overview of a repository tree
Overview(OverviewArgs),
}

#[derive(Debug, Args)]
Expand Down Expand Up @@ -184,6 +186,21 @@ pub struct ImpactArgs {
pub files: Vec<String>,
}

#[derive(Debug, Args)]
pub struct OverviewArgs {
/// Repository root or directory to summarize
#[arg(value_name = "PATH")]
pub path: String,

/// Maximum number of lines to emit
#[arg(long, default_value = "80")]
pub max_lines: usize,

/// Maximum depth to expand before collapsing
#[arg(long, default_value = "4")]
pub depth: usize,
}

impl CliArgs {
pub fn parse_and_expand() -> Result<Self> {
let mut args = Self::parse();
Expand All @@ -200,7 +217,11 @@ impl CliArgs {
})
.collect();

if args.command.is_none() && args.files.is_empty() {
if args.command.is_some() {
return Ok(args);
}

if args.files.is_empty() {
eprintln!("Error: no file path provided. Run with --help for usage.");
std::process::exit(1);
}
Expand All @@ -220,7 +241,9 @@ impl CliArgs {
if let Some(Commands::Impact(_)) = &args.command {
return Ok(args);
}

if let Some(Commands::Overview(_)) = &args.command {
return Ok(args);
}
Ok(args)
}
}
166 changes: 130 additions & 36 deletions cli/src/changes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::git::{
filter_paths_by_scope, get_changed_files, get_repo_root, get_staged_changes,
get_working_tree_changes, list_commits_for_paths, read_file_at_rev,
};
use ast::lang::graphs::{ArrayGraph, Node, NodeType, Edge, EdgeType};
use ast::lang::graphs::{ArrayGraph, Edge, EdgeType, Node, NodeType};
use console::style;
use lsp::Language;
use serde::Serialize;
Expand Down Expand Up @@ -157,7 +157,10 @@ async fn run_list_commits(
}

if commits.is_empty() {
out.writeln(format!("{}", style("No commits found for the specified paths").yellow()))?;
out.writeln(format!(
"{}",
style("No commits found for the specified paths").yellow()
))?;
return Ok(());
}

Expand Down Expand Up @@ -204,7 +207,6 @@ async fn run_diff(
show_progress: bool,
output_mode: OutputMode,
) -> Result<()> {

let validated_types = parse_node_types(types)?;

let mode_description = if args.staged {
Expand Down Expand Up @@ -260,7 +262,11 @@ async fn run_diff(
let files = get_changed_files(repo_path, parts[0], parts[1])?;
(files, parts[0].to_string(), Some(parts[1].to_string()))
} else {
(get_working_tree_changes(repo_path)?, "HEAD".to_string(), None)
(
get_working_tree_changes(repo_path)?,
"HEAD".to_string(),
None,
)
};

let scoped_files = filter_paths_by_scope(changed_files, paths);
Expand All @@ -279,8 +285,11 @@ async fn run_diff(
} else {
out.writeln(format!(
"{}",
style(format!("warning: '{}' does not exist in this repository", p))
.yellow()
style(format!(
"warning: '{}' does not exist in this repository",
p
))
.yellow()
))?;
}
}
Expand Down Expand Up @@ -348,7 +357,11 @@ async fn run_diff(
}
} else {
if !output_mode.is_json() {
out.writeln(format!(" {} {}", style(file).cyan(), style("(not parsed)").dim()))?;
out.writeln(format!(
" {} {}",
style(file).cyan(),
style("(not parsed)").dim()
))?;
printed_file_list = true;
}
}
Expand Down Expand Up @@ -377,9 +390,8 @@ async fn run_diff(
Error::internal(format!("Failed to create temp dir structure: {}", e))
})?;
}
std::fs::write(&dest, &content).map_err(|e| {
Error::internal(format!("Failed to write temp file: {}", e))
})?;
std::fs::write(&dest, &content)
.map_err(|e| Error::internal(format!("Failed to write temp file: {}", e)))?;
files.push(dest.to_string_lossy().to_string());
}
}
Expand Down Expand Up @@ -420,9 +432,8 @@ async fn run_diff(
Error::internal(format!("Failed to create temp dir structure: {}", e))
})?;
}
std::fs::write(&dest, &content).map_err(|e| {
Error::internal(format!("Failed to write temp file: {}", e))
})?;
std::fs::write(&dest, &content)
.map_err(|e| Error::internal(format!("Failed to write temp file: {}", e)))?;
before_files.push(dest.to_string_lossy().to_string());
}
}
Expand All @@ -438,8 +449,8 @@ async fn run_diff(
}

// Canonicalize roots to resolve symlinks (e.g., macOS /var -> /private/var)
let canon_repo = std::fs::canonicalize(repo_path)
.unwrap_or_else(|_| std::path::PathBuf::from(repo_path));
let canon_repo =
std::fs::canonicalize(repo_path).unwrap_or_else(|_| std::path::PathBuf::from(repo_path));
let canon_tmp = tmp_dir
.path()
.canonicalize()
Expand Down Expand Up @@ -502,10 +513,8 @@ async fn run_diff(
.iter()
.map(|n| norm_key(n, &canon_after_root))
.collect();
let removed_node_keys: HashSet<String> = removed
.iter()
.map(|n| norm_key(n, canon_tmp_str))
.collect();
let removed_node_keys: HashSet<String> =
removed.iter().map(|n| norm_key(n, canon_tmp_str)).collect();
let added_edges: Vec<&Edge> = after_edge_keys
.difference(&before_edge_keys)
.filter_map(|k| after_edge_by_key.get(k.as_str()).copied())
Expand Down Expand Up @@ -533,7 +542,12 @@ async fn run_diff(
.filter(|(a, _)| filter_node(&a))
.collect();

if added.is_empty() && removed.is_empty() && modified.is_empty() && added_edges.is_empty() && removed_edges.is_empty() {
if added.is_empty()
&& removed.is_empty()
&& modified.is_empty()
&& added_edges.is_empty()
&& removed_edges.is_empty()
{
if let Some(sp) = &spinner {
sp.finish_with_message("No graph-level changes found");
}
Expand Down Expand Up @@ -574,7 +588,15 @@ async fn run_diff(
scope: paths.to_vec(),
files: scoped_files,
summary: DeltaSummary {
files_changed: total_changed_file_count(&added, &removed, &modified, &added_edges, &removed_edges, &canon_after_root, canon_tmp_str),
files_changed: total_changed_file_count(
&added,
&removed,
&modified,
&added_edges,
&removed_edges,
&canon_after_root,
canon_tmp_str,
),
nodes_added: added.len(),
nodes_removed: removed.len(),
nodes_modified: modified.len(),
Expand All @@ -600,7 +622,17 @@ async fn run_diff(
if let Some(sp) = &spinner {
sp.set_message("Rendering graph change summary...");
}
print_delta(out, &added, &removed, &modified, &added_edges, &removed_edges, canon_repo_str, &canon_after_root, canon_tmp_str)?;
print_delta(
out,
&added,
&removed,
&modified,
&added_edges,
&removed_edges,
canon_repo_str,
&canon_after_root,
canon_tmp_str,
)?;
if let Some(sp) = &spinner {
sp.finish_with_message("Graph change summary ready");
}
Expand Down Expand Up @@ -641,10 +673,26 @@ fn total_changed_file_count(
added
.iter()
.map(|n| rel_path(&n.node_data.file, after_root))
.chain(removed.iter().map(|n| rel_path(&n.node_data.file, before_root)))
.chain(modified.iter().map(|(n, _)| rel_path(&n.node_data.file, after_root)))
.chain(added_edges.iter().map(|e| rel_path(&e.source.node_data.file, after_root)))
.chain(removed_edges.iter().map(|e| rel_path(&e.source.node_data.file, before_root)))
.chain(
removed
.iter()
.map(|n| rel_path(&n.node_data.file, before_root)),
)
.chain(
modified
.iter()
.map(|(n, _)| rel_path(&n.node_data.file, after_root)),
)
.chain(
added_edges
.iter()
.map(|e| rel_path(&e.source.node_data.file, after_root)),
)
.chain(
removed_edges
.iter()
.map(|e| rel_path(&e.source.node_data.file, before_root)),
)
.collect::<HashSet<_>>()
.len()
}
Expand Down Expand Up @@ -689,7 +737,10 @@ fn norm_key_from_ref(node_ref: &ast::lang::graphs::NodeRef, root: &str) -> Strin
.map(|s| s.trim_start_matches('/'))
.unwrap_or(file.as_str())
};
format!("{}-{}-{}", node_ref.node_type, node_ref.node_data.name, rel_file)
format!(
"{}-{}-{}",
node_ref.node_type, node_ref.node_data.name, rel_file
)
}

fn index_graph_by_norm_key<'a>(
Expand Down Expand Up @@ -826,7 +877,10 @@ fn print_delta(
}
for (after_node, before_node) in modified {
let rp = rel_path(&after_node.node_data.file, after_root);
file_modified.entry(rp).or_default().push((after_node, before_node));
file_modified
.entry(rp)
.or_default()
.push((after_node, before_node));
}
for edge in added_edges {
let rp = rel_path(&edge.source.node_data.file, after_root);
Expand Down Expand Up @@ -877,10 +931,22 @@ fn print_delta(

let mut summary_parts: Vec<String> = Vec::new();
if node_count > 0 {
summary_parts.push(format!("{} node{}", node_count, if node_count == 1 { "" } else { "s" }));
summary_parts.push(format!(
"{} node{}",
node_count,
if node_count == 1 { "" } else { "s" }
));
}
if edge_add_count + edge_rem_count > 0 {
summary_parts.push(format!("{} edge change{}", edge_add_count + edge_rem_count, if edge_add_count + edge_rem_count == 1 { "" } else { "s" }));
summary_parts.push(format!(
"{} edge change{}",
edge_add_count + edge_rem_count,
if edge_add_count + edge_rem_count == 1 {
""
} else {
"s"
}
));
}
let summary = summary_parts.join(", ");

Expand All @@ -896,7 +962,12 @@ fn print_delta(
let mut sorted = nodes.clone();
sorted.sort_by_key(|(a, _)| a.node_data.start);
for (after_node, before_node) in sorted {
let line_range = style(format!("L{}-L{}", after_node.node_data.start + 1, after_node.node_data.end + 1)).dim();
let line_range = style(format!(
"L{}-L{}",
after_node.node_data.start + 1,
after_node.node_data.end + 1
))
.dim();
out.writeln(format!(
" {} {} {} {}",
style("~").yellow().bold(),
Expand All @@ -908,8 +979,16 @@ fn print_delta(
let after_sig = node_signature(&after_node);
match (before_sig, after_sig) {
(Some(b), Some(a)) if b != a => {
out.writeln(format!(" {} {}", style("-").red(), style(&b).red().bright()))?;
out.writeln(format!(" {} {}", style("+").green(), style(&a).green().bright()))?;
out.writeln(format!(
" {} {}",
style("-").red(),
style(&b).red().bright()
))?;
out.writeln(format!(
" {} {}",
style("+").green(),
style(&a).green().bright()
))?;
}
_ => {}
}
Expand Down Expand Up @@ -1013,12 +1092,27 @@ fn print_delta(

out.writeln(format!(
"{} {} {} {} {} {}",
style(format!("{} file{}", total_file_count, if total_file_count == 1 { "" } else { "s" })).bold(),
style(format!(
"{} file{}",
total_file_count,
if total_file_count == 1 { "" } else { "s" }
))
.bold(),
style(format!("{} added", added.len())).green(),
style(format!("{} removed", removed.len())).red(),
style(format!("{} modified", modified.len())).yellow(),
style(format!("{} new edge{}", added_edges.len(), if added_edges.len() == 1 { "" } else { "s" })).green(),
style(format!("{} dropped edge{}", removed_edges.len(), if removed_edges.len() == 1 { "" } else { "s" })).red(),
style(format!(
"{} new edge{}",
added_edges.len(),
if added_edges.len() == 1 { "" } else { "s" }
))
.green(),
style(format!(
"{} dropped edge{}",
removed_edges.len(),
if removed_edges.len() == 1 { "" } else { "s" }
))
.red(),
))?;

Ok(())
Expand Down
Loading
Loading