1 Commits

Author SHA1 Message Date
7ccc757dfa refactor(ssh): Cleaned up. 2026-06-22 11:39:13 -04:00
18 changed files with 281 additions and 324 deletions

View File

@@ -2,41 +2,61 @@
1. Commands are still leaking.
2. Add color flag and support non colored output.
2. **db.odin** — Inconsistencies in how struct vs sqlite are named.
3. Rewrite `write_command_help` to use text/tables
3. Add color flag and support non colored output.
4. Generate md and man pages again.
4. Use text/tables for command output
5. Json may be an expensive encoding for remotes. Confirm with spall, and use null terminated strings if necessary.
5. Generate md and man pages again.
6. Make sure official path separators are used when appropriate, rather than '/'.
6. **db.odin:324-327** — Map iteration (`remote_set`) is non-deterministic. Same file can produce different JSON on each backup, causing spurious DB diffs. Sort remotes before storing.
7. Consistently ignore allocator errors
7. Make sure official path separators are used when appropriate, rather than '/'.
8. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
8. **cmd_restore.odin:20-30 & cmd_remove.odin:19-29** — Identical path-resolution block copy-pasted. `is_abs` guard is redundant since `filepath.abs` is a no-op on absolute paths. Extract a helper.
9. Add a text filter to the multi_select.
9. **cmd_restore.odin:44**`os.mkdir_all` error silently discarded. Subsequent write failure will be confusing.
10. Add tests for untested commands.
10. **config.odin:178**`search_paths` silently ignores `os.user_home_dir` error. If home is empty, `~` isn't expanded. Same class of bug as issue 3.
11. add --format -f flag to commands that draw tables.
11. **db.odin:115**`json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data.
12. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
12. **db.odin:352-353**`hex.encode` error ignored. `string(hex_bytes)` aliases the byte slice.
13. procedures should be ordered by use, main at the top, then in the order they are called from main.
13. **cmd_sync.odin:80, cmd_list.odin:33**`make([]string, 2)` for table rows never freed. Leaks per row. Defer to memory pass.
14. Shell completion
14. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
15. Bring back windows support / cross-compilation.
15. Add a text filter to the multi_select.
16. Test all cmds / terminal branches.
16. Add tests for untested commands.
17. Fix error messages to use fmt.eprintf (stderr) instead of fmt.printf (stdout)
17. 2 scan tests silently skip when fd isn't installed, tests pass without actually testing anything. These should use #assert to be sure that fd is in path.
18. Pass allocator to findr?
18. add --format -f flag to commands that draw tables.
19. Update `read_wire_string` to use a slice.
19. Replace `testing.expect` calls with `testing.expect_value` calls where appropriate.
20. Change struct field names from PascalCase to snake_case.
21. procedures should be ordered by use, main at the top, then in the order they are called from main.
22. Shell completion
23. Bring back windows support / cross-compilation.
24. Test all cmds / terminal branches.
25. Replace `fmt.tprintf("/tmp/envr-test-...-%d", os.get_pid())` + `os.mkdir_all` in test files with `os.mkdir_temp` (race-free, honors `$TMPDIR`, matches `findr/test_env.odin` pattern).
26. Adopt `core:log` across `db.odin`, `crypto.odin`, `config.odin`, `ssh.odin` — replace ~30 scattered `fmt.printf("Error ...")` calls with leveled logging for consistent stderr routing and source locations.
27. "Encryption failed" in tests.
28. Pass allocator to findr?
29. Update `read_wire_string` to use a slice.
## Double-check AI output

View File

@@ -5,7 +5,6 @@ import "core:fmt"
import "core:io"
import "core:os"
import "core:strings"
import "core:text/table"
Command :: struct {
name: string,
@@ -254,47 +253,56 @@ at before, restore your backup with:
%senvr%s [command]
%sAvailable Commands:%s
`,
COLOR_HEADINGS,
ANSI_RESET,
COLOR_FLAGS,
ANSI_RESET,
COLOR_HEADINGS,
ANSI_RESET,
flush = false,
)
tbl: table.Table
table.init(&tbl, context.temp_allocator, context.temp_allocator)
table.padding(&tbl, 2, 0)
table.caption(&tbl, "Available Commands:")
for c in COMMANDS {
name := c.name
// TODO: Can we do better?
name_start := len(c.name)
fmt.wprintf(w, " %s%s", COLOR_COMMANDS, c.name, flush = false)
for a in c.aliases {
name = strings.join([]string{name, a}, ", ", tbl.format_allocator)
fmt.wprintf(w, ", %s", a, flush = false)
name_start += len(a) + 2
}
table.row(&tbl, table.format(&tbl, "%s%s%s", COLOR_COMMANDS, name, ANSI_RESET), c.short)
fmt.wprint(w, ANSI_RESET)
padding := 20 - name_start
if padding > 0 {
for _ in 0 ..< padding {
io.write_byte(w, ' ')
}
}
fmt.wprintf(w, " %s\n", c.short, flush = false)
}
write_borderless_table(w, &tbl)
table_reset(&tbl)
table.caption(&tbl, "Flags:")
table.row(&tbl, COLOR_FLAGS + "-h, --help" + ANSI_RESET, `show this documentation`)
table.row(
&tbl,
COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " <path>",
`config file (default "~/.envr/config.json")`,
)
write_borderless_table(w, &tbl)
fmt.wprintf(
w,
`Use "%senvr%s [command] --help" for more information about a command.`,
COLOR_FLAGS,
ANSI_RESET,
"\n" +
COLOR_HEADINGS +
"Flags:" +
ANSI_RESET +
"\n\n " +
COLOR_FLAGS +
"-h, --help" +
ANSI_RESET +
" help for envr\n" +
COLOR_FLAGS +
` -c, --config-file` +
ANSI_RESET +
` <path> config file (default "~/.envr/config.json")
Use "` +
COLOR_FLAGS +
"envr" +
ANSI_RESET +
` [command] --help" for more information about a command.
`,
flush = false,
)
}

View File

@@ -15,10 +15,7 @@ cmd_backup :: proc(cmd: ^Command) {
return
}
// TODO: allow new_env_file to accept allocator?
// TODO: Write a test that covers this leak
file, ok := new_env_file(path)
defer delete_envfile(&file)
if !ok {
return
}

View File

@@ -7,21 +7,28 @@ import "core:path/filepath"
// TODO: What happens if you pass a non existent path to cmd_check?
// TODO: UX could be improved, so "run envr add ." if file not exists.
cmd_check :: proc(cmd: ^Command) {
_check_path: string
check_path: string
if len(cmd.args) > 0 {
_check_path = cmd.args[0]
check_path = cmd.args[0]
} else {
cwd, cwd_err := os.get_working_directory(context.temp_allocator)
if cwd_err != nil {
fmt.wprintf(cmd.err, "Error getting current directory: %v\n", cwd_err, flush = false)
return
}
_check_path = cwd
check_path = cwd
}
check_path, abs_err := filepath.abs(_check_path, context.temp_allocator)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
abs_path: string
if filepath.is_abs(check_path) {
abs_path = check_path
} else {
resolved, abs_err := filepath.abs(check_path)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
abs_path = resolved
}
db, db_ok := db_open(cmd.config_path)
@@ -30,20 +37,20 @@ cmd_check :: proc(cmd: ^Command) {
}
defer db_close(&db)
is_dir := os.is_directory(check_path)
is_dir := os.is_directory(abs_path)
// TODO: set a reasonable default
files_in_path := make([dynamic]string, context.temp_allocator)
if is_dir {
scanned, scan_ok := scan_path(check_path, db.cfg)
scanned, scan_ok := scan_path(abs_path, db.cfg)
if !scan_ok {
fmt.wprintln(cmd.err, "Error scanning directory for .env files", flush = false)
return
}
files_in_path = scanned
} else {
append(&files_in_path, check_path)
append(&files_in_path, abs_path)
}
db_files, list_ok := db_list(&db)

View File

@@ -7,7 +7,7 @@ import "core:testing"
@(test)
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env", "/c/.env"}
db := []EnvFile{{path = "/a/.env"}, {path = "/b/.env"}}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 1, fmt.tprintf("expected 1 unbacked, got %d", len(result)))
@@ -23,7 +23,7 @@ test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
@(test)
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env"}
db := []EnvFile{{path = "/a/.env"}, {path = "/b/.env"}}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))
@@ -32,7 +32,7 @@ test_find_unbacked_all_backed :: proc(t: ^testing.T) {
@(test)
test_find_unbacked_no_local :: proc(t: ^testing.T) {
local: []string
db := []EnvFile{{path = "/a/.env"}}
db := []EnvFile{{Path = "/a/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))

View File

@@ -9,8 +9,8 @@ import "core:terminal"
import "core:text/table"
ListEntry :: struct {
dir: string `json:"directory"`,
path: string `json:"path"`,
Directory: string `json:"directory"`,
Path: string `json:"path"`,
}
// TODO: Support --format flag
@@ -40,10 +40,10 @@ cmd_list :: proc(cmd: ^Command) {
for row in rows {
dir_str := strings.concatenate(
{row.dir, os.Path_Separator_String},
{row.Dir, os.Path_Separator_String},
context.temp_allocator,
)
filename := filepath.base(row.path)
filename := filepath.base(row.Path)
table.row(&t, dir_str, filename)
}
@@ -53,12 +53,12 @@ cmd_list :: proc(cmd: ^Command) {
// TODO: Should we instead print full entries here?
entries: [dynamic]ListEntry
for row in rows {
filename := filepath.base(row.path)
filename := filepath.base(row.Path)
append(
&entries,
ListEntry {
dir = strings.concatenate({row.dir, "/"}, context.temp_allocator),
path = filename,
Directory = strings.concatenate({row.Dir, "/"}, context.temp_allocator),
Path = filename,
},
)
}

View File

@@ -16,10 +16,17 @@ cmd_remove :: proc(cmd: ^Command) {
return
}
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
// TODO: Is this the best way to do it?
abs_path: string
if filepath.is_abs(path) {
abs_path = path
} else {
resolved, abs_err := filepath.abs(path)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
abs_path = resolved
}
db, db_ok := db_open(cmd.config_path)

View File

@@ -16,11 +16,18 @@ cmd_restore :: proc(cmd: ^Command) {
fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
return
}
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
// TODO: Is this the right way to handle this?
abs_path: string
if filepath.is_abs(path) {
abs_path = path
} else {
resolved, abs_err := filepath.abs(path)
if abs_err != nil {
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return
}
abs_path = resolved
}
db, db_ok := db_open(cmd.config_path)
@@ -34,20 +41,15 @@ cmd_restore :: proc(cmd: ^Command) {
return
}
dir := filepath.dir(file.path)
if err := os.mkdir_all(dir); err != nil {
fmt.wprintf(cmd.err, "Failed to create directory: %v\n", err, flush = false)
dir := filepath.dir(file.Path)
os.mkdir_all(dir)
return
}
write_err := os.write_entire_file(file.path, file.contents)
write_err := os.write_entire_file(file.Path, file.contents)
if write_err != nil {
fmt.wprintf(cmd.err, "Error writing file: %v\n", write_err, flush = false)
return
}
fmt.wprintf(cmd.out, "Restored %s\n", file.path, flush = false)
fmt.wprintf(cmd.out, "Restored %s\n", file.Path, flush = false)
}

View File

@@ -72,11 +72,7 @@ cmd_scan :: proc(cmd: ^Command) {
selected, result := multi_select("Select .env files to backup:", files[:])
defer delete(selected)
if result == .Cancel {
fmt.wprintln(
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
flush = false,
)
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false)
return
}
@@ -85,9 +81,7 @@ cmd_scan :: proc(cmd: ^Command) {
if !selected[i] {
continue
}
// TODO: Test cover this leak
env_file, ok := new_env_file(files[i])
defer delete_envfile(&env_file)
if !ok {
fmt.wprintf(cmd.err, "Error reading %s\n", files[i], flush = false)
continue
@@ -102,23 +96,12 @@ cmd_scan :: proc(cmd: ^Command) {
if added_count > 0 {
fmt.wprintf(
cmd.out,
ansi.CSI +
ansi.BOLD +
";" +
ansi.FG_GREEN +
ansi.SGR +
"Successfully added %d file(s) to backup." +
ANSI_RESET +
"\n",
ansi.CSI + ansi.BOLD + ";" + ansi.FG_GREEN + ansi.SGR + "Successfully added %d file(s) to backup." + ANSI_RESET + "\n",
added_count,
flush = false,
)
} else {
fmt.wprintln(
cmd.out,
ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET,
flush = false,
)
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET, flush = false)
}
}

View File

@@ -7,8 +7,8 @@ import "core:terminal"
import "core:text/table"
SyncEntry :: struct {
path: string `json:"path"`,
status: string `json:"status"`,
Path: string `json:"path"`,
Status: string `json:"status"`,
}
// TODO: Check for quiet failures.
@@ -44,8 +44,8 @@ cmd_sync :: proc(cmd: ^Command) {
}
results[i] = SyncEntry {
path = file.path,
status = status,
Path = file.Path,
Status = status,
}
}
@@ -62,7 +62,7 @@ cmd_sync :: proc(cmd: ^Command) {
)
for res in results {
table.row(&t, res.path, res.status)
table.row(&t, res.Path, res.Status)
}
table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width)

View File

@@ -208,19 +208,17 @@ find_git_roots :: proc(
}
search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]string {
home, err := os.user_home_dir(context.temp_allocator)
if err != nil {
panic("Failed to find home directory")
}
// TODO: handle error
home, _ := os.user_home_dir(context.temp_allocator)
paths := new_clone(cfg.scan_config.include, allocator)
paths, _ := new_clone(cfg.scan_config.include, allocator)
for &include in paths {
// TODO: Do we need to manually expand ~/ in odin?
expanded, _ := strings.replace(include, "~", home, 1, allocator)
if filepath.is_abs(expanded) {
include = expanded
} else {
// TODO: show errors?
resolved, err := filepath.abs(expanded, allocator)
if err == nil {
include = resolved

View File

@@ -71,7 +71,8 @@ test_new_config_exclude_patterns :: proc(t: ^testing.T) {
@(test)
test_save_load_config_roundtrip :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-cfg-rt-*")
base := fmt.tprintf("/tmp/envr-test-cfg-rt-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -104,7 +105,8 @@ test_load_config_missing :: proc(t: ^testing.T) {
@(test)
test_save_config_no_clobber :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-cfg-noclobber-*")
base := fmt.tprintf("/tmp/envr-test-cfg-noclobber-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -121,7 +123,8 @@ test_save_config_no_clobber :: proc(t: ^testing.T) {
@(test)
test_save_config_force_overwrites :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-cfg-force-*")
base := fmt.tprintf("/tmp/envr-test-cfg-force-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)

124
db.odin
View File

@@ -1,6 +1,5 @@
package main
import "base:runtime"
import "core:crypto/hash"
import "core:encoding/hex"
import "core:encoding/ini"
@@ -39,21 +38,21 @@ Db :: struct {
}
EnvFile :: struct {
path: string,
dir: string,
remotes: [dynamic]string,
sha256: string,
Path: string,
Dir: string,
Remotes: [dynamic]string,
Sha256: string,
contents: string,
}
@(deprecated = "call db_close to clean up EnvFiles")
delete_envfile :: proc(f: ^EnvFile) {
delete(f.path)
for &remote in f.remotes {
delete(f.Path)
for &remote in f.Remotes {
delete(remote)
}
delete(f.remotes)
delete(f.sha256)
delete(f.Remotes)
delete(f.Sha256)
delete(f.contents)
}
@@ -61,18 +60,6 @@ db_open :: proc(cfg_path: string) -> (db: Db, ok: bool) {
db = db_init() or_return
db.cfg = load_config(cfg_path, db_allocator(&db)) or_return
if len(db.cfg.keys) == 0 {
fmt.eprintf("Error: no SSH keys configured in %s\n", cfg_path)
db_close(&db)
return db, false
}
_, keys_ok := ssh_to_x25519(db.cfg.keys[:], context.temp_allocator)
if !keys_ok {
db_close(&db)
return db, false
}
// TODO: Use different allocators?
data_path := data_path(db.cfg.config_path, context.temp_allocator)
if os.exists(data_path) {
@@ -151,8 +138,6 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
return true
}
// db_close will fail silently if cfg.keys is empty. If you want to save the
// Db, be sure to use db_open rather than db_init
db_close :: proc(db: ^Db) {
allocator := db_allocator(db)
@@ -164,7 +149,7 @@ db_close :: proc(db: ^Db) {
mem.dynamic_arena_destroy(&db.arena)
}
if db.changed && len(db.cfg.keys) > 0 {
if db.changed {
rc := sqlite.exec(db.conn, "VACUUM", nil, nil, nil)
if rc != sqlite.OK {
fmt.printf("Error vacuuming database: %s\n", sqlite.errmsg(db.conn))
@@ -183,8 +168,7 @@ db_close :: proc(db: ^Db) {
// TODO: PAss allocator chain
encrypted, enc_ok := encrypt(sqlite_data, db.cfg.keys[:])
if !enc_ok {
fmt.eprintln("Database encryption failed")
fmt.println("Error: encryption failed")
return
}
@@ -235,20 +219,17 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
remotes_json := string(sqlite.column_text(stmt, 1))
remotes: [dynamic]string = ---
if len(remotes_json) > 0 {
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
if err != nil {
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
}
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
}
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
append(
&results,
EnvFile {
path = path,
dir = filepath.dir(path),
remotes = remotes,
sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
Path = path,
Dir = filepath.dir(path),
Remotes = remotes,
Sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
contents = clone_cstring(sqlite.column_text(stmt, 3), allocator),
},
)
@@ -259,7 +240,7 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
// TODO: Should we use context.temp_allocator for proc scoped lifetimes?
db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
remotes_json, marshal_err := json.marshal(file.remotes, allocator = context.temp_allocator)
remotes_json, marshal_err := json.marshal(file.Remotes, allocator = context.temp_allocator)
if marshal_err != nil {
fmt.printf("Error marshaling remotes: %v\n", marshal_err)
return false
@@ -277,7 +258,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
defer sqlite.finalize(stmt)
// TODO: deal with elsewhere?
cpath := to_cstring(file.path)
cpath := to_cstring(file.Path)
defer delete(cpath)
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
if rc != sqlite.OK {
@@ -293,7 +274,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
return false
}
csha := to_cstring(file.sha256)
csha := to_cstring(file.Sha256)
defer delete(csha)
rc = sqlite.bind_text(stmt, 3, csha, -1, nil)
if rc != sqlite.OK {
@@ -320,11 +301,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
}
// Result will be freed when `db_close` is called.
//
// Expects an absolute path
db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
assert(os.is_absolute_path(path))
sql: cstring = "SELECT path, remotes, sha256, contents FROM envr_env_files WHERE path = ?"
stmt: sqlite.Stmt
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
@@ -356,19 +333,16 @@ db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
remotes_json := string(sqlite.column_text(stmt, 1))
remotes: [dynamic]string = ---
if len(remotes_json) > 0 {
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
if err != nil {
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
}
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
}
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
return EnvFile {
path = file_path,
dir = filepath.dir(file_path),
remotes = remotes,
sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
Path = file_path,
Dir = filepath.dir(file_path),
Remotes = remotes,
Sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
contents = clone_cstring(sqlite.column_text(stmt, 3), allocator),
},
true
@@ -406,7 +380,6 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
return true
}
// Caller is responsible for the returned memory
new_env_file :: proc(path: string) -> (EnvFile, bool) {
abs_path, abs_err := filepath.abs(path)
if abs_err != nil {
@@ -420,18 +393,21 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
remotes := get_git_remotes(dir, context.allocator)
data, read_err := os.read_entire_file_from_path(abs_path, context.allocator)
defer delete(data)
if read_err != nil {
fmt.printf("Error reading file %s: %v\n", abs_path, read_err)
return EnvFile{}, false
}
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
hex_bytes := hex.encode(digest, context.allocator)
// TODO: Handle error
hex_bytes, _ := hex.encode(digest)
return EnvFile {
path = abs_path,
dir = dir,
remotes = remotes,
sha256 = string(hex_bytes),
Path = abs_path,
Dir = dir,
Remotes = remotes,
Sha256 = string(hex_bytes),
contents = string(data),
},
true
@@ -441,9 +417,9 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
allocator := db_allocator(db)
result: SyncFlag = {}
old_path := f.path
old_path := f.Path
if !os.exists(f.dir) {
if !os.exists(f.Dir) {
moved, err := try_move_dir(db, f, allocator)
if !moved {
return {}, err
@@ -451,10 +427,10 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
result += {.DirUpdated}
}
if !os.exists(f.path) {
write_err := os.write_entire_file(f.path, f.contents)
if !os.exists(f.Path) {
write_err := os.write_entire_file(f.Path, f.contents)
if write_err != nil {
fmt.eprintf("db_sync: failed to write %s: %v\n", f.path, write_err)
fmt.eprintf("db_sync: failed to write %s: %v\n", f.Path, write_err)
return result, .WriteFailed
}
@@ -464,17 +440,21 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
return result + {.Restored}, .None
}
data, read_err := os.read_entire_file_from_path(f.path, allocator)
data, read_err := os.read_entire_file_from_path(f.Path, allocator)
if read_err != nil {
fmt.eprintf("db_sync: failed to read %s: %v\n", f.path, read_err)
fmt.eprintf("db_sync: failed to read %s: %v\n", f.Path, read_err)
return result, .ReadFailed
}
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
hex_bytes := hex.encode(digest, allocator)
hex_bytes, hex_err := hex.encode(digest, allocator)
if hex_err != nil {
fmt.eprintf("db_sync: failed to encode hash for %s: %v\n", f.Path, hex_err)
return result, .ReadFailed
}
current_sha := string(hex_bytes)
if current_sha == f.sha256 {
if current_sha == f.Sha256 {
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
@@ -482,7 +462,7 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
}
f.contents = string(data)
f.sha256 = current_sha
f.Sha256 = current_sha
if !db_persist(db, f, old_path) {
return result, .DbFailed
}
@@ -490,7 +470,7 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
}
db_persist :: proc(db: ^Db, f: ^EnvFile, old_path: string) -> bool {
if f.path != old_path {
if f.Path != old_path {
if !db_delete(db, old_path) {
return false
}
@@ -524,11 +504,11 @@ try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, S
case 0:
return false, .DirMissing
case 1:
f.dir, _ = strings.clone(matched_dir, allocator)
base := filepath.base(f.path)
new_path, _ := filepath.join({f.dir, base}, allocator)
f.path = new_path
f.remotes = get_git_remotes(f.dir, allocator)
f.Dir, _ = strings.clone(matched_dir, allocator)
base := filepath.base(f.Path)
new_path, _ := filepath.join({f.Dir, base}, allocator)
f.Path = new_path
f.Remotes = get_git_remotes(f.Dir, allocator)
return true, .None
case:
return false, .MultipleDirs
@@ -536,7 +516,7 @@ try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, S
}
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
for r1 in f.remotes {
for r1 in f.Remotes {
for r2 in remotes {
if r1 == r2 {
return true

View File

@@ -11,14 +11,6 @@ import "sqlite"
FIXTURES :: "fixtures"
test_temp_dir :: proc(t: ^testing.T, prefix: string) -> string {
dir, err := os.mkdir_temp("", prefix, context.temp_allocator)
if err != nil {
testing.fail_now(t, fmt.tprintf("Failed to create temp dir: %v", err))
}
return dir
}
fixture_key :: proc() -> SshKeyPair {
priv, _ := strings.concatenate(
[]string{FIXTURES, "/keys/insecure-test-key"},
@@ -119,14 +111,13 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) {
}
defer delete(encrypted)
ewrd_dir := test_temp_dir(t, "envr-test-ewrd-*")
defer os.remove_all(ewrd_dir)
tmp_enc_path, _ := filepath.join([]string{ewrd_dir, "data.envr"}, context.temp_allocator)
tmp_enc_path := fmt.tprintf("/tmp/envr-test-ewrd-%d.envr", os.get_pid())
write_err := os.write_entire_file(tmp_enc_path, encrypted)
testing.expectf(t, write_err == nil, "failed to write encrypted file: %v", write_err)
if write_err != nil {
return
}
defer os.remove(tmp_enc_path)
read_back, rb_err := os.read_entire_file_from_path(tmp_enc_path, context.allocator)
testing.expectf(t, rb_err == nil, "failed to read back encrypted file: %v", rb_err)
@@ -232,15 +223,11 @@ test_full_db_cycle :: proc(t: ^testing.T) {
}
defer delete(encrypted)
cycle_dir := test_temp_dir(t, "envr-test-cycle-*")
defer os.remove_all(cycle_dir)
envr_dir_path, _ := filepath.join([]string{cycle_dir, ".envr"}, context.temp_allocator)
{
err := os.mkdir_all(envr_dir_path)
testing.expect_value(t, err, nil)
}
envr_dir_path := fmt.tprintf("/tmp/envr-test-cycle-%d/.envr", os.get_pid())
os.mkdir_all(envr_dir_path)
data_path, _ := filepath.join([]string{envr_dir_path, "data.envr"}, context.temp_allocator)
data_path, _ := filepath.join([]string{envr_dir_path, "data.envr"})
defer delete(data_path)
write_err := os.write_entire_file(data_path, encrypted)
testing.expectf(t, write_err == nil, "failed to write data.envr: %v", write_err)
if write_err != nil {

View File

@@ -13,14 +13,14 @@ import "sqlite"
make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile {
f := EnvFile {
path = path,
dir = "",
sha256 = sha,
Path = path,
Dir = "",
Sha256 = sha,
contents = contents,
remotes = make([dynamic]string, 0, len(remotes), context.temp_allocator),
Remotes = make([dynamic]string, 0, len(remotes), context.temp_allocator),
}
for r in remotes {
append(&f.remotes, r)
append(&f.Remotes, r)
}
return f
}
@@ -37,7 +37,7 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
contents := "SECRET=value"
f := make_test_env_file(path, sha, contents, []string{"git@github.com:user/repo.git"})
defer delete(f.remotes)
defer delete(f.Remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
@@ -46,11 +46,11 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
testing.expect(t, fetch_ok, "fetch should succeed")
if !fetch_ok do return
testing.expect_value(t, fetched.path, path)
testing.expect_value(t, fetched.sha256, sha)
testing.expect_value(t, fetched.Path, path)
testing.expect_value(t, fetched.Sha256, sha)
testing.expect_value(t, fetched.contents, contents)
testing.expect_value(t, len(fetched.remotes), 1)
testing.expect_value(t, fetched.remotes[0], "git@github.com:user/repo.git")
testing.expect_value(t, len(fetched.Remotes), 1)
testing.expect_value(t, fetched.Remotes[0], "git@github.com:user/repo.git")
}
@(test)
@@ -71,11 +71,11 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
testing.expect(t, ok, "failed to create test db")
f1 := make_test_env_file("/project/.env", "sha1", "KEY=old")
defer delete(f1.remotes)
defer delete(f1.Remotes)
testing.expect(t, db_insert(&db, f1), "first insert should succeed")
f2 := make_test_env_file("/project/.env", "sha2", "KEY=new")
defer delete(f2.remotes)
defer delete(f2.Remotes)
testing.expect(t, db_insert(&db, f2), "second insert should succeed")
results, list_ok := db_list(&db)
@@ -89,7 +89,7 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
// defer delete_envfile(&fetched)
testing.expect_value(t, fetched.contents, "KEY=new")
testing.expect_value(t, fetched.sha256, "sha2")
testing.expect_value(t, fetched.Sha256, "sha2")
}
@(test)
@@ -100,7 +100,7 @@ test_db_delete_existing :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.remotes)
defer delete(f.Remotes)
db_insert(&db, f)
testing.expect(t, db_delete(&db, "/project/.env"), "delete should return true")
@@ -126,9 +126,9 @@ test_db_list_multiple :: proc(t: ^testing.T) {
defer db_close(&db)
f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"})
defer delete(f1.remotes)
defer delete(f1.Remotes)
f2 := make_test_env_file("/proj2/.env", "sha2", "B=2", []string{"git@github.com:b/repo.git"})
defer delete(f2.remotes)
defer delete(f2.Remotes)
f3 := make_test_env_file("/proj3/.env", "sha3", "C=3")
db_insert(&db, f1)
@@ -162,7 +162,7 @@ test_db_insert_sets_changed :: proc(t: ^testing.T) {
testing.expect(t, !db.changed, "changed should start false")
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.remotes)
defer delete(f.Remotes)
db_insert(&db, f)
testing.expect(t, db.changed, "changed should be true after insert")
@@ -176,7 +176,7 @@ test_db_delete_sets_changed :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.remotes)
defer delete(f.Remotes)
db_insert(&db, f)
db.changed = false
@@ -192,7 +192,7 @@ test_db_serialize :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file("/project/.env", "sha", "KEY=val")
defer delete(f.remotes)
defer delete(f.Remotes)
db_insert(&db, f)
sz: i64
@@ -207,10 +207,10 @@ test_db_serialize :: proc(t: ^testing.T) {
@(test)
test_shares_remote_overlap :: proc(t: ^testing.T) {
f := EnvFile {
remotes = make([dynamic]string, 2, context.temp_allocator),
Remotes = make([dynamic]string, 2, context.temp_allocator),
}
append(&f.remotes, "git@github.com:user/repo.git")
append(&f.remotes, "git@gitlab.com:user/repo.git")
append(&f.Remotes, "git@github.com:user/repo.git")
append(&f.Remotes, "git@gitlab.com:user/repo.git")
remotes := []string{"git@github.com:user/repo.git"}
testing.expect(t, shares_remote(&f, remotes), "should share remote")
@@ -219,9 +219,9 @@ test_shares_remote_overlap :: proc(t: ^testing.T) {
@(test)
test_shares_remote_no_overlap :: proc(t: ^testing.T) {
f := EnvFile {
remotes = make([dynamic]string, 1, context.temp_allocator),
Remotes = make([dynamic]string, 1, context.temp_allocator),
}
append(&f.remotes, "git@github.com:user/repo.git")
append(&f.Remotes, "git@github.com:user/repo.git")
remotes := []string{"git@github.com:other/repo.git"}
testing.expect(t, !shares_remote(&f, remotes), "should not share remote")
@@ -230,7 +230,7 @@ test_shares_remote_no_overlap :: proc(t: ^testing.T) {
@(test)
test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
f := EnvFile {
remotes = make([dynamic]string, 0, context.temp_allocator),
Remotes = make([dynamic]string, 0, context.temp_allocator),
}
remotes := []string{"git@github.com:user/repo.git"}
@@ -240,9 +240,9 @@ test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
@(test)
test_shares_remote_empty_check_remotes :: proc(t: ^testing.T) {
f := EnvFile {
remotes = make([dynamic]string, 1, context.temp_allocator),
Remotes = make([dynamic]string, 1, context.temp_allocator),
}
append(&f.remotes, "git@github.com:user/repo.git")
append(&f.Remotes, "git@github.com:user/repo.git")
remotes: []string
testing.expect(t, !shares_remote(&f, remotes), "empty check remotes should not share")
@@ -251,7 +251,7 @@ test_shares_remote_empty_check_remotes :: proc(t: ^testing.T) {
@(test)
test_shares_remote_both_empty :: proc(t: ^testing.T) {
f := EnvFile {
remotes = make([dynamic]string, 0),
Remotes = make([dynamic]string, 0),
}
remotes: []string
@@ -267,7 +267,8 @@ delete_remotes :: proc(remotes: [dynamic]string) {
@(test)
test_get_git_remotes_single :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-remotes-*")
base := fmt.tprintf("/tmp/envr-test-remotes-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
git_dir := fmt.tprintf("%s/.git", base)
@@ -287,7 +288,8 @@ test_get_git_remotes_single :: proc(t: ^testing.T) {
@(test)
test_get_git_remotes_multiple :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-remotes-multi-*")
base := fmt.tprintf("/tmp/envr-test-remotes-multi-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
git_dir := fmt.tprintf("%s/.git", base)
@@ -305,7 +307,8 @@ test_get_git_remotes_multiple :: proc(t: ^testing.T) {
@(test)
test_get_git_remotes_no_config :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-remotes-none-*")
base := fmt.tprintf("/tmp/envr-test-remotes-none-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
remotes := get_git_remotes(base, context.temp_allocator)
@@ -315,7 +318,8 @@ test_get_git_remotes_no_config :: proc(t: ^testing.T) {
@(test)
test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-remotes-empty-*")
base := fmt.tprintf("/tmp/envr-test-remotes-empty-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
git_dir := fmt.tprintf("%s/.git", base)
@@ -333,7 +337,8 @@ test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
@(test)
test_new_env_file :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-envfile-*")
base := fmt.tprintf("/tmp/envr-test-envfile-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -343,15 +348,14 @@ test_new_env_file :: proc(t: ^testing.T) {
file, ok := new_env_file(env_path)
testing.expect(t, ok, "new_env_file should succeed")
if !ok do return
defer delete(file.contents)
defer delete(file.remotes)
defer delete(file.sha256)
defer delete(file.path)
defer delete(file.Remotes)
defer delete(file.Sha256)
defer delete(file.Path)
testing.expect(t, filepath.is_abs(file.path), "path should be absolute")
testing.expect(t, strings.has_suffix(file.path, "/.env"), "path should end with /.env")
testing.expect(t, filepath.is_abs(file.Path), "path should be absolute")
testing.expect(t, strings.has_suffix(file.Path, "/.env"), "path should end with /.env")
testing.expect(t, file.contents == "SECRET=value\n", "contents mismatch")
testing.expect(t, len(file.sha256) == 64, "sha256 should be 64 hex chars")
testing.expect(t, len(file.Sha256) == 64, "sha256 should be 64 hex chars")
}
@(test)
@@ -362,7 +366,8 @@ test_new_env_file_missing :: proc(t: ^testing.T) {
@(test)
test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-leak-*")
base := fmt.tprintf("/tmp/envr-test-leak-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -381,7 +386,8 @@ test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
@(test)
test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-leak-existing-*")
base := fmt.tprintf("/tmp/envr-test-leak-existing-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
@@ -403,7 +409,7 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
"SECRET=value",
[]string{"git@github.com:user/repo.git"},
)
defer delete(f.remotes)
defer delete(f.Remotes)
testing.expect(t, db_insert(&db, f), "insert should succeed")
db_close(&db)
@@ -416,7 +422,8 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
@(test)
test_db_sync_noop :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-sync-noop-*")
base := fmt.tprintf("/tmp/envr-test-sync-noop-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -429,7 +436,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
transmute([]u8)content,
context.temp_allocator,
)
hex_bytes := hex.encode(digest, context.temp_allocator)
hex_bytes, _ := hex.encode(digest, context.temp_allocator)
sha := string(hex_bytes)
db, ok := db_init()
@@ -437,7 +444,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file(env_path, sha, content)
f.dir = base
f.Dir = base
db_insert(&db, f)
result, sync_err := db_sync(&db, &f)
@@ -447,7 +454,8 @@ test_db_sync_noop :: proc(t: ^testing.T) {
@(test)
test_db_sync_backed_up :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-sync-backup-*")
base := fmt.tprintf("/tmp/envr-test-sync-backup-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -460,7 +468,7 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file(env_path, "old_sha", "KEY=original")
f.dir = base
f.Dir = base
db_insert(&db, f)
result, sync_err := db_sync(&db, &f)
@@ -470,7 +478,8 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
@(test)
test_db_sync_restored :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-sync-restore-*")
base := fmt.tprintf("/tmp/envr-test-sync-restore-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
env_path := fmt.tprintf("%s/.env", base)
@@ -480,8 +489,8 @@ test_db_sync_restored :: proc(t: ^testing.T) {
defer db_close(&db)
f := make_test_env_file(env_path, "some_sha", "SECRET=value")
f.dir = base
defer delete(f.remotes)
f.Dir = base
defer delete(f.Remotes)
db_insert(&db, f)
result, err := db_sync(&db, &f)
@@ -511,7 +520,7 @@ test_db_sync_dir_missing :: proc(t: ^testing.T) {
@(test)
test_db_sync_moved :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-test-sync-moved-*")
base := fmt.tprintf("/tmp/envr-test-sync-moved-%d", os.get_pid())
search_root := fmt.tprintf("%s/search", base)
repo_dir := fmt.tprintf("%s/myproject", search_root)
git_dir := fmt.tprintf("%s/.git", repo_dir)
@@ -546,8 +555,8 @@ test_db_sync_moved :: proc(t: ^testing.T) {
testing.expect(t, .Restored in result, "should have Restored flag")
expected_path := fmt.tprintf("%s/.env", repo_dir)
testing.expect_value(t, f.path, expected_path)
testing.expect_value(t, f.dir, repo_dir)
testing.expect_value(t, f.Path, expected_path)
testing.expect_value(t, f.Dir, repo_dir)
_, old_exists := db_fetch(&db, "/old/nonexistent/path/.env")
testing.expect(t, !old_exists, "old path should be deleted from db")

View File

@@ -19,7 +19,7 @@ scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string,
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string {
backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
for file in db_files {
backed_set[file.path] = true
backed_set[file.Path] = true
}
unbacked := make([dynamic]string, 0, len(db_files) / 2, context.temp_allocator)

View File

@@ -8,7 +8,8 @@ import "core:testing"
@(test)
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-scan-test-*")
base := fmt.tprintf("/tmp/envr-scan-test-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
git_init := os.Process_Desc {
@@ -18,23 +19,20 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
stderr = os.stderr,
}
p, err := os.process_start(git_init)
testing.expectf(t, err == nil, "Failed to run git: %v", err)
if err != nil do return
state, wait_err := os.process_wait(p)
testing.expectf(t, wait_err == nil, "Failed to wait: %v", wait_err)
if wait_err != nil do return
testing.expect(t, state.success, "command should succeed")
if err != nil {
return
}
_, wait_err := os.process_wait(p)
if wait_err != nil {
return
}
gitignore_path := fmt.tprintf("%s/.gitignore", base)
err = os.write_entire_file(gitignore_path, ".env*\n")
testing.expectf(t, err == nil, "Failed: %v", err)
_ = os.write_entire_file(gitignore_path, ".env*\n")
err = os.write_entire_file(fmt.tprintf("%s/.env", base), "SECRET=1")
testing.expectf(t, err == nil, "Failed: %v", err)
err = os.write_entire_file(fmt.tprintf("%s/.env.testing", base), "TEST=1")
testing.expectf(t, err == nil, "Failed: %v", err)
err = os.write_entire_file(fmt.tprintf("%s/config.yaml", base), "key: value")
testing.expectf(t, err == nil, "Failed: %v", err)
_ = os.write_entire_file(fmt.tprintf("%s/.env", base), "SECRET=1")
_ = os.write_entire_file(fmt.tprintf("%s/.env.testing", base), "TEST=1")
_ = os.write_entire_file(fmt.tprintf("%s/config.yaml", base), "key: value")
cfg := Config {
scan_config = ScanConfig{matcher = "\\.env"},
@@ -73,7 +71,8 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
@(test)
test_scan_path_empty_dir :: proc(t: ^testing.T) {
base := test_temp_dir(t, "envr-scan-empty-*")
base := fmt.tprintf("/tmp/envr-scan-empty-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfg := Config {

View File

@@ -1,7 +1,5 @@
package main
import "core:fmt"
import "core:io"
import "core:text/table"
import "core:unicode/utf8"
@@ -36,44 +34,3 @@ ansi_aware_width :: proc(str: string) -> int {
return width
}
write_borderless_table :: proc(w: io.Writer, t: ^table.Table) {
table.build(t, ansi_aware_width)
write_table_separator :: proc(w: io.Writer, tbl: ^table.Table) {
io.write_byte(w, '\n')
}
if t.caption != "" {
table.write_text_align(
w,
fmt.tprintf("%s%s%s", COLOR_HEADINGS, t.caption, ANSI_RESET),
.Left,
0, //t.lpad,
0, //t.rpad,
t.tblw + t.nr_cols - 1 - ansi_aware_width(t.caption) - t.lpad - t.rpad,
)
io.write_byte(w, '\n')
}
write_table_separator(w, t)
for row in 0 ..< t.nr_rows {
for col in 0 ..< t.nr_cols {
table.write_table_cell(w, t, row, col)
}
io.write_byte(w, '\n')
if t.has_header_row && row == table.header_row(t) {
write_table_separator(w, t)
}
}
write_table_separator(w, t)
}
table_reset :: proc(t: ^table.Table) {
clear(&t.cells)
clear(&t.colw)
t.caption = ""
t.tblw = 0
t.nr_cols = 0
t.nr_rows = 0
}