Skip to content

Commit 6ddb6f6

Browse files
Add diff entry point
1 parent 7a08199 commit 6ddb6f6

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

rust/rubydex/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ license = "MIT"
99
name = "rubydex_cli"
1010
path = "src/main.rs"
1111

12+
[[bin]]
13+
name = "diff"
14+
path = "src/bin/diff.rs"
15+
required-features = ["dev-tools"]
16+
1217
[lib]
1318
crate-type = ["rlib"]
1419

1520
[features]
21+
dev-tools = []
1622
test_utils = ["dep:tempfile"]
1723

1824
[dependencies]

rust/rubydex/src/bin/diff.rs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
use std::process::Command;
2+
3+
use clap::Parser;
4+
use rubydex::{
5+
diff::{self, GraphDiff},
6+
indexing, listing,
7+
model::graph::Graph,
8+
resolution::Resolver,
9+
};
10+
11+
#[derive(Parser, Debug)]
12+
#[command(name = "diff", about = "Diff two git refs to test graph equality")]
13+
struct Args {
14+
#[arg(help = "Path to git repository")]
15+
path: String,
16+
17+
#[arg(help = "First git ref (e.g., main, HEAD~1, abc123)")]
18+
ref_a: String,
19+
20+
#[arg(help = "Second git ref")]
21+
ref_b: String,
22+
}
23+
24+
fn checkout(path: &str, git_ref: &str) -> Result<(), String> {
25+
let output = Command::new("git")
26+
.args(["checkout", git_ref])
27+
.current_dir(path)
28+
.output()
29+
.map_err(|e| format!("Failed to run git checkout: {e}"))?;
30+
31+
if !output.status.success() {
32+
return Err(format!(
33+
"git checkout {} failed: {}",
34+
git_ref,
35+
String::from_utf8_lossy(&output.stderr)
36+
));
37+
}
38+
Ok(())
39+
}
40+
41+
fn get_current_ref(path: &str) -> Result<String, String> {
42+
let output = Command::new("git")
43+
.args(["rev-parse", "HEAD"])
44+
.current_dir(path)
45+
.output()
46+
.map_err(|e| format!("Failed to run git rev-parse: {e}"))?;
47+
48+
if !output.status.success() {
49+
return Err("git rev-parse HEAD failed".to_string());
50+
}
51+
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
52+
}
53+
54+
fn build_graph(path: &str) -> Graph {
55+
let (file_paths, _) = listing::collect_file_paths(vec![path.to_string()]);
56+
57+
let mut graph = Graph::new();
58+
indexing::index_files(&mut graph, file_paths);
59+
60+
let mut resolver = Resolver::new(&mut graph);
61+
resolver.resolve_all();
62+
63+
graph
64+
}
65+
66+
fn print_diff(diff: &GraphDiff, graph_a: &Graph, graph_b: &Graph) {
67+
if !diff.added_declarations.is_empty() {
68+
println!("\nAdded declarations ({}):", diff.added_declarations.len());
69+
for id in &diff.added_declarations {
70+
if let Some(decl) = graph_b.declarations().get(id) {
71+
println!(" + {}", decl.name());
72+
}
73+
}
74+
}
75+
76+
if !diff.removed_declarations.is_empty() {
77+
println!("\nRemoved declarations ({}):", diff.removed_declarations.len());
78+
for id in &diff.removed_declarations {
79+
if let Some(decl) = graph_a.declarations().get(id) {
80+
println!(" - {}", decl.name());
81+
}
82+
}
83+
}
84+
85+
if !diff.changed_declarations.is_empty() {
86+
println!("\nChanged declarations ({}):", diff.changed_declarations.len());
87+
for id in &diff.changed_declarations {
88+
if let Some(decl) = graph_a.declarations().get(id) {
89+
println!(" ~ {}", decl.name());
90+
print_declaration_diff(graph_a, graph_b, *id);
91+
}
92+
}
93+
}
94+
95+
if !diff.added_definitions.is_empty() {
96+
println!("\nAdded definitions: {}", diff.added_definitions.len());
97+
}
98+
if !diff.removed_definitions.is_empty() {
99+
println!("Removed definitions: {}", diff.removed_definitions.len());
100+
}
101+
102+
if !diff.added_references.is_empty() {
103+
println!("\nAdded references: {}", diff.added_references.len());
104+
}
105+
if !diff.removed_references.is_empty() {
106+
println!("Removed references: {}", diff.removed_references.len());
107+
}
108+
109+
if !diff.added_names.is_empty() {
110+
println!("\nAdded names: {}", diff.added_names.len());
111+
}
112+
if !diff.removed_names.is_empty() {
113+
println!("Removed names: {}", diff.removed_names.len());
114+
}
115+
if !diff.changed_names.is_empty() {
116+
println!("Changed names: {}", diff.changed_names.len());
117+
}
118+
}
119+
120+
fn print_declaration_diff(graph_a: &Graph, graph_b: &Graph, id: rubydex::model::ids::DeclarationId) {
121+
let (Some(decl_a), Some(decl_b)) = (graph_a.declarations().get(&id), graph_b.declarations().get(&id)) else {
122+
return;
123+
};
124+
125+
let (Some(ns_a), Some(ns_b)) = (decl_a.as_namespace(), decl_b.as_namespace()) else {
126+
return;
127+
};
128+
129+
if ns_a.members() != ns_b.members() {
130+
for (str_id, decl_id) in ns_a.members() {
131+
if !ns_b.members().contains_key(str_id)
132+
&& let Some(decl) = graph_a.declarations().get(decl_id)
133+
{
134+
println!(" - {}", decl.name());
135+
}
136+
}
137+
for (str_id, decl_id) in ns_b.members() {
138+
if !ns_a.members().contains_key(str_id)
139+
&& let Some(decl) = graph_b.declarations().get(decl_id)
140+
{
141+
println!(" + {}", decl.name());
142+
}
143+
}
144+
}
145+
146+
let anc_a = ns_a.ancestors();
147+
let anc_b = ns_b.ancestors();
148+
let ancestors_a: Vec<_> = anc_a.iter().collect();
149+
let ancestors_b: Vec<_> = anc_b.iter().collect();
150+
if ancestors_a != ancestors_b {
151+
println!(" ancestors differ");
152+
}
153+
154+
if ns_a.descendants() != ns_b.descendants() {
155+
println!(" descendants differ");
156+
}
157+
158+
if ns_a.singleton_class() != ns_b.singleton_class() {
159+
println!(" singleton_class differs");
160+
}
161+
}
162+
163+
fn main() -> Result<(), String> {
164+
let args = Args::parse();
165+
166+
// Save current ref to restore later
167+
let original_ref = get_current_ref(&args.path)?;
168+
169+
// Build graph for ref_a
170+
println!("Checking out {}...", args.ref_a);
171+
checkout(&args.path, &args.ref_a)?;
172+
println!("Building graph for {}...", args.ref_a);
173+
let graph_a = build_graph(&args.path);
174+
println!(
175+
" {} declarations, {} definitions",
176+
graph_a.declarations().len(),
177+
graph_a.definitions().len()
178+
);
179+
180+
// Build graph for ref_b
181+
println!("Checking out {}...", args.ref_b);
182+
checkout(&args.path, &args.ref_b)?;
183+
println!("Building graph for {}...", args.ref_b);
184+
let graph_b = build_graph(&args.path);
185+
println!(
186+
" {} declarations, {} definitions",
187+
graph_b.declarations().len(),
188+
graph_b.definitions().len()
189+
);
190+
191+
// Restore original ref
192+
println!("Restoring {original_ref}...");
193+
checkout(&args.path, &original_ref)?;
194+
195+
// Diff
196+
println!("\nComparing graphs...");
197+
match diff::diff(&graph_a, &graph_b) {
198+
Some(diff) => {
199+
println!("Graphs differ!");
200+
print_diff(&diff, &graph_a, &graph_b);
201+
}
202+
None => {
203+
println!("Graphs are identical!");
204+
}
205+
}
206+
207+
Ok(())
208+
}

0 commit comments

Comments
 (0)