feat(commit): text templating in commit messages
This commit is contained in:
parent
4e78cefd56
commit
2ca32769d5
30 changed files with 281 additions and 115 deletions
16
README.md
16
README.md
|
|
@ -394,7 +394,7 @@ Values support effectively all of Go's `text/template` syntax. Templates are eva
|
|||
|
||||
`text/template` is a Turing-complete templating library that supports most of what you'd expect in a scripting language. Actions are given with ``{{ action }}`` syntax and support pipelines and nested templates, along with a lot more. I recommend reading the documentation if you want to do anything more complicated than described here.
|
||||
|
||||
To fit `text/template` nicely into this tool, pda has a sparse set of additional functions built-in. For example, `default` values, `enum`s, `require`d values, `lists`, among others.
|
||||
To fit `text/template` nicely into this tool, pda has a sparse set of additional functions built-in. For example, `default` values, `enum`s, `require`d values, `time`, `lists`, among others. These same functions are also available in `git.default_commit_message` templates, along with `{{ summary }}` which returns the action that triggered the commit (e.g. "set foo", "removed bar").
|
||||
|
||||
Below is more detail on the extra functions added by this tool.
|
||||
|
||||
|
|
@ -438,6 +438,15 @@ pda get my_name
|
|||
|
||||
<p align="center"></p><!-- spacer -->
|
||||
|
||||
`time` returns the current UTC time in RFC3339 format.
|
||||
```bash
|
||||
pda set note "Created at {{ time }}"
|
||||
pda get note
|
||||
# Created at 2025-01-15T12:00:00Z
|
||||
```
|
||||
|
||||
<p align="center"></p><!-- spacer -->
|
||||
|
||||
`enum` restricts acceptable values.
|
||||
```bash
|
||||
pda set level "Log level: {{ enum .LEVEL "info" "warn" "error" }}"
|
||||
|
|
@ -854,8 +863,9 @@ auto_fetch = false
|
|||
auto_commit = false
|
||||
# auto push after committing
|
||||
auto_push = false
|
||||
# commit message template ({{.Time}} is replaced with RFC3339 timestamp)
|
||||
default_commit_message = "sync: {{.Time}}"
|
||||
# commit message if none manually specified
|
||||
# supports templates, see: #templates section
|
||||
default_commit_message = "{{ summary }} {{ time }}"
|
||||
```
|
||||
|
||||
<p align="center"></p><!-- spacer -->
|
||||
|
|
|
|||
48
cmd/commit_message.go
Normal file
48
cmd/commit_message.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright © 2025 Lewis Wynne <lew@ily.rs>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// renderCommitMessage renders a commit message template. It extends the
|
||||
// shared template FuncMap with {{ summary }}, which returns the action
|
||||
// description for the current commit. On any error the raw template string
|
||||
// is returned so that commits are never blocked by a bad template.
|
||||
func renderCommitMessage(tmpl string, summary string) string {
|
||||
funcMap := templateFuncMap()
|
||||
funcMap["summary"] = func() string { return summary }
|
||||
|
||||
t, err := template.New("commit").Option("missingkey=zero").Funcs(funcMap).Parse(tmpl)
|
||||
if err != nil {
|
||||
return tmpl
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := t.Execute(&buf, nil); err != nil {
|
||||
return tmpl
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
53
cmd/commit_message_test.go
Normal file
53
cmd/commit_message_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenderCommitMessage(t *testing.T) {
|
||||
t.Run("summary and time", func(t *testing.T) {
|
||||
msg := renderCommitMessage("{{ summary }} {{ time }}", "set foo")
|
||||
if !strings.HasPrefix(msg, "set foo ") {
|
||||
t.Errorf("expected prefix 'set foo ', got %q", msg)
|
||||
}
|
||||
parts := strings.SplitN(msg, " ", 3)
|
||||
if len(parts) < 3 || !strings.Contains(parts[2], "T") {
|
||||
t.Errorf("expected RFC3339 time, got %q", msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty summary", func(t *testing.T) {
|
||||
msg := renderCommitMessage("{{ summary }} {{ time }}", "")
|
||||
if !strings.HasPrefix(msg, " ") {
|
||||
t.Errorf("expected leading space (empty summary), got %q", msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default function", func(t *testing.T) {
|
||||
msg := renderCommitMessage(`{{ default "sync" (summary) }}`, "")
|
||||
if msg != "sync" {
|
||||
t.Errorf("expected 'sync', got %q", msg)
|
||||
}
|
||||
msg = renderCommitMessage(`{{ default "sync" (summary) }}`, "set foo")
|
||||
if msg != "set foo" {
|
||||
t.Errorf("expected 'set foo', got %q", msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("env function", func(t *testing.T) {
|
||||
t.Setenv("PDA_TEST_USER", "alice")
|
||||
msg := renderCommitMessage(`{{ env "PDA_TEST_USER" }}: {{ summary }}`, "set foo")
|
||||
if msg != "alice: set foo" {
|
||||
t.Errorf("expected 'alice: set foo', got %q", msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bad template returns raw", func(t *testing.T) {
|
||||
raw := "{{ bad template"
|
||||
msg := renderCommitMessage(raw, "test")
|
||||
if msg != raw {
|
||||
t.Errorf("expected raw %q, got %q", raw, msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ func defaultConfig() Config {
|
|||
AutoFetch: false,
|
||||
AutoCommit: false,
|
||||
AutoPush: false,
|
||||
DefaultCommitMessage: "sync: {{.Time}}",
|
||||
DefaultCommitMessage: "{{ summary }} {{ time }}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ func delStore(cmd *cobra.Command, args []string) error {
|
|||
if err := executeDeletion(path); err != nil {
|
||||
return err
|
||||
}
|
||||
return autoSync()
|
||||
return autoSync(fmt.Sprintf("removed @%s", dbName))
|
||||
}
|
||||
|
||||
func executeDeletion(path string) error {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ func del(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var removedNames []string
|
||||
for _, dbName := range storeOrder {
|
||||
st := byStore[dbName]
|
||||
p, err := store.storePath(dbName)
|
||||
|
|
@ -123,13 +124,14 @@ func del(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("cannot remove '%s': no such key", t.full)
|
||||
}
|
||||
entries = append(entries[:idx], entries[idx+1:]...)
|
||||
removedNames = append(removedNames, t.display)
|
||||
}
|
||||
if err := writeStoreFile(p, entries, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return autoSync()
|
||||
return autoSync("removed " + strings.Join(removedNames, ", "))
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
50
cmd/get.go
50
cmd/get.go
|
|
@ -27,8 +27,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
|
|
@ -150,57 +148,11 @@ func applyTemplate(tplBytes []byte, substitutions []string) ([]byte, error) {
|
|||
val := parts[1]
|
||||
vars[key] = val
|
||||
}
|
||||
funcMap := template.FuncMap{
|
||||
"require": func(v any) (string, error) {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return "", fmt.Errorf("required value is missing or empty")
|
||||
}
|
||||
return s, nil
|
||||
},
|
||||
"default": func(def string, v any) string {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
return s
|
||||
},
|
||||
"env": os.Getenv,
|
||||
"enum": func(v any, allowed ...string) (string, error) {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return "", fmt.Errorf("enum value is missing or empty")
|
||||
}
|
||||
if slices.Contains(allowed, s) {
|
||||
return s, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid value '%s', allowed: %v", s, allowed)
|
||||
},
|
||||
"int": func(v any) (int, error) {
|
||||
s := fmt.Sprint(v)
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot convert to int: %w", err)
|
||||
}
|
||||
return i, nil
|
||||
},
|
||||
"list": func(v any) []string {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
},
|
||||
}
|
||||
tpl, err := template.New("cmd").
|
||||
Delims("{{", "}}").
|
||||
// Render missing map keys as zero values so the default helper can decide on fallbacks.
|
||||
Option("missingkey=zero").
|
||||
Funcs(funcMap).
|
||||
Funcs(templateFuncMap()).
|
||||
Parse(string(tplBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ func mvStore(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
copy, _ := cmd.Flags().GetBool("copy")
|
||||
var summary string
|
||||
if copy {
|
||||
data, err := os.ReadFile(fromPath)
|
||||
if err != nil {
|
||||
|
|
@ -111,13 +112,15 @@ func mvStore(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("cannot copy store '%s': %v", fromName, err)
|
||||
}
|
||||
okf("copied @%s to @%s", fromName, toName)
|
||||
summary = fmt.Sprintf("copied @%s to @%s", fromName, toName)
|
||||
} else {
|
||||
if err := os.Rename(fromPath, toPath); err != nil {
|
||||
return fmt.Errorf("cannot rename store '%s': %v", fromName, err)
|
||||
}
|
||||
okf("renamed @%s to @%s", fromName, toName)
|
||||
summary = fmt.Sprintf("moved @%s to @%s", fromName, toName)
|
||||
}
|
||||
return autoSync()
|
||||
return autoSync(summary)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -182,12 +182,15 @@ func mvImpl(cmd *cobra.Command, args []string, keepSource bool) error {
|
|||
}
|
||||
}
|
||||
|
||||
var summary string
|
||||
if keepSource {
|
||||
okf("copied %s to %s", fromSpec.Display(), toSpec.Display())
|
||||
summary = "copied " + fromSpec.Display() + " to " + toSpec.Display()
|
||||
} else {
|
||||
okf("renamed %s to %s", fromSpec.Display(), toSpec.Display())
|
||||
summary = "moved " + fromSpec.Display() + " to " + toSpec.Display()
|
||||
}
|
||||
return autoSync()
|
||||
return autoSync(summary)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ func restore(cmd *cobra.Command, args []string) error {
|
|||
|
||||
// When a specific store is given, all entries go there (original behaviour).
|
||||
// Otherwise, route entries to their original store via the "store" field.
|
||||
var summary string
|
||||
if explicitStore {
|
||||
p, err := store.storePath(targetDB)
|
||||
if err != nil {
|
||||
|
|
@ -140,6 +141,7 @@ func restore(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
okf("restored %d entries into @%s", restored, targetDB)
|
||||
summary = fmt.Sprintf("imported %d entries into @%s", restored, targetDB)
|
||||
} else {
|
||||
restored, err := restoreEntries(decoder, nil, targetDB, opts)
|
||||
if err != nil {
|
||||
|
|
@ -149,9 +151,10 @@ func restore(cmd *cobra.Command, args []string) error {
|
|||
return err
|
||||
}
|
||||
okf("restored %d entries", restored)
|
||||
summary = fmt.Sprintf("imported %d entries", restored)
|
||||
}
|
||||
|
||||
return autoSync()
|
||||
return autoSync(summary)
|
||||
}
|
||||
|
||||
func reportRestoreFilters(displayTarget string, restored int, matchers []glob.Glob, keyPatterns []string, storeMatchers []glob.Glob, storePatterns []string) error {
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ func set(cmd *cobra.Command, args []string) error {
|
|||
return fmt.Errorf("cannot set '%s': %v", args[0], err)
|
||||
}
|
||||
|
||||
return autoSync()
|
||||
return autoSync("set " + spec.Display())
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
13
cmd/sync.go
13
cmd/sync.go
|
|
@ -23,9 +23,6 @@ THE SOFTWARE.
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
@ -35,7 +32,7 @@ var syncCmd = &cobra.Command{
|
|||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
msg, _ := cmd.Flags().GetString("message")
|
||||
return sync(true, msg)
|
||||
return sync(true, msg, "sync")
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +41,7 @@ func init() {
|
|||
rootCmd.AddCommand(syncCmd)
|
||||
}
|
||||
|
||||
func sync(manual bool, customMsg string) error {
|
||||
func sync(manual bool, customMsg string, summary string) error {
|
||||
repoDir, err := ensureVCSInitialized()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -66,7 +63,7 @@ func sync(manual bool, customMsg string) error {
|
|||
if changed {
|
||||
msg := customMsg
|
||||
if msg == "" {
|
||||
msg = strings.ReplaceAll(config.Git.DefaultCommitMessage, "{{.Time}}", time.Now().UTC().Format(time.RFC3339))
|
||||
msg = renderCommitMessage(config.Git.DefaultCommitMessage, summary)
|
||||
if manual {
|
||||
printHint("use -m to set a custom commit message")
|
||||
}
|
||||
|
|
@ -123,12 +120,12 @@ func sync(manual bool, customMsg string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func autoSync() error {
|
||||
func autoSync(summary string) error {
|
||||
if !config.Git.AutoCommit {
|
||||
return nil
|
||||
}
|
||||
if _, err := ensureVCSInitialized(); err != nil {
|
||||
return nil
|
||||
}
|
||||
return sync(false, "")
|
||||
return sync(false, "", summary)
|
||||
}
|
||||
|
|
|
|||
85
cmd/template.go
Normal file
85
cmd/template.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
Copyright © 2025 Lewis Wynne <lew@ily.rs>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// templateFuncMap returns the shared FuncMap used by both value templates
|
||||
// (pda get) and commit message templates.
|
||||
func templateFuncMap() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"require": func(v any) (string, error) {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return "", fmt.Errorf("required value is missing or empty")
|
||||
}
|
||||
return s, nil
|
||||
},
|
||||
"default": func(def string, v any) string {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
return s
|
||||
},
|
||||
"env": os.Getenv,
|
||||
"enum": func(v any, allowed ...string) (string, error) {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return "", fmt.Errorf("enum value is missing or empty")
|
||||
}
|
||||
if slices.Contains(allowed, s) {
|
||||
return s, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid value '%s', allowed: %v", s, allowed)
|
||||
},
|
||||
"int": func(v any) (int, error) {
|
||||
s := fmt.Sprint(v)
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot convert to int: %w", err)
|
||||
}
|
||||
return i, nil
|
||||
},
|
||||
"list": func(v any) []string {
|
||||
s := fmt.Sprint(v)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
},
|
||||
"time": func() string { return time.Now().UTC().Format(time.RFC3339) },
|
||||
}
|
||||
}
|
||||
38
main_test.go
38
main_test.go
|
|
@ -36,20 +36,6 @@ import (
|
|||
var update = flag.Bool("update", false, "update test files with results")
|
||||
|
||||
func TestMain(t *testing.T) {
|
||||
t.Setenv("PDA_DATA", t.TempDir())
|
||||
configDir := t.TempDir()
|
||||
t.Setenv("PDA_CONFIG", configDir)
|
||||
|
||||
// Pre-create an age identity so encryption tests don't print a
|
||||
// creation message with a non-deterministic path.
|
||||
id, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
t.Fatalf("generate identity: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(configDir, "identity.txt"), []byte(id.String()+"\n"), 0o600); err != nil {
|
||||
t.Fatalf("write identity: %v", err)
|
||||
}
|
||||
|
||||
ts, err := cmdtest.Read("testdata")
|
||||
if err != nil {
|
||||
t.Fatalf("read testdata: %v", err)
|
||||
|
|
@ -59,5 +45,29 @@ func TestMain(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
ts.Commands["pda"] = cmdtest.Program(bin)
|
||||
|
||||
// Each .ct file gets its own isolated data and config directories
|
||||
// inside its ROOTDIR, so tests cannot leak state to each other.
|
||||
ts.Setup = func(rootDir string) error {
|
||||
dataDir := filepath.Join(rootDir, "data")
|
||||
configDir := filepath.Join(rootDir, "config")
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Setenv("PDA_DATA", dataDir)
|
||||
os.Setenv("PDA_CONFIG", configDir)
|
||||
|
||||
// Pre-create an age identity so encryption tests don't print
|
||||
// a creation message with a non-deterministic path.
|
||||
id, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(configDir, "identity.txt"), []byte(id.String()+"\n"), 0o600)
|
||||
}
|
||||
|
||||
ts.Run(t, *update)
|
||||
}
|
||||
|
|
|
|||
8
testdata/config-init.ct
vendored
8
testdata/config-init.ct
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Init creates a config file
|
||||
$ pda config init
|
||||
ok generated config: /tmp/TestMain2533282848/002/config.toml
|
||||
ok generated config: ${ROOTDIR}/config/config.toml
|
||||
|
||||
# Second init fails
|
||||
$ pda config init --> FAIL
|
||||
|
|
@ -9,7 +9,7 @@ hint use '--update' to update your config, or '--new' to get a fresh copy
|
|||
|
||||
# Init --new overwrites
|
||||
$ pda config init --new
|
||||
ok generated config: /tmp/TestMain2533282848/002/config.toml
|
||||
ok generated config: ${ROOTDIR}/config/config.toml
|
||||
|
||||
# --update preserves user changes
|
||||
$ pda config set list.always_show_all_stores false
|
||||
|
|
@ -18,7 +18,7 @@ $ pda config get list.always_show_all_stores
|
|||
false
|
||||
$ pda config init --update
|
||||
$ pda config get list.always_show_all_stores
|
||||
ok updated config: /tmp/TestMain2533282848/002/config.toml
|
||||
ok updated config: ${ROOTDIR}/config/config.toml
|
||||
false
|
||||
|
||||
# --new and --update are mutually exclusive
|
||||
|
|
@ -27,4 +27,4 @@ FAIL --new and --update are mutually exclusive
|
|||
|
||||
# Reset for other tests
|
||||
$ pda config init --new
|
||||
ok generated config: /tmp/TestMain2533282848/002/config.toml
|
||||
ok generated config: ${ROOTDIR}/config/config.toml
|
||||
|
|
|
|||
2
testdata/config-list.ct
vendored
2
testdata/config-list.ct
vendored
|
|
@ -15,4 +15,4 @@ list.default_columns = key,store,value,ttl
|
|||
git.auto_fetch = false
|
||||
git.auto_commit = false
|
||||
git.auto_push = false
|
||||
git.default_commit_message = sync: {{.Time}}
|
||||
git.default_commit_message = {{ summary }} {{ time }}
|
||||
|
|
|
|||
12
testdata/list-all.ct
vendored
12
testdata/list-all.ct
vendored
|
|
@ -3,8 +3,8 @@ $ pda set lax@laa 1
|
|||
$ pda set lax@lab 2
|
||||
$ pda ls --key "lax" --format tsv
|
||||
Key Store Value TTL
|
||||
lax laa 1 no expiry
|
||||
lax lab 2 no expiry
|
||||
lax laa 1 none
|
||||
lax lab 2 none
|
||||
$ pda ls --key "lax" --count
|
||||
2
|
||||
$ pda ls --key "lax" --format json
|
||||
|
|
@ -12,15 +12,15 @@ $ pda ls --key "lax" --format json
|
|||
# Positional arg narrows to one store
|
||||
$ pda ls laa --key "lax" --format tsv
|
||||
Key Store Value TTL
|
||||
lax laa 1 no expiry
|
||||
lax laa 1 none
|
||||
# --store glob filter
|
||||
$ pda ls --store "la?" --key "lax" --format tsv
|
||||
Key Store Value TTL
|
||||
lax laa 1 no expiry
|
||||
lax lab 2 no expiry
|
||||
lax laa 1 none
|
||||
lax lab 2 none
|
||||
$ pda ls --store "laa" --key "lax" --format tsv
|
||||
Key Store Value TTL
|
||||
lax laa 1 no expiry
|
||||
lax laa 1 none
|
||||
# --store cannot be combined with positional arg
|
||||
$ pda ls --store "laa" laa --> FAIL
|
||||
FAIL cannot use --store with a store argument
|
||||
|
|
|
|||
2
testdata/list-config-hide-header.ct
vendored
2
testdata/list-config-hide-header.ct
vendored
|
|
@ -3,7 +3,7 @@ $ pda config set list.always_hide_header true
|
|||
$ pda set a@lchh 1
|
||||
$ pda ls lchh --format tsv
|
||||
ok list.always_hide_header set to 'true'
|
||||
a lchh 1 no expiry
|
||||
a lchh 1 none
|
||||
|
||||
# Reset
|
||||
$ pda config set list.always_hide_header false
|
||||
|
|
|
|||
4
testdata/list-format-csv.ct
vendored
4
testdata/list-format-csv.ct
vendored
|
|
@ -3,5 +3,5 @@ $ pda set a@csv 1
|
|||
$ pda set b@csv 2
|
||||
$ pda ls csv --format csv
|
||||
Key,Store,Value,TTL
|
||||
a,csv,1,no expiry
|
||||
b,csv,2,no expiry
|
||||
a,csv,1,none
|
||||
b,csv,2,none
|
||||
|
|
|
|||
4
testdata/list-format-markdown.ct
vendored
4
testdata/list-format-markdown.ct
vendored
|
|
@ -4,5 +4,5 @@ $ pda set b@md 2
|
|||
$ pda ls md --format markdown
|
||||
| Key | Store | Value | TTL |
|
||||
| --- | --- | --- | --- |
|
||||
| a | md | 1 | no expiry |
|
||||
| b | md | 2 | no expiry |
|
||||
| a | md | 1 | none |
|
||||
| b | md | 2 | none |
|
||||
|
|
|
|||
6
testdata/list-key-filter.ct
vendored
6
testdata/list-key-filter.ct
vendored
|
|
@ -3,10 +3,10 @@ $ pda set a2@lg 2
|
|||
$ pda set b1@lg 3
|
||||
$ pda ls lg --key "a*" --format tsv
|
||||
Key Store Value TTL
|
||||
a1 lg 1 no expiry
|
||||
a2 lg 2 no expiry
|
||||
a1 lg 1 none
|
||||
a2 lg 2 none
|
||||
$ pda ls lg --key "b*" --format tsv
|
||||
Key Store Value TTL
|
||||
b1 lg 3 no expiry
|
||||
b1 lg 3 none
|
||||
$ pda ls lg --key "c*" --> FAIL
|
||||
FAIL cannot ls '@lg': no matches for key pattern 'c*'
|
||||
|
|
|
|||
4
testdata/list-key-value-filter.ct
vendored
4
testdata/list-key-value-filter.ct
vendored
|
|
@ -3,9 +3,9 @@ $ pda set apiurl@kv https://api.example.com
|
|||
$ pda set dbpass@kv s3cret
|
||||
$ pda ls kv -k "db*" -v "**localhost**" --format tsv
|
||||
Key Store Value TTL
|
||||
dburl kv postgres://localhost:5432 no expiry
|
||||
dburl kv postgres://localhost:5432 none
|
||||
$ pda ls kv -k "*url*" -v "**example**" --format tsv
|
||||
Key Store Value TTL
|
||||
apiurl kv https://api.example.com no expiry
|
||||
apiurl kv https://api.example.com none
|
||||
$ pda ls kv -k "db*" -v "**nomatch**" --> FAIL
|
||||
FAIL cannot ls '@kv': no matches for key pattern 'db*' and value pattern '**nomatch**'
|
||||
|
|
|
|||
2
testdata/list-no-header.ct
vendored
2
testdata/list-no-header.ct
vendored
|
|
@ -1,4 +1,4 @@
|
|||
# --no-header suppresses the header row
|
||||
$ pda set a@nh 1
|
||||
$ pda ls nh --format tsv --no-header
|
||||
a nh 1 no expiry
|
||||
a nh 1 none
|
||||
|
|
|
|||
2
testdata/list-no-keys.ct
vendored
2
testdata/list-no-keys.ct
vendored
|
|
@ -2,4 +2,4 @@
|
|||
$ pda set a@nk 1
|
||||
$ pda ls nk --format tsv --no-keys
|
||||
Store Value TTL
|
||||
nk 1 no expiry
|
||||
nk 1 none
|
||||
|
|
|
|||
2
testdata/list-no-values.ct
vendored
2
testdata/list-no-values.ct
vendored
|
|
@ -2,4 +2,4 @@
|
|||
$ pda set a@nv 1
|
||||
$ pda ls nv --format tsv --no-values
|
||||
Key Store TTL
|
||||
a nv no expiry
|
||||
a nv none
|
||||
|
|
|
|||
4
testdata/list-stores.ct
vendored
4
testdata/list-stores.ct
vendored
|
|
@ -3,7 +3,7 @@ $ pda set a@lsalpha 1
|
|||
$ pda set b@lsbeta 2
|
||||
$ pda ls lsalpha --format tsv
|
||||
Key Store Value TTL
|
||||
a lsalpha 1 no expiry
|
||||
a lsalpha 1 none
|
||||
$ pda ls lsbeta --format tsv
|
||||
Key Store Value TTL
|
||||
b lsbeta 2 no expiry
|
||||
b lsbeta 2 none
|
||||
|
|
|
|||
6
testdata/list-value-filter.ct
vendored
6
testdata/list-value-filter.ct
vendored
|
|
@ -4,12 +4,12 @@ $ pda set greeting@vt < tmpval
|
|||
$ pda set number@vt 42
|
||||
$ pda ls vt --value "**world**" --format tsv
|
||||
Key Store Value TTL
|
||||
greeting vt hello world (..1 more chars) no expiry
|
||||
greeting vt hello world (..1 more chars) none
|
||||
$ pda ls vt --value "**https**" --format tsv
|
||||
Key Store Value TTL
|
||||
url vt https://example.com no expiry
|
||||
url vt https://example.com none
|
||||
$ pda ls vt --value "*" --format tsv
|
||||
Key Store Value TTL
|
||||
number vt 42 no expiry
|
||||
number vt 42 none
|
||||
$ pda ls vt --value "**nomatch**" --> FAIL
|
||||
FAIL cannot ls '@vt': no matches for value pattern '**nomatch**'
|
||||
|
|
|
|||
4
testdata/list-value-multi-filter.ct
vendored
4
testdata/list-value-multi-filter.ct
vendored
|
|
@ -4,5 +4,5 @@ $ pda set greeting@vm < tmpval
|
|||
$ pda set number@vm 42
|
||||
$ pda ls vm --value "**world**" --value "42" --format tsv
|
||||
Key Store Value TTL
|
||||
greeting vm hello world (..1 more chars) no expiry
|
||||
number vm 42 no expiry
|
||||
greeting vm hello world (..1 more chars) none
|
||||
number vm 42 none
|
||||
|
|
|
|||
2
testdata/multistore.ct
vendored
2
testdata/multistore.ct
vendored
|
|
@ -7,4 +7,4 @@ $ pda get x@ms2
|
|||
y
|
||||
$ pda ls ms2 --format tsv
|
||||
Key Store Value TTL
|
||||
x ms2 y no expiry
|
||||
x ms2 y none
|
||||
|
|
|
|||
4
testdata/remove-dedupe.ct
vendored
4
testdata/remove-dedupe.ct
vendored
|
|
@ -3,8 +3,8 @@ $ pda set foo@rdd 1
|
|||
$ pda set bar@rdd 2
|
||||
$ pda ls rdd --format tsv
|
||||
Key Store Value TTL
|
||||
bar rdd 2 no expiry
|
||||
foo rdd 1 no expiry
|
||||
bar rdd 2 none
|
||||
foo rdd 1 none
|
||||
$ pda rm foo@rdd --key "*@rdd" -y
|
||||
$ pda get bar@rdd --> FAIL
|
||||
FAIL cannot get 'bar@rdd': no such key
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue