mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 18:48:33 -04:00
feat: Removed runtime git dependency.
This also allowed us to drop the Features code.
This commit is contained in:
28
README.md
28
README.md
@@ -12,14 +12,13 @@ the tool [of your choosing](#backup-options).
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🔐 **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
- **Encrypted Storage**: All `.env` files are encrypted using your ssh key and
|
||||||
[libsodium](https://github.com/jedisct1/libsodium) encryption.
|
[libsodium](https://github.com/jedisct1/libsodium).
|
||||||
- 🔄 **Automatic Sync**: Update the database with one command, which can easily
|
- **Automatic Sync**: Update the database with one command, which can easily
|
||||||
be run on a cron.
|
be run on a cron.
|
||||||
- 🔍 **Smart Scanning**: Automatically discover and import `.env` files in your
|
- **Smart Scanning**: Automatically discover and import `.env` files in your
|
||||||
home directory.
|
home directory.
|
||||||
- ✨ **Interactive CLI**: User-friendly prompts for file selection and management.
|
- **Rename Detection**: Automatically find and updates renamed/moved
|
||||||
- 🗂️ **Rename Detection**: Automatically finds and updates renamed/moved
|
|
||||||
repositories.
|
repositories.
|
||||||
|
|
||||||
## TODOS
|
## TODOS
|
||||||
@@ -28,15 +27,11 @@ repositories.
|
|||||||
- [x] Allow configuration of ssh key.
|
- [x] Allow configuration of ssh key.
|
||||||
- [x] Allow multiple ssh keys.
|
- [x] Allow multiple ssh keys.
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- An SSH key pair (for encryption/decryption)
|
|
||||||
- The following binaries:
|
|
||||||
- [fd](https://github.com/sharkdp/fd)
|
|
||||||
- [git](https://git-scm.com)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
You will need an SSH key pair for encryption and decryption. You can generate one
|
||||||
|
with `ssh-keygen -t ed25519`. It will be saved to `~/.ssh/id_ed25519`.
|
||||||
|
|
||||||
### With Odin
|
### With Odin
|
||||||
|
|
||||||
If you already have `odin` installed:
|
If you already have `odin` installed:
|
||||||
@@ -96,7 +91,12 @@ The configuration file is created during initialization:
|
|||||||
],
|
],
|
||||||
"scan": {
|
"scan": {
|
||||||
"matcher": "\\.env",
|
"matcher": "\\.env",
|
||||||
"exclude": "*.envrc",
|
"exclude": [
|
||||||
|
"*\\.envrc",
|
||||||
|
"\\.local/",
|
||||||
|
"node_modules",
|
||||||
|
"vendor"
|
||||||
|
],
|
||||||
"include": "~"
|
"include": "~"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,13 +35,7 @@ Stdout will be captured by redirecting `os.stdout` to a pipe.
|
|||||||
|
|
||||||
## Hard to test (interactive / external deps)
|
## 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)
|
### `cmd_scan` (cmd_scan.odin)
|
||||||
- Needs `fd` installed
|
|
||||||
- Test with fixture git repo containing `.env` files
|
- Test with fixture git repo containing `.env` files
|
||||||
- Test `find_unbacked` integration (already partially tested in `cmd_check_test.odin`)
|
- Test `find_unbacked` integration (already partially tested in `cmd_check_test.odin`)
|
||||||
- Non-TTY JSON output path
|
- 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.
|
- DB integration tests should use in-memory SQLite (`:memory:`) where possible.
|
||||||
- Temp dir fixtures should follow the pattern in `scan_test.odin`.
|
- 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.
|
- Tests that manipulate the `HOME` env var must use a mutex to prevent races with parallel test execution.
|
||||||
|
|||||||
8
TODOS.md
8
TODOS.md
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
1. Consider giving db its own allocator
|
1. Consider giving db its own allocator
|
||||||
|
|
||||||
|
25. Generate md and man pages again.
|
||||||
|
|
||||||
2. **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.
|
2. **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.
|
||||||
|
|
||||||
3. **db.odin:135, 250** — String interpolation into SQL (`VACUUM INTO '%s'`, `ATTACH DATABASE '%s'`). Currently safe because input is controlled, but fragile.
|
3. **db.odin:135, 250** — String interpolation into SQL (`VACUUM INTO '%s'`, `ATTACH DATABASE '%s'`). Currently safe because input is controlled, but fragile.
|
||||||
@@ -38,7 +40,9 @@
|
|||||||
|
|
||||||
23. procedures should be ordered by use, main at the top, then in the order they are called from main.
|
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. Shell completion
|
||||||
|
|
||||||
|
25. Bring back windows support / cross-compilation.
|
||||||
|
|
||||||
## Double-check AI output
|
## Double-check AI output
|
||||||
|
|
||||||
@@ -66,8 +70,6 @@
|
|||||||
- [ ] db.odin
|
- [ ] db.odin
|
||||||
- [ ] db_integration_test.odin
|
- [ ] db_integration_test.odin
|
||||||
- [ ] db_test.odin
|
- [ ] db_test.odin
|
||||||
- [x] features.odin
|
|
||||||
- [x] features_test.odin
|
|
||||||
- [x] main.odin
|
- [x] main.odin
|
||||||
- [x] prompt.odin
|
- [x] prompt.odin
|
||||||
- [ ] scan.odin
|
- [ ] scan.odin
|
||||||
|
|||||||
92
WINDOWS.md
92
WINDOWS.md
@@ -1,92 +0,0 @@
|
|||||||
# Windows Compatibility Guide
|
|
||||||
|
|
||||||
This document outlines Windows compatibility issues and solutions for the envr project.
|
|
||||||
|
|
||||||
## Critical Issues
|
|
||||||
|
|
||||||
### 1. Path Handling Bug (MUST FIX)
|
|
||||||
|
|
||||||
**File:** `app/env_file.go:209`
|
|
||||||
|
|
||||||
**Issue:** Uses `path.Join` instead of `filepath.Join`, which won't work correctly on Windows due to different path separators.
|
|
||||||
|
|
||||||
**Current code:**
|
|
||||||
```go
|
|
||||||
f.Path = path.Join(newDir, path.Base(f.Path))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fixed code:**
|
|
||||||
```go
|
|
||||||
f.Path = filepath.Join(newDir, filepath.Base(f.Path))
|
|
||||||
```
|
|
||||||
|
|
||||||
## External Dependencies
|
|
||||||
|
|
||||||
The application relies on external tools that need to be installed separately on Windows:
|
|
||||||
|
|
||||||
### Required Tools
|
|
||||||
|
|
||||||
1. **fd** - Fast file finder
|
|
||||||
- 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
|
|
||||||
- Unix file permissions (`0755`, `0644`) are used throughout the codebase
|
|
||||||
- These are safely ignored on Windows - no changes needed
|
|
||||||
|
|
||||||
### Editor Configuration
|
|
||||||
**File:** `cmd/edit_config.go:20-24`
|
|
||||||
|
|
||||||
**Issue:** Relies on `$EDITOR` environment variable which is less common on Windows.
|
|
||||||
|
|
||||||
**Current behavior:** Fails if `$EDITOR` is not set
|
|
||||||
|
|
||||||
**Recommended improvement:** Add fallback detection for Windows editors:
|
|
||||||
```go
|
|
||||||
editor := os.Getenv("EDITOR")
|
|
||||||
if editor == "" {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
editor = "notepad.exe" // or "code.exe" for VS Code
|
|
||||||
} else {
|
|
||||||
fmt.Println("Error: $EDITOR environment variable is not set")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation Instructions for Windows
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||
3. Build and run as normal:
|
|
||||||
```powershell
|
|
||||||
go build
|
|
||||||
.\envr.exe init
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing on Windows
|
|
||||||
|
|
||||||
After applying the critical path fix, the core functionality should work correctly on Windows. The application has been designed with cross-platform compatibility in mind, using:
|
|
||||||
|
|
||||||
- `filepath` package for path operations (mostly)
|
|
||||||
- `os.UserHomeDir()` for home directory detection
|
|
||||||
- Standard Go file operations
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- **1 critical bug** must be fixed for Windows compatibility
|
|
||||||
- **2 external tools** need to be installed
|
|
||||||
- **1 minor enhancement** recommended for better Windows UX
|
|
||||||
- Overall architecture is Windows-compatible
|
|
||||||
7
cli.odin
7
cli.odin
@@ -43,13 +43,6 @@ key somewhere, otherwise your data could be lost forever.`,
|
|||||||
{"list", "envr list", "View your tracked files", "", {}},
|
{"list", "envr list", "View your tracked files", "", {}},
|
||||||
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
|
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
|
||||||
{"check", "envr check [path]", "Check if files are backed up", "", {}},
|
{"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", "", {}},
|
{"version", "envr version", "Show envr's version", "", {}},
|
||||||
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
|
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
73
db.odin
73
db.odin
@@ -2,12 +2,12 @@ package main
|
|||||||
|
|
||||||
import "core:crypto/hash"
|
import "core:crypto/hash"
|
||||||
import "core:encoding/hex"
|
import "core:encoding/hex"
|
||||||
|
import "core:encoding/ini"
|
||||||
import "core:encoding/json"
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:time"
|
|
||||||
|
|
||||||
import "sqlite"
|
import "sqlite"
|
||||||
|
|
||||||
@@ -51,14 +51,6 @@ delete_envfile :: proc(f: ^EnvFile) {
|
|||||||
delete(f.contents)
|
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) {
|
db_open :: proc(cfg_path: string) -> (Db, bool) {
|
||||||
cfg, ok := load_config(cfg_path)
|
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 {
|
get_git_remotes :: proc(dir: string) -> [dynamic]string {
|
||||||
remotes: [dynamic]string
|
remotes: [dynamic]string
|
||||||
remote_set: map[string]bool
|
remote_set: map[string]bool
|
||||||
|
defer delete(remote_set)
|
||||||
|
|
||||||
b: strings.Builder
|
config_path, _ := filepath.join({dir, ".git", "config"}, context.temp_allocator)
|
||||||
strings.builder_init(&b)
|
m, _, ok := ini.load_map_from_path(config_path, context.allocator)
|
||||||
defer strings.builder_destroy(&b)
|
if !ok {
|
||||||
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 {
|
|
||||||
return remotes
|
return remotes
|
||||||
}
|
}
|
||||||
|
defer ini.delete_map(m)
|
||||||
|
|
||||||
args := []string{"git", "remote", "-v"}
|
for section_name, section in m {
|
||||||
desc := os.Process_Desc {
|
if strings.has_prefix(section_name, "remote ") {
|
||||||
command = args,
|
if url, ok := section["url"]; ok {
|
||||||
stdout = tmp_file,
|
remote_set[url] = true
|
||||||
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 remote, _ in remote_set {
|
for remote in remote_set {
|
||||||
cloned, _ := strings.clone(remote)
|
cloned, _ := strings.clone(remote)
|
||||||
append(&remotes, cloned)
|
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) {
|
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)
|
roots, roots_ok := find_git_roots(d.cfg)
|
||||||
if !roots_ok {
|
if !roots_ok {
|
||||||
return {}, false
|
return {}, false
|
||||||
|
|||||||
82
db_test.odin
82
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")
|
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)
|
||||||
test_make_temp_path_format :: proc(t: ^testing.T) {
|
test_get_git_remotes_single :: proc(t: ^testing.T) {
|
||||||
p := make_temp_path()
|
base := fmt.tprintf("/tmp/envr-test-remotes-%d", os.get_pid())
|
||||||
testing.expect(t, strings.has_suffix(p, ".db"), "should end with .db")
|
os.mkdir_all(base)
|
||||||
testing.expect(t, strings.contains(p, fmt.tprintf("%d", os.get_pid())), "should contain PID")
|
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)
|
@(test)
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ at before, restore your backup with:
|
|||||||
|
|
||||||
* [envr backup](envr_backup.md) - Import a .env file into envr
|
* [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 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 edit-config](envr_edit-config.md) - Edit your config with your default editor
|
||||||
* [envr init](envr_init.md) - Set up envr
|
* [envr init](envr_init.md) - Set up envr
|
||||||
* [envr list](envr_list.md) - View your tracked files
|
* [envr list](envr_list.md) - View your tracked files
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|
||||||
@@ -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 ""
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user