mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 18:48:33 -04:00
Compare commits
1 Commits
f825bc2b09
...
7ccc757dfa
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ccc757dfa |
56
TODOS.md
56
TODOS.md
@@ -2,41 +2,61 @@
|
|||||||
|
|
||||||
1. Commands are still leaking.
|
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
|
## Double-check AI output
|
||||||
|
|
||||||
|
|||||||
62
cli.odin
62
cli.odin
@@ -5,7 +5,6 @@ import "core:fmt"
|
|||||||
import "core:io"
|
import "core:io"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:text/table"
|
|
||||||
|
|
||||||
Command :: struct {
|
Command :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -254,47 +253,56 @@ at before, restore your backup with:
|
|||||||
|
|
||||||
%senvr%s [command]
|
%senvr%s [command]
|
||||||
|
|
||||||
|
%sAvailable Commands:%s
|
||||||
`,
|
`,
|
||||||
COLOR_HEADINGS,
|
COLOR_HEADINGS,
|
||||||
ANSI_RESET,
|
ANSI_RESET,
|
||||||
COLOR_FLAGS,
|
COLOR_FLAGS,
|
||||||
ANSI_RESET,
|
ANSI_RESET,
|
||||||
|
COLOR_HEADINGS,
|
||||||
|
ANSI_RESET,
|
||||||
flush = false,
|
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 {
|
for c in COMMANDS {
|
||||||
name := c.name
|
name_start := len(c.name)
|
||||||
// TODO: Can we do better?
|
fmt.wprintf(w, " %s%s", COLOR_COMMANDS, c.name, flush = false)
|
||||||
for a in c.aliases {
|
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(
|
fmt.wprintf(
|
||||||
w,
|
w,
|
||||||
`Use "%senvr%s [command] --help" for more information about a command.`,
|
"\n" +
|
||||||
COLOR_FLAGS,
|
COLOR_HEADINGS +
|
||||||
ANSI_RESET,
|
"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,
|
flush = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ cmd_backup :: proc(cmd: ^Command) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: allow new_env_file to accept allocator?
|
|
||||||
// TODO: Write a test that covers this leak
|
|
||||||
file, ok := new_env_file(path)
|
file, ok := new_env_file(path)
|
||||||
defer delete_envfile(&file)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,28 @@ import "core:path/filepath"
|
|||||||
// TODO: What happens if you pass a non existent path to cmd_check?
|
// 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.
|
// TODO: UX could be improved, so "run envr add ." if file not exists.
|
||||||
cmd_check :: proc(cmd: ^Command) {
|
cmd_check :: proc(cmd: ^Command) {
|
||||||
_check_path: string
|
check_path: string
|
||||||
if len(cmd.args) > 0 {
|
if len(cmd.args) > 0 {
|
||||||
_check_path = cmd.args[0]
|
check_path = cmd.args[0]
|
||||||
} else {
|
} else {
|
||||||
cwd, cwd_err := os.get_working_directory(context.temp_allocator)
|
cwd, cwd_err := os.get_working_directory(context.temp_allocator)
|
||||||
if cwd_err != nil {
|
if cwd_err != nil {
|
||||||
fmt.wprintf(cmd.err, "Error getting current directory: %v\n", cwd_err, flush = false)
|
fmt.wprintf(cmd.err, "Error getting current directory: %v\n", cwd_err, flush = false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_check_path = cwd
|
check_path = cwd
|
||||||
}
|
}
|
||||||
check_path, abs_err := filepath.abs(_check_path, context.temp_allocator)
|
|
||||||
if abs_err != nil {
|
abs_path: string
|
||||||
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
|
if filepath.is_abs(check_path) {
|
||||||
return
|
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)
|
db, db_ok := db_open(cmd.config_path)
|
||||||
@@ -30,20 +37,20 @@ cmd_check :: proc(cmd: ^Command) {
|
|||||||
}
|
}
|
||||||
defer db_close(&db)
|
defer db_close(&db)
|
||||||
|
|
||||||
is_dir := os.is_directory(check_path)
|
is_dir := os.is_directory(abs_path)
|
||||||
|
|
||||||
// TODO: set a reasonable default
|
// TODO: set a reasonable default
|
||||||
files_in_path := make([dynamic]string, context.temp_allocator)
|
files_in_path := make([dynamic]string, context.temp_allocator)
|
||||||
|
|
||||||
if is_dir {
|
if is_dir {
|
||||||
scanned, scan_ok := scan_path(check_path, db.cfg)
|
scanned, scan_ok := scan_path(abs_path, db.cfg)
|
||||||
if !scan_ok {
|
if !scan_ok {
|
||||||
fmt.wprintln(cmd.err, "Error scanning directory for .env files", flush = false)
|
fmt.wprintln(cmd.err, "Error scanning directory for .env files", flush = false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
files_in_path = scanned
|
files_in_path = scanned
|
||||||
} else {
|
} else {
|
||||||
append(&files_in_path, check_path)
|
append(&files_in_path, abs_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
db_files, list_ok := db_list(&db)
|
db_files, list_ok := db_list(&db)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import "core:testing"
|
|||||||
@(test)
|
@(test)
|
||||||
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
|
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
|
||||||
local := []string{"/a/.env", "/b/.env", "/c/.env"}
|
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[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 1, fmt.tprintf("expected 1 unbacked, got %d", len(result)))
|
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)
|
||||||
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
|
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
|
||||||
local := []string{"/a/.env", "/b/.env"}
|
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[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))
|
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)
|
||||||
test_find_unbacked_no_local :: proc(t: ^testing.T) {
|
test_find_unbacked_no_local :: proc(t: ^testing.T) {
|
||||||
local: []string
|
local: []string
|
||||||
db := []EnvFile{{path = "/a/.env"}}
|
db := []EnvFile{{Path = "/a/.env"}}
|
||||||
|
|
||||||
result := find_unbacked(local, db[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))
|
testing.expect(t, len(result) == 0, fmt.tprintf("expected 0 unbacked, got %d", len(result)))
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import "core:terminal"
|
|||||||
import "core:text/table"
|
import "core:text/table"
|
||||||
|
|
||||||
ListEntry :: struct {
|
ListEntry :: struct {
|
||||||
dir: string `json:"directory"`,
|
Directory: string `json:"directory"`,
|
||||||
path: string `json:"path"`,
|
Path: string `json:"path"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Support --format flag
|
// TODO: Support --format flag
|
||||||
@@ -40,10 +40,10 @@ cmd_list :: proc(cmd: ^Command) {
|
|||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
dir_str := strings.concatenate(
|
dir_str := strings.concatenate(
|
||||||
{row.dir, os.Path_Separator_String},
|
{row.Dir, os.Path_Separator_String},
|
||||||
context.temp_allocator,
|
context.temp_allocator,
|
||||||
)
|
)
|
||||||
filename := filepath.base(row.path)
|
filename := filepath.base(row.Path)
|
||||||
|
|
||||||
table.row(&t, dir_str, filename)
|
table.row(&t, dir_str, filename)
|
||||||
}
|
}
|
||||||
@@ -53,12 +53,12 @@ cmd_list :: proc(cmd: ^Command) {
|
|||||||
// TODO: Should we instead print full entries here?
|
// TODO: Should we instead print full entries here?
|
||||||
entries: [dynamic]ListEntry
|
entries: [dynamic]ListEntry
|
||||||
for row in rows {
|
for row in rows {
|
||||||
filename := filepath.base(row.path)
|
filename := filepath.base(row.Path)
|
||||||
append(
|
append(
|
||||||
&entries,
|
&entries,
|
||||||
ListEntry {
|
ListEntry {
|
||||||
dir = strings.concatenate({row.dir, "/"}, context.temp_allocator),
|
Directory = strings.concatenate({row.Dir, "/"}, context.temp_allocator),
|
||||||
path = filename,
|
Path = filename,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,17 @@ cmd_remove :: proc(cmd: ^Command) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
abs_path, abs_err := filepath.abs(path, context.temp_allocator)
|
// TODO: Is this the best way to do it?
|
||||||
if abs_err != nil {
|
abs_path: string
|
||||||
fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
|
if filepath.is_abs(path) {
|
||||||
return
|
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)
|
db, db_ok := db_open(cmd.config_path)
|
||||||
|
|||||||
@@ -16,11 +16,18 @@ cmd_restore :: proc(cmd: ^Command) {
|
|||||||
fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
|
fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
|
||||||
return
|
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)
|
db, db_ok := db_open(cmd.config_path)
|
||||||
@@ -34,20 +41,15 @@ cmd_restore :: proc(cmd: ^Command) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := filepath.dir(file.path)
|
dir := filepath.dir(file.Path)
|
||||||
if err := os.mkdir_all(dir); err != nil {
|
os.mkdir_all(dir)
|
||||||
fmt.wprintf(cmd.err, "Failed to create directory: %v\n", err, flush = false)
|
|
||||||
|
|
||||||
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 {
|
if write_err != nil {
|
||||||
fmt.wprintf(cmd.err, "Error writing file: %v\n", write_err, flush = false)
|
fmt.wprintf(cmd.err, "Error writing file: %v\n", write_err, flush = false)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.wprintf(cmd.out, "Restored %s\n", file.path, flush = false)
|
fmt.wprintf(cmd.out, "Restored %s\n", file.Path, flush = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,11 +72,7 @@ cmd_scan :: proc(cmd: ^Command) {
|
|||||||
selected, result := multi_select("Select .env files to backup:", files[:])
|
selected, result := multi_select("Select .env files to backup:", files[:])
|
||||||
defer delete(selected)
|
defer delete(selected)
|
||||||
if result == .Cancel {
|
if result == .Cancel {
|
||||||
fmt.wprintln(
|
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET, flush = false)
|
||||||
cmd.out,
|
|
||||||
ansi.CSI + ansi.FAINT + ansi.SGR + "Cancelled." + ANSI_RESET,
|
|
||||||
flush = false,
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +81,7 @@ cmd_scan :: proc(cmd: ^Command) {
|
|||||||
if !selected[i] {
|
if !selected[i] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// TODO: Test cover this leak
|
|
||||||
env_file, ok := new_env_file(files[i])
|
env_file, ok := new_env_file(files[i])
|
||||||
defer delete_envfile(&env_file)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
fmt.wprintf(cmd.err, "Error reading %s\n", files[i], flush = false)
|
fmt.wprintf(cmd.err, "Error reading %s\n", files[i], flush = false)
|
||||||
continue
|
continue
|
||||||
@@ -102,23 +96,12 @@ cmd_scan :: proc(cmd: ^Command) {
|
|||||||
if added_count > 0 {
|
if added_count > 0 {
|
||||||
fmt.wprintf(
|
fmt.wprintf(
|
||||||
cmd.out,
|
cmd.out,
|
||||||
ansi.CSI +
|
ansi.CSI + ansi.BOLD + ";" + ansi.FG_GREEN + ansi.SGR + "Successfully added %d file(s) to backup." + ANSI_RESET + "\n",
|
||||||
ansi.BOLD +
|
|
||||||
";" +
|
|
||||||
ansi.FG_GREEN +
|
|
||||||
ansi.SGR +
|
|
||||||
"Successfully added %d file(s) to backup." +
|
|
||||||
ANSI_RESET +
|
|
||||||
"\n",
|
|
||||||
added_count,
|
added_count,
|
||||||
flush = false,
|
flush = false,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
fmt.wprintln(
|
fmt.wprintln(cmd.out, ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET, flush = false)
|
||||||
cmd.out,
|
|
||||||
ansi.CSI + ansi.FAINT + ansi.SGR + "No files were added." + ANSI_RESET,
|
|
||||||
flush = false,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import "core:terminal"
|
|||||||
import "core:text/table"
|
import "core:text/table"
|
||||||
|
|
||||||
SyncEntry :: struct {
|
SyncEntry :: struct {
|
||||||
path: string `json:"path"`,
|
Path: string `json:"path"`,
|
||||||
status: string `json:"status"`,
|
Status: string `json:"status"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check for quiet failures.
|
// TODO: Check for quiet failures.
|
||||||
@@ -44,8 +44,8 @@ cmd_sync :: proc(cmd: ^Command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
results[i] = SyncEntry {
|
results[i] = SyncEntry {
|
||||||
path = file.path,
|
Path = file.Path,
|
||||||
status = status,
|
Status = status,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ cmd_sync :: proc(cmd: ^Command) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for res in results {
|
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)
|
table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width)
|
||||||
|
|||||||
10
config.odin
10
config.odin
@@ -208,19 +208,17 @@ find_git_roots :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]string {
|
search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]string {
|
||||||
home, err := os.user_home_dir(context.temp_allocator)
|
// TODO: handle error
|
||||||
if err != nil {
|
home, _ := os.user_home_dir(context.temp_allocator)
|
||||||
panic("Failed to find home directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
paths := new_clone(cfg.scan_config.include, allocator)
|
paths, _ := new_clone(cfg.scan_config.include, allocator)
|
||||||
|
|
||||||
for &include in paths {
|
for &include in paths {
|
||||||
|
// TODO: Do we need to manually expand ~/ in odin?
|
||||||
expanded, _ := strings.replace(include, "~", home, 1, allocator)
|
expanded, _ := strings.replace(include, "~", home, 1, allocator)
|
||||||
if filepath.is_abs(expanded) {
|
if filepath.is_abs(expanded) {
|
||||||
include = expanded
|
include = expanded
|
||||||
} else {
|
} else {
|
||||||
// TODO: show errors?
|
|
||||||
resolved, err := filepath.abs(expanded, allocator)
|
resolved, err := filepath.abs(expanded, allocator)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
include = resolved
|
include = resolved
|
||||||
|
|||||||
@@ -71,7 +71,8 @@ test_new_config_exclude_patterns :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_save_load_config_roundtrip :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
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)
|
||||||
test_save_config_no_clobber :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
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)
|
||||||
test_save_config_force_overwrites :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
cfgPath, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||||
|
|||||||
124
db.odin
124
db.odin
@@ -1,6 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "base:runtime"
|
|
||||||
import "core:crypto/hash"
|
import "core:crypto/hash"
|
||||||
import "core:encoding/hex"
|
import "core:encoding/hex"
|
||||||
import "core:encoding/ini"
|
import "core:encoding/ini"
|
||||||
@@ -39,21 +38,21 @@ Db :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EnvFile :: struct {
|
EnvFile :: struct {
|
||||||
path: string,
|
Path: string,
|
||||||
dir: string,
|
Dir: string,
|
||||||
remotes: [dynamic]string,
|
Remotes: [dynamic]string,
|
||||||
sha256: string,
|
Sha256: string,
|
||||||
contents: string,
|
contents: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(deprecated = "call db_close to clean up EnvFiles")
|
@(deprecated = "call db_close to clean up EnvFiles")
|
||||||
delete_envfile :: proc(f: ^EnvFile) {
|
delete_envfile :: proc(f: ^EnvFile) {
|
||||||
delete(f.path)
|
delete(f.Path)
|
||||||
for &remote in f.remotes {
|
for &remote in f.Remotes {
|
||||||
delete(remote)
|
delete(remote)
|
||||||
}
|
}
|
||||||
delete(f.remotes)
|
delete(f.Remotes)
|
||||||
delete(f.sha256)
|
delete(f.Sha256)
|
||||||
delete(f.contents)
|
delete(f.contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,18 +60,6 @@ db_open :: proc(cfg_path: string) -> (db: Db, ok: bool) {
|
|||||||
db = db_init() or_return
|
db = db_init() or_return
|
||||||
db.cfg = load_config(cfg_path, db_allocator(&db)) 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?
|
// TODO: Use different allocators?
|
||||||
data_path := data_path(db.cfg.config_path, context.temp_allocator)
|
data_path := data_path(db.cfg.config_path, context.temp_allocator)
|
||||||
if os.exists(data_path) {
|
if os.exists(data_path) {
|
||||||
@@ -151,8 +138,6 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
|||||||
return true
|
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) {
|
db_close :: proc(db: ^Db) {
|
||||||
allocator := db_allocator(db)
|
allocator := db_allocator(db)
|
||||||
|
|
||||||
@@ -164,7 +149,7 @@ db_close :: proc(db: ^Db) {
|
|||||||
mem.dynamic_arena_destroy(&db.arena)
|
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)
|
rc := sqlite.exec(db.conn, "VACUUM", nil, nil, nil)
|
||||||
if rc != sqlite.OK {
|
if rc != sqlite.OK {
|
||||||
fmt.printf("Error vacuuming database: %s\n", sqlite.errmsg(db.conn))
|
fmt.printf("Error vacuuming database: %s\n", sqlite.errmsg(db.conn))
|
||||||
@@ -183,8 +168,7 @@ db_close :: proc(db: ^Db) {
|
|||||||
// TODO: PAss allocator chain
|
// TODO: PAss allocator chain
|
||||||
encrypted, enc_ok := encrypt(sqlite_data, db.cfg.keys[:])
|
encrypted, enc_ok := encrypt(sqlite_data, db.cfg.keys[:])
|
||||||
if !enc_ok {
|
if !enc_ok {
|
||||||
fmt.eprintln("Database encryption failed")
|
fmt.println("Error: encryption failed")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,20 +219,17 @@ db_list :: proc(db: ^Db) -> ([]EnvFile, bool) {
|
|||||||
remotes_json := string(sqlite.column_text(stmt, 1))
|
remotes_json := string(sqlite.column_text(stmt, 1))
|
||||||
remotes: [dynamic]string = ---
|
remotes: [dynamic]string = ---
|
||||||
if len(remotes_json) > 0 {
|
if len(remotes_json) > 0 {
|
||||||
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
||||||
if err != nil {
|
|
||||||
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||||
|
|
||||||
append(
|
append(
|
||||||
&results,
|
&results,
|
||||||
EnvFile {
|
EnvFile {
|
||||||
path = path,
|
Path = path,
|
||||||
dir = filepath.dir(path),
|
Dir = filepath.dir(path),
|
||||||
remotes = remotes,
|
Remotes = remotes,
|
||||||
sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
|
Sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
|
||||||
contents = clone_cstring(sqlite.column_text(stmt, 3), 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?
|
// TODO: Should we use context.temp_allocator for proc scoped lifetimes?
|
||||||
db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
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 {
|
if marshal_err != nil {
|
||||||
fmt.printf("Error marshaling remotes: %v\n", marshal_err)
|
fmt.printf("Error marshaling remotes: %v\n", marshal_err)
|
||||||
return false
|
return false
|
||||||
@@ -277,7 +258,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
|||||||
defer sqlite.finalize(stmt)
|
defer sqlite.finalize(stmt)
|
||||||
|
|
||||||
// TODO: deal with elsewhere?
|
// TODO: deal with elsewhere?
|
||||||
cpath := to_cstring(file.path)
|
cpath := to_cstring(file.Path)
|
||||||
defer delete(cpath)
|
defer delete(cpath)
|
||||||
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
||||||
if rc != sqlite.OK {
|
if rc != sqlite.OK {
|
||||||
@@ -293,7 +274,7 @@ db_insert :: proc(db: ^Db, file: EnvFile) -> bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
csha := to_cstring(file.sha256)
|
csha := to_cstring(file.Sha256)
|
||||||
defer delete(csha)
|
defer delete(csha)
|
||||||
rc = sqlite.bind_text(stmt, 3, csha, -1, nil)
|
rc = sqlite.bind_text(stmt, 3, csha, -1, nil)
|
||||||
if rc != sqlite.OK {
|
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.
|
// Result will be freed when `db_close` is called.
|
||||||
//
|
|
||||||
// Expects an absolute path
|
|
||||||
db_fetch :: proc(db: ^Db, path: string) -> (EnvFile, bool) {
|
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 = ?"
|
sql: cstring = "SELECT path, remotes, sha256, contents FROM envr_env_files WHERE path = ?"
|
||||||
stmt: sqlite.Stmt
|
stmt: sqlite.Stmt
|
||||||
rc := sqlite.prepare_v2(db.conn, sql, -1, &stmt, nil)
|
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_json := string(sqlite.column_text(stmt, 1))
|
||||||
remotes: [dynamic]string = ---
|
remotes: [dynamic]string = ---
|
||||||
if len(remotes_json) > 0 {
|
if len(remotes_json) > 0 {
|
||||||
err := json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
||||||
if err != nil {
|
|
||||||
fmt.eprintf("Warning: malformed remotes JSON: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||||
|
|
||||||
return EnvFile {
|
return EnvFile {
|
||||||
path = file_path,
|
Path = file_path,
|
||||||
dir = filepath.dir(file_path),
|
Dir = filepath.dir(file_path),
|
||||||
remotes = remotes,
|
Remotes = remotes,
|
||||||
sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
|
Sha256 = clone_cstring(sqlite.column_text(stmt, 2), allocator),
|
||||||
contents = clone_cstring(sqlite.column_text(stmt, 3), allocator),
|
contents = clone_cstring(sqlite.column_text(stmt, 3), allocator),
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -406,7 +380,6 @@ db_delete :: proc(db: ^Db, path: string) -> bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caller is responsible for the returned memory
|
|
||||||
new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
||||||
abs_path, abs_err := filepath.abs(path)
|
abs_path, abs_err := filepath.abs(path)
|
||||||
if abs_err != nil {
|
if abs_err != nil {
|
||||||
@@ -420,18 +393,21 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
|||||||
remotes := get_git_remotes(dir, context.allocator)
|
remotes := get_git_remotes(dir, context.allocator)
|
||||||
|
|
||||||
data, read_err := os.read_entire_file_from_path(abs_path, context.allocator)
|
data, read_err := os.read_entire_file_from_path(abs_path, context.allocator)
|
||||||
|
defer delete(data)
|
||||||
if read_err != nil {
|
if read_err != nil {
|
||||||
fmt.printf("Error reading file %s: %v\n", abs_path, read_err)
|
fmt.printf("Error reading file %s: %v\n", abs_path, read_err)
|
||||||
return EnvFile{}, false
|
return EnvFile{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
|
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 {
|
return EnvFile {
|
||||||
path = abs_path,
|
Path = abs_path,
|
||||||
dir = dir,
|
Dir = dir,
|
||||||
remotes = remotes,
|
Remotes = remotes,
|
||||||
sha256 = string(hex_bytes),
|
Sha256 = string(hex_bytes),
|
||||||
contents = string(data),
|
contents = string(data),
|
||||||
},
|
},
|
||||||
true
|
true
|
||||||
@@ -441,9 +417,9 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
|||||||
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
||||||
allocator := db_allocator(db)
|
allocator := db_allocator(db)
|
||||||
result: SyncFlag = {}
|
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)
|
moved, err := try_move_dir(db, f, allocator)
|
||||||
if !moved {
|
if !moved {
|
||||||
return {}, err
|
return {}, err
|
||||||
@@ -451,10 +427,10 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
|||||||
result += {.DirUpdated}
|
result += {.DirUpdated}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !os.exists(f.path) {
|
if !os.exists(f.Path) {
|
||||||
write_err := os.write_entire_file(f.path, f.contents)
|
write_err := os.write_entire_file(f.Path, f.contents)
|
||||||
if write_err != nil {
|
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
|
return result, .WriteFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,17 +440,21 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
|||||||
return result + {.Restored}, .None
|
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 {
|
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
|
return result, .ReadFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
|
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)
|
current_sha := string(hex_bytes)
|
||||||
|
|
||||||
if current_sha == f.sha256 {
|
if current_sha == f.Sha256 {
|
||||||
if !db_persist(db, f, old_path) {
|
if !db_persist(db, f, old_path) {
|
||||||
return result, .DbFailed
|
return result, .DbFailed
|
||||||
}
|
}
|
||||||
@@ -482,7 +462,7 @@ db_sync :: proc(db: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.contents = string(data)
|
f.contents = string(data)
|
||||||
f.sha256 = current_sha
|
f.Sha256 = current_sha
|
||||||
if !db_persist(db, f, old_path) {
|
if !db_persist(db, f, old_path) {
|
||||||
return result, .DbFailed
|
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 {
|
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) {
|
if !db_delete(db, old_path) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -524,11 +504,11 @@ try_move_dir :: proc(db: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, S
|
|||||||
case 0:
|
case 0:
|
||||||
return false, .DirMissing
|
return false, .DirMissing
|
||||||
case 1:
|
case 1:
|
||||||
f.dir, _ = strings.clone(matched_dir, allocator)
|
f.Dir, _ = strings.clone(matched_dir, allocator)
|
||||||
base := filepath.base(f.path)
|
base := filepath.base(f.Path)
|
||||||
new_path, _ := filepath.join({f.dir, base}, allocator)
|
new_path, _ := filepath.join({f.Dir, base}, allocator)
|
||||||
f.path = new_path
|
f.Path = new_path
|
||||||
f.remotes = get_git_remotes(f.dir, allocator)
|
f.Remotes = get_git_remotes(f.Dir, allocator)
|
||||||
return true, .None
|
return true, .None
|
||||||
case:
|
case:
|
||||||
return false, .MultipleDirs
|
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 {
|
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
||||||
for r1 in f.remotes {
|
for r1 in f.Remotes {
|
||||||
for r2 in remotes {
|
for r2 in remotes {
|
||||||
if r1 == r2 {
|
if r1 == r2 {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -11,14 +11,6 @@ import "sqlite"
|
|||||||
|
|
||||||
FIXTURES :: "fixtures"
|
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 {
|
fixture_key :: proc() -> SshKeyPair {
|
||||||
priv, _ := strings.concatenate(
|
priv, _ := strings.concatenate(
|
||||||
[]string{FIXTURES, "/keys/insecure-test-key"},
|
[]string{FIXTURES, "/keys/insecure-test-key"},
|
||||||
@@ -119,14 +111,13 @@ test_encrypt_write_read_decrypt :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
defer delete(encrypted)
|
defer delete(encrypted)
|
||||||
|
|
||||||
ewrd_dir := test_temp_dir(t, "envr-test-ewrd-*")
|
tmp_enc_path := fmt.tprintf("/tmp/envr-test-ewrd-%d.envr", os.get_pid())
|
||||||
defer os.remove_all(ewrd_dir)
|
|
||||||
tmp_enc_path, _ := filepath.join([]string{ewrd_dir, "data.envr"}, context.temp_allocator)
|
|
||||||
write_err := os.write_entire_file(tmp_enc_path, encrypted)
|
write_err := os.write_entire_file(tmp_enc_path, encrypted)
|
||||||
testing.expectf(t, write_err == nil, "failed to write encrypted file: %v", write_err)
|
testing.expectf(t, write_err == nil, "failed to write encrypted file: %v", write_err)
|
||||||
if write_err != nil {
|
if write_err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer os.remove(tmp_enc_path)
|
||||||
|
|
||||||
read_back, rb_err := os.read_entire_file_from_path(tmp_enc_path, context.allocator)
|
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)
|
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)
|
defer delete(encrypted)
|
||||||
|
|
||||||
cycle_dir := test_temp_dir(t, "envr-test-cycle-*")
|
envr_dir_path := fmt.tprintf("/tmp/envr-test-cycle-%d/.envr", os.get_pid())
|
||||||
defer os.remove_all(cycle_dir)
|
os.mkdir_all(envr_dir_path)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
write_err := os.write_entire_file(data_path, encrypted)
|
||||||
testing.expectf(t, write_err == nil, "failed to write data.envr: %v", write_err)
|
testing.expectf(t, write_err == nil, "failed to write data.envr: %v", write_err)
|
||||||
if write_err != nil {
|
if write_err != nil {
|
||||||
|
|||||||
117
db_test.odin
117
db_test.odin
@@ -13,14 +13,14 @@ import "sqlite"
|
|||||||
|
|
||||||
make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile {
|
make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {}) -> EnvFile {
|
||||||
f := EnvFile {
|
f := EnvFile {
|
||||||
path = path,
|
Path = path,
|
||||||
dir = "",
|
Dir = "",
|
||||||
sha256 = sha,
|
Sha256 = sha,
|
||||||
contents = contents,
|
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 {
|
for r in remotes {
|
||||||
append(&f.remotes, r)
|
append(&f.Remotes, r)
|
||||||
}
|
}
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
|
|||||||
contents := "SECRET=value"
|
contents := "SECRET=value"
|
||||||
|
|
||||||
f := make_test_env_file(path, sha, contents, []string{"git@github.com:user/repo.git"})
|
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")
|
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")
|
testing.expect(t, fetch_ok, "fetch should succeed")
|
||||||
if !fetch_ok do return
|
if !fetch_ok do return
|
||||||
|
|
||||||
testing.expect_value(t, fetched.path, path)
|
testing.expect_value(t, fetched.Path, path)
|
||||||
testing.expect_value(t, fetched.sha256, sha)
|
testing.expect_value(t, fetched.Sha256, sha)
|
||||||
testing.expect_value(t, fetched.contents, contents)
|
testing.expect_value(t, fetched.contents, contents)
|
||||||
testing.expect_value(t, len(fetched.remotes), 1)
|
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, fetched.Remotes[0], "git@github.com:user/repo.git")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -71,11 +71,11 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
|
|
||||||
f1 := make_test_env_file("/project/.env", "sha1", "KEY=old")
|
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")
|
testing.expect(t, db_insert(&db, f1), "first insert should succeed")
|
||||||
|
|
||||||
f2 := make_test_env_file("/project/.env", "sha2", "KEY=new")
|
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")
|
testing.expect(t, db_insert(&db, f2), "second insert should succeed")
|
||||||
|
|
||||||
results, list_ok := db_list(&db)
|
results, list_ok := db_list(&db)
|
||||||
@@ -89,7 +89,7 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
|
|||||||
// defer delete_envfile(&fetched)
|
// defer delete_envfile(&fetched)
|
||||||
|
|
||||||
testing.expect_value(t, fetched.contents, "KEY=new")
|
testing.expect_value(t, fetched.contents, "KEY=new")
|
||||||
testing.expect_value(t, fetched.sha256, "sha2")
|
testing.expect_value(t, fetched.Sha256, "sha2")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -100,7 +100,7 @@ test_db_delete_existing :: proc(t: ^testing.T) {
|
|||||||
defer db_close(&db)
|
defer db_close(&db)
|
||||||
|
|
||||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||||
defer delete(f.remotes)
|
defer delete(f.Remotes)
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
|
|
||||||
testing.expect(t, db_delete(&db, "/project/.env"), "delete should return true")
|
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)
|
defer db_close(&db)
|
||||||
|
|
||||||
f1 := make_test_env_file("/proj1/.env", "sha1", "A=1", []string{"git@github.com:a/repo.git"})
|
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"})
|
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")
|
f3 := make_test_env_file("/proj3/.env", "sha3", "C=3")
|
||||||
|
|
||||||
db_insert(&db, f1)
|
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")
|
testing.expect(t, !db.changed, "changed should start false")
|
||||||
|
|
||||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||||
defer delete(f.remotes)
|
defer delete(f.Remotes)
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
|
|
||||||
testing.expect(t, db.changed, "changed should be true after insert")
|
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)
|
defer db_close(&db)
|
||||||
|
|
||||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||||
defer delete(f.remotes)
|
defer delete(f.Remotes)
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
db.changed = false
|
db.changed = false
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ test_db_serialize :: proc(t: ^testing.T) {
|
|||||||
defer db_close(&db)
|
defer db_close(&db)
|
||||||
|
|
||||||
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
f := make_test_env_file("/project/.env", "sha", "KEY=val")
|
||||||
defer delete(f.remotes)
|
defer delete(f.Remotes)
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
|
|
||||||
sz: i64
|
sz: i64
|
||||||
@@ -207,10 +207,10 @@ test_db_serialize :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_shares_remote_overlap :: proc(t: ^testing.T) {
|
test_shares_remote_overlap :: proc(t: ^testing.T) {
|
||||||
f := EnvFile {
|
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@github.com:user/repo.git")
|
||||||
append(&f.remotes, "git@gitlab.com:user/repo.git")
|
append(&f.Remotes, "git@gitlab.com:user/repo.git")
|
||||||
|
|
||||||
remotes := []string{"git@github.com:user/repo.git"}
|
remotes := []string{"git@github.com:user/repo.git"}
|
||||||
testing.expect(t, shares_remote(&f, remotes), "should share remote")
|
testing.expect(t, shares_remote(&f, remotes), "should share remote")
|
||||||
@@ -219,9 +219,9 @@ test_shares_remote_overlap :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_shares_remote_no_overlap :: proc(t: ^testing.T) {
|
test_shares_remote_no_overlap :: proc(t: ^testing.T) {
|
||||||
f := EnvFile {
|
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"}
|
remotes := []string{"git@github.com:other/repo.git"}
|
||||||
testing.expect(t, !shares_remote(&f, remotes), "should not share remote")
|
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)
|
||||||
test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
|
test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
|
||||||
f := EnvFile {
|
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"}
|
remotes := []string{"git@github.com:user/repo.git"}
|
||||||
@@ -240,9 +240,9 @@ test_shares_remote_empty_file_remotes :: proc(t: ^testing.T) {
|
|||||||
@(test)
|
@(test)
|
||||||
test_shares_remote_empty_check_remotes :: proc(t: ^testing.T) {
|
test_shares_remote_empty_check_remotes :: proc(t: ^testing.T) {
|
||||||
f := EnvFile {
|
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
|
remotes: []string
|
||||||
testing.expect(t, !shares_remote(&f, remotes), "empty check remotes should not share")
|
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)
|
||||||
test_shares_remote_both_empty :: proc(t: ^testing.T) {
|
test_shares_remote_both_empty :: proc(t: ^testing.T) {
|
||||||
f := EnvFile {
|
f := EnvFile {
|
||||||
remotes = make([dynamic]string, 0),
|
Remotes = make([dynamic]string, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
remotes: []string
|
remotes: []string
|
||||||
@@ -267,7 +267,8 @@ delete_remotes :: proc(remotes: [dynamic]string) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_get_git_remotes_single :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
git_dir := fmt.tprintf("%s/.git", base)
|
git_dir := fmt.tprintf("%s/.git", base)
|
||||||
@@ -287,7 +288,8 @@ test_get_git_remotes_single :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_get_git_remotes_multiple :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
git_dir := fmt.tprintf("%s/.git", base)
|
git_dir := fmt.tprintf("%s/.git", base)
|
||||||
@@ -305,7 +307,8 @@ test_get_git_remotes_multiple :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_get_git_remotes_no_config :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
remotes := get_git_remotes(base, context.temp_allocator)
|
remotes := get_git_remotes(base, context.temp_allocator)
|
||||||
@@ -315,7 +318,8 @@ test_get_git_remotes_no_config :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
git_dir := fmt.tprintf("%s/.git", base)
|
git_dir := fmt.tprintf("%s/.git", base)
|
||||||
@@ -333,7 +337,8 @@ test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_new_env_file :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
env_path := fmt.tprintf("%s/.env", 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)
|
file, ok := new_env_file(env_path)
|
||||||
testing.expect(t, ok, "new_env_file should succeed")
|
testing.expect(t, ok, "new_env_file should succeed")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer delete(file.contents)
|
defer delete(file.Remotes)
|
||||||
defer delete(file.remotes)
|
defer delete(file.Sha256)
|
||||||
defer delete(file.sha256)
|
defer delete(file.Path)
|
||||||
defer delete(file.path)
|
|
||||||
|
|
||||||
testing.expect(t, filepath.is_abs(file.path), "path should be absolute")
|
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, strings.has_suffix(file.Path, "/.env"), "path should end with /.env")
|
||||||
testing.expect(t, file.contents == "SECRET=value\n", "contents mismatch")
|
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)
|
@(test)
|
||||||
@@ -362,7 +366,8 @@ test_new_env_file_missing :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
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)
|
||||||
test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
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",
|
"SECRET=value",
|
||||||
[]string{"git@github.com:user/repo.git"},
|
[]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")
|
testing.expect(t, db_insert(&db, f), "insert should succeed")
|
||||||
db_close(&db)
|
db_close(&db)
|
||||||
|
|
||||||
@@ -416,7 +422,8 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_sync_noop :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
env_path := fmt.tprintf("%s/.env", base)
|
env_path := fmt.tprintf("%s/.env", base)
|
||||||
@@ -429,7 +436,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
|
|||||||
transmute([]u8)content,
|
transmute([]u8)content,
|
||||||
context.temp_allocator,
|
context.temp_allocator,
|
||||||
)
|
)
|
||||||
hex_bytes := hex.encode(digest, context.temp_allocator)
|
hex_bytes, _ := hex.encode(digest, context.temp_allocator)
|
||||||
sha := string(hex_bytes)
|
sha := string(hex_bytes)
|
||||||
|
|
||||||
db, ok := db_init()
|
db, ok := db_init()
|
||||||
@@ -437,7 +444,7 @@ test_db_sync_noop :: proc(t: ^testing.T) {
|
|||||||
defer db_close(&db)
|
defer db_close(&db)
|
||||||
|
|
||||||
f := make_test_env_file(env_path, sha, content)
|
f := make_test_env_file(env_path, sha, content)
|
||||||
f.dir = base
|
f.Dir = base
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
|
|
||||||
result, sync_err := db_sync(&db, &f)
|
result, sync_err := db_sync(&db, &f)
|
||||||
@@ -447,7 +454,8 @@ test_db_sync_noop :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_sync_backed_up :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
env_path := fmt.tprintf("%s/.env", 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)
|
defer db_close(&db)
|
||||||
|
|
||||||
f := make_test_env_file(env_path, "old_sha", "KEY=original")
|
f := make_test_env_file(env_path, "old_sha", "KEY=original")
|
||||||
f.dir = base
|
f.Dir = base
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
|
|
||||||
result, sync_err := db_sync(&db, &f)
|
result, sync_err := db_sync(&db, &f)
|
||||||
@@ -470,7 +478,8 @@ test_db_sync_backed_up :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_sync_restored :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
env_path := fmt.tprintf("%s/.env", base)
|
env_path := fmt.tprintf("%s/.env", base)
|
||||||
@@ -480,8 +489,8 @@ test_db_sync_restored :: proc(t: ^testing.T) {
|
|||||||
defer db_close(&db)
|
defer db_close(&db)
|
||||||
|
|
||||||
f := make_test_env_file(env_path, "some_sha", "SECRET=value")
|
f := make_test_env_file(env_path, "some_sha", "SECRET=value")
|
||||||
f.dir = base
|
f.Dir = base
|
||||||
defer delete(f.remotes)
|
defer delete(f.Remotes)
|
||||||
db_insert(&db, f)
|
db_insert(&db, f)
|
||||||
|
|
||||||
result, err := db_sync(&db, &f)
|
result, err := db_sync(&db, &f)
|
||||||
@@ -511,7 +520,7 @@ test_db_sync_dir_missing :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_sync_moved :: proc(t: ^testing.T) {
|
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)
|
search_root := fmt.tprintf("%s/search", base)
|
||||||
repo_dir := fmt.tprintf("%s/myproject", search_root)
|
repo_dir := fmt.tprintf("%s/myproject", search_root)
|
||||||
git_dir := fmt.tprintf("%s/.git", repo_dir)
|
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")
|
testing.expect(t, .Restored in result, "should have Restored flag")
|
||||||
|
|
||||||
expected_path := fmt.tprintf("%s/.env", repo_dir)
|
expected_path := fmt.tprintf("%s/.env", repo_dir)
|
||||||
testing.expect_value(t, f.path, expected_path)
|
testing.expect_value(t, f.Path, expected_path)
|
||||||
testing.expect_value(t, f.dir, repo_dir)
|
testing.expect_value(t, f.Dir, repo_dir)
|
||||||
|
|
||||||
_, old_exists := db_fetch(&db, "/old/nonexistent/path/.env")
|
_, old_exists := db_fetch(&db, "/old/nonexistent/path/.env")
|
||||||
testing.expect(t, !old_exists, "old path should be deleted from db")
|
testing.expect(t, !old_exists, "old path should be deleted from db")
|
||||||
|
|||||||
@@ -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 {
|
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string {
|
||||||
backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
|
backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
|
||||||
for file in db_files {
|
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)
|
unbacked := make([dynamic]string, 0, len(db_files) / 2, context.temp_allocator)
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
git_init := os.Process_Desc {
|
git_init := os.Process_Desc {
|
||||||
@@ -18,23 +19,20 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
|
|||||||
stderr = os.stderr,
|
stderr = os.stderr,
|
||||||
}
|
}
|
||||||
p, err := os.process_start(git_init)
|
p, err := os.process_start(git_init)
|
||||||
testing.expectf(t, err == nil, "Failed to run git: %v", err)
|
if err != nil {
|
||||||
if err != nil do return
|
return
|
||||||
state, wait_err := os.process_wait(p)
|
}
|
||||||
testing.expectf(t, wait_err == nil, "Failed to wait: %v", wait_err)
|
_, wait_err := os.process_wait(p)
|
||||||
if wait_err != nil do return
|
if wait_err != nil {
|
||||||
testing.expect(t, state.success, "command should succeed")
|
return
|
||||||
|
}
|
||||||
|
|
||||||
gitignore_path := fmt.tprintf("%s/.gitignore", base)
|
gitignore_path := fmt.tprintf("%s/.gitignore", base)
|
||||||
err = os.write_entire_file(gitignore_path, ".env*\n")
|
_ = os.write_entire_file(gitignore_path, ".env*\n")
|
||||||
testing.expectf(t, err == nil, "Failed: %v", err)
|
|
||||||
|
|
||||||
err = os.write_entire_file(fmt.tprintf("%s/.env", base), "SECRET=1")
|
_ = os.write_entire_file(fmt.tprintf("%s/.env", base), "SECRET=1")
|
||||||
testing.expectf(t, err == nil, "Failed: %v", err)
|
_ = os.write_entire_file(fmt.tprintf("%s/.env.testing", base), "TEST=1")
|
||||||
err = os.write_entire_file(fmt.tprintf("%s/.env.testing", base), "TEST=1")
|
_ = os.write_entire_file(fmt.tprintf("%s/config.yaml", base), "key: value")
|
||||||
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)
|
|
||||||
|
|
||||||
cfg := Config {
|
cfg := Config {
|
||||||
scan_config = ScanConfig{matcher = "\\.env"},
|
scan_config = ScanConfig{matcher = "\\.env"},
|
||||||
@@ -73,7 +71,8 @@ test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_scan_path_empty_dir :: proc(t: ^testing.T) {
|
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)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfg := Config {
|
cfg := Config {
|
||||||
|
|||||||
43
table.odin
43
table.odin
@@ -1,7 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:io"
|
|
||||||
import "core:text/table"
|
import "core:text/table"
|
||||||
import "core:unicode/utf8"
|
import "core:unicode/utf8"
|
||||||
|
|
||||||
@@ -36,44 +34,3 @@ ansi_aware_width :: proc(str: string) -> int {
|
|||||||
return width
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user