mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
Compare commits
3 Commits
b66e905482
...
07e60e4ab2
| Author | SHA1 | Date | |
|---|---|---|---|
| 07e60e4ab2 | |||
| 0523c09601 | |||
| f137fc79fc |
2
TODOS.md
2
TODOS.md
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
27. Commands are still leaking.
|
27. Commands are still leaking.
|
||||||
|
|
||||||
|
28. **db.odin** — Inconsistencies in how struct vs sqlite are named.
|
||||||
|
|
||||||
2. Generate md and man pages again.
|
2. Generate md and man pages again.
|
||||||
|
|
||||||
3. **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: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.
|
||||||
|
|||||||
@@ -54,8 +54,6 @@ cmd_check :: proc(cmd: ^Command) {
|
|||||||
if !list_ok {
|
if !list_ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer delete(db_files)
|
|
||||||
defer for &file in db_files {delete_envfile(&file)}
|
|
||||||
|
|
||||||
not_backed := find_unbacked(files_in_path[:], db_files[:])
|
not_backed := find_unbacked(files_in_path[:], db_files[:])
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ cmd_edit_config :: proc(cmd: ^Command) {
|
|||||||
|
|
||||||
config_path := cmd.config_path
|
config_path := cmd.config_path
|
||||||
|
|
||||||
_, stat_err := os.stat(config_path, context.allocator)
|
if !os.exists(config_path) {
|
||||||
if stat_err != nil {
|
|
||||||
fmt.wprintf(
|
fmt.wprintf(
|
||||||
cmd.err,
|
cmd.err,
|
||||||
"Config file does not exist at %s. Run 'envr init' first.\n",
|
"Config file does not exist at %s. Run 'envr init' first.\n",
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ cmd_list :: proc(cmd: ^Command) {
|
|||||||
if !list_ok {
|
if !list_ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer delete(rows)
|
|
||||||
defer for &row in rows {delete_envfile(&row)}
|
|
||||||
|
|
||||||
if terminal.is_terminal(os.stdout) {
|
if terminal.is_terminal(os.stdout) {
|
||||||
headers := []string{"Directory", "Path"}
|
headers := []string{"Directory", "Path"}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ cmd_scan :: proc(cmd: ^Command) {
|
|||||||
}
|
}
|
||||||
defer db_close(&db)
|
defer db_close(&db)
|
||||||
|
|
||||||
search_dirs := search_paths(db.cfg)
|
search_dirs := search_paths(db.cfg, context.temp_allocator)
|
||||||
if len(search_dirs) == 0 {
|
if len(search_dirs) == 0 {
|
||||||
fmt.wprintln(
|
fmt.wprintln(
|
||||||
cmd.err,
|
cmd.err,
|
||||||
|
|||||||
@@ -24,63 +24,42 @@ cmd_sync :: proc(cmd: ^Command) {
|
|||||||
if !list_ok {
|
if !list_ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer delete(files)
|
|
||||||
|
|
||||||
// TODO: Set sane default size
|
results := make([]SyncEntry, len(files), context.temp_allocator)
|
||||||
results: [dynamic]SyncEntry
|
|
||||||
defer delete(results)
|
|
||||||
|
|
||||||
for &file in files {
|
for &file, i in files {
|
||||||
old_path: string
|
result, err := db_sync(&db, &file)
|
||||||
old_path, _ = strings.clone(file.Path, context.temp_allocator)
|
|
||||||
|
|
||||||
result, err_msg := db_sync(&db, &file)
|
|
||||||
|
|
||||||
status: string
|
status: string
|
||||||
is_dir_updated := .DirUpdated in result
|
if err != .None {
|
||||||
|
status = sync_error_message(err)
|
||||||
switch {
|
} else if .BackedUp in result {
|
||||||
case .Error in result:
|
status = .DirUpdated in result ? "Moved & Backed Up" : "Backed Up"
|
||||||
if len(err_msg) > 0 {
|
} else if .Restored in result {
|
||||||
status = err_msg
|
status = .DirUpdated in result ? "Moved & Restored" : "Restored"
|
||||||
} else {
|
} else if .DirUpdated in result {
|
||||||
status = "error"
|
|
||||||
}
|
|
||||||
case .BackedUp in result:
|
|
||||||
status = "Backed Up"
|
|
||||||
case .Restored in result:
|
|
||||||
status = "Restored"
|
|
||||||
case .DirUpdated in result:
|
|
||||||
status = "Moved"
|
status = "Moved"
|
||||||
case:
|
} else {
|
||||||
status = "OK"
|
status = "OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_dir_updated {
|
// TODO: Handle errors
|
||||||
if !db_delete(&db, old_path) {
|
path_str, _ := strings.clone(file.Path, context.temp_allocator)
|
||||||
return
|
status_str, _ := strings.clone(status, context.temp_allocator)
|
||||||
}
|
results[i] = SyncEntry {
|
||||||
|
Path = path_str,
|
||||||
|
Status = status_str,
|
||||||
}
|
}
|
||||||
if db_update_required(result) {
|
|
||||||
if !db_insert(&db, file) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
path_str, _ := strings.clone(file.Path)
|
|
||||||
status_str, _ := strings.clone(status)
|
|
||||||
append(&results, SyncEntry{Path = path_str, Status = status_str})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if terminal.is_terminal(os.stdout) {
|
if terminal.is_terminal(os.stdout) {
|
||||||
headers := []string{"File", "Status"}
|
headers := []string{"File", "Status"}
|
||||||
table_rows := make([dynamic][]string, 0, len(results))
|
// TODO: Use [2]string instead of slice here
|
||||||
|
table_rows := make([dynamic][]string, 0, len(results), context.temp_allocator)
|
||||||
|
|
||||||
for res in results {
|
for res in results {
|
||||||
row_slice := make([]string, 2)
|
row_slice := [2]string{res.Path, res.Status}
|
||||||
row_slice[0] = res.Path
|
append(&table_rows, row_slice[:])
|
||||||
row_slice[1] = res.Status
|
|
||||||
append(&table_rows, row_slice)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render_table(cmd.out, headers, table_rows[:])
|
render_table(cmd.out, headers, table_rows[:])
|
||||||
@@ -94,3 +73,23 @@ cmd_sync :: proc(cmd: ^Command) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sync_error_message :: proc(e: SyncError) -> string {
|
||||||
|
switch e {
|
||||||
|
case .None:
|
||||||
|
return ""
|
||||||
|
case .DirMissing:
|
||||||
|
return "directory missing"
|
||||||
|
case .MultipleDirs:
|
||||||
|
return "multiple directories found"
|
||||||
|
case .GitRootFailed:
|
||||||
|
return "failed to find git roots"
|
||||||
|
case .WriteFailed:
|
||||||
|
return "failed to write file"
|
||||||
|
case .ReadFailed:
|
||||||
|
return "failed to read file"
|
||||||
|
case .DbFailed:
|
||||||
|
return "failed to update database"
|
||||||
|
}
|
||||||
|
return "unknown error"
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
51
config.odin
51
config.odin
@@ -25,17 +25,16 @@ Config :: struct {
|
|||||||
config_path: string `json:"-"`,
|
config_path: string `json:"-"`,
|
||||||
}
|
}
|
||||||
|
|
||||||
load_config :: proc(config_path: string) -> (Config, bool) {
|
load_config :: proc(config_path: string, allocator := context.allocator) -> (Config, bool) {
|
||||||
data, read_err := os.read_entire_file_from_path(config_path, context.allocator)
|
// TODO: Should we use context.allocator + defer delete()?
|
||||||
|
data, read_err := os.read_entire_file_from_path(config_path, context.temp_allocator)
|
||||||
if read_err != nil {
|
if read_err != nil {
|
||||||
fmt.println("No config file found. Please run `envr init` to generate one.")
|
fmt.println("No config file found. Please run `envr init` to generate one.")
|
||||||
return Config{}, false
|
return Config{}, false
|
||||||
}
|
}
|
||||||
defer delete(data)
|
|
||||||
|
|
||||||
cfg: Config
|
cfg: Config
|
||||||
// TODO: use json 5
|
err := json.unmarshal(data, &cfg, .JSON5, allocator)
|
||||||
err := json.unmarshal(data, &cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.printf("Error parsing config: %v\n", err)
|
fmt.printf("Error parsing config: %v\n", err)
|
||||||
return Config{}, false
|
return Config{}, false
|
||||||
@@ -53,22 +52,22 @@ default_config_path :: proc(home: string, allocator := context.allocator) -> str
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
delete_config :: proc(cfg: ^Config) {
|
delete_config :: proc(cfg: ^Config, allocator := context.allocator) {
|
||||||
for key in cfg.Keys {
|
for key in cfg.Keys {
|
||||||
delete(key.Private)
|
delete(key.Private, allocator)
|
||||||
delete(key.Public)
|
delete(key.Public, allocator)
|
||||||
}
|
}
|
||||||
delete(cfg.Keys)
|
delete(cfg.Keys)
|
||||||
|
|
||||||
delete(cfg.ScanConfig.Matcher)
|
delete(cfg.ScanConfig.Matcher, allocator)
|
||||||
|
|
||||||
for exclude in cfg.ScanConfig.Exclude {
|
for exclude in cfg.ScanConfig.Exclude {
|
||||||
delete(exclude)
|
delete(exclude, allocator)
|
||||||
}
|
}
|
||||||
delete(cfg.ScanConfig.Exclude)
|
delete(cfg.ScanConfig.Exclude)
|
||||||
|
|
||||||
for include in cfg.ScanConfig.Include {
|
for include in cfg.ScanConfig.Include {
|
||||||
delete(include)
|
delete(include, allocator)
|
||||||
}
|
}
|
||||||
delete(cfg.ScanConfig.Include)
|
delete(cfg.ScanConfig.Include)
|
||||||
}
|
}
|
||||||
@@ -188,32 +187,40 @@ find_ssh_private_keys :: proc() -> (keys: [dynamic]string, ok: bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
find_git_roots :: proc(cfg: Config) -> (roots: [dynamic]string, ok: bool) {
|
find_git_roots :: proc(
|
||||||
paths := search_paths(cfg)
|
cfg: Config,
|
||||||
|
allocator := context.temp_allocator,
|
||||||
|
) -> (
|
||||||
|
roots: [dynamic]string,
|
||||||
|
ok: bool,
|
||||||
|
) {
|
||||||
|
paths := search_paths(cfg, allocator)
|
||||||
|
// TODO: Pass allocator to findr
|
||||||
|
// findr.find_repos(paths[:], &roots, os.get_processor_core_count(), allocator)
|
||||||
findr.find_repos(paths[:], &roots, os.get_processor_core_count())
|
findr.find_repos(paths[:], &roots, os.get_processor_core_count())
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
search_paths :: proc(cfg: Config) -> (paths: [dynamic]string) {
|
search_paths :: proc(cfg: Config, allocator := context.allocator) -> [dynamic]string {
|
||||||
// TODO: Is this okay?
|
|
||||||
// TODO: handle error
|
// TODO: handle error
|
||||||
home, _ := os.user_home_dir(context.temp_allocator)
|
home, _ := os.user_home_dir(context.temp_allocator)
|
||||||
|
|
||||||
for include in cfg.ScanConfig.Include {
|
paths, _ := new_clone(cfg.ScanConfig.Include, allocator)
|
||||||
|
|
||||||
|
for &include in paths {
|
||||||
// TODO: Do we need to manually expand ~/ in odin?
|
// TODO: Do we need to manually expand ~/ in odin?
|
||||||
expanded, _ := strings.replace(include, "~", home, 1)
|
expanded, _ := strings.replace(include, "~", home, 1, allocator)
|
||||||
if filepath.is_abs(expanded) {
|
if filepath.is_abs(expanded) {
|
||||||
append(&paths, expanded)
|
include = expanded
|
||||||
} else {
|
} else {
|
||||||
defer delete(expanded)
|
resolved, err := filepath.abs(expanded, allocator)
|
||||||
resolved, err := filepath.abs(expanded)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
append(&paths, resolved)
|
include = resolved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return paths^
|
||||||
}
|
}
|
||||||
|
|
||||||
envr_dir :: proc(config_path: string) -> string {
|
envr_dir :: proc(config_path: string) -> string {
|
||||||
|
|||||||
@@ -187,14 +187,10 @@ test_search_paths_expands_tilde :: proc(t: ^testing.T) {
|
|||||||
cfg := Config {
|
cfg := Config {
|
||||||
ScanConfig = ScanConfig{Include = make([dynamic]string, 0, 1)},
|
ScanConfig = ScanConfig{Include = make([dynamic]string, 0, 1)},
|
||||||
}
|
}
|
||||||
defer delete(cfg.ScanConfig.Include)
|
|
||||||
append(&cfg.ScanConfig.Include, "~")
|
append(&cfg.ScanConfig.Include, "~")
|
||||||
|
defer delete(cfg.ScanConfig.Include)
|
||||||
|
|
||||||
paths := search_paths(cfg)
|
paths := search_paths(cfg, context.temp_allocator)
|
||||||
defer delete(paths)
|
|
||||||
for path in paths {
|
|
||||||
defer delete(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
testing.expect(t, len(paths) == 1, "should have 1 path")
|
testing.expect(t, len(paths) == 1, "should have 1 path")
|
||||||
if len(paths) > 0 {
|
if len(paths) > 0 {
|
||||||
|
|||||||
317
db.odin
317
db.odin
@@ -5,6 +5,7 @@ import "core:encoding/hex"
|
|||||||
import "core:encoding/ini"
|
import "core:encoding/ini"
|
||||||
import "core:encoding/json"
|
import "core:encoding/json"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
@@ -12,18 +13,21 @@ import "core:strings"
|
|||||||
import "sqlite"
|
import "sqlite"
|
||||||
|
|
||||||
SyncFlagEnum :: enum {
|
SyncFlagEnum :: enum {
|
||||||
Noop,
|
|
||||||
DirUpdated,
|
DirUpdated,
|
||||||
Restored,
|
Restored,
|
||||||
BackedUp,
|
BackedUp,
|
||||||
Error,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncFlag :: bit_set[SyncFlagEnum]
|
SyncFlag :: bit_set[SyncFlagEnum]
|
||||||
|
|
||||||
SyncDirection :: enum {
|
SyncError :: enum {
|
||||||
TrustDatabase,
|
None,
|
||||||
TrustFilesystem,
|
DirMissing,
|
||||||
|
MultipleDirs,
|
||||||
|
GitRootFailed,
|
||||||
|
WriteFailed,
|
||||||
|
ReadFailed,
|
||||||
|
DbFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
Db :: struct {
|
Db :: struct {
|
||||||
@@ -31,6 +35,7 @@ Db :: struct {
|
|||||||
db: ^rawptr,
|
db: ^rawptr,
|
||||||
cfg: Config,
|
cfg: Config,
|
||||||
changed: bool,
|
changed: bool,
|
||||||
|
arena: mem.Dynamic_Arena,
|
||||||
}
|
}
|
||||||
|
|
||||||
EnvFile :: struct {
|
EnvFile :: struct {
|
||||||
@@ -41,6 +46,7 @@ EnvFile :: struct {
|
|||||||
contents: string,
|
contents: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(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 {
|
||||||
@@ -52,32 +58,15 @@ delete_envfile :: proc(f: ^EnvFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
|
db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
|
||||||
database.cfg = load_config(cfg_path) or_return
|
database = db_init() or_return
|
||||||
|
database.cfg = load_config(cfg_path, db_allocator(&database)) or_return
|
||||||
{
|
|
||||||
db: ^rawptr
|
|
||||||
rc := sqlite.db_open(":memory:", &db)
|
|
||||||
if rc != sqlite.OK {
|
|
||||||
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
|
|
||||||
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
|
|
||||||
if rc != sqlite.OK {
|
|
||||||
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
|
|
||||||
sqlite.db_close(db)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
database.db = db
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use different allocators?
|
// TODO: Use different allocators?
|
||||||
data_path := data_path(database.cfg.config_path, context.temp_allocator)
|
data_path := data_path(database.cfg.config_path, context.temp_allocator)
|
||||||
if os.exists(data_path) {
|
if os.exists(data_path) {
|
||||||
if ok = db_restore_from_encrypted(&database, data_path); !ok {
|
if ok = db_restore_from_encrypted(&database, data_path); !ok {
|
||||||
sqlite.db_close(database.db)
|
sqlite.db_close(database.db)
|
||||||
return
|
return database, false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// DB was created
|
// DB was created
|
||||||
@@ -87,14 +76,42 @@ db_open :: proc(cfg_path: string) -> (database: Db, ok: bool) {
|
|||||||
return database, true
|
return database, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a database an allocator and fresh, empty table, with zero encryption.
|
||||||
|
// In production, you most likely want to use `db_open`.
|
||||||
|
db_init :: proc() -> (database: Db, ok: bool) {
|
||||||
|
db: ^rawptr
|
||||||
|
rc := sqlite.db_open(":memory:", &db)
|
||||||
|
if rc != sqlite.OK {
|
||||||
|
fmt.printf("Error opening in-memory database: %s\n", sqlite.db_errmsg(db))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
|
||||||
|
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
|
||||||
|
if rc != sqlite.OK {
|
||||||
|
fmt.printf("Error creating table: %s\n", sqlite.db_errmsg(db))
|
||||||
|
sqlite.db_close(db)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
database.db = db
|
||||||
|
|
||||||
|
mem.dynamic_arena_init(&database.arena)
|
||||||
|
|
||||||
|
return database, true
|
||||||
|
}
|
||||||
|
|
||||||
|
db_allocator :: proc(db: ^Db) -> mem.Allocator {
|
||||||
|
return mem.dynamic_arena_allocator(&db.arena)
|
||||||
|
}
|
||||||
|
|
||||||
db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
||||||
encrypted_data, read_err := os.read_entire_file_from_path(data_path, context.allocator)
|
encrypted_data, read_err := os.read_entire_file_from_path(data_path, context.temp_allocator)
|
||||||
defer delete(encrypted_data)
|
|
||||||
if read_err != nil {
|
if read_err != nil {
|
||||||
fmt.printf("Error reading encrypted database: %v\n", read_err)
|
fmt.printf("Error reading encrypted database: %v\n", read_err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use context.temp_allocator
|
||||||
plaintext, dec_ok := decrypt(encrypted_data, db.cfg.Keys[:])
|
plaintext, dec_ok := decrypt(encrypted_data, db.cfg.Keys[:])
|
||||||
if !dec_ok {
|
if !dec_ok {
|
||||||
fmt.println("Error: decryption failed")
|
fmt.println("Error: decryption failed")
|
||||||
@@ -128,8 +145,15 @@ db_restore_from_encrypted :: proc(db: ^Db, data_path: string) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db_close :: proc(d: ^Db) {
|
db_close :: proc(d: ^Db) {
|
||||||
defer sqlite.db_close(d.db)
|
allocator := db_allocator(d)
|
||||||
defer delete_config(&d.cfg)
|
|
||||||
|
defer {
|
||||||
|
sqlite.db_close(d.db)
|
||||||
|
|
||||||
|
delete_config(&d.cfg, allocator)
|
||||||
|
|
||||||
|
mem.dynamic_arena_destroy(&d.arena)
|
||||||
|
}
|
||||||
|
|
||||||
if d.changed {
|
if d.changed {
|
||||||
rc := sqlite.db_exec(d.db, "VACUUM", nil, nil, nil)
|
rc := sqlite.db_exec(d.db, "VACUUM", nil, nil, nil)
|
||||||
@@ -147,13 +171,14 @@ db_close :: proc(d: ^Db) {
|
|||||||
defer sqlite.free(data)
|
defer sqlite.free(data)
|
||||||
|
|
||||||
sqlite_data := data[:sz]
|
sqlite_data := data[:sz]
|
||||||
|
// TODO: PAss allocator chain
|
||||||
encrypted, enc_ok := encrypt(sqlite_data, d.cfg.Keys[:])
|
encrypted, enc_ok := encrypt(sqlite_data, d.cfg.Keys[:])
|
||||||
if !enc_ok {
|
if !enc_ok {
|
||||||
fmt.println("Error: encryption failed")
|
fmt.println("Error: encryption failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data_path := data_path(d.cfg.config_path)
|
data_path := data_path(d.cfg.config_path, allocator)
|
||||||
envr_d := envr_dir(d.cfg.config_path)
|
envr_d := envr_dir(d.cfg.config_path)
|
||||||
os.mkdir_all(envr_d)
|
os.mkdir_all(envr_d)
|
||||||
|
|
||||||
@@ -168,7 +193,8 @@ db_close :: proc(d: ^Db) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]EnvFile, ok: bool) {
|
// Results will be freed when `db_close` is called.
|
||||||
|
db_list :: proc(d: ^Db) -> ([]EnvFile, bool) {
|
||||||
stmt: ^rawptr
|
stmt: ^rawptr
|
||||||
rc := sqlite.prepare_v2(
|
rc := sqlite.prepare_v2(
|
||||||
d.db,
|
d.db,
|
||||||
@@ -179,10 +205,13 @@ db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]En
|
|||||||
)
|
)
|
||||||
if rc != sqlite.OK {
|
if rc != sqlite.OK {
|
||||||
fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(d.db))
|
fmt.printf("Error preparing query: %s\n", sqlite.db_errmsg(d.db))
|
||||||
return
|
return []EnvFile{}, false
|
||||||
}
|
}
|
||||||
defer sqlite.finalize(stmt)
|
defer sqlite.finalize(stmt)
|
||||||
|
|
||||||
|
allocator := db_allocator(d)
|
||||||
|
results := make([dynamic]EnvFile, 0, 10, allocator)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
rc = sqlite.step(stmt)
|
rc = sqlite.step(stmt)
|
||||||
if rc == sqlite.DONE {
|
if rc == sqlite.DONE {
|
||||||
@@ -190,7 +219,7 @@ db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]En
|
|||||||
}
|
}
|
||||||
if rc != sqlite.ROW {
|
if rc != sqlite.ROW {
|
||||||
fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(d.db))
|
fmt.printf("Error stepping query: %s\n", sqlite.db_errmsg(d.db))
|
||||||
return
|
#no_bounds_check return results[:], false
|
||||||
}
|
}
|
||||||
|
|
||||||
remotes_json := string(sqlite.column_text(stmt, 1))
|
remotes_json := string(sqlite.column_text(stmt, 1))
|
||||||
@@ -212,17 +241,16 @@ db_list :: proc(d: ^Db, allocator := context.allocator) -> (results: [dynamic]En
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true
|
#no_bounds_check return results[:], true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Should we use context.temp_allocator for proc scoped lifetimes?
|
||||||
db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
|
db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
|
||||||
remotes_json, marshal_err := json.marshal(file.Remotes)
|
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
|
||||||
}
|
}
|
||||||
defer delete(remotes_json)
|
|
||||||
|
|
||||||
sql: cstring =
|
sql: cstring =
|
||||||
"INSERT OR REPLACE INTO " +
|
"INSERT OR REPLACE INTO " +
|
||||||
@@ -278,7 +306,8 @@ db_insert :: proc(d: ^Db, file: EnvFile) -> bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFile, bool) {
|
// Result will be freed when `db_close` is called.
|
||||||
|
db_fetch :: proc(d: ^Db, path: string) -> (EnvFile, bool) {
|
||||||
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: ^rawptr
|
stmt: ^rawptr
|
||||||
rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil)
|
rc := sqlite.prepare_v2(d.db, sql, -1, &stmt, nil)
|
||||||
@@ -288,6 +317,8 @@ db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFi
|
|||||||
}
|
}
|
||||||
defer sqlite.finalize(stmt)
|
defer sqlite.finalize(stmt)
|
||||||
|
|
||||||
|
allocator := db_allocator(d)
|
||||||
|
|
||||||
cpath := to_cstring(path, allocator)
|
cpath := to_cstring(path, allocator)
|
||||||
defer delete(cpath, allocator)
|
defer delete(cpath, allocator)
|
||||||
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
rc = sqlite.bind_text(stmt, 1, cpath, -1, nil)
|
||||||
@@ -311,7 +342,7 @@ db_fetch :: proc(d: ^Db, path: string, allocator := context.allocator) -> (EnvFi
|
|||||||
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
json.unmarshal_string(remotes_json, &remotes, allocator = allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
file_path := clone_cstring(sqlite.column_text(stmt, 0))
|
file_path := clone_cstring(sqlite.column_text(stmt, 0), allocator)
|
||||||
|
|
||||||
return EnvFile {
|
return EnvFile {
|
||||||
Path = file_path,
|
Path = file_path,
|
||||||
@@ -364,7 +395,8 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
|||||||
|
|
||||||
dir := filepath.dir(abs_path)
|
dir := filepath.dir(abs_path)
|
||||||
|
|
||||||
remotes := get_git_remotes(dir)
|
// TODO: Should we use the db allocator here?
|
||||||
|
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)
|
defer delete(data)
|
||||||
@@ -387,126 +419,101 @@ new_env_file :: proc(path: string) -> (EnvFile, bool) {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, string) {
|
// Reconciles `f` with the filesystem and persists changes to the database.
|
||||||
return env_file_sync(f, .TrustFilesystem, d)
|
db_sync :: proc(d: ^Db, f: ^EnvFile) -> (SyncFlag, SyncError) {
|
||||||
}
|
allocator := db_allocator(d)
|
||||||
|
|
||||||
// If SyncFlag is .BackedUp, Caller is responsible for calling delete on f.contents and f.Sha256
|
|
||||||
env_file_sync :: proc(f: ^EnvFile, dir: SyncDirection, d: ^Db) -> (SyncFlag, string) {
|
|
||||||
result: SyncFlag = {}
|
result: SyncFlag = {}
|
||||||
|
old_path := f.Path
|
||||||
|
|
||||||
_, stat_err := os.stat(f.Dir, context.allocator)
|
if !os.exists(f.Dir) {
|
||||||
if stat_err != nil {
|
moved, err := try_move_dir(d, f, allocator)
|
||||||
moved_dirs: [dynamic]string
|
if !moved {
|
||||||
|
return {}, err
|
||||||
if d != nil {
|
|
||||||
dirs, dirs_ok := find_moved_dirs(d, f)
|
|
||||||
if !dirs_ok {
|
|
||||||
return {.Error}, "failed to find moved dirs"
|
|
||||||
}
|
|
||||||
moved_dirs = dirs
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(moved_dirs) == 0 {
|
|
||||||
return {.Error}, "directory missing"
|
|
||||||
} else if len(moved_dirs) == 1 {
|
|
||||||
update_dir(f, moved_dirs[0])
|
|
||||||
result = {.DirUpdated}
|
|
||||||
} else {
|
|
||||||
return {.Error}, "multiple directories found"
|
|
||||||
}
|
}
|
||||||
|
result += {.DirUpdated}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, file_stat_err := os.stat(f.Path, context.allocator)
|
if !os.exists(f.Path) {
|
||||||
if file_stat_err != nil {
|
|
||||||
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 {
|
||||||
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
fmt.eprintf("db_sync: failed to write %s: %v\n", f.Path, write_err)
|
||||||
return {.Error}, msg
|
return result, .WriteFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
return result + {.Restored}, ""
|
if !db_persist(d, f, old_path) {
|
||||||
|
return result, .DbFailed
|
||||||
|
}
|
||||||
|
return result + {.Restored}, .None
|
||||||
}
|
}
|
||||||
|
|
||||||
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
data, read_err := os.read_entire_file_from_path(f.Path, allocator)
|
||||||
if read_err != nil {
|
if read_err != nil {
|
||||||
msg, _ := strings.concatenate(
|
fmt.eprintf("db_sync: failed to read %s: %v\n", f.Path, read_err)
|
||||||
{"failed to read file for SHA comparison: ", fmt.tprintf("%v", read_err)},
|
return result, .ReadFailed
|
||||||
)
|
|
||||||
return {.Error}, msg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data)
|
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
|
||||||
// TODO: Handle error
|
hex_bytes, hex_err := hex.encode(digest, allocator)
|
||||||
hex_bytes, _ := hex.encode(digest)
|
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 {
|
||||||
return result, ""
|
if !db_persist(d, f, old_path) {
|
||||||
}
|
return result, .DbFailed
|
||||||
|
|
||||||
switch dir {
|
|
||||||
case .TrustDatabase:
|
|
||||||
write_err := os.write_entire_file(f.Path, f.contents)
|
|
||||||
if write_err != nil {
|
|
||||||
msg, _ := strings.concatenate({"failed to write file: ", fmt.tprintf("%v", write_err)})
|
|
||||||
return {.Error}, msg
|
|
||||||
}
|
}
|
||||||
return result + {.Restored}, ""
|
return result, .None
|
||||||
case .TrustFilesystem:
|
|
||||||
if !env_file_backup(f) {
|
|
||||||
return {.Error}, "failed to backup file"
|
|
||||||
}
|
|
||||||
return result + {.BackedUp}, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
find_moved_dirs :: proc(d: ^Db, f: ^EnvFile) -> ([dynamic]string, bool) {
|
|
||||||
roots, roots_ok := find_git_roots(d.cfg)
|
|
||||||
if !roots_ok {
|
|
||||||
return {}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
moved: [dynamic]string
|
|
||||||
for root in roots {
|
|
||||||
remotes := get_git_remotes(root)
|
|
||||||
if shares_remote(f, remotes[:]) {
|
|
||||||
cloned, _ := strings.clone(root)
|
|
||||||
append(&moved, cloned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return moved, true
|
|
||||||
}
|
|
||||||
|
|
||||||
update_dir :: proc(f: ^EnvFile, new_dir: string) {
|
|
||||||
f.Dir = new_dir
|
|
||||||
base := filepath.base(f.Path)
|
|
||||||
new_path, _ := strings.concatenate({new_dir, "/", base})
|
|
||||||
f.Path = new_path
|
|
||||||
f.Remotes = get_git_remotes(new_dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads the contents of the the file at f.Path into f.contents
|
|
||||||
//
|
|
||||||
// Caller is responsible for calling delete on f.contents and f.Sha256
|
|
||||||
env_file_backup :: proc(f: ^EnvFile) -> bool {
|
|
||||||
data, read_err := os.read_entire_file_from_path(f.Path, context.allocator)
|
|
||||||
if read_err != nil {
|
|
||||||
fmt.printf("Error reading file %s: %v\n", f.Path, read_err)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.contents = string(data)
|
f.contents = string(data)
|
||||||
digest := hash.hash_bytes(hash.Algorithm.SHA256, data, context.temp_allocator)
|
f.Sha256 = current_sha
|
||||||
hex_bytes, alloc_err := hex.encode(digest)
|
if !db_persist(d, f, old_path) {
|
||||||
if alloc_err != nil {
|
return result, .DbFailed
|
||||||
fmt.printf("Error generating hash for file %s: %v\n", f.Path, alloc_err)
|
}
|
||||||
return false
|
return result + {.BackedUp}, .None
|
||||||
|
}
|
||||||
|
|
||||||
|
db_persist :: proc(d: ^Db, f: ^EnvFile, old_path: string) -> bool {
|
||||||
|
if f.Path != old_path {
|
||||||
|
if !db_delete(d, old_path) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db_insert(d, f^)
|
||||||
|
}
|
||||||
|
|
||||||
|
try_move_dir :: proc(d: ^Db, f: ^EnvFile, allocator: mem.Allocator) -> (bool, SyncError) {
|
||||||
|
// TODO: Should thread the allocator through, right?
|
||||||
|
roots, ok := find_git_roots(d.cfg, allocator)
|
||||||
|
if !ok {
|
||||||
|
return false, .GitRootFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
match_count := 0
|
||||||
|
matched_dir: string
|
||||||
|
for root in roots {
|
||||||
|
remotes := get_git_remotes(root, context.temp_allocator)
|
||||||
|
if shares_remote(f, remotes[:]) {
|
||||||
|
match_count += 1
|
||||||
|
matched_dir = root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch match_count {
|
||||||
|
case 0:
|
||||||
|
return false, .DirMissing
|
||||||
|
case 1:
|
||||||
|
f.Dir = matched_dir
|
||||||
|
base := filepath.base(f.Path)
|
||||||
|
new_path, _ := filepath.join({f.Dir, base}, allocator)
|
||||||
|
f.Path = new_path
|
||||||
|
f.Remotes = get_git_remotes(f.Dir, allocator)
|
||||||
|
return true, .None
|
||||||
|
case:
|
||||||
|
return false, .MultipleDirs
|
||||||
}
|
}
|
||||||
f.Sha256 = string(hex_bytes)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
||||||
@@ -520,38 +527,34 @@ shares_remote :: proc(f: ^EnvFile, remotes: []string) -> bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
get_git_remotes :: proc(dir: string) -> [dynamic]string {
|
get_git_remotes :: proc(dir: string, allocator: mem.Allocator) -> [dynamic]string {
|
||||||
remotes: [dynamic]string
|
|
||||||
remote_set: map[string]bool
|
|
||||||
defer delete(remote_set)
|
|
||||||
|
|
||||||
config_path, _ := filepath.join({dir, ".git", "config"}, context.temp_allocator)
|
config_path, _ := filepath.join({dir, ".git", "config"}, context.temp_allocator)
|
||||||
m, _, ok := ini.load_map_from_path(config_path, context.allocator)
|
// TODO: Handle error
|
||||||
|
m, _, ok := ini.load_map_from_path(config_path, context.temp_allocator)
|
||||||
if !ok {
|
if !ok {
|
||||||
return remotes
|
return nil
|
||||||
}
|
}
|
||||||
defer ini.delete_map(m)
|
|
||||||
|
remotes := make([dynamic]string, 0, 1, allocator)
|
||||||
|
|
||||||
for section_name, section in m {
|
for section_name, section in m {
|
||||||
if strings.has_prefix(section_name, "remote ") {
|
if strings.has_prefix(section_name, "remote ") {
|
||||||
if url, ok := section["url"]; ok {
|
if url, ok := section["url"]; ok {
|
||||||
remote_set[url] = true
|
found := false
|
||||||
|
for r in remotes {
|
||||||
|
if r == url {found = true; break}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
cloned, _ := strings.clone(url, allocator)
|
||||||
|
append(&remotes, cloned)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for remote in remote_set {
|
|
||||||
cloned, _ := strings.clone(remote)
|
|
||||||
append(&remotes, cloned)
|
|
||||||
}
|
|
||||||
|
|
||||||
return remotes
|
return remotes
|
||||||
}
|
}
|
||||||
|
|
||||||
db_update_required :: proc(status: SyncFlag) -> bool {
|
|
||||||
return .BackedUp in status || .DirUpdated in status
|
|
||||||
}
|
|
||||||
|
|
||||||
to_cstring :: proc {
|
to_cstring :: proc {
|
||||||
string_to_cstring,
|
string_to_cstring,
|
||||||
strings.to_cstring,
|
strings.to_cstring,
|
||||||
@@ -566,7 +569,7 @@ string_to_cstring :: proc(s: string, allocator := context.allocator) -> cstring
|
|||||||
return cs
|
return cs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caller is responsible for freeing the result
|
// Unless an explicit allocator is passed, caller is responsible for freeing the result
|
||||||
clone_cstring :: proc(c: cstring, allocator := context.allocator) -> string {
|
clone_cstring :: proc(c: cstring, allocator := context.allocator) -> string {
|
||||||
str, err := strings.clone_from_cstring(c, allocator)
|
str, err := strings.clone_from_cstring(c, allocator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
338
db_test.odin
338
db_test.odin
@@ -1,5 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "core:crypto/hash"
|
||||||
|
import "core:encoding/hex"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
@@ -8,30 +10,13 @@ import "core:testing"
|
|||||||
|
|
||||||
import "sqlite"
|
import "sqlite"
|
||||||
|
|
||||||
make_test_db :: proc() -> (Db, bool) {
|
|
||||||
db: ^rawptr
|
|
||||||
rc := sqlite.db_open(":memory:", &db)
|
|
||||||
if rc != sqlite.OK {
|
|
||||||
return Db{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
create_sql: cstring = "CREATE TABLE IF NOT EXISTS envr_env_files (path TEXT PRIMARY KEY NOT NULL, remotes TEXT, sha256 TEXT NOT NULL, contents TEXT NOT NULL)"
|
|
||||||
rc = sqlite.db_exec(db, create_sql, nil, nil, nil)
|
|
||||||
if rc != sqlite.OK {
|
|
||||||
sqlite.db_close(db)
|
|
||||||
return Db{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return Db{db = db}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
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)),
|
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)
|
||||||
@@ -41,10 +26,10 @@ make_test_env_file :: proc(path, sha, contents: string, remotes: []string = {})
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_insert_and_fetch :: proc(t: ^testing.T) {
|
test_db_insert_and_fetch :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
path := "/project/.env"
|
path := "/project/.env"
|
||||||
sha := "abc123"
|
sha := "abc123"
|
||||||
@@ -56,7 +41,7 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, db_insert(&d, f), "insert should succeed")
|
testing.expect(t, db_insert(&d, f), "insert should succeed")
|
||||||
|
|
||||||
fetched, fetch_ok := db_fetch(&d, "/project/.env")
|
fetched, fetch_ok := db_fetch(&d, "/project/.env")
|
||||||
defer delete_envfile(&fetched)
|
// defer delete_envfile(&fetched)
|
||||||
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
|
||||||
|
|
||||||
@@ -69,10 +54,10 @@ test_db_insert_and_fetch :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_fetch_missing :: proc(t: ^testing.T) {
|
test_db_fetch_missing :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
_, fetch_ok := db_fetch(&d, "/nonexistent/.env")
|
_, fetch_ok := db_fetch(&d, "/nonexistent/.env")
|
||||||
testing.expect(t, !fetch_ok, "fetch missing should return false")
|
testing.expect(t, !fetch_ok, "fetch missing should return false")
|
||||||
@@ -80,10 +65,9 @@ test_db_fetch_missing :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_insert_or_replace :: proc(t: ^testing.T) {
|
test_db_insert_or_replace :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
|
defer db_close(&d)
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
|
||||||
defer sqlite.db_close(d.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)
|
||||||
@@ -95,18 +79,13 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
results, list_ok := db_list(&d)
|
results, list_ok := db_list(&d)
|
||||||
testing.expect(t, list_ok, "list should succeed")
|
testing.expect(t, list_ok, "list should succeed")
|
||||||
if !list_ok do return
|
|
||||||
defer delete(results)
|
|
||||||
for &result in results {
|
|
||||||
defer delete_envfile(&result)
|
|
||||||
}
|
|
||||||
|
|
||||||
testing.expect(t, len(results) == 1, "should have 1 row, not 2")
|
testing.expect(t, len(results) == 1, "should have 1 row, not 2")
|
||||||
|
|
||||||
fetched, fetch_ok := db_fetch(&d, "/project/.env")
|
fetched, fetch_ok := db_fetch(&d, "/project/.env")
|
||||||
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
|
||||||
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")
|
||||||
@@ -114,10 +93,10 @@ test_db_insert_or_replace :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_delete_existing :: proc(t: ^testing.T) {
|
test_db_delete_existing :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
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)
|
||||||
@@ -131,20 +110,19 @@ test_db_delete_existing :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_delete_missing :: proc(t: ^testing.T) {
|
test_db_delete_missing :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
testing.expect(t, !db_delete(&d, "/nonexistent/.env"), "delete missing should return false")
|
testing.expect(t, !db_delete(&d, "/nonexistent/.env"), "delete missing should return false")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_list_multiple :: proc(t: ^testing.T) {
|
test_db_list_multiple :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
defer db_close(&d)
|
||||||
defer sqlite.db_close(d.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)
|
||||||
@@ -158,36 +136,27 @@ test_db_list_multiple :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
results, list_ok := db_list(&d)
|
results, list_ok := db_list(&d)
|
||||||
testing.expect(t, list_ok, "list should succeed")
|
testing.expect(t, list_ok, "list should succeed")
|
||||||
if !list_ok do return
|
|
||||||
defer delete(results)
|
|
||||||
defer {
|
|
||||||
for &result in results {
|
|
||||||
delete_envfile(&result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testing.expect_value(t, len(results), 3)
|
testing.expect_value(t, len(results), 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_list_empty :: proc(t: ^testing.T) {
|
test_db_list_empty :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
defer db_close(&d)
|
||||||
defer sqlite.db_close(d.db)
|
|
||||||
|
|
||||||
results, list_ok := db_list(&d)
|
results, list_ok := db_list(&d)
|
||||||
testing.expect(t, list_ok, "list should succeed on empty db")
|
testing.expect(t, list_ok, "list should succeed on empty db")
|
||||||
testing.expect(t, len(results) == 0, "should have 0 rows")
|
testing.expect(t, len(results) == 0, "should have 0 rows")
|
||||||
if list_ok do delete(results)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_insert_sets_changed :: proc(t: ^testing.T) {
|
test_db_insert_sets_changed :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
testing.expect(t, !d.changed, "changed should start false")
|
testing.expect(t, !d.changed, "changed should start false")
|
||||||
|
|
||||||
@@ -200,10 +169,10 @@ test_db_insert_sets_changed :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_delete_sets_changed :: proc(t: ^testing.T) {
|
test_db_delete_sets_changed :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
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)
|
||||||
@@ -216,10 +185,10 @@ test_db_delete_sets_changed :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_db_serialize :: proc(t: ^testing.T) {
|
test_db_serialize :: proc(t: ^testing.T) {
|
||||||
d, ok := make_test_db()
|
d, ok := db_init()
|
||||||
testing.expect(t, ok, "failed to create test db")
|
testing.expect(t, ok, "failed to create test db")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
defer sqlite.db_close(d.db)
|
defer db_close(&d)
|
||||||
|
|
||||||
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)
|
||||||
@@ -234,37 +203,6 @@ test_db_serialize :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, sz > 0, "serialized size should be > 0")
|
testing.expect(t, sz > 0, "serialized size should be > 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_db_update_required_noop :: proc(t: ^testing.T) {
|
|
||||||
testing.expect(t, !db_update_required({}), "Noop should not require update")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_db_update_required_backed_up :: proc(t: ^testing.T) {
|
|
||||||
testing.expect(t, db_update_required({.BackedUp}), "BackedUp should require update")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_db_update_required_dir_updated :: proc(t: ^testing.T) {
|
|
||||||
testing.expect(t, db_update_required({.DirUpdated}), "DirUpdated should require update")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_db_update_required_restored :: proc(t: ^testing.T) {
|
|
||||||
testing.expect(t, !db_update_required({.Restored}), "Restored alone should not require update")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_db_update_required_error :: proc(t: ^testing.T) {
|
|
||||||
testing.expect(t, !db_update_required({.Error}), "Error alone should not require update")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_db_update_required_combined :: proc(t: ^testing.T) {
|
|
||||||
combined := SyncFlag{.DirUpdated, .Restored}
|
|
||||||
testing.expect(t, db_update_required(combined), "DirUpdated|Restored should require update")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_shares_remote_overlap :: proc(t: ^testing.T) {
|
test_shares_remote_overlap :: proc(t: ^testing.T) {
|
||||||
f := EnvFile {
|
f := EnvFile {
|
||||||
@@ -340,8 +278,7 @@ test_get_git_remotes_single :: proc(t: ^testing.T) {
|
|||||||
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||||
testing.expect(t, err == nil, "should write .git/config")
|
testing.expect(t, err == nil, "should write .git/config")
|
||||||
|
|
||||||
remotes := get_git_remotes(base)
|
remotes := get_git_remotes(base, context.temp_allocator)
|
||||||
defer delete_remotes(remotes)
|
|
||||||
|
|
||||||
testing.expect(t, len(remotes) == 1, "should find 1 remote")
|
testing.expect(t, len(remotes) == 1, "should find 1 remote")
|
||||||
if len(remotes) != 1 do return
|
if len(remotes) != 1 do return
|
||||||
@@ -362,8 +299,7 @@ test_get_git_remotes_multiple :: proc(t: ^testing.T) {
|
|||||||
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||||
testing.expect(t, err == nil, "should write .git/config")
|
testing.expect(t, err == nil, "should write .git/config")
|
||||||
|
|
||||||
remotes := get_git_remotes(base)
|
remotes := get_git_remotes(base, context.temp_allocator)
|
||||||
defer delete_remotes(remotes)
|
|
||||||
|
|
||||||
testing.expect(t, len(remotes) == 2, "should find 2 remotes")
|
testing.expect(t, len(remotes) == 2, "should find 2 remotes")
|
||||||
}
|
}
|
||||||
@@ -374,8 +310,7 @@ test_get_git_remotes_no_config :: proc(t: ^testing.T) {
|
|||||||
os.mkdir_all(base)
|
os.mkdir_all(base)
|
||||||
defer os.remove_all(base)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
remotes := get_git_remotes(base)
|
remotes := get_git_remotes(base, context.temp_allocator)
|
||||||
defer delete_remotes(remotes)
|
|
||||||
|
|
||||||
testing.expect(t, len(remotes) == 0, "should return empty when no .git/config")
|
testing.expect(t, len(remotes) == 0, "should return empty when no .git/config")
|
||||||
}
|
}
|
||||||
@@ -394,8 +329,7 @@ test_get_git_remotes_no_remotes :: proc(t: ^testing.T) {
|
|||||||
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||||
testing.expect(t, err == nil, "should write .git/config")
|
testing.expect(t, err == nil, "should write .git/config")
|
||||||
|
|
||||||
remotes := get_git_remotes(base)
|
remotes := get_git_remotes(base, context.temp_allocator)
|
||||||
defer delete_remotes(remotes)
|
|
||||||
|
|
||||||
testing.expect(t, len(remotes) == 0, "should return empty when no remote sections")
|
testing.expect(t, len(remotes) == 0, "should return empty when no remote sections")
|
||||||
}
|
}
|
||||||
@@ -429,49 +363,6 @@ test_new_env_file_missing :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, !ok, "missing file should return false")
|
testing.expect(t, !ok, "missing file should return false")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_env_file_backup :: proc(t: ^testing.T) {
|
|
||||||
base := fmt.tprintf("/tmp/envr-test-backup-%d", os.get_pid())
|
|
||||||
os.mkdir_all(base)
|
|
||||||
defer os.remove_all(base)
|
|
||||||
|
|
||||||
env_path := fmt.tprintf("%s/.env", base)
|
|
||||||
err := os.write_entire_file(env_path, "KEY=12345\n")
|
|
||||||
testing.expect(t, err == nil, ".env file should exist")
|
|
||||||
|
|
||||||
f := EnvFile {
|
|
||||||
Path = env_path,
|
|
||||||
}
|
|
||||||
defer delete(f.contents)
|
|
||||||
defer delete(f.Sha256)
|
|
||||||
testing.expect(t, env_file_backup(&f), "backup should succeed")
|
|
||||||
testing.expect_value(t, f.contents, "KEY=12345\n")
|
|
||||||
testing.expect_value(t, len(f.Sha256), 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_env_file_backup_missing :: proc(t: ^testing.T) {
|
|
||||||
f := EnvFile {
|
|
||||||
Path = "/tmp/envr-nonexistent-backup/.env",
|
|
||||||
}
|
|
||||||
testing.expect(t, !env_file_backup(&f), "missing file should return false")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
|
||||||
test_update_dir :: proc(t: ^testing.T) {
|
|
||||||
f := EnvFile {
|
|
||||||
Path = "/old/project/.env",
|
|
||||||
Dir = "/old/project",
|
|
||||||
Remotes = make([dynamic]string, 0),
|
|
||||||
}
|
|
||||||
defer delete_envfile(&f)
|
|
||||||
|
|
||||||
update_dir(&f, "/new/location")
|
|
||||||
|
|
||||||
testing.expect_value(t, f.Dir, "/new/location")
|
|
||||||
testing.expect_value(t, f.Path, "/new/location/.env")
|
|
||||||
}
|
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
|
test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
|
||||||
base := fmt.tprintf("/tmp/envr-test-leak-%d", os.get_pid())
|
base := fmt.tprintf("/tmp/envr-test-leak-%d", os.get_pid())
|
||||||
@@ -481,13 +372,14 @@ test_closing_db_has_no_leaks :: proc(t: ^testing.T) {
|
|||||||
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||||
|
|
||||||
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
{
|
||||||
defer delete_config(&cfg)
|
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||||
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
||||||
|
delete_config(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
db, ok := db_open(cfg_path)
|
db, ok := db_open(cfg_path)
|
||||||
testing.expect(t, ok, "db should open")
|
testing.expect(t, ok, "db should open")
|
||||||
if !ok do return
|
|
||||||
db_close(&db)
|
db_close(&db)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,15 +392,22 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
|
|||||||
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
cfg_path, err := filepath.join([]string{base, "config.json"}, context.temp_allocator)
|
||||||
testing.expect(t, err == nil, "cfgPath should build successfully")
|
testing.expect(t, err == nil, "cfgPath should build successfully")
|
||||||
|
|
||||||
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
{
|
||||||
defer delete_config(&cfg)
|
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||||
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
||||||
|
delete_config(&cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// First open/close creates data.envr on disk
|
// First open/close creates data.envr on disk
|
||||||
db, ok := db_open(cfg_path)
|
db, ok := db_open(cfg_path)
|
||||||
testing.expect(t, ok, "db should open")
|
testing.expect(t, ok, "db should open")
|
||||||
if !ok do return
|
if !ok do return
|
||||||
f := make_test_env_file("/project/.env", "abc123", "SECRET=value", []string{"git@github.com:user/repo.git"})
|
f := make_test_env_file(
|
||||||
|
"/project/.env",
|
||||||
|
"abc123",
|
||||||
|
"SECRET=value",
|
||||||
|
[]string{"git@github.com:user/repo.git"},
|
||||||
|
)
|
||||||
defer delete(f.Remotes)
|
defer delete(f.Remotes)
|
||||||
testing.expect(t, db_insert(&db, f), "insert should succeed")
|
testing.expect(t, db_insert(&db, f), "insert should succeed")
|
||||||
db_close(&db)
|
db_close(&db)
|
||||||
@@ -520,3 +419,150 @@ test_open_existing_db_has_no_leaks :: proc(t: ^testing.T) {
|
|||||||
db_close(&db2)
|
db_close(&db2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_db_sync_noop :: proc(t: ^testing.T) {
|
||||||
|
base := fmt.tprintf("/tmp/envr-test-sync-noop-%d", os.get_pid())
|
||||||
|
os.mkdir_all(base)
|
||||||
|
defer os.remove_all(base)
|
||||||
|
|
||||||
|
env_path := fmt.tprintf("%s/.env", base)
|
||||||
|
content := "KEY=value\n"
|
||||||
|
write_err := os.write_entire_file(env_path, transmute([]u8)content)
|
||||||
|
testing.expect(t, write_err == nil, "should write .env file")
|
||||||
|
|
||||||
|
digest := hash.hash_bytes(
|
||||||
|
hash.Algorithm.SHA256,
|
||||||
|
transmute([]u8)content,
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
hex_bytes, _ := hex.encode(digest, context.temp_allocator)
|
||||||
|
sha := string(hex_bytes)
|
||||||
|
|
||||||
|
d, ok := db_init()
|
||||||
|
testing.expect(t, ok, "failed to create test db")
|
||||||
|
defer db_close(&d)
|
||||||
|
|
||||||
|
f := make_test_env_file(env_path, sha, content)
|
||||||
|
f.Dir = base
|
||||||
|
db_insert(&d, f)
|
||||||
|
|
||||||
|
result, sync_err := db_sync(&d, &f)
|
||||||
|
testing.expect(t, sync_err == .None, "sync should not error")
|
||||||
|
testing.expect(t, result == {}, "should be noop")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_db_sync_backed_up :: proc(t: ^testing.T) {
|
||||||
|
base := fmt.tprintf("/tmp/envr-test-sync-backup-%d", os.get_pid())
|
||||||
|
os.mkdir_all(base)
|
||||||
|
defer os.remove_all(base)
|
||||||
|
|
||||||
|
env_path := fmt.tprintf("%s/.env", base)
|
||||||
|
changed_content := "KEY=changed\n"
|
||||||
|
write_err := os.write_entire_file(env_path, transmute([]u8)changed_content)
|
||||||
|
testing.expect(t, write_err == nil, "should write .env file")
|
||||||
|
|
||||||
|
d, ok := db_init()
|
||||||
|
testing.expect(t, ok, "failed to create test db")
|
||||||
|
defer db_close(&d)
|
||||||
|
|
||||||
|
f := make_test_env_file(env_path, "old_sha", "KEY=original")
|
||||||
|
f.Dir = base
|
||||||
|
db_insert(&d, f)
|
||||||
|
|
||||||
|
result, sync_err := db_sync(&d, &f)
|
||||||
|
testing.expect(t, sync_err == .None, "sync should not error")
|
||||||
|
testing.expect(t, .BackedUp in result, "should be backed up")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_db_sync_restored :: proc(t: ^testing.T) {
|
||||||
|
base := fmt.tprintf("/tmp/envr-test-sync-restore-%d", os.get_pid())
|
||||||
|
os.mkdir_all(base)
|
||||||
|
defer os.remove_all(base)
|
||||||
|
|
||||||
|
env_path := fmt.tprintf("%s/.env", base)
|
||||||
|
|
||||||
|
d, ok := db_init()
|
||||||
|
testing.expect(t, ok, "failed to create test db")
|
||||||
|
defer db_close(&d)
|
||||||
|
|
||||||
|
f := make_test_env_file(env_path, "some_sha", "SECRET=value")
|
||||||
|
f.Dir = base
|
||||||
|
defer delete(f.Remotes)
|
||||||
|
db_insert(&d, f)
|
||||||
|
|
||||||
|
result, err := db_sync(&d, &f)
|
||||||
|
testing.expect(t, err == .None, "sync should not error")
|
||||||
|
testing.expect(t, .Restored in result, "should be restored")
|
||||||
|
|
||||||
|
data, read_err := os.read_entire_file_from_path(env_path, context.temp_allocator)
|
||||||
|
testing.expect(t, read_err == nil, "file should exist after restore")
|
||||||
|
if read_err == nil {
|
||||||
|
testing.expect_value(t, string(data), "SECRET=value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_db_sync_dir_missing :: proc(t: ^testing.T) {
|
||||||
|
d, ok := db_init()
|
||||||
|
testing.expect(t, ok, "failed to create test db")
|
||||||
|
defer db_close(&d)
|
||||||
|
|
||||||
|
f := make_test_env_file("/nonexistent/path/.env", "sha", "KEY=val")
|
||||||
|
db_insert(&d, f)
|
||||||
|
|
||||||
|
result, err := db_sync(&d, &f)
|
||||||
|
testing.expect(t, err == .DirMissing, "should return DirMissing error")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_db_sync_moved :: proc(t: ^testing.T) {
|
||||||
|
base := fmt.tprintf("/tmp/envr-test-sync-moved-%d", os.get_pid())
|
||||||
|
search_root := fmt.tprintf("%s/search", base)
|
||||||
|
repo_dir := fmt.tprintf("%s/myproject", search_root)
|
||||||
|
git_dir := fmt.tprintf("%s/.git", repo_dir)
|
||||||
|
defer os.remove_all(base)
|
||||||
|
|
||||||
|
os.mkdir_all(git_dir)
|
||||||
|
|
||||||
|
config_content := "[remote \"origin\"]\n\turl = git@github.com:user/repo.git\n"
|
||||||
|
config_path := fmt.tprintf("%s/config", git_dir)
|
||||||
|
write_err := os.write_entire_file(config_path, transmute([]u8)config_content)
|
||||||
|
testing.expect(t, write_err == nil, "should write .git/config")
|
||||||
|
|
||||||
|
d, ok := db_init()
|
||||||
|
testing.expect(t, ok, "failed to create test db")
|
||||||
|
defer db_close(&d)
|
||||||
|
|
||||||
|
d.cfg.ScanConfig.Include = make([dynamic]string, 0, 1, context.temp_allocator)
|
||||||
|
append(&d.cfg.ScanConfig.Include, search_root)
|
||||||
|
|
||||||
|
f := make_test_env_file(
|
||||||
|
"/old/nonexistent/path/.env",
|
||||||
|
"some_sha",
|
||||||
|
"SECRET=value",
|
||||||
|
[]string{"git@github.com:user/repo.git"},
|
||||||
|
)
|
||||||
|
testing.expect(t, db_insert(&d, f), "insert should succeed")
|
||||||
|
|
||||||
|
result, err := db_sync(&d, &f)
|
||||||
|
testing.expect(t, err == .None, "sync should not error")
|
||||||
|
if err != .None do return
|
||||||
|
testing.expect(t, .DirUpdated in result, "should have DirUpdated flag")
|
||||||
|
testing.expect(t, .Restored in result, "should have Restored flag")
|
||||||
|
|
||||||
|
expected_path := fmt.tprintf("%s/.env", repo_dir)
|
||||||
|
testing.expect_value(t, f.Path, expected_path)
|
||||||
|
testing.expect_value(t, f.Dir, repo_dir)
|
||||||
|
|
||||||
|
_, old_exists := db_fetch(&d, "/old/nonexistent/path/.env")
|
||||||
|
testing.expect(t, !old_exists, "old path should be deleted from db")
|
||||||
|
|
||||||
|
new_fetched, new_ok := db_fetch(&d, expected_path)
|
||||||
|
testing.expect(t, new_ok, "new path should exist in db")
|
||||||
|
if new_ok {
|
||||||
|
testing.expect_value(t, new_fetched.contents, "SECRET=value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
13
table.odin
13
table.odin
@@ -6,7 +6,7 @@ import "core:io"
|
|||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
||||||
col_widths := make([dynamic]int, 0, len(headers))
|
col_widths := make([dynamic]int, 0, len(headers), context.temp_allocator)
|
||||||
for i in 0 ..< len(headers) {
|
for i in 0 ..< len(headers) {
|
||||||
append(&col_widths, strings.rune_count(headers[i]))
|
append(&col_widths, strings.rune_count(headers[i]))
|
||||||
}
|
}
|
||||||
@@ -20,11 +20,14 @@ render_table :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
b: strings.Builder
|
b: strings.Builder
|
||||||
strings.builder_init(&b)
|
strings.builder_init(&b, context.temp_allocator)
|
||||||
defer strings.builder_destroy(&b)
|
|
||||||
defer delete(col_widths)
|
|
||||||
|
|
||||||
hline :: proc(w: io.Writer, b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) {
|
hline :: proc(
|
||||||
|
w: io.Writer,
|
||||||
|
b: ^strings.Builder,
|
||||||
|
left, mid, right: string,
|
||||||
|
widths: [dynamic]int,
|
||||||
|
) {
|
||||||
strings.write_string(b, left)
|
strings.write_string(b, left)
|
||||||
for i in 0 ..< len(widths) {
|
for i in 0 ..< len(widths) {
|
||||||
for _ in 0 ..< widths[i] + 2 {
|
for _ in 0 ..< widths[i] + 2 {
|
||||||
|
|||||||
Reference in New Issue
Block a user