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
↓ nina-669fea79.tar.gzatlas at this save
files changed4 files
VERSION+1 −1build.zig.zon+1 −1src/commands.zig+67 −16src/output.zig+60 −13build.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 allowssrc/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 }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 {