mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
refactor(odin): port check command.
This commit is contained in:
2
TODOS.md
2
TODOS.md
@@ -47,3 +47,5 @@ Note: These todos can wait until all the subcommands have been ported.
|
||||
## REFACTOR
|
||||
|
||||
20. **cmd_list.odin** — Non-TTY branch builds `ListEntry` structs and marshals JSON separately. Now that `render_json_rows` (issue 1) accepts an `io.Writer` and uses `json.marshal`, unify both branches to use it. Note: will change JSON keys from `"directory"/"path"` to `"Directory"/"Path"`.
|
||||
|
||||
21. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
|
||||
|
||||
1
cli.odin
1
cli.odin
@@ -44,6 +44,7 @@ IMPLEMENTED_COMMANDS := []string{
|
||||
"remove",
|
||||
"restore",
|
||||
"edit-config",
|
||||
"check",
|
||||
}
|
||||
|
||||
parse_args :: proc() -> (cmd: Command, ok: bool) {
|
||||
|
||||
79
cmd_check.odin
Normal file
79
cmd_check.odin
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
|
||||
cmd_check :: proc(cmd: ^Command) {
|
||||
check_path: string
|
||||
if len(cmd.args) > 0 {
|
||||
check_path = cmd.args[0]
|
||||
} else {
|
||||
cwd, cwd_err := os.get_working_directory(context.allocator)
|
||||
if cwd_err != nil {
|
||||
fmt.printf("Error getting current directory: %v\n", cwd_err)
|
||||
return
|
||||
}
|
||||
check_path = cwd
|
||||
}
|
||||
|
||||
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.printf("Error getting absolute path: %v\n", abs_err)
|
||||
return
|
||||
}
|
||||
abs_path = resolved
|
||||
}
|
||||
|
||||
db, db_ok := db_open()
|
||||
if !db_ok {
|
||||
return
|
||||
}
|
||||
defer db_close(&db)
|
||||
|
||||
is_dir := os.is_directory(abs_path)
|
||||
|
||||
files_in_path: [dynamic]string
|
||||
|
||||
if is_dir {
|
||||
if !can_scan() {
|
||||
fmt.println("Error: please install fd to use the check command (https://github.com/sharkdp/fd)")
|
||||
return
|
||||
}
|
||||
|
||||
scanned, scan_ok := scan_path(abs_path, db.cfg)
|
||||
if !scan_ok {
|
||||
fmt.println("Error scanning directory for .env files")
|
||||
return
|
||||
}
|
||||
files_in_path = scanned
|
||||
} else {
|
||||
append(&files_in_path, abs_path)
|
||||
}
|
||||
|
||||
db_files, list_ok := db_list(&db)
|
||||
if !list_ok {
|
||||
return
|
||||
}
|
||||
|
||||
not_backed := find_unbacked(files_in_path[:], db_files[:])
|
||||
|
||||
if len(not_backed) == 0 {
|
||||
if len(files_in_path) == 0 {
|
||||
fmt.println("No .env files found in the specified directory.")
|
||||
} else {
|
||||
fmt.println("✓ All .env files in the directory are backed up.")
|
||||
}
|
||||
} else {
|
||||
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed))
|
||||
for file in not_backed {
|
||||
fmt.printf(" %s\n", file)
|
||||
}
|
||||
fmt.println("\nRun 'envr sync' to back up these files.")
|
||||
}
|
||||
}
|
||||
43
cmd_check_test.odin
Normal file
43
cmd_check_test.odin
Normal file
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
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"}}
|
||||
|
||||
result := find_unbacked(local, db[:])
|
||||
testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result)))
|
||||
if len(result) > 0 {
|
||||
testing.expect(t, result[0] == "/c/.env", fmt.aprintf("expected /c/.env, got %s", result[0]))
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
|
||||
local := []string{"/a/.env", "/b/.env"}
|
||||
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
|
||||
|
||||
result := find_unbacked(local, db[:])
|
||||
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_find_unbacked_no_local :: proc(t: ^testing.T) {
|
||||
local: []string
|
||||
db := []EnvFile{{Path = "/a/.env"}}
|
||||
|
||||
result := find_unbacked(local, db[:])
|
||||
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_find_unbacked_none_backed :: proc(t: ^testing.T) {
|
||||
local := []string{"/a/.env", "/b/.env"}
|
||||
db: []EnvFile
|
||||
|
||||
result := find_unbacked(local, db[:])
|
||||
testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result)))
|
||||
}
|
||||
@@ -31,6 +31,8 @@ main :: proc() {
|
||||
cmd_restore(&cmd)
|
||||
case "edit-config":
|
||||
cmd_edit_config(&cmd)
|
||||
case "check":
|
||||
cmd_check(&cmd)
|
||||
case:
|
||||
fmt.printf("Unknown command: %s\n", cmd.name)
|
||||
print_usage()
|
||||
|
||||
139
scan.odin
Normal file
139
scan.odin
Normal file
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
import "core:sync"
|
||||
|
||||
fd_counter: sync.Atomic_Mutex
|
||||
fd_seq: int
|
||||
|
||||
next_fd_tmp_path :: proc() -> string {
|
||||
sync.atomic_mutex_lock(&fd_counter)
|
||||
n := fd_seq
|
||||
fd_seq += 1
|
||||
sync.atomic_mutex_unlock(&fd_counter)
|
||||
return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n)
|
||||
}
|
||||
|
||||
build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -> []string {
|
||||
args := make([dynamic]string, 0, 3 + 2 * len(cfg.ScanConfig.Exclude) + 2)
|
||||
append(&args, "fd")
|
||||
append(&args, "-a")
|
||||
append(&args, cfg.ScanConfig.Matcher)
|
||||
|
||||
for exclude in cfg.ScanConfig.Exclude {
|
||||
append(&args, "-E")
|
||||
append(&args, exclude)
|
||||
}
|
||||
|
||||
if include_ignored {
|
||||
append(&args, "-HI")
|
||||
} else {
|
||||
append(&args, "-H")
|
||||
}
|
||||
|
||||
append(&args, search_path)
|
||||
return args[:]
|
||||
}
|
||||
|
||||
run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
|
||||
tmp_path := next_fd_tmp_path()
|
||||
tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC)
|
||||
if tmp_err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
desc := os.Process_Desc {
|
||||
command = args,
|
||||
stdout = tmp_file,
|
||||
stderr = nil,
|
||||
}
|
||||
|
||||
p, start_err := os.process_start(desc)
|
||||
os.close(tmp_file)
|
||||
if start_err != nil {
|
||||
os.remove(tmp_path)
|
||||
return
|
||||
}
|
||||
|
||||
state, wait_err := os.process_wait(p)
|
||||
if wait_err != nil || state.exit_code != 0 {
|
||||
os.remove(tmp_path)
|
||||
return
|
||||
}
|
||||
|
||||
data, read_err := os.read_entire_file_from_path(tmp_path, context.allocator)
|
||||
os.remove(tmp_path)
|
||||
if read_err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
output := string(data)
|
||||
output = strings.trim_space(output)
|
||||
if len(output) == 0 {
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
raw_lines := strings.split(output, "\n")
|
||||
for line in raw_lines {
|
||||
trimmed, _ := strings.clone(strings.trim_space(line))
|
||||
if len(trimmed) > 0 {
|
||||
append(&lines, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) {
|
||||
all_args := build_fd_args(search_path, cfg, true)
|
||||
all_files, all_ok := run_fd(all_args)
|
||||
if !all_ok {
|
||||
return
|
||||
}
|
||||
|
||||
unignored_args := build_fd_args(search_path, cfg, false)
|
||||
unignored_files, unignored_ok := run_fd(unignored_args)
|
||||
if !unignored_ok {
|
||||
return
|
||||
}
|
||||
|
||||
unignored_set: map[string]bool
|
||||
for file in unignored_files {
|
||||
unignored_set[file] = true
|
||||
}
|
||||
|
||||
for file in all_files {
|
||||
if !(file in unignored_set) {
|
||||
append(&paths, file)
|
||||
}
|
||||
}
|
||||
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
can_scan :: proc() -> bool {
|
||||
feats := check_features()
|
||||
return has_feature(feats, .Fd)
|
||||
}
|
||||
|
||||
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> [dynamic]string {
|
||||
backed_set: map[string]bool
|
||||
for file in db_files {
|
||||
backed_set[file.Path] = true
|
||||
}
|
||||
|
||||
unbacked: [dynamic]string
|
||||
for file in local_files {
|
||||
if !(file in backed_set) {
|
||||
append(&unbacked, file)
|
||||
}
|
||||
}
|
||||
return unbacked
|
||||
}
|
||||
|
||||
94
scan_test.odin
Normal file
94
scan_test.odin
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
import "core:os"
|
||||
import "core:path/filepath"
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
|
||||
if !can_scan() {
|
||||
return
|
||||
}
|
||||
|
||||
base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid())
|
||||
os.mkdir_all(base)
|
||||
defer os.remove_all(base)
|
||||
|
||||
git_init := os.Process_Desc{
|
||||
command = []string{"git", "init"},
|
||||
working_dir = base,
|
||||
stdout = os.stderr,
|
||||
stderr = os.stderr,
|
||||
}
|
||||
p, err := os.process_start(git_init)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, wait_err := os.process_wait(p)
|
||||
if wait_err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gitignore_path := fmt.aprintf("%s/.gitignore", base)
|
||||
_ = os.write_entire_file(gitignore_path, ".env*\n")
|
||||
|
||||
_ = os.write_entire_file(fmt.aprintf("%s/.env", base), "SECRET=1")
|
||||
_ = os.write_entire_file(fmt.aprintf("%s/.env.testing", base), "TEST=1")
|
||||
_ = os.write_entire_file(fmt.aprintf("%s/config.yaml", base), "key: value")
|
||||
|
||||
cfg := Config{
|
||||
ScanConfig = ScanConfig{
|
||||
Matcher = "\\.env",
|
||||
Exclude = []string{},
|
||||
Include = []string{},
|
||||
},
|
||||
}
|
||||
|
||||
results, ok := scan_path(base, cfg)
|
||||
testing.expect(t, ok, "scan_path should succeed")
|
||||
|
||||
found_env := false
|
||||
found_testing := false
|
||||
found_config := false
|
||||
|
||||
for path in results {
|
||||
_, filename := filepath.split(path)
|
||||
if filename == ".env" {
|
||||
found_env = true
|
||||
}
|
||||
if filename == ".env.testing" {
|
||||
found_testing = true
|
||||
}
|
||||
if filename == "config.yaml" {
|
||||
found_config = true
|
||||
}
|
||||
}
|
||||
|
||||
testing.expect(t, found_env, "should find .env (gitignored)")
|
||||
testing.expect(t, found_testing, "should find .env.testing (gitignored)")
|
||||
testing.expect(t, !found_config, "should NOT find config.yaml (not gitignored)")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_scan_path_empty_dir :: proc(t: ^testing.T) {
|
||||
if !can_scan() {
|
||||
return
|
||||
}
|
||||
|
||||
base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid())
|
||||
os.mkdir_all(base)
|
||||
defer os.remove_all(base)
|
||||
|
||||
cfg := Config{
|
||||
ScanConfig = ScanConfig{
|
||||
Matcher = "\\.env",
|
||||
Exclude = []string{},
|
||||
Include = []string{},
|
||||
},
|
||||
}
|
||||
|
||||
results, ok := scan_path(base, cfg)
|
||||
testing.expect(t, ok, "scan_path should succeed")
|
||||
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results)))
|
||||
}
|
||||
@@ -13,7 +13,3 @@ cmd_scan :: proc(cmd: ^Command) {
|
||||
cmd_sync :: proc(cmd: ^Command) {
|
||||
fmt.println("TODO: sync")
|
||||
}
|
||||
|
||||
cmd_check :: proc(cmd: ^Command) {
|
||||
fmt.println("TODO: check")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user