tiny.smg.churn
Defined in tiny.smg.
API (5)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: tools/smg/src/churn.zig
zig
const std = @import("std");const git = @import("git.zig");const graph_mod = @import("graph.zig");const view = @import("view.zig");const sys = @import("sys");const text = @import("text/root.zig");pub const Entity = struct { name: []const u8, touches: usize,};pub const File = struct { path: []const u8, touches: usize,};pub const Result = struct { total_commits: usize, time_range: []const u8, entities: []const Entity, files: []const File,};const Range = struct { file: []const u8, start: i64, end: i64, name: []const u8,};const Hunk = struct { commit: []const u8, file: []const u8, start: i64, end: i64,};const Touch = struct { commit: []const u8, name: []const u8,};pub fn compute(allocator: std.mem.Allocator, graph: graph_mod.Graph, root: []const u8, days: i64) !Result { const time_range = try std.fmt.allocPrint(allocator, "last {d} days", .{days}); const since = try std.fmt.allocPrint(allocator, "{d}.days.ago", .{days}); const output = try git.run(allocator, root, &.{ "git", "log", "--unified=0", "--no-color", "-p", "--since", since }, 16 * 1024 * 1024); if (!output.ok) return .{ .total_commits = 0, .time_range = time_range, .entities = &.{}, .files = &.{} }; const ranges = try buildFileIndex(allocator, graph); const hunks = try parseUnifiedDiff(allocator, output.stdout); var commits: std.ArrayList([]const u8) = .empty; var files: std.ArrayList(File) = .empty; var touches: std.ArrayList(Touch) = .empty; for (hunks) |hunk| { try appendUniqueString(allocator, &commits, hunk.commit); try addFileTouch(allocator, &files, hunk.file); for (ranges) |range| { if (!std.mem.eql(u8, range.file, hunk.file)) continue; if (range.start <= hunk.end and hunk.start <= range.end) try appendUniqueTouch(allocator, &touches, .{ .commit = hunk.commit, .name = range.name }); } } var entities: std.ArrayList(Entity) = .empty; for (touches.items) |touch| try addEntityTouch(allocator, &entities, touch.name); std.mem.sort(Entity, entities.items, {}, entityLess); std.mem.sort(File, files.items, {}, fileLess); return .{ .total_commits = commits.items.len, .time_range = time_range, .entities = try entities.toOwnedSlice(allocator), .files = try files.toOwnedSlice(allocator), };}pub fn empty(allocator: std.mem.Allocator, days: i64) !Result { return .{ .total_commits = 0, .time_range = try std.fmt.allocPrint(allocator, "last {d} days", .{days}), .entities = &.{}, .files = &.{}, };}fn buildFileIndex(allocator: std.mem.Allocator, graph: graph_mod.Graph) ![]const Range { var out: std.ArrayList(Range) = .empty; for (try view.allNodes(graph, allocator, null)) |node| { const file = node.file orelse continue; const line = node.line orelse continue; const end = node.end_line orelse line; try out.append(allocator, .{ .file = file, .start = line, .end = end, .name = node.name }); } std.mem.sort(Range, out.items, {}, rangeLess); return try out.toOwnedSlice(allocator);}fn parseUnifiedDiff(allocator: std.mem.Allocator, output: []const u8) ![]const Hunk { var hunks: std.ArrayList(Hunk) = .empty; var current_commit: []const u8 = ""; var current_file: []const u8 = ""; var lines = std.mem.splitScalar(u8, output, '\n'); while (lines.next()) |line| { if (std.mem.startsWith(u8, line, "commit ")) { const raw = text.trim(line[7..]); current_commit = try allocator.dupe(u8, raw[0..@min(raw.len, 12)]); } else if (std.mem.startsWith(u8, line, "+++ b/")) { current_file = try allocator.dupe(u8, line[6..]); } else if (std.mem.startsWith(u8, line, "@@ ") and current_commit.len != 0 and current_file.len != 0) { var parts = std.mem.splitScalar(u8, line, ' '); _ = parts.next(); _ = parts.next(); const new_range = parts.next() orelse continue; if (new_range.len == 0 or new_range[0] != '+') continue; const parsed = try parseRange(new_range[1..]); if (parsed.start <= 0) continue; try hunks.append(allocator, .{ .commit = current_commit, .file = current_file, .start = parsed.start, .end = parsed.start + @max(parsed.count - 1, 0), }); } } return try hunks.toOwnedSlice(allocator);}const ParsedRange = struct { start: i64, count: i64,};fn parseRange(value: []const u8) !ParsedRange { if (std.mem.indexOfScalar(u8, value, ',')) |index| { return .{ .start = try std.fmt.parseInt(i64, value[0..index], 10), .count = try std.fmt.parseInt(i64, value[index + 1 ..], 10), }; } return .{ .start = try std.fmt.parseInt(i64, value, 10), .count = 1 };}fn appendUniqueString(allocator: std.mem.Allocator, list: *std.ArrayList([]const u8), value: []const u8) !void { for (list.items) |item| if (std.mem.eql(u8, item, value)) return; try list.append(allocator, value);}fn appendUniqueTouch(allocator: std.mem.Allocator, list: *std.ArrayList(Touch), value: Touch) !void { for (list.items) |item| { if (std.mem.eql(u8, item.commit, value.commit) and std.mem.eql(u8, item.name, value.name)) return; } try list.append(allocator, value);}fn addFileTouch(allocator: std.mem.Allocator, list: *std.ArrayList(File), path: []const u8) !void { for (list.items) |*item| { if (std.mem.eql(u8, item.path, path)) { item.touches += 1; return; } } try list.append(allocator, .{ .path = path, .touches = 1 });}fn addEntityTouch(allocator: std.mem.Allocator, list: *std.ArrayList(Entity), name: []const u8) !void { for (list.items) |*item| { if (std.mem.eql(u8, item.name, name)) { item.touches += 1; return; } } try list.append(allocator, .{ .name = name, .touches = 1 });}fn rangeLess(_: void, a: Range, b: Range) bool { const file = std.mem.order(u8, a.file, b.file); if (file != .eq) return file == .lt; if (a.start != b.start) return a.start < b.start; return std.mem.lessThan(u8, a.name, b.name);}fn entityLess(_: void, a: Entity, b: Entity) bool { if (a.touches != b.touches) return a.touches > b.touches; return std.mem.lessThan(u8, a.name, b.name);}fn fileLess(_: void, a: File, b: File) bool { if (a.touches != b.touches) return a.touches > b.touches; return std.mem.lessThan(u8, a.path, b.path);}test "parse ranges like python churn parser" { try std.testing.expectEqual(ParsedRange{ .start = 10, .count = 5 }, try parseRange("10,5")); try std.testing.expectEqual(ParsedRange{ .start = 42, .count = 1 }, try parseRange("42"));}test "parse unified diff hunks like python churn parser" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); const hunks = try parseUnifiedDiff(allocator, \\commit aaa111 \\diff --git a/a.py b/a.py \\--- a/a.py \\+++ b/a.py \\@@ -1,2 +1,3 @@ something \\@@ -20,1 +21,1 @@ other \\commit bbb222 \\diff --git a/b.py b/b.py \\--- a/b.py \\+++ b/b.py \\@@ -5,0 +5,2 @@ thing \\ ); try std.testing.expectEqual(@as(usize, 3), hunks.len); try std.testing.expectEqualStrings("aaa111", hunks[0].commit); try std.testing.expectEqualStrings("a.py", hunks[0].file); try std.testing.expectEqual(@as(i64, 1), hunks[0].start); try std.testing.expectEqual(@as(i64, 3), hunks[0].end); try std.testing.expectEqualStrings("aaa111", hunks[1].commit); try std.testing.expectEqualStrings("a.py", hunks[1].file); try std.testing.expectEqualStrings("bbb222", hunks[2].commit); try std.testing.expectEqualStrings("b.py", hunks[2].file);}test "churn deduplicates entity hunks per commit" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const allocator = arena.allocator(); var graph = graph_mod.init(allocator); try graph_mod.addNode(&graph, .{ .name = "mod.foo", .type = "function", .file = "a.py", .line = 1, .end_line = 15 }); const ranges = try buildFileIndex(allocator, graph); const hunks = try parseUnifiedDiff(allocator, \\commit aaa111 \\diff --git a/a.py b/a.py \\--- a/a.py \\+++ b/a.py \\@@ -2,1 +2,2 @@ def foo(): \\@@ -8,1 +9,2 @@ def foo(): \\commit bbb222 \\diff --git a/a.py b/a.py \\--- a/a.py \\+++ b/a.py \\@@ -5,1 +6,2 @@ def foo(): \\ ); var touches: std.ArrayList(Touch) = .empty; for (hunks) |hunk| { for (ranges) |range| { if (std.mem.eql(u8, range.file, hunk.file) and range.start <= hunk.end and hunk.start <= range.end) { try appendUniqueTouch(allocator, &touches, .{ .commit = hunk.commit, .name = range.name }); } } } var entities: std.ArrayList(Entity) = .empty; for (touches.items) |touch| try addEntityTouch(allocator, &entities, touch.name); try std.testing.expectEqual(@as(usize, 1), entities.items.len); try std.testing.expectEqualStrings("mod.foo", entities.items[0].name); try std.testing.expectEqual(@as(usize, 2), entities.items[0].touches);}test "compute maps git hunks to graph entities" { 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-churn-test-{x}-{x}", .{ @as(u64, @intCast(@max(0, sys.time.realMilliTimestamp()))), @intFromPtr(&arena) }); defer sys.fs.deleteTree(root) catch {}; try sys.fs.createDirPath(try std.fs.path.join(allocator, &.{ root, "src" })); try expectGit(allocator, root, &.{ "git", "init", "-q" }); try expectGit(allocator, root, &.{ "git", "config", "user.email", "test@example.com" }); try expectGit(allocator, root, &.{ "git", "config", "user.name", "Test User" }); const path = try std.fs.path.join(allocator, &.{ root, "src", "app.py" }); try text.writeFile(path, \\def hot(): \\ value = 1 \\ return value \\ \\def cold(): \\ return 0 \\ ); try expectGit(allocator, root, &.{ "git", "add", "src/app.py" }); try expectGit(allocator, root, &.{ "git", "commit", "-q", "-m", "initial" }); try text.writeFile(path, \\def hot(): \\ value = 2 \\ extra = value + 1 \\ return extra \\ \\def cold(): \\ return 0 \\ ); try expectGit(allocator, root, &.{ "git", "add", "src/app.py" }); try expectGit(allocator, root, &.{ "git", "commit", "-q", "-m", "touch hot" }); var graph = graph_mod.init(allocator); try graph_mod.addNode(&graph, .{ .name = "src.app.hot", .type = "function", .file = "src/app.py", .line = 1, .end_line = 4 }); try graph_mod.addNode(&graph, .{ .name = "src.app.cold", .type = "function", .file = "src/app.py", .line = 6, .end_line = 7 }); const result = try compute(allocator, graph, root, 90); try std.testing.expectEqual(@as(usize, 2), result.total_commits); try std.testing.expectEqualStrings("last 90 days", result.time_range); try std.testing.expectEqual(@as(usize, 2), result.entities.len); try std.testing.expectEqualStrings("src.app.hot", result.entities[0].name); try std.testing.expectEqual(@as(usize, 2), result.entities[0].touches); try std.testing.expectEqualStrings("src.app.cold", result.entities[1].name); try std.testing.expectEqual(@as(usize, 1), result.entities[1].touches); try std.testing.expectEqual(@as(usize, 1), result.files.len); try std.testing.expectEqualStrings("src/app.py", result.files[0].path); try std.testing.expectEqual(@as(usize, 2), result.files[0].touches);}fn expectGit(allocator: std.mem.Allocator, root: []const u8, argv: []const []const u8) !void { const output = try git.run(allocator, root, argv, 1024 * 1024); try std.testing.expect(output.ok);}Source: tools/smg/src/root.zig:15
zig
pub const churn = @import("churn.zig");Complete call list for churn.compute
7 direct calls.
tools.smg.src.churn.addEntityTouch[function] — private; no exact target attools/smg/src/churn.zig:162in nearest public ownertiny.smg.churntools.smg.src.churn.addFileTouch[function] — private; no exact target attools/smg/src/churn.zig:152in nearest public ownertiny.smg.churntools.smg.src.churn.appendUniqueString[function] — private; no exact target attools/smg/src/churn.zig:140in nearest public ownertiny.smg.churntools.smg.src.churn.appendUniqueTouch[function] — private; no exact target attools/smg/src/churn.zig:145in nearest public ownertiny.smg.churntools.smg.src.churn.buildFileIndex[function] — private; no exact target attools/smg/src/churn.zig:83in nearest public ownertiny.smg.churntools.smg.src.churn.parseUnifiedDiff[function] — private; no exact target attools/smg/src/churn.zig:95in nearest public ownertiny.smg.churntiny.smg.git.run[function] attools/smg/src/git.zig:9
Audit
| Definitions | 6 |
|---|---|
| Public names | 6 |
| Members | 8 |
| Version | 26.7.0 |
| Revision | daab053ee433 |