Skip to documentation
SLOP

tiny.smg.churn

Reference tiny.smg churn

Defined in tiny.smg.

API (5)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.smgchurn
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallstest; no linktools.smg.src.churntest: compute maps git hunks to graph...private; no linktools.smg.src.churnaddEntityTouchprivate; no linktools.smg.src.churnaddFileTouchprivate; no linktools.smg.src.churnappendUniqueStringprivate; no linktools.smg.src.churnappendUniqueTouchprivate; no linktools.smg.src.churnbuildFileIndex+2 morechurncompute
Static calls · unresolved targets: 2 · external targets: 0.

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.

Audit

Definitions6
Public names6
Members8
Version26.7.0
Revisiondaab053ee433