diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd543678..f42c3cac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,6 +100,20 @@ import "charm.land/log/v2" log.Debug("some message", "someVariable", someVariable) ``` +### Linting + +CI runs this check, but for PRs from forks it may require maintainer approval before it starts. Running `task lint` locally saves a round-trip. + +```sh +task lint +``` + +To auto-fix formatting issues (line length, imports, etc.): + +```sh +task lint:fix +``` + ### Running the Docs Locally - Run the docs site by running `task docs` diff --git a/internal/tui/modelUtils.go b/internal/tui/modelUtils.go index 43711346..9e2cc741 100644 --- a/internal/tui/modelUtils.go +++ b/internal/tui/modelUtils.go @@ -171,10 +171,14 @@ func (m *Model) executeKeybinding(key string) tea.Cmd { return nil } -// runCustomCommand executes a user-defined command. -// commandTemplate is a template string that will be parsed with the input data. -// contextData is a map of key-value pairs of data specific to the context the command is being run in. -func (m *Model) runCustomCommand(commandTemplate string, contextData *map[string]any) tea.Cmd { +// resolveTemplateInput builds the input map for a keybinding command template. +// It merges context-specific data and resolves RepoPath via the repoPaths config mapping. +// ctxRepoPath is the path of the repo gh-dash was started from (may be empty). +func resolveTemplateInput( + contextData *map[string]any, + repoPaths map[string]string, + ctxRepoPath string, +) map[string]any { // A generic map is a pretty easy & flexible way to populate a template if there's no pressing need // for structured data, existing structs, etc. Especially if holes in the data are expected. // Common data shared across contexts could be set here. @@ -189,12 +193,26 @@ func (m *Model) runCustomCommand(commandTemplate string, contextData *map[string if input["RepoName"] != nil { if repoPath, ok := common.GetRepoLocalPath( input["RepoName"].(string), - m.ctx.Config.RepoPaths, + repoPaths, ); ok { input["RepoPath"] = repoPath } } + // Fall back to the current repo path if RepoPath was not resolved via repoPaths config + if input["RepoPath"] == nil && ctxRepoPath != "" { + input["RepoPath"] = ctxRepoPath + } + + return input +} + +// runCustomCommand executes a user-defined command. +// commandTemplate is a template string that will be parsed with the input data. +// contextData is a map of key-value pairs of data specific to the context the command is being run in. +func (m *Model) runCustomCommand(commandTemplate string, contextData *map[string]any) tea.Cmd { + input := resolveTemplateInput(contextData, m.ctx.Config.RepoPaths, m.ctx.RepoPath) + cmd, err := template.New("keybinding_command").Parse(commandTemplate) if err != nil { log.Fatal("Failed parse keybinding template", "error", err) diff --git a/internal/tui/ui_test.go b/internal/tui/ui_test.go index e7201dfa..667a0168 100644 --- a/internal/tui/ui_test.go +++ b/internal/tui/ui_test.go @@ -814,6 +814,42 @@ func TestCommandTemplateMissingVariable(t *testing.T) { require.Error(t, err, "template with missing variable should return an error") } +func TestRepoPathFallbackToCtxRepoPath(t *testing.T) { + // When repoPaths config is empty, RepoPath should fall back to ctxRepoPath + // (the repo gh-dash was started from). + contextData := map[string]any{ + "RepoName": "owner/repo", + "IssueNumber": 42, + } + resolved := resolveTemplateInput( + &contextData, map[string]string{}, "/home/user/projects/repo", + ) + tmpl := "cd {{.RepoPath}} && gh issue edit {{.IssueNumber}}" + result, err := executeCommandTemplate(t, tmpl, resolved) + require.NoError(t, err) + require.Equal(t, "cd /home/user/projects/repo && gh issue edit 42", result) +} + +func TestRepoPathConfigTakesPriority(t *testing.T) { + // Explicit repoPaths config should win over ctxRepoPath. + contextData := map[string]any{ + "RepoName": "owner/repo", + "IssueNumber": 42, + } + resolved := resolveTemplateInput( + &contextData, + map[string]string{"owner/repo": "/configured/path"}, + "/home/user/projects/repo", + ) + result, err := executeCommandTemplate( + t, + "cd {{.RepoPath}} && gh issue edit {{.IssueNumber}}", + resolved, + ) + require.NoError(t, err) + require.Equal(t, "cd /configured/path && gh issue edit 42", result) +} + func TestSyncMainContentWidth(t *testing.T) { tests := []struct { name string