feat: link each changelog entry to its PR and commit (#11)

Like Immich's and RomM's release notes: every changelog entry now links to its pull request and its commit, in `CHANGELOG.md` and in the release notes on Gitea:

```
- Search with fzf's scoring and query syntax ([#11](…/pulls/11)) ([65dc2d7](…/commit/65dc2d7…))
```

- `cliff.toml`: the entry template adds the short commit hash as a link; a postprocessor turns the squash commit's `(#N)` into a link to the PR.
- The app shows release notes as text (`U`, and the status pane's changelog to come), so `Release.Notes()` turns Markdown links back into their text: `Search (#11) (65dc2d7)`.

Checked with `git cliff --unreleased` on main: every entry since v0.1.0 gets both links. Test for the link stripping.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
keb committed 2026-10-11 11:20:19 +02:00
1 parent 1c8d69d957
commit 56d3d25c9c
19 files changed
+396 -92

No files matched your search

+48 -1
View File
@@ -19,6 +19,7 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
"git.kebcloud.no/keb/lazysecrets/internal/ui/keys"
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
)
func TestMain(m *testing.M) {
@@ -57,7 +58,7 @@ func newDriverOpts(t *testing.T, opts Options, providers ...provider.Provider) *
m := New(providers, opts)
m.detail.Now = func() time.Time { return time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) }
d := &driver{t: t, m: m}
d.send(tea.WindowSizeMsg{Width: 100, Height: 30})
d.send(tea.WindowSizeMsg{Width: 100, Height: 33}) // 30 for the main panes, 3 for status
d.run(m.Init())
return d
}
@@ -1189,3 +1190,49 @@ func TestDetailsFollowFocus(t *testing.T) {
t.Errorf("value not shown:\n%s", d.screen())
}
}
func TestStatusPane(t *testing.T) {
t.Parallel()
var opened []string
d := newDriverOpts(t, Options{Version: "0.1.0", ConfigFiles: []string{"/cfg/config.yaml"}, StatePath: "/state/state.json",
OpenURL: func(u string) error { opened = append(opened, u); return nil }},
fake.Azure(0), signedOut{fake.Azure(0)})
if !strings.Contains(d.screen(), "2 accounts · 1 need sign-in") {
t.Errorf("status line:\n%s", d.screen())
}
d.keys("0")
if d.m.focus != paneStatus {
t.Fatalf("focus %v", d.m.focus)
}
scr := d.screen()
for _, want := range []string{"version v0.1.0", "GPL-3.0-or-later", "Azure · 2 subscriptions", "Azure · admin · sign-in required",
"/cfg/config.yaml", "/state/state.json", "lazysecrets/…/docs"} {
if !strings.Contains(scr, want) {
t.Errorf("missing %q:\n%s", want, scr)
}
}
// Into its rows: L signs in to the failing account, y copies a path,
// o opens a link.
d.keys("enter")
if d.m.focus != paneDetail {
t.Fatalf("enter: focus %v", d.m.focus)
}
d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Provider == "Azure:admin@contoso.com/t1" })
if a := d.m.loginFor(); a == nil || a.Account != "Azure · admin" {
t.Errorf("L signs in to %+v", a)
}
d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Label == "config" })
d.keys("y")
if got := d.clip().content; got != "/cfg/config.yaml" {
t.Errorf("y copied %q", got)
}
d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Label == "docs" })
d.keys("o")
if len(opened) != 1 || !strings.HasSuffix(opened[0], "/src/branch/main/docs") {
t.Errorf("o opened %q", opened)
}
d.keys("shift+tab")
if d.m.focus != paneStatus {
t.Errorf("back to %v", d.m.focus)
}
}