authorgravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-05-28 14:18:16 -07:00
committergravatar for hello@nektro.netMeghan Denny <hello@nektro.net> 2026-05-28 14:18:16 -07:00
logf35b2ecf00c0ed2b2bc8d2da8ce367765db0a2e3
tree6da0541a0e1b5a2202cd3e86937a10b0a0169ee7
parentcedfb308d15faefbe034d2b4cd88438674e4c2d3
signaturebadge-check Signed by SSH key SHA256:4hHJbtBRU58AYXwjL7fkz2fnQHdiye8x1QpTCQ0sHNw

cmd: add the 'explain' command


7 files changed, 242 insertions(+), 0 deletions(-)

.github/workflows/push.yml+4
......@@ -59,6 +59,10 @@ jobs:
5959 - run: ./test_repo.sh https://github.com/nektro/zigmod-test-local-dep
6060 - run: ./test_repo.sh https://github.com/nektro/zigmod-test-c-code
6161
62 - run: git diff <(cat __snapshots__/explain.tree.txt) <(zigmod explain --locked --format tree)
63 - run: git diff <(cat __snapshots__/explain.mermaid.txt) <(zigmod explain --locked --format mermaid)
64 - run: git diff <(cat __snapshots__/explain.dot.txt) <(zigmod explain --locked --format dot)
65
6266 # Github Release
6367 - run: ./changelog.sh
6468 - run: ./make_release.sh ${{ secrets.GITHUB_TOKEN }}
__snapshots__/explain.dot.txt created+33
......@@ -0,0 +1,33 @@
1digraph {
2 "root" -> "zigmod";
3 "root" -> "win32";
4 "root" -> "extras";
5 "root" -> "ansi";
6 "zigmod" -> "yaml";
7 "zigmod" -> "ansi";
8 "zigmod" -> "known-folders";
9 "zigmod" -> "licenses";
10 "zigmod" -> "zfetch";
11 "zigmod" -> "detect-license";
12 "zigmod" -> "inquirer";
13 "zigmod" -> "ini";
14 "zigmod" -> "time";
15 "zigmod" -> "extras";
16 "zigmod" -> "git";
17 "zigmod" -> "json";
18 "yaml" -> "extras";
19 "zfetch" -> "hzzp";
20 "zfetch" -> "iguanaTLS";
21 "detect-license" -> "licenses-text";
22 "detect-license" -> "leven";
23 "inquirer" -> "ansi";
24 "time" -> "extras";
25 "git" -> "time";
26 "git" -> "extras";
27 "git" -> "tracer";
28 "tracer" -> "extras";
29 "json" -> "extras";
30 "json" -> "tracer";
31 "json" -> "intrusive-parser";
32 "intrusive-parser" -> "extras";
33}
__snapshots__/explain.mermaid.txt created+32
......@@ -0,0 +1,32 @@
1graph TD;
2 root-->zigmod;
3 root-->win32;
4 root-->extras;
5 root-->ansi;
6 zigmod-->yaml;
7 zigmod-->ansi;
8 zigmod-->known-folders;
9 zigmod-->licenses;
10 zigmod-->zfetch;
11 zigmod-->detect-license;
12 zigmod-->inquirer;
13 zigmod-->ini;
14 zigmod-->time;
15 zigmod-->extras;
16 zigmod-->git;
17 zigmod-->json;
18 yaml-->extras;
19 zfetch-->hzzp;
20 zfetch-->iguanaTLS;
21 detect-license-->licenses-text;
22 detect-license-->leven;
23 inquirer-->ansi;
24 time-->extras;
25 git-->time;
26 git-->extras;
27 git-->tracer;
28 tracer-->extras;
29 json-->extras;
30 json-->tracer;
31 json-->intrusive-parser;
32 intrusive-parser-->extras;
__snapshots__/explain.tree.txt created+35
......@@ -0,0 +1,35 @@
1root
2 zigmod
3 yaml
4
5 extras
6 ansi
7 known-folders
8 licenses
9 zfetch
10 hzzp
11 iguanaTLS
12 detect-license
13 licenses-text
14 leven
15 inquirer
16 ansi
17 ini
18 time
19 extras
20 extras
21 git
22 time
23 extras
24 extras
25 tracer
26 extras
27 json
28 extras
29 tracer
30 extras
31 intrusive-parser
32 extras
33 win32
34 extras
35 ansi
docs/commands/explain.md created+37
......@@ -0,0 +1,37 @@
1## `explain` command
2
3```
4zigmod explain
5```
6
7Use this command to create a visual description of your dependency graph.
8
9## `--locked`
10
11Pass this to use the dependency versions from `zigmod.lock` instead of your current `.zigmod` folder.
12
13## `--format`
14
15May be followed by one of the following values:
16
17- `tree`
18- `mermaid`
19- `dot`
20
21## `--format tree`
22
23<img width="1077" height="866" alt="Image" src="https://github.com/user-attachments/assets/98d41e63-f5bd-4c7d-a5a3-272b9499d925" />
24
25## `--format mermaid`
26
27https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams
28
29<img width="1173" height="373" alt="Image" src="https://github.com/user-attachments/assets/0ad012b2-cfc9-4a7a-b3fa-9f5fefb969ab" />
30
31## `--format dot`
32
33This format prints output compatible with the `dot` program from https://graphviz.org/.
34
35Run `dot -Tpng <(zigmod explain --locked --format dot) -o explain.png` to generate the image.
36
37<img width="1781" height="443" alt="Image" src="https://github.com/user-attachments/assets/217c5855-537e-4a93-ba49-b5fe6f78cdd2" />
src/cmd/explain.zig created+100
......@@ -0,0 +1,100 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const extras = @import("extras");
4
5const zigmod = @import("../lib.zig");
6const u = @import("./../util/funcs.zig");
7const common = @import("./../common.zig");
8
9//
10//
11
12pub fn execute(self_name: []const u8, args: [][:0]u8) !void {
13 _ = self_name;
14
15 const gpa = std.heap.c_allocator;
16 const cachepath = try u.find_cachepath();
17 const dir = std.fs.cwd();
18 const should_lock = args.len >= 1 and std.mem.eql(u8, args[0], "--locked");
19 const format_i: usize = if (should_lock) 1 else 0;
20 const Format = enum {
21 tree,
22 mermaid,
23 dot,
24 };
25 const format_s = if (args.len >= 1 + format_i and std.mem.eql(u8, args[format_i], "--format")) args[format_i + 1] else "";
26 const format = std.meta.stringToEnum(Format, format_s) orelse u.fail("unrecognized --format: {s}", .{format_s});
27
28 var options = common.CollectOptions{
29 .log = false,
30 .update = false,
31 .alloc = gpa,
32 .lock = if (should_lock) try common.parse_lockfile(gpa, dir) else null,
33 };
34 const top_module = try common.collect_deps_deep(cachepath, dir, &options);
35
36 var seencache = std.ArrayList([48]u8).init(gpa);
37 defer seencache.deinit();
38
39 const stdout = std.io.getStdOut();
40 const w = stdout.writer();
41
42 switch (format) {
43 .tree => {
44 try printTree(w, top_module, 0);
45 },
46 .mermaid => {
47 try w.writeAll("graph TD;\n");
48 try printMermaid(w, top_module, &seencache);
49 },
50 .dot => {
51 try w.writeAll("digraph {\n");
52 try printDot(w, top_module, &seencache);
53 try w.writeAll("}\n");
54 },
55 }
56}
57
58fn printTree(writer: anytype, module: zigmod.Module, depth: u16) !void {
59 try writer.writeByteNTimes('\t', depth);
60 try writer.writeAll(module.name);
61 try writer.writeByte('\n');
62
63 for (module.deps) |dep| {
64 try printTree(writer, dep, depth + 1);
65 }
66}
67
68fn printMermaid(writer: anytype, module: zigmod.Module, seencache: *std.ArrayList([48]u8)) !void {
69 for (seencache.items) |item| {
70 if (std.mem.eql(u8, &module.id, &item)) {
71 return;
72 }
73 }
74 try seencache.append(module.id);
75
76 for (module.deps) |dep| {
77 if (dep.name.len == 0) continue;
78 try writer.print(" {s}-->{s};\n", .{ module.name, dep.name });
79 }
80 for (module.deps) |dep| {
81 try printMermaid(writer, dep, seencache);
82 }
83}
84
85fn printDot(writer: anytype, module: zigmod.Module, seencache: *std.ArrayList([48]u8)) !void {
86 for (seencache.items) |item| {
87 if (std.mem.eql(u8, &module.id, &item)) {
88 return;
89 }
90 }
91 try seencache.append(module.id);
92
93 for (module.deps) |dep| {
94 if (dep.name.len == 0) continue;
95 try writer.print(" \"{s}\" -> \"{s}\";\n", .{ module.name, dep.name });
96 }
97 for (module.deps) |dep| {
98 try printDot(writer, dep, seencache);
99 }
100}
src/lib.zig+1
......@@ -13,6 +13,7 @@ pub const commands = struct {
1313 pub const zpm = @import("./cmd/zpm.zig");
1414 pub const license = @import("./cmd/license.zig");
1515 pub const generate = @import("./cmd/generate.zig");
16 pub const explain = @import("./cmd/explain.zig");
1617};
1718
1819pub fn init() !void {