diff --git a/README.md b/README.md index 44e8eba..4b0b856 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ repositories. - An SSH key pair (for encryption/decryption) - The following binaries: - [fd](https://github.com/sharkdp/fd) - - [git](https://git-scm.com) ## Installation diff --git a/TEST_PLAN.md b/TEST_PLAN.md index 17b3852..814b402 100644 --- a/TEST_PLAN.md +++ b/TEST_PLAN.md @@ -35,13 +35,7 @@ Stdout will be captured by redirecting `os.stdout` to a pipe. ## Hard to test (interactive / external deps) -### `cmd_deps` (cmd_deps.odin) -- Needs `git` and/or `fd` in PATH -- Test TTY and non-TTY paths -- Skip if dependencies not available (with `#assert` like TODO 28 suggests) - ### `cmd_scan` (cmd_scan.odin) -- Needs `fd` installed - Test with fixture git repo containing `.env` files - Test `find_unbacked` integration (already partially tested in `cmd_check_test.odin`) - Non-TTY JSON output path @@ -66,5 +60,4 @@ Stdout will be captured by redirecting `os.stdout` to a pipe. - DB integration tests should use in-memory SQLite (`:memory:`) where possible. - Temp dir fixtures should follow the pattern in `scan_test.odin`. -- External dependency tests (`fd`, `git`) should use `#assert` to ensure the dependency is present rather than silently skipping (TODO 28). - Tests that manipulate the `HOME` env var must use a mutex to prevent races with parallel test execution. diff --git a/TODOS.md b/TODOS.md index 76b2187..e29f471 100644 --- a/TODOS.md +++ b/TODOS.md @@ -38,7 +38,7 @@ 23. procedures should be ordered by use, main at the top, then in the order they are called from main. -24. Remove git dependency. +24. [x] Remove git dependency. ## Double-check AI output diff --git a/WINDOWS.md b/WINDOWS.md index 22d1c65..739947f 100644 --- a/WINDOWS.md +++ b/WINDOWS.md @@ -30,10 +30,6 @@ The application relies on external tools that need to be installed separately on - Install via: `winget install sharkdp.fd` or `choco install fd` - Alternative: `scoop install fd` -2. **git** - Version control system - - Install via: `winget install Git.Git` or download from git-scm.com - - Usually already available on most development machines - ## Minor Compatibility Notes ### File Permissions @@ -65,7 +61,6 @@ if editor == "" { 1. Install required dependencies: ```powershell winget install sharkdp.fd - winget install Git.Git ``` 2. Fix the path handling bug in `app/env_file.go:209` diff --git a/cli.odin b/cli.odin index a55e6c8..26ff45c 100644 --- a/cli.odin +++ b/cli.odin @@ -43,13 +43,6 @@ key somewhere, otherwise your data could be lost forever.`, {"list", "envr list", "View your tracked files", "", {}}, {"remove", "envr remove ", "Remove a .env file from your database", "", {}}, {"check", "envr check [path]", "Check if files are backed up", "", {}}, - { - "deps", - "envr deps", - "Check for missing binaries", - "envr relies on external binaries for certain functionality.\n\nThe check command reports on which binaries are available and which are not.", - {}, - }, {"version", "envr version", "Show envr's version", "", {}}, {"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}}, { diff --git a/cmd_deps.odin b/cmd_deps.odin deleted file mode 100644 index 71aa218..0000000 --- a/cmd_deps.odin +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import "core:fmt" -import "core:os" -import "core:terminal" - -// TODO: Improve table rendering -cmd_deps :: proc(cmd: ^Command) { - feats := check_features() - - headers := []string{"Feature", "Status"} - rows: [dynamic][]string - - if .Git in feats { - append(&rows, []string{"Git", "\u2713 Available"}) - } else { - append(&rows, []string{"Git", "\u2717 Missing"}) - } - - if terminal.is_terminal(os.stdout) { - render_table(cmd.out, headers, rows[:]) - } else { - render_json_rows(cmd.out, headers, rows[:]) - fmt.wprint(cmd.out, "\n", flush = false) - } -} - diff --git a/db.odin b/db.odin index cceb75a..1234891 100644 --- a/db.odin +++ b/db.odin @@ -2,12 +2,12 @@ package main import "core:crypto/hash" import "core:encoding/hex" +import "core:encoding/ini" import "core:encoding/json" import "core:fmt" import "core:os" import "core:path/filepath" import "core:strings" -import "core:time" import "sqlite" @@ -51,14 +51,6 @@ delete_envfile :: proc(f: ^EnvFile) { delete(f.contents) } -make_temp_path :: proc() -> string { - ts := time.time_to_unix(time.now()) - b: strings.Builder - strings.builder_init(&b) - defer strings.builder_destroy(&b) - fmt.sbprintf(&b, "/tmp/envr-%d-%d.db", os.get_pid(), ts) - return strings.to_string(b) -} db_open :: proc(cfg_path: string) -> (Db, bool) { cfg, ok := load_config(cfg_path) @@ -236,59 +228,24 @@ db_restore_from_encrypted :: proc(db: ^rawptr, cfg: Config) -> bool { get_git_remotes :: proc(dir: string) -> [dynamic]string { remotes: [dynamic]string remote_set: map[string]bool + defer delete(remote_set) - b: strings.Builder - strings.builder_init(&b) - defer strings.builder_destroy(&b) - fmt.sbprintf(&b, "%s-git-remotes", make_temp_path()) - tmp_path := strings.to_string(b) - tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC) - if tmp_err != nil { + config_path, _ := filepath.join({dir, ".git", "config"}, context.temp_allocator) + m, _, ok := ini.load_map_from_path(config_path, context.allocator) + if !ok { return remotes } + defer ini.delete_map(m) - args := []string{"git", "remote", "-v"} - desc := os.Process_Desc { - command = args, - stdout = tmp_file, - stderr = nil, - working_dir = dir, - } - - p, start_err := os.process_start(desc) - os.close(tmp_file) - if start_err != nil { - os.remove(tmp_path) - return remotes - } - - state, wait_err := os.process_wait(p) - if wait_err != nil || state.exit_code != 0 { - os.remove(tmp_path) - return remotes - } - - data, read_err := os.read_entire_file_from_path(tmp_path, context.allocator) - defer delete(data) - os.remove(tmp_path) - if read_err != nil { - return remotes - } - - lines := strings.split(string(data), "\n") - - for &line in lines { - line = strings.trim_space(line) - if len(line) == 0 { - continue - } - parts := strings.fields(line) - if len(parts) >= 2 { - remote_set[parts[1]] = true + for section_name, section in m { + if strings.has_prefix(section_name, "remote ") { + if url, ok := section["url"]; ok { + remote_set[url] = true + } } } - for remote, _ in remote_set { + for remote in remote_set { cloned, _ := strings.clone(remote) append(&remotes, cloned) } @@ -516,12 +473,6 @@ update_dir :: proc(f: ^EnvFile, new_dir: string) { } find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) { - feats := check_features() - if .Git not_in feats { - fmt.println("Error: git is required for moved dir detection") - return {}, false - } - roots, roots_ok := find_git_roots(d.cfg) if !roots_ok { return {}, false diff --git a/db_test.odin b/db_test.odin index ba0587a..217e758 100644 --- a/db_test.odin +++ b/db_test.odin @@ -319,11 +319,85 @@ test_shares_remote_both_empty :: proc(t: ^testing.T) { testing.expect(t, !shares_remote(&f, remotes), "both empty should not share") } +delete_remotes :: proc(remotes: [dynamic]string) { + for &r in remotes { + delete(r) + } + delete(remotes) +} + @(test) -test_make_temp_path_format :: proc(t: ^testing.T) { - p := make_temp_path() - testing.expect(t, strings.has_suffix(p, ".db"), "should end with .db") - testing.expect(t, strings.contains(p, fmt.tprintf("%d", os.get_pid())), "should contain PID") +test_get_git_remotes_single :: proc(t: ^testing.T) { + 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) + os.mkdir_all(git_dir) + + config_content := "[core]\n\trepositoryformatversion = 0\n[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n" + config_path := fmt.tprintf("%s/config", git_dir) + err := os.write_entire_file(config_path, transmute([]u8)config_content) + testing.expect(t, err == nil, "should write .git/config") + + remotes := get_git_remotes(base) + defer delete_remotes(remotes) + + testing.expect(t, len(remotes) == 1, "should find 1 remote") + if len(remotes) != 1 do return + testing.expect_value(t, remotes[0], "git@github.com:user/repo.git") +} + +@(test) +test_get_git_remotes_multiple :: proc(t: ^testing.T) { + 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) + os.mkdir_all(git_dir) + + config_content := "[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n[remote \"upstream\"]\n\turl = https://gitlab.com/upstream/repo.git\n" + config_path := fmt.tprintf("%s/config", git_dir) + err := os.write_entire_file(config_path, transmute([]u8)config_content) + testing.expect(t, err == nil, "should write .git/config") + + remotes := get_git_remotes(base) + defer delete_remotes(remotes) + + testing.expect(t, len(remotes) == 2, "should find 2 remotes") +} + +@(test) +test_get_git_remotes_no_config :: proc(t: ^testing.T) { + 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) + defer delete_remotes(remotes) + + testing.expect(t, len(remotes) == 0, "should return empty when no .git/config") +} + +@(test) +test_get_git_remotes_no_remotes :: proc(t: ^testing.T) { + 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) + os.mkdir_all(git_dir) + + config_content := "[core]\n\trepositoryformatversion = 0\n\tbare = false\n" + config_path := fmt.tprintf("%s/config", git_dir) + err := os.write_entire_file(config_path, transmute([]u8)config_content) + testing.expect(t, err == nil, "should write .git/config") + + remotes := get_git_remotes(base) + defer delete_remotes(remotes) + + testing.expect(t, len(remotes) == 0, "should return empty when no remote sections") } @(test) diff --git a/docs/cli/envr.md b/docs/cli/envr.md index 7066b1f..71141c5 100644 --- a/docs/cli/envr.md +++ b/docs/cli/envr.md @@ -45,7 +45,6 @@ at before, restore your backup with: * [envr backup](envr_backup.md) - Import a .env file into envr * [envr check](envr_check.md) - check if files in the current directory are backed up -* [envr deps](envr_deps.md) - Check for missing binaries * [envr edit-config](envr_edit-config.md) - Edit your config with your default editor * [envr init](envr_init.md) - Set up envr * [envr list](envr_list.md) - View your tracked files diff --git a/docs/cli/envr_deps.md b/docs/cli/envr_deps.md deleted file mode 100644 index d78911b..0000000 --- a/docs/cli/envr_deps.md +++ /dev/null @@ -1,24 +0,0 @@ -## envr deps - -Check for missing binaries - -### Synopsis - -envr relies on external binaries for certain functionality. - -The check command reports on which binaries are available and which are not. - -``` -envr deps [flags] -``` - -### Options - -``` - -h, --help help for deps -``` - -### SEE ALSO - -* [envr](envr.md) - Manage your .env files. - diff --git a/features.odin b/features.odin deleted file mode 100644 index eaf2876..0000000 --- a/features.odin +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import "base:runtime" -import "core:mem" -import "core:os" -import "core:strings" - -Feature :: enum { - Git, -} - -AvailableFeatures :: bit_set[Feature] - -check_features :: proc() -> AvailableFeatures { - feats: AvailableFeatures - - s: mem.Scratch - mem.scratch_init(&s, 4 * mem.DEFAULT_PAGE_SIZE) - defer mem.scratch_destroy(&s) - - context.temp_allocator = mem.scratch_allocator(&s) - - path_env := os.get_env("PATH", context.temp_allocator) - paths := strings.split(path_env, ":", context.temp_allocator) - - if find_binary(paths, "git") != "" { - feats += {.Git} - } - - return feats -} - -find_binary :: proc( - paths: []string, - name: string, - allocator: runtime.Allocator = context.temp_allocator, -) -> string { - for p in paths { - candidate := strings.join({strings.trim_right(p, "/"), name}, "/", allocator) - _, err := os.stat(candidate, allocator) - if err == nil { - return candidate - } - } - return "" -} - diff --git a/features_test.odin b/features_test.odin deleted file mode 100644 index c48fd97..0000000 --- a/features_test.odin +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import "core:os" -import "core:strings" -import "core:testing" - -@(test) -test_find_binary_exists :: proc(t: ^testing.T) { - path := os.get_env("PATH", context.temp_allocator) - paths := strings.split(path, ":", context.temp_allocator) - - result := find_binary(paths, "sh") - testing.expect(t, result != "", "sh should be found on PATH") -} - -@(test) -test_find_binary_not_exists :: proc(t: ^testing.T) { - old_path := os.get_env("PATH", context.temp_allocator) - defer { - if old_path != "" { - os.set_env("PATH", old_path) - } - } - - os.set_env("PATH", "/tmp/envr-nope") - - path := os.get_env("PATH", context.temp_allocator) - paths := strings.split(path, ":", context.temp_allocator) - - - result := find_binary(paths, "no_such_binary_xyz") - testing.expect(t, result == "", "nonexistent binary should not be found") -} - diff --git a/main.odin b/main.odin index 6274545..5f63ed7 100644 --- a/main.odin +++ b/main.odin @@ -18,8 +18,6 @@ main :: proc() { cmd_init(&cmd) case "version": cmd_version(&cmd) - case "deps": - cmd_deps(&cmd) case "list": cmd_list(&cmd) case "backup", "add":