tiny.smg.diff
Defined in tiny.smg.
API (9)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: tools/smg/src/diff.zig
zig
const std = @import("std");const pretty = @import("pretty");const sys = @import("sys");const smg = @import("root.zig");const git = @import("git.zig");const graph_mod = @import("graph.zig");const scan = @import("scan/root.zig");const view = @import("view.zig");const model = @import("model.zig");const text = @import("text/root.zig");const pretty_json = pretty.json;pub const NodeChange = struct { name: []const u8, field: []const u8, old: ?[]const u8, new: ?[]const u8,};pub const ChangedNode = struct { node: model.Node, changes: []const NodeChange,};pub const RenamedNode = struct { old_name: []const u8, new_name: []const u8, old_node: model.Node, new_node: model.Node, match_type: []const u8,};pub const Result = struct { added_nodes: []const model.Node = &.{}, removed_nodes: []const model.Node = &.{}, changed_nodes: []const ChangedNode = &.{}, renamed_nodes: []const RenamedNode = &.{}, added_edges: []const model.Edge = &.{}, removed_edges: []const model.Edge = &.{}, pub fn empty(self: Result) bool { return self.added_nodes.len == 0 and self.removed_nodes.len == 0 and self.changed_nodes.len == 0 and self.renamed_nodes.len == 0 and self.added_edges.len == 0 and self.removed_edges.len == 0; }};pub fn graphs(allocator: std.mem.Allocator, old: graph_mod.Graph, new: graph_mod.Graph, detect_renames: bool) !Result { var result = Result{}; var added_nodes: std.ArrayList(model.Node) = .empty; var removed_nodes: std.ArrayList(model.Node) = .empty; var changed_nodes: std.ArrayList(ChangedNode) = .empty; var added_edges: std.ArrayList(model.Edge) = .empty; var removed_edges: std.ArrayList(model.Edge) = .empty; for (new.nodes.items) |node| { if (graph_mod.getNode(&old, node.name) == null) try added_nodes.append(allocator, node); } for (old.nodes.items) |node| { if (graph_mod.getNode(&new, node.name) == null) try removed_nodes.append(allocator, node); } for (new.nodes.items) |node| { const old_node = graph_mod.getNode(&old, node.name) orelse continue; const changes = try nodeChanges(allocator, old_node, node); if (changes.len != 0) try changed_nodes.append(allocator, .{ .node = node, .changes = changes }); } var new_edge_iterator = new.edge_index.iterator(); while (new_edge_iterator.next()) |entry| { const edge = new.edges.items[entry.value_ptr.*]; if (!hasIndexedEdge(old, entry.key_ptr.*, edge)) try added_edges.append(allocator, edge); } var old_edge_iterator = old.edge_index.iterator(); while (old_edge_iterator.next()) |entry| { const edge = old.edges.items[entry.value_ptr.*]; if (!hasIndexedEdge(new, entry.key_ptr.*, edge)) try removed_edges.append(allocator, edge); } view.sortNodes(added_nodes.items); view.sortNodes(removed_nodes.items); sortChangedNodes(changed_nodes.items); view.sortEdges(added_edges.items); view.sortEdges(removed_edges.items); result.added_nodes = try added_nodes.toOwnedSlice(allocator); result.removed_nodes = try removed_nodes.toOwnedSlice(allocator); result.changed_nodes = try changed_nodes.toOwnedSlice(allocator); result.added_edges = try added_edges.toOwnedSlice(allocator); result.removed_edges = try removed_edges.toOwnedSlice(allocator); result.renamed_nodes = &.{}; if (detect_renames and result.added_nodes.len != 0 and result.removed_nodes.len != 0) result = try detectRenames(allocator, result); return result;}fn sortChangedNodes(nodes: []ChangedNode) void { std.mem.sort(ChangedNode, nodes, {}, changedNodeLessThan);}fn changedNodeLessThan(_: void, left: ChangedNode, right: ChangedNode) bool { return std.mem.lessThan(u8, left.node.name, right.node.name);}fn hasIndexedEdge(graph: graph_mod.Graph, key: []const u8, edge: model.Edge) bool { const index = graph.edge_index.get(key) orelse return false; const candidate = graph.edges.items[index]; return std.mem.eql(u8, candidate.source, edge.source) and std.mem.eql(u8, candidate.rel, edge.rel) and std.mem.eql(u8, candidate.target, edge.target);}fn nodeChanges(allocator: std.mem.Allocator, old: model.Node, new: model.Node) ![]const NodeChange { var changes: std.ArrayList(NodeChange) = .empty; if (!std.mem.eql(u8, old.type, new.type)) try changes.append(allocator, .{ .name = new.name, .field = "type", .old = old.type, .new = new.type }); if (!optionalEqual(old.file, new.file)) try changes.append(allocator, .{ .name = new.name, .field = "file", .old = old.file, .new = new.file }); if (old.line != new.line) try changes.append(allocator, .{ .name = new.name, .field = "line", .old = try lineText(allocator, old.line), .new = try lineText(allocator, new.line) }); if (!optionalEqual(old.docstring, new.docstring)) try changes.append(allocator, .{ .name = new.name, .field = "docstring", .old = old.docstring, .new = new.docstring }); const old_content = model.pairValue(old.metadata, "content_hash"); const new_content = model.pairValue(new.metadata, "content_hash"); if (!optionalEqual(old_content, new_content)) try changes.append(allocator, .{ .name = new.name, .field = "content_hash", .old = old_content, .new = new_content }); const old_structure = model.pairValue(old.metadata, "structure_hash"); const new_structure = model.pairValue(new.metadata, "structure_hash"); if (!optionalEqual(old_structure, new_structure)) try changes.append(allocator, .{ .name = new.name, .field = "structure_hash", .old = old_structure, .new = new_structure }); return try changes.toOwnedSlice(allocator);}fn optionalEqual(a: ?[]const u8, b: ?[]const u8) bool { if (a == null and b == null) return true; if (a == null or b == null) return false; return std.mem.eql(u8, a.?, b.?);}fn lineText(allocator: std.mem.Allocator, value: ?i64) ![]const u8 { if (value) |line| return try std.fmt.allocPrint(allocator, "{d}", .{line}); return "None";}fn detectRenames(allocator: std.mem.Allocator, result: Result) !Result { var matched_added: std.ArrayList([]const u8) = .empty; var matched_removed: std.ArrayList([]const u8) = .empty; var renamed: std.ArrayList(RenamedNode) = .empty; for (result.added_nodes) |added| { const structure_hash = model.pairValue(added.metadata, "structure_hash") orelse continue; var candidates: std.ArrayList(model.Node) = .empty; for (result.removed_nodes) |removed| { if (containsString(matched_removed.items, removed.name)) continue; const removed_structure = model.pairValue(removed.metadata, "structure_hash") orelse continue; if (std.mem.eql(u8, removed_structure, structure_hash)) try candidates.append(allocator, removed); } if (candidates.items.len == 0) continue; const content_hash = model.pairValue(added.metadata, "content_hash"); var exact: std.ArrayList(model.Node) = .empty; if (content_hash) |hash| { for (candidates.items) |candidate| { const candidate_hash = model.pairValue(candidate.metadata, "content_hash") orelse continue; if (std.mem.eql(u8, candidate_hash, hash)) try exact.append(allocator, candidate); } } var match: ?model.Node = null; var match_type: []const u8 = "structure"; if (exact.items.len == 1) { match = exact.items[0]; match_type = "content"; } else if (candidates.items.len == 1) { match = candidates.items[0]; } if (match) |node| { try renamed.append(allocator, .{ .old_name = node.name, .new_name = added.name, .old_node = node, .new_node = added, .match_type = match_type }); try matched_added.append(allocator, added.name); try matched_removed.append(allocator, node.name); } } try fuzzyMatch(allocator, result.added_nodes, result.removed_nodes, &renamed, &matched_added, &matched_removed); var added_nodes: std.ArrayList(model.Node) = .empty; var removed_nodes: std.ArrayList(model.Node) = .empty; for (result.added_nodes) |node| { if (!containsString(matched_added.items, node.name)) try added_nodes.append(allocator, node); } for (result.removed_nodes) |node| { if (!containsString(matched_removed.items, node.name)) try removed_nodes.append(allocator, node); } return .{ .added_nodes = try added_nodes.toOwnedSlice(allocator), .removed_nodes = try removed_nodes.toOwnedSlice(allocator), .changed_nodes = result.changed_nodes, .renamed_nodes = try renamed.toOwnedSlice(allocator), .added_edges = result.added_edges, .removed_edges = result.removed_edges, };}fn fuzzyMatch(allocator: std.mem.Allocator, added_nodes: []const model.Node, removed_nodes: []const model.Node, renamed: *std.ArrayList(RenamedNode), matched_added: *std.ArrayList([]const u8), matched_removed: *std.ArrayList([]const u8)) !void { for (added_nodes) |added| { if (containsString(matched_added.items, added.name)) continue; const added_tokens = try tokenize(allocator, added.name); if (added_tokens.len == 0) continue; var best_score: f64 = 0.0; var best_match: ?model.Node = null; for (removed_nodes) |removed| { if (containsString(matched_removed.items, removed.name)) continue; if (!std.mem.eql(u8, removed.type, added.type)) continue; const removed_tokens = try tokenize(allocator, removed.name); if (removed_tokens.len == 0) continue; const smaller = @min(added_tokens.len, removed_tokens.len); const larger = @max(added_tokens.len, removed_tokens.len); if (@as(f64, @floatFromInt(smaller)) / @as(f64, @floatFromInt(larger)) < 0.5) continue; const score = jaccard(added_tokens, removed_tokens); if (score > best_score) { best_score = score; best_match = removed; } } if (best_match) |node| { if (best_score >= 0.8) { try renamed.append(allocator, .{ .old_name = node.name, .new_name = added.name, .old_node = node, .new_node = added, .match_type = "fuzzy" }); try matched_added.append(allocator, added.name); try matched_removed.append(allocator, node.name); } } }}fn tokenize(allocator: std.mem.Allocator, name: []const u8) ![]const []const u8 { var out: std.ArrayList([]const u8) = .empty; var start: usize = 0; var index: usize = 0; while (index <= name.len) : (index += 1) { if (index == name.len or name[index] == '.' or name[index] == '_') { if (index > start) try appendUniqueString(allocator, &out, name[start..index]); start = index + 1; } } return try out.toOwnedSlice(allocator);}fn jaccard(a: []const []const u8, b: []const []const u8) f64 { var intersection: usize = 0; for (a) |item| { if (containsString(b, item)) intersection += 1; } var union_count: usize = a.len; for (b) |item| { if (!containsString(a, item)) union_count += 1; } if (union_count == 0) return 0.0; return @as(f64, @floatFromInt(intersection)) / @as(f64, @floatFromInt(union_count));}pub fn loadGraphFromGit(allocator: std.mem.Allocator, root: []const u8, ref: []const u8, invalid: *bool, limits: smg.Limits) !?graph_mod.Graph { invalid.* = false; if (!std.mem.eql(u8, ref, "HEAD")) { if (!try git.refExists(allocator, root, ref)) { invalid.* = true; return null; } } const temp_root = try std.fmt.allocPrint(allocator, "/tmp/smg-diff-ref-{x}-{x}", .{ @as(u64, @intCast(@max(0, sys.time.realMilliTimestamp()))), @intFromPtr(&allocator) }); defer allocator.free(temp_root); defer sys.fs.deleteTree(temp_root) catch {}; const add = try git.run(allocator, root, &.{ "git", "worktree", "add", "--detach", "--quiet", temp_root, ref }, 1024 * 1024); if (!add.ok) return null; defer { _ = git.run(allocator, root, &.{ "git", "worktree", "remove", "--force", temp_root }, 1024 * 1024) catch {}; } var graph = graph_mod.init(allocator); _ = try scan.scanPathsWithOptions(allocator, &graph, temp_root, &.{temp_root}, false, .{ .limits = limits }); return graph;}test "loadGraphFromGit scans committed source ref" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const root = try std.fmt.allocPrint(allocator, "/tmp/smg-diff-git-test-{x}-{x}", .{ @as(u64, @intCast(@max(0, sys.time.realMilliTimestamp()))), @intFromPtr(&arena) }); defer sys.fs.deleteTree(root) catch {}; try sys.fs.createDirPath(root); try std.testing.expect((try git.run(allocator, root, &.{ "git", "init" }, 1024 * 1024)).ok); const source_path = try std.fs.path.join(allocator, &.{ root, "app.py" }); try text.writeFile(source_path, "def alpha():\n return 1\n"); try std.testing.expect((try git.run(allocator, root, &.{ "git", "add", "app.py" }, 1024 * 1024)).ok); try std.testing.expect((try git.run(allocator, root, &.{ "git", "-c", "user.name=smg", "-c", "user.email=smg@example.invalid", "commit", "-m", "base" }, 1024 * 1024)).ok); try text.writeFile(source_path, "def alpha():\n return 1\n\ndef beta():\n return 2\n"); var invalid = false; const loaded = (try loadGraphFromGit(allocator, root, "HEAD", &invalid, smg.default_limits)).?; try std.testing.expect(!invalid); try std.testing.expect(graph_mod.getNode(&loaded, "app.alpha") != null); try std.testing.expect(graph_mod.getNode(&loaded, "app.beta") == null);}pub fn toJson(allocator: std.mem.Allocator, result: Result, ref: []const u8) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); var writer = pretty_json.Writer.init(&out.writer, .minified); try writer.beginObject(); try writer.objectField("ref"); try writer.write(ref); try writer.objectField("added_nodes"); try writeNodeNames(&writer, result.added_nodes); try writer.objectField("removed_nodes"); try writeNodeNames(&writer, result.removed_nodes); try writer.objectField("changed_nodes"); try writer.beginArray(); for (result.changed_nodes) |changed| { try writer.beginObject(); try writer.objectField("name"); try writer.write(changed.node.name); try writer.objectField("changes"); try writer.beginArray(); for (changed.changes) |change| { try writer.beginObject(); try writer.objectField("field"); try writer.write(change.field); try writer.objectField("old"); try writer.write(change.old); try writer.objectField("new"); try writer.write(change.new); try writer.endObject(); } try writer.endArray(); try writer.endObject(); } try writer.endArray(); try writer.objectField("renamed_nodes"); try writer.beginArray(); for (result.renamed_nodes) |renamed| { try writer.beginObject(); try writer.objectField("old_name"); try writer.write(renamed.old_name); try writer.objectField("new_name"); try writer.write(renamed.new_name); try writer.objectField("match_type"); try writer.write(renamed.match_type); try writer.endObject(); } try writer.endArray(); try writer.objectField("added_edges"); try writeEdges(&writer, result.added_edges); try writer.objectField("removed_edges"); try writeEdges(&writer, result.removed_edges); try writer.objectField("summary"); try writer.beginObject(); try writer.objectField("nodes_added"); try writer.write(result.added_nodes.len); try writer.objectField("nodes_removed"); try writer.write(result.removed_nodes.len); try writer.objectField("nodes_changed"); try writer.write(result.changed_nodes.len); try writer.objectField("nodes_renamed"); try writer.write(result.renamed_nodes.len); try writer.objectField("edges_added"); try writer.write(result.added_edges.len); try writer.objectField("edges_removed"); try writer.write(result.removed_edges.len); try writer.endObject(); try writer.endObject(); try out.writer.writeByte('\n'); return try out.toOwnedSlice();}pub fn toText(allocator: std.mem.Allocator, result: Result, ref: []const u8, old_missing: bool) ![]const u8 { var out: std.Io.Writer.Allocating = .init(allocator); errdefer out.deinit(); if (old_missing) try out.writer.print("No graph found at ref {s}. Showing full graph as new.\n", .{ref}); if (result.empty()) { try out.writer.print("No structural changes vs {s}.\n", .{ref}); return try out.toOwnedSlice(); } try out.writer.print("Diff vs {s}\n\n", .{ref}); if (result.added_nodes.len != 0) { try out.writer.print("+ {d} node(s) added\n", .{result.added_nodes.len}); for (result.added_nodes) |node| try out.writer.print(" + [{s}] {s}\n", .{ node.type, node.name }); } if (result.removed_nodes.len != 0) { try out.writer.print("- {d} node(s) removed\n", .{result.removed_nodes.len}); for (result.removed_nodes) |node| try out.writer.print(" - [{s}] {s}\n", .{ node.type, node.name }); } if (result.changed_nodes.len != 0) { try out.writer.print("~ {d} node(s) changed\n", .{result.changed_nodes.len}); for (result.changed_nodes) |changed| { try out.writer.print(" ~ {s}\n", .{changed.node.name}); for (changed.changes) |change| try out.writer.print(" {s}: {s} → {s}\n", .{ change.field, change.old orelse "None", change.new orelse "None" }); } } if (result.renamed_nodes.len != 0) { try out.writer.print("~ {d} node(s) renamed/moved\n", .{result.renamed_nodes.len}); for (result.renamed_nodes) |renamed| { const tag: []const u8 = if (std.mem.eql(u8, renamed.match_type, "content")) "exact" else "structural"; try out.writer.print(" ~ {s} → {s} ({s} match)\n", .{ renamed.old_name, renamed.new_name, tag }); } } if (result.added_edges.len != 0) { try out.writer.print("+ {d} edge(s) added\n", .{result.added_edges.len}); for (result.added_edges[0..@min(result.added_edges.len, 20)]) |edge| try out.writer.print(" + {s} --{s}--> {s}\n", .{ edge.source, edge.rel, edge.target }); if (result.added_edges.len > 20) try out.writer.print(" ... and {d} more\n", .{result.added_edges.len - 20}); } if (result.removed_edges.len != 0) { try out.writer.print("- {d} edge(s) removed\n", .{result.removed_edges.len}); for (result.removed_edges[0..@min(result.removed_edges.len, 20)]) |edge| try out.writer.print(" - {s} --{s}--> {s}\n", .{ edge.source, edge.rel, edge.target }); if (result.removed_edges.len > 20) try out.writer.print(" ... and {d} more\n", .{result.removed_edges.len - 20}); } try out.writer.writeAll("\nNodes: "); var wrote = false; if (result.added_nodes.len != 0) { try out.writer.print("+{d}", .{result.added_nodes.len}); wrote = true; } if (result.removed_nodes.len != 0) { if (wrote) try out.writer.writeAll(", "); try out.writer.print("-{d}", .{result.removed_nodes.len}); wrote = true; } if (result.changed_nodes.len != 0) { if (wrote) try out.writer.writeAll(", "); try out.writer.print("~{d}", .{result.changed_nodes.len}); wrote = true; } if (result.renamed_nodes.len != 0) { if (wrote) try out.writer.writeAll(", "); try out.writer.print("↷{d}", .{result.renamed_nodes.len}); } try out.writer.print(" | Edges: +{d} -{d}\n", .{ result.added_edges.len, result.removed_edges.len }); return try out.toOwnedSlice();}fn writeNodeNames(writer: *pretty_json.Writer, nodes: []const model.Node) !void { try writer.beginArray(); for (nodes) |node| try writer.write(node.name); try writer.endArray();}fn writeEdges(writer: *pretty_json.Writer, edges: []const model.Edge) !void { try writer.beginArray(); for (edges) |edge| { try writer.beginObject(); try writer.objectField("source"); try writer.write(edge.source); try writer.objectField("rel"); try writer.write(edge.rel); try writer.objectField("target"); try writer.write(edge.target); try writer.endObject(); } try writer.endArray();}fn containsString(values: []const []const u8, value: []const u8) bool { for (values) |item| { if (std.mem.eql(u8, item, value)) return true; } return false;}fn appendUniqueString(allocator: std.mem.Allocator, list: *std.ArrayList([]const u8), value: []const u8) !void { if (!containsString(list.items, value)) try list.append(allocator, value);}fn linearHasEdge(graph: graph_mod.Graph, edge: model.Edge) bool { for (graph.edges.items) |candidate| { if (std.mem.eql(u8, candidate.source, edge.source) and std.mem.eql(u8, candidate.rel, edge.rel) and std.mem.eql(u8, candidate.target, edge.target)) return true; } return false;}fn linearEdgeDiff( allocator: std.mem.Allocator, subject: graph_mod.Graph, other: graph_mod.Graph,) ![]const model.Edge { var edges: std.ArrayList(model.Edge) = .empty; for (subject.edges.items) |edge| { if (!linearHasEdge(other, edge)) try edges.append(allocator, edge); } view.sortEdges(edges.items); return try edges.toOwnedSlice(allocator);}fn expectEdgeIdentityEqual(expected: []const model.Edge, actual: []const model.Edge) !void { try std.testing.expectEqual(expected.len, actual.len); for (expected, actual) |left, right| { try std.testing.expectEqualStrings(left.source, right.source); try std.testing.expectEqualStrings(left.rel, right.rel); try std.testing.expectEqualStrings(left.target, right.target); }}const DifferentialFixture = struct { old: graph_mod.Graph, new: graph_mod.Graph,};fn differentialFixture(allocator: std.mem.Allocator, seed: usize) !DifferentialFixture { const node_count = 32; var names: [node_count][]const u8 = undefined; for (&names, 0..) |*name, index| { name.* = try std.fmt.allocPrint(allocator, "node_{d:0>2}", .{index}); } var old = graph_mod.init(allocator); var new = graph_mod.init(allocator); for (0..node_count) |step| { const old_index = (step * 5 + seed) % node_count; const new_index = (step * 7 + seed) % node_count; try graph_mod.addNode(&old, .{ .name = names[old_index], .type = model.NodeType.function }); try graph_mod.addNode(&new, .{ .name = names[new_index], .type = model.NodeType.function }); } for (0..node_count) |step| { const old_index = (step * 5 + seed) % node_count; const new_index = (step * 7 + seed) % node_count; try graph_mod.addEdge(&old, .{ .source = names[old_index], .rel = "common", .target = names[(old_index + 1) % node_count], }); try graph_mod.addEdge(&new, .{ .source = names[new_index], .rel = "common", .target = names[(new_index + 1) % node_count], }); } for (0..8) |index| { const target = names[(index + 9) % node_count]; try graph_mod.addEdge(&old, .{ .source = names[index], .rel = "old", .target = target }); try graph_mod.addEdge(&new, .{ .source = names[7 - index], .rel = "new", .target = names[(7 - index + 9) % node_count], }); } return .{ .old = old, .new = new };}test "diff detects added removed changed and edges" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var old = graph_mod.init(allocator); try graph_mod.addNode(&old, .{ .name = "app", .type = model.NodeType.module }); try graph_mod.addNode(&old, .{ .name = "app.main", .type = model.NodeType.function, .file = "app.py", .line = 1, .docstring = "Entry" }); try graph_mod.addNode(&old, .{ .name = "app.Server", .type = model.NodeType.class, .file = "app.py", .line = 10 }); try graph_mod.addEdge(&old, .{ .source = "app", .rel = model.RelType.contains, .target = "app.main" }); try graph_mod.addEdge(&old, .{ .source = "app", .rel = model.RelType.contains, .target = "app.Server" }); var new = graph_mod.init(allocator); try graph_mod.addNode(&new, .{ .name = "app", .type = model.NodeType.module }); try graph_mod.addNode(&new, .{ .name = "app.main", .type = model.NodeType.function, .file = "app.py", .line = 5, .docstring = "Updated" }); try graph_mod.addNode(&new, .{ .name = "app.helper", .type = model.NodeType.function }); try graph_mod.addEdge(&new, .{ .source = "app", .rel = model.RelType.contains, .target = "app.main" }); try graph_mod.addEdge(&new, .{ .source = "app", .rel = model.RelType.contains, .target = "app.helper" }); try graph_mod.addEdge(&new, .{ .source = "app.main", .rel = model.RelType.calls, .target = "app.helper" }); const result = try graphs(allocator, old, new, true); try std.testing.expectEqual(@as(usize, 1), result.added_nodes.len); try std.testing.expectEqual(@as(usize, 1), result.removed_nodes.len); try std.testing.expectEqual(@as(usize, 1), result.changed_nodes.len); try std.testing.expectEqual(@as(usize, 2), result.added_edges.len); try std.testing.expectEqual(@as(usize, 1), result.removed_edges.len); try std.testing.expectEqualStrings("app.helper", result.added_nodes[0].name);}test "diff preserves edge identity and text output" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var old = graph_mod.init(allocator); var new = graph_mod.init(allocator); const names = [_][]const u8{ "root", "shared", "old_only", "new_only" }; for (names) |name| { const node: model.Node = .{ .name = name, .type = model.NodeType.function }; try graph_mod.addNode(&old, node); try graph_mod.addNode(&new, node); } const old_metadata = [_]model.Pair{.{ .key = "weight", .value = "1" }}; const new_metadata = [_]model.Pair{.{ .key = "weight", .value = "2" }}; try graph_mod.addEdge(&old, .{ .source = "root", .rel = model.RelType.contains, .target = "shared", .metadata = &old_metadata, }); try graph_mod.addEdge(&new, .{ .source = "root", .rel = model.RelType.contains, .target = "shared", .metadata = &new_metadata, }); try graph_mod.addEdge(&old, .{ .source = "root", .rel = model.RelType.calls, .target = "old_only", }); try graph_mod.addEdge(&new, .{ .source = "root", .rel = model.RelType.calls, .target = "new_only", }); const result = try graphs(allocator, old, new, false); try std.testing.expectEqual(@as(usize, 1), result.added_edges.len); try std.testing.expectEqual(@as(usize, 1), result.removed_edges.len); try std.testing.expectEqualStrings("new_only", result.added_edges[0].target); try std.testing.expectEqualStrings("old_only", result.removed_edges[0].target); try std.testing.expectEqualStrings( "Diff vs base\n\n" ++ "+ 1 edge(s) added\n" ++ " + root --calls--> new_only\n" ++ "- 1 edge(s) removed\n" ++ " - root --calls--> old_only\n" ++ "\nNodes: | Edges: +1 -1\n", try toText(allocator, result, "base", false), );}test "diff verifies indexed edge identity without graph allocation" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var old = graph_mod.init(allocator); var new = graph_mod.init(allocator); const long_source = try allocator.alloc(u8, 4096); @memset(long_source, 'x'); const names = [_][]const u8{ "a\x1fb", "d", "a", "c\x1fd", long_source, "long-target", }; for (names) |name| { const node: model.Node = .{ .name = name, .type = model.NodeType.function }; try graph_mod.addNode(&old, node); try graph_mod.addNode(&new, node); } try graph_mod.addEdge(&old, .{ .source = "a\x1fb", .rel = "c", .target = "d" }); try graph_mod.addEdge(&new, .{ .source = "a", .rel = "b", .target = "c\x1fd" }); const long_edge: model.Edge = .{ .source = long_source, .rel = model.RelType.calls, .target = "long-target", }; try graph_mod.addEdge(&old, long_edge); try graph_mod.addEdge(&new, long_edge); var failing = std.testing.FailingAllocator.init(allocator, .{ .fail_index = 0 }); old.allocator = failing.allocator(); new.allocator = failing.allocator(); const result = try graphs(allocator, old, new, false); try std.testing.expectEqual(@as(usize, 1), result.added_edges.len); try std.testing.expectEqual(@as(usize, 1), result.removed_edges.len); try std.testing.expectEqualStrings("c\x1fd", result.added_edges[0].target); try std.testing.expectEqualStrings("a\x1fb", result.removed_edges[0].source);}test "diff index matches linear edge oracle across insertion orders" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); for (0..16) |seed| { const fixture = try differentialFixture(allocator, seed); const result = try graphs(allocator, fixture.old, fixture.new, false); const expected_added = try linearEdgeDiff(allocator, fixture.new, fixture.old); const expected_removed = try linearEdgeDiff(allocator, fixture.old, fixture.new); try expectEdgeIdentityEqual(expected_added, result.added_edges); try expectEdgeIdentityEqual(expected_removed, result.removed_edges); }}test "diff detects hash and fuzzy renames" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var old = graph_mod.init(allocator); var new = graph_mod.init(allocator); const old_meta = [_]model.Pair{ .{ .key = "content_hash", .value = "same" }, .{ .key = "structure_hash", .value = "shape" }, }; const new_meta = [_]model.Pair{ .{ .key = "content_hash", .value = "same" }, .{ .key = "structure_hash", .value = "shape" }, }; try graph_mod.addNode(&old, .{ .name = "app.foo", .type = model.NodeType.function, .metadata = &old_meta }); try graph_mod.addNode(&new, .{ .name = "app.bar", .type = model.NodeType.function, .metadata = &new_meta }); const result = try graphs(allocator, old, new, true); try std.testing.expectEqual(@as(usize, 1), result.renamed_nodes.len); try std.testing.expectEqualStrings("content", result.renamed_nodes[0].match_type);}Source: tools/smg/src/root.zig:20
zig
pub const diff = @import("diff.zig");Complete caller list for diff.graphs
9 direct callers.
tools.smg.src.command.analyze.command.deltaFilter[function] — private; no exact target attools/smg/src/command/analyze/command.zig:82in nearest public ownertiny.smg.command.analyze.commandtiny.smg.command.diff.run[function] attools/smg/src/command/diff.zig:11tools.smg.src.diff.test_diff_detects_added_removed_changed_and_edges[function] — test; no exact target attools/smg/src/diff.zig:526in nearest public ownertiny.smg.difftools.smg.src.diff.test_diff_detects_hash_and_fuzzy_renames[function] — test; no exact target attools/smg/src/diff.zig:658in nearest public ownertiny.smg.difftools.smg.src.diff.test_diff_index_matches_linear_edge_oracle_across_insertion_orders[function] — test; no exact target attools/smg/src/diff.zig:644in nearest public ownertiny.smg.difftools.smg.src.diff.test_diff_preserves_edge_identity_and_text_output[function] — test; no exact target attools/smg/src/diff.zig:552in nearest public ownertiny.smg.difftools.smg.src.diff.test_diff_verifies_indexed_edge_identity_without_graph_allocation[function] — test; no exact target attools/smg/src/diff.zig:604in nearest public ownertiny.smg.difftools.smg.src.help.click.top.report.write[function] — private; no exact target attools/smg/src/help/click/top/report.zig:5in nearest public ownertools.smg.src.help.click.top.reporttiny.smg.watch.rescan[function] attools/smg/src/watch.zig:177
Audit
| Definitions | 10 |
|---|---|
| Public names | 10 |
| Members | 17 |
| Version | 26.7.0 |
| Revision | daab053ee433 |