mirror of
https://github.com/boolean-maybe/tiki
synced 2026-04-21 13:37:20 +00:00
new.md precedence
This commit is contained in:
parent
1737e042a7
commit
c0621840e1
6 changed files with 262 additions and 12 deletions
|
|
@ -46,6 +46,12 @@ All `config.yaml` files found are merged together. A project config only needs t
|
|||
|
||||
Search order: user config dir (base) → `.doc/config.yaml` (project) → cwd (highest priority).
|
||||
|
||||
### new.md (task template)
|
||||
|
||||
`new.md` is searched in the same three locations but is **not merged** — the single highest-priority file found wins. If a project provides `.doc/new.md`, it completely replaces the user-level template. If no `new.md` is found anywhere, a built-in embedded template is used.
|
||||
|
||||
Search order: user config dir → `.doc/new.md` (project) → cwd. Last match wins.
|
||||
|
||||
### workflow.yaml merging
|
||||
|
||||
`workflow.yaml` is searched in all three locations. Files that exist are loaded and merged sequentially.
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ You can customize these to match your team's workflow. All filters and actions i
|
|||
## Task Template
|
||||
|
||||
When you create a new tiki — whether in the TUI or command line — field defaults come from a template file.
|
||||
Place `new.md` in your user config directory to override the built-in defaults
|
||||
Place `new.md` in your config directory to override the built-in defaults
|
||||
The file uses YAML frontmatter for field defaults
|
||||
|
||||
### Built-in default
|
||||
|
|
|
|||
|
|
@ -327,6 +327,9 @@ func GetUserConfigWorkflowFile() string {
|
|||
// defaultWorkflowFilename is the default name for the workflow configuration file
|
||||
const defaultWorkflowFilename = "workflow.yaml"
|
||||
|
||||
// templateFilename is the default name for the task template file
|
||||
const templateFilename = "new.md"
|
||||
|
||||
// FindWorkflowFiles returns all workflow.yaml files that exist and have non-empty views.
|
||||
// Ordering: user config file first (base), then project config file (overrides), then cwd.
|
||||
// This lets LoadPlugins load the base and merge overrides on top.
|
||||
|
|
@ -397,9 +400,38 @@ func FindWorkflowFile() string {
|
|||
return files[0]
|
||||
}
|
||||
|
||||
// GetTemplateFile returns the path to the user's custom new.md template
|
||||
func GetTemplateFile() string {
|
||||
return mustGetPathManager().TemplateFile()
|
||||
// FindTemplateFile returns the highest-priority new.md file that exists,
|
||||
// searching user config → .doc/ (project) → cwd. Returns empty string if
|
||||
// none found, in which case the caller should fall back to the embedded template.
|
||||
func FindTemplateFile() string {
|
||||
pm := mustGetPathManager()
|
||||
|
||||
// candidate paths in discovery order: user config (base) → project → cwd (highest)
|
||||
candidates := []string{
|
||||
pm.TemplateFile(),
|
||||
filepath.Join(pm.ProjectConfigDir(), templateFilename),
|
||||
templateFilename,
|
||||
}
|
||||
|
||||
var best string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, path := range candidates {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
abs = path
|
||||
}
|
||||
if seen[abs] {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
continue
|
||||
}
|
||||
seen[abs] = true
|
||||
best = path
|
||||
}
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
// EnsureDirs creates all necessary directories with appropriate permissions
|
||||
|
|
|
|||
|
|
@ -293,7 +293,6 @@ func TestGlobalAccessorFunctions(t *testing.T) {
|
|||
{"GetTaskDir", GetTaskDir},
|
||||
{"GetDokiDir", GetDokiDir},
|
||||
{"GetProjectConfigFile", GetProjectConfigFile},
|
||||
{"GetTemplateFile", GetTemplateFile},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -344,6 +343,142 @@ func TestInitPaths(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFindTemplateFile_CwdOverridesProject(t *testing.T) {
|
||||
// user config with new.md
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
userTikiDir := filepath.Join(userDir, "tiki")
|
||||
if err := os.MkdirAll(userTikiDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(userTikiDir, "new.md"), []byte("---\npriority: 1\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// project .doc/ with new.md
|
||||
projectDir := t.TempDir()
|
||||
docDir := filepath.Join(projectDir, ".doc")
|
||||
if err := os.MkdirAll(docDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(docDir, "new.md"), []byte("---\npriority: 2\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// cwd with new.md (highest priority)
|
||||
cwdDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(cwdDir, "new.md"), []byte("---\npriority: 3\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
originalDir, _ := os.Getwd()
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
_ = os.Chdir(cwdDir)
|
||||
|
||||
ResetPathManager()
|
||||
pm := mustGetPathManager()
|
||||
pm.projectRoot = projectDir
|
||||
|
||||
got := FindTemplateFile()
|
||||
gotAbs, _ := filepath.Abs(got)
|
||||
wantAbs, _ := filepath.Abs("new.md")
|
||||
if gotAbs != wantAbs {
|
||||
t.Errorf("FindTemplateFile() = %q, want cwd file %q", gotAbs, wantAbs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindTemplateFile_ProjectOverridesUser(t *testing.T) {
|
||||
// user config with new.md
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
userTikiDir := filepath.Join(userDir, "tiki")
|
||||
if err := os.MkdirAll(userTikiDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(userTikiDir, "new.md"), []byte("---\npriority: 1\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// project .doc/ with new.md
|
||||
projectDir := t.TempDir()
|
||||
docDir := filepath.Join(projectDir, ".doc")
|
||||
if err := os.MkdirAll(docDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(docDir, "new.md"), []byte("---\npriority: 2\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// cwd with NO new.md
|
||||
cwdDir := t.TempDir()
|
||||
originalDir, _ := os.Getwd()
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
_ = os.Chdir(cwdDir)
|
||||
|
||||
ResetPathManager()
|
||||
pm := mustGetPathManager()
|
||||
pm.projectRoot = projectDir
|
||||
|
||||
got := FindTemplateFile()
|
||||
want := filepath.Join(docDir, "new.md")
|
||||
if got != want {
|
||||
t.Errorf("FindTemplateFile() = %q, want project file %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindTemplateFile_UserOnlyFallback(t *testing.T) {
|
||||
// user config with new.md
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
userTikiDir := filepath.Join(userDir, "tiki")
|
||||
if err := os.MkdirAll(userTikiDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(userTikiDir, "new.md"), []byte("---\npriority: 1\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// no project .doc/, no cwd new.md
|
||||
cwdDir := t.TempDir()
|
||||
originalDir, _ := os.Getwd()
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
_ = os.Chdir(cwdDir)
|
||||
|
||||
ResetPathManager()
|
||||
pm := mustGetPathManager()
|
||||
pm.projectRoot = cwdDir
|
||||
|
||||
got := FindTemplateFile()
|
||||
want := filepath.Join(userTikiDir, "new.md")
|
||||
if got != want {
|
||||
t.Errorf("FindTemplateFile() = %q, want user config file %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindTemplateFile_NoneFound(t *testing.T) {
|
||||
// empty user config dir (no new.md)
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
if err := os.MkdirAll(filepath.Join(userDir, "tiki"), 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// empty cwd
|
||||
cwdDir := t.TempDir()
|
||||
originalDir, _ := os.Getwd()
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
_ = os.Chdir(cwdDir)
|
||||
|
||||
ResetPathManager()
|
||||
pm := mustGetPathManager()
|
||||
pm.projectRoot = cwdDir
|
||||
|
||||
got := FindTemplateFile()
|
||||
if got != "" {
|
||||
t.Errorf("FindTemplateFile() = %q, want empty string when no file exists", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetPathManager(t *testing.T) {
|
||||
// Save original XDG
|
||||
origXDG := os.Getenv("XDG_CONFIG_HOME")
|
||||
|
|
|
|||
|
|
@ -27,21 +27,23 @@ type templateFrontmatter struct {
|
|||
Points int `yaml:"points"`
|
||||
}
|
||||
|
||||
// loadTemplateTask reads new.md from user config directory, or falls back to embedded template.
|
||||
// loadTemplateTask reads new.md from the highest-priority location
|
||||
// (cwd > .doc/ > user config), or falls back to the embedded template.
|
||||
func loadTemplateTask() *taskpkg.Task {
|
||||
// Try to load from user config directory first
|
||||
templatePath := config.GetTemplateFile()
|
||||
templatePath := config.FindTemplateFile()
|
||||
|
||||
if templatePath == "" {
|
||||
slog.Debug("no new.md found in any search path, using embedded template")
|
||||
return loadEmbeddedTemplate()
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(templatePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
slog.Debug("new.md not found in user config dir, using embedded template", "path", templatePath)
|
||||
return loadEmbeddedTemplate()
|
||||
}
|
||||
slog.Warn("failed to read new.md template", "path", templatePath, "error", err)
|
||||
return loadEmbeddedTemplate()
|
||||
}
|
||||
|
||||
slog.Debug("loaded new.md template", "path", templatePath)
|
||||
return parseTaskTemplate(data)
|
||||
}
|
||||
|
||||
|
|
|
|||
75
store/tikistore/template_test.go
Normal file
75
store/tikistore/template_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package tikistore
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/boolean-maybe/tiki/config"
|
||||
)
|
||||
|
||||
func TestLoadTemplateTask_CwdWins(t *testing.T) {
|
||||
// user config with priority 1
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
userTikiDir := filepath.Join(userDir, "tiki")
|
||||
if err := os.MkdirAll(userTikiDir, 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(userTikiDir, "new.md"),
|
||||
[]byte("---\ntitle: user\npriority: 1\ntype: story\nstatus: backlog\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// cwd with priority 5 (should win)
|
||||
cwdDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(cwdDir, "new.md"),
|
||||
[]byte("---\ntitle: cwd\npriority: 5\ntype: bug\nstatus: backlog\n---"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
originalDir, _ := os.Getwd()
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
_ = os.Chdir(cwdDir)
|
||||
|
||||
config.ResetPathManager()
|
||||
|
||||
task := loadTemplateTask()
|
||||
if task == nil {
|
||||
t.Fatal("loadTemplateTask() returned nil")
|
||||
}
|
||||
if task.Title != "cwd" {
|
||||
t.Errorf("title = %q, want \"cwd\"", task.Title)
|
||||
}
|
||||
if task.Priority != 5 {
|
||||
t.Errorf("priority = %d, want 5", task.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTemplateTask_EmbeddedFallback(t *testing.T) {
|
||||
// no new.md anywhere
|
||||
userDir := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", userDir)
|
||||
if err := os.MkdirAll(filepath.Join(userDir, "tiki"), 0750); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cwdDir := t.TempDir()
|
||||
originalDir, _ := os.Getwd()
|
||||
defer func() { _ = os.Chdir(originalDir) }()
|
||||
_ = os.Chdir(cwdDir)
|
||||
|
||||
config.ResetPathManager()
|
||||
|
||||
task := loadTemplateTask()
|
||||
if task == nil {
|
||||
t.Fatal("loadTemplateTask() returned nil, expected embedded template")
|
||||
}
|
||||
// embedded template has type: story and status: backlog
|
||||
if task.Type != "story" {
|
||||
t.Errorf("type = %q, want \"story\" from embedded template", task.Type)
|
||||
}
|
||||
if task.Status != "backlog" {
|
||||
t.Errorf("status = %q, want \"backlog\" from embedded template", task.Status)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue