feat: list cmd.

This commit is contained in:
2026-05-27 18:41:03 -04:00
parent 336bd37613
commit aedbeb28bd
3 changed files with 89 additions and 28 deletions

View File

@@ -7,7 +7,7 @@ const age = @import("age.zig");
const Config = @import("Config.zig");
/// controls the keys and filepaths used for saving
config: Config,
opts: OpenOptions,
/// The underlying data store.
sql_db: sqlite.Db,
@@ -54,7 +54,7 @@ pub fn open(
}
}
return open_decrypted(opts.config, tmp_db_path);
return open_decrypted(opts, tmp_db_path);
}
const OpenOptions = struct {
@@ -68,7 +68,7 @@ const OpenOptions = struct {
};
/// Create a new instance of the database
fn open_decrypted(config: Config, tmp_db_path: [:0]const u8) !@This() {
fn open_decrypted(opts: OpenOptions, tmp_db_path: [:0]const u8) !@This() {
var db = try sqlite.Db.init(.{
.mode = .{ .File = tmp_db_path },
.open_flags = .{
@@ -89,7 +89,7 @@ fn open_decrypted(config: Config, tmp_db_path: [:0]const u8) !@This() {
return .{
.sql_db = db,
.config = config,
.opts = opts,
};
}
@@ -119,28 +119,27 @@ pub fn close(
self: *@This(),
io: std.Io,
gpa: std.mem.Allocator,
opts: OpenOptions,
) !void {
defer self.sql_db.deinit();
if (self.changed) {
const tmp_db_path = try std.fs.path.join(gpa, &.{ opts.tmp, "envr.db" });
const tmp_db_path = try std.fs.path.join(gpa, &.{ self.opts.tmp, "envr.db" });
defer gpa.free(tmp_db_path);
try self.sql_db.exec("VACUUM INTO ?", .{}, .{tmp_db_path});
const db_path = try std.fs.path.join(gpa, &.{ opts.home, ".envr", "data.age" });
const db_path = try std.fs.path.join(gpa, &.{ self.opts.home, ".envr", "data.age" });
defer gpa.free(db_path);
{
// TODO: Use std.MultiArrayList? Had json issues
var public_keys: std.ArrayList([]const u8) = try .initCapacity(
gpa,
opts.config.keys.len,
self.opts.config.keys.len,
);
defer public_keys.deinit(gpa);
for (opts.config.keys) |key| {
for (self.opts.config.keys) |key| {
public_keys.appendAssumeCapacity(key.private);
}
@@ -153,7 +152,7 @@ pub fn close(
/// Returns a list of all the .env files present in the database.
/// The caller is responsible for freeing memory
fn list(self: *@This(), gpa: std.mem.Allocator) ![]EnvFile {
pub fn list(self: *@This(), gpa: std.mem.Allocator) ![]EnvFile {
var stmt = try self.sql_db.prepare(
"select path, remotes, sha256, contents from envr_env_files",
);
@@ -284,7 +283,7 @@ test "Closing a fresh database does not create a file" {
tmp_dir.dir.access(io, db_path, .{ .read = true }),
);
try db.close(io, gpa, .{ .home = home, .tmp = tmp });
try db.close(io, gpa);
try std.testing.expectError(
error.FileNotFound,
@@ -469,5 +468,5 @@ test "list displays the database's keys" {
try std.testing.expectEqualStrings(&std.fmt.bytesToHex(&hash, .lower), file.sha256);
}
try db.close(io, gpa, .{ .home = home, .tmp = tmp });
try db.close(io, gpa);
}

View File

@@ -2,7 +2,6 @@ const std = @import("std");
const Io = std.Io;
const config = @import("config");
const comma = @import("comma");
const envr = @import("envr");
@@ -51,6 +50,20 @@ fn run(
environ_map.get("PATH").?,
);
},
.list => {
var stdout_buffer: [1024]u8 = undefined;
var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
const stdout_writer = &stdout_file_writer.interface;
return envr.list(
io,
arena,
stdout_writer,
environ_map.get("HOME").?,
// TODO: Don't hardcode this?
"/tmp",
);
},
.unknown => {
return fallback_to_go(io, arena, args);
},
@@ -137,10 +150,10 @@ fn fallback_to_go(
test "simple test" {
const gpa = std.testing.allocator;
var list: std.ArrayList(i32) = .empty;
defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
try list.append(gpa, 42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
var alist: std.ArrayList(i32) = .empty;
defer alist.deinit(gpa); // Try commenting this out and see if zig detects the memory leak!
try alist.append(gpa, 42);
try std.testing.expectEqual(@as(i32, 42), alist.pop());
}
test "fuzz example" {
@@ -152,23 +165,23 @@ fn testOne(context: void, smith: *std.testing.Smith) !void {
// Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case!
const gpa = std.testing.allocator;
var list: std.ArrayList(u8) = .empty;
defer list.deinit(gpa);
var alist: std.ArrayList(u8) = .empty;
defer alist.deinit(gpa);
while (!smith.eos()) switch (smith.value(enum { add_data, dup_data })) {
.add_data => {
const slice = try list.addManyAsSlice(gpa, smith.value(u4));
const slice = try alist.addManyAsSlice(gpa, smith.value(u4));
smith.bytes(slice);
},
.dup_data => {
if (list.items.len == 0) continue;
if (list.items.len > std.math.maxInt(u32)) return error.SkipZigTest;
const len = smith.valueRangeAtMost(u32, 1, @min(32, list.items.len));
const off = smith.valueRangeAtMost(u32, 0, @intCast(list.items.len - len));
try list.appendSlice(gpa, list.items[off..][0..len]);
if (alist.items.len == 0) continue;
if (alist.items.len > std.math.maxInt(u32)) return error.SkipZigTest;
const len = smith.valueRangeAtMost(u32, 1, @min(32, alist.items.len));
const off = smith.valueRangeAtMost(u32, 0, @intCast(alist.items.len - len));
try alist.appendSlice(gpa, alist.items[off..][0..len]);
try std.testing.expectEqualSlices(
u8,
list.items[off..][0..len],
list.items[list.items.len - len ..],
alist.items[off..][0..len],
alist.items[alist.items.len - len ..],
);
},
};

View File

@@ -5,6 +5,9 @@ const Io = std.Io;
const comma = @import("comma");
const Command = comma.Command;
const Config = @import("Config.zig");
const Db = @import("Db.zig");
pub const root: Command = .new(.{
.name = "envr",
.short = "Manage your .env files.",
@@ -49,6 +52,10 @@ pub const root: Command = .new(.{
\\ The deps command reports which binaries are available and which are not."
,
},
.{
.name = "list",
.short = "View your tracked files",
},
.{
.name = "version",
.short = "Show envr's version",
@@ -56,17 +63,59 @@ pub const root: Command = .new(.{
},
});
pub fn list(
io: Io,
arena: std.mem.Allocator,
out: *std.Io.Writer,
home: []const u8,
tmp: []const u8,
) !void {
// TODO: Don't hardcode
const cfgPath = try std.fs.path.join(arena, &.{ home, ".envr", "config.json" });
const cfg: Config = (try Config.load(io, arena, cfgPath)).value;
var db: Db = try .open(io, arena, .{
.config = cfg,
.home = home,
.tmp = tmp,
});
_ = try out.write("Path\n");
const files = try db.list(arena);
for (files) |file| {
// TODO: Table printer
try out.print("{s}\n", .{file.path});
}
try out.flush();
return db.close(io, arena); // TODO: Defer this
}
test {
std.testing.refAllDecls(@import("Config.zig"));
std.testing.refAllDecls(@import("Db.zig"));
}
test "enum type" {
const got: root.Type = @enumFromInt(2);
const got: root.Type = @enumFromInt(3);
try std.testing.expectEqual(.version, got);
}
test "parse deps" {
const args = &[_][]const u8{"deps"};
const cmd = root.parse(args);
try std.testing.expectEqual(.deps, cmd);
}
test "parse list" {
const args = &[_][]const u8{"list"};
const cmd = root.parse(args);
try std.testing.expectEqual(.list, cmd);
}
test "parse version" {
const args = &[_][]const u8{"version"};
const cmd = root.parse(args);