Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
28 changes: 23 additions & 5 deletions internal/tui/modelUtils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions internal/tui/ui_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading