669fea798465

feat: Show release changelog in nina update (v3.1.6)

time
2026-06-11 15:44 UTC · 4 months ago
machine
koh2
parent
a178ca844c84
4 files VERSION+1 −1build.zig.zon+1 −1src/commands.zig+67 −16src/output.zig+60 −13
VERSION+1 −1
@@ -1 +1 @@1-3.1.51+3.1.6

view file

build.zig.zon+1 −1
@@ -9,7 +9,7 @@99     .name = .nina,1010     // This is a [Semantic Version](https://semver.org/).1111     // In a future version of Zig it will be used for package deduplication.12-    .version = "3.1.5",12+    .version = "3.1.6",1313     // Together with name, this represents a globally unique package1414     // identifier. This field is generated by the Zig toolchain when the1515     // package is first created, and then *never changes*. This allows

view file

src/commands.zig+67 −16
@@ -19,15 +19,15 @@1919     environ: *const std.process.Environ.Map,2020     machine: types.Machine,2121     cfg: types.NinaConfig,22-    args: []const []const u8,   // positional args after subcommand23-    sub: []const u8,            // subcommand (e.g. "list" in "service list")22+    args: []const []const u8, // positional args after subcommand23+    sub: []const u8, // subcommand (e.g. "list" in "service list")2424     dry: bool,2525     check: bool,2626     no_apply: bool,2727     all: bool,2828     last: u32,2929     json: bool,30-    user: bool,            // --user: operate on user (vs system) services30+    user: bool, // --user: operate on user (vs system) services3131 };3232 3333 // A system is flake-managed when the config says so OR a flake.nix is present in@@ -87,7 +87,9 @@8787     defer ctx.gpa.free(gens);8888 8989     var current: u32 = 0;90-    for (gens) |g| if (g.current) { current = g.number; };90+    for (gens) |g| if (g.current) {91+        current = g.number;92+    };9193 9294     // Newest generation older than current. If there is none, we're already at the9395     // oldest — stop cleanly instead of letting rollback fail with 'build failed'.@@ -318,12 +320,31 @@318320     defer parsed.deinit();319321 320322     const root = parsed.value;321-    if (root != .object) { output.printWarning("malformed release metadata"); return; }323+    if (root != .object) {324+        output.printWarning("malformed release metadata");325+        return;326+    }322327 323328     const remote_ver = blk: {324-        const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };325-        break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };329+        const v = root.object.get("version") orelse {330+            output.printWarning("malformed release metadata");331+            return;332+        };333+        break :blk switch (v) {334+            .string => |s| s,335+            else => {336+                output.printWarning("malformed release metadata");337+                return;338+            },339+        };326340     };341+    const release_notes = blk: {342+        const v = root.object.get("release_notes") orelse break :blk "";343+        break :blk switch (v) {344+            .string => |s| s,345+            else => "",346+        };347+    };327348 328349     if (std.mem.eql(u8, remote_ver, version.VERSION)) {329350         output.updateSelfCurrent(version.VERSION);@@ -331,6 +352,7 @@331352     }332353 333354     output.updateSelfAvailable(version.VERSION, remote_ver);355+    output.updateSelfReleaseNotes(release_notes);334356     if (ctx.check) return;335357 336358     var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;@@ -369,18 +391,42 @@369391         if (pv != .object) break :blk null;370392         break :blk pv.object.get(version.PLATFORM);371393     };372-    if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }394+    if (plat_info == null) {395+        output.updateSelfNoPlatform(version.PLATFORM);396+        return;397+    }373398 374399     const dl_url = blk: {375-        const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };376-        break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };400+        const v = plat_info.?.object.get("url") orelse {401+            output.printWarning("malformed release metadata");402+            return;403+        };404+        break :blk switch (v) {405+            .string => |s| s,406+            else => {407+                output.printWarning("malformed release metadata");408+                return;409+            },410+        };377411     };378412 379413     const raw_cksum = blk: {380414         if (plat_info.?.object.get("blake3")) |v|381-            break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };415+            break :blk switch (v) {416+                .string => |s| s,417+                else => {418+                    output.printWarning("malformed release metadata");419+                    return;420+                },421+            };382422         if (plat_info.?.object.get("checksum")) |v|383-            break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };423+            break :blk switch (v) {424+                .string => |s| s,425+                else => {426+                    output.printWarning("malformed release metadata");427+                    return;428+                },429+            };384430         output.printWarning("missing checksum in release metadata");385431         return;386432     };@@ -397,7 +443,10 @@397443 398444     output.updateSelfVerifying();399445     exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {400-        error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },446+        error.ChecksumMismatch => {447+            output.updateSelfChecksumFailed();448+            return;449+        },401450         else => return e,402451     };403452 @@ -1278,8 +1327,10 @@12781327                             const desc = if (e2.value_ptr.* == .object)12791328                                 if (e2.value_ptr.object.get("description")) |d|12801329                                     if (d == .string) d.string else ""1281-                                else ""1282-                            else "";1330+                                else1331+                                    ""1332+                            else1333+                                "";12831334                             output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);12841335                         }12851336                     }@@ -1667,7 +1718,7 @@16671718     const md = yd.calculateMonthDay();16681719     const ds = es.getDaySeconds();16691720     return std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}Z", .{1670-        yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,1721+        yd.year,              md.month.numeric(),      @as(u32, md.day_index) + 1,16711722         ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),16721723     }) catch return fallback;16731724 }

view file

src/output.zig+60 −13
@@ -25,6 +25,7 @@2525 const KAO_CONTENT = "( ´ ∀ ` )  "; // clean freed2626 const KAO_HAPPY = "( ´ ▽ ` )  "; // doctor all clear / mood all good2727 const KAO_SLEEPY = "(˘ω˘ )  "; // try / develop exit28+const KAO_READY = "( •̀ᴗ•́ )  "; // update start2829 const KAO_SAD = "(;ω;)  "; // network or ssh errors only — never user typos2930 3031 // Exported for TUI use (search.zig references these to avoid \x1b in that file)@@ -638,14 +639,14 @@638639     }639640     p("\n", .{});640641     const quick = [_][2][]const u8{641-        .{ "apply",         "rebuild and switch" },642-        .{ "search <q>",    "find packages" },642+        .{ "apply", "rebuild and switch" },643+        .{ "search <q>", "find packages" },643644         .{ "install <pkg>", "add to profile or system" },644-        .{ "status",        "machine health" },645-        .{ "back",          "roll back one generation" },646-        .{ "home apply",    "apply home config" },647-        .{ "home back",     "roll back home config" },648-        .{ "home history",  "list home generations" },645+        .{ "status", "machine health" },646+        .{ "back", "roll back one generation" },647+        .{ "home apply", "apply home config" },648+        .{ "home back", "roll back home config" },649+        .{ "home history", "list home generations" },649650     };650651     for (quick) |cmd| {651652         p("   {s:<16}  {s}{s}{s}\n", .{ cmd[0], c(DIM), cmd[1], c(RESET) });@@ -696,9 +697,9 @@696697         .{ "pin", "pin flake inputs" },697698         .{ "hash", "compute nix hash" },698699         .{ "fetch", "fetch and hash a url" },699-        .{ "goodbye",       "uninstall nina" },700-        .{ "hello",        "list machines" },701-        .{ "help",         "this message" },700+        .{ "goodbye", "uninstall nina" },701+        .{ "hello", "list machines" },702+        .{ "help", "this message" },702703     };703704     for (cmds) |cmd| {704705         p("   {s:<18}  {s}{s}{s}\n", .{ cmd[0], c(DIM), cmd[1], c(RESET) });@@ -756,7 +757,7 @@756757 // --- Self-update (nina update) ---757758 758759 pub fn updateSelfStart() void {759-    p("{s}checking for updates\n", .{hdr()});760+    p("{s}{s}checking for updates\n", .{ KAO_READY, hdr() });760761 }761762 762763 pub fn updateSelfCurrent(ver: []const u8) void {@@ -764,8 +765,54 @@764765 }765766 766767 pub fn updateSelfAvailable(current: []const u8, latest: []const u8) void {767-    p("{s}current  {s}\n", .{ step(), current });768-    p("{s}latest   {s}{s}{s}\n", .{ step(), c(BOLD), latest, c(RESET) });768+    p("{s}update available\n", .{hdr()});769+    p("{s}current  {s}{s}{s}\n", .{ step(), c(DIM), current, c(RESET) });770+    p("{s}latest   {s}{s}{s}{s}\n", .{ step(), c(BOLD), c(GREEN), latest, c(RESET) });771+}772+773+fn releaseNotesHeading(line: []const u8) []const u8 {774+    var i: usize = 0;775+    while (i < line.len and line[i] == '#') : (i += 1) {}776+    return std.mem.trim(u8, line[i..], " \t");777+}778+779+fn releaseNotesBullet(line: []const u8) ?[]const u8 {780+    if (line.len < 2) return null;781+    const marker = line[0];782+    if ((marker == '-' or marker == '*' or marker == '+') and line[1] == ' ') return line[2..];783+    if (std.ascii.isDigit(marker)) {784+        var i: usize = 1;785+        while (i < line.len and std.ascii.isDigit(line[i])) : (i += 1) {}786+        if (i + 1 < line.len and line[i] == '.' and line[i + 1] == ' ') return line[i + 2 ..];787+    }788+    return null;789+}790+791+pub fn updateSelfReleaseNotes(notes: []const u8) void {792+    const trimmed = std.mem.trim(u8, notes, " \t\r\n");793+    if (trimmed.len == 0) return;794+795+    p("{s}changelog\n", .{hdr()});796+    var lines = std.mem.splitScalar(u8, trimmed, '\n');797+    while (lines.next()) |raw_line| {798+        const line = if (raw_line.len > 0 and raw_line[raw_line.len - 1] == '\r') raw_line[0 .. raw_line.len - 1] else raw_line;799+        const text = std.mem.trim(u8, line, " \t");800+        if (text.len == 0) {801+            p("\n", .{});802+            continue;803+        }804+        if (text[0] == '#') {805+            const title = releaseNotesHeading(text);806+            if (title.len == 0) continue;807+            p("   {s}{s}{s}{s}\n", .{ c(BOLD), c(CYAN), title, c(RESET) });808+            continue;809+        }810+        if (releaseNotesBullet(text)) |item| {811+            p("   {s}- {s}{s}\n", .{ c(CYAN), item, c(RESET) });812+            continue;813+        }814+        p("   {s}\n", .{text});815+    }769816 }770817 771818 pub fn updateSelfDownloading(ver: []const u8, platform: []const u8) void {

view file