4 Commits
Author SHA1 Message Date
keb 4e4014a043 feat: the update changelog covers every version you missed
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s
Latest reads the last 10 releases in one request instead of only the
newest, so U and lazysecrets update --check show the changes of every
release since the running version, each under its version. Drafts and
pre-releases are skipped. A changelog taller than the screen is cut
with a pointer to the release pages.
2026-10-13 19:44:26 +02:00
renovate-bot Bot 5cd6d90555 chore(deps): update non-major (#59)
Co-authored-by: Renovate Bot <renovate-bot@noreply.git.kebcloud.no>
2026-10-13 17:19:14 +02:00
keb dc99779885 docs: the README shows the GitHub install URL too (#58)
Reviewed-on: #58
Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
2026-10-13 15:47:21 +02:00
keb 47ef86293d feat: look for repos in the usual folders, not just ~/git (#57)
Without roots in the config, repos are found under whichever of ~/git,
~/repo, ~/repos, ~/projects, ~/code, ~/source and ~/src exist. Missing
ones cost a stat each. A root that's a symlink to another is scanned
once, and a repo under two roots keeps the first one's name.

Reviewed-on: #57
Co-authored-by: Karl-Einar Bergstrøm <karl-einarb@outlook.com>
2026-10-13 15:37:55 +02:00
13 changed files with 153 additions and 38 deletions

No files matched your search

+2
View File
@@ -36,6 +36,8 @@ A terminal UI for finding, reading and sharing secrets. Inspired by the fantasti
On macOS or Linux (arm64 or x86_64): On macOS or Linux (arm64 or x86_64):
```sh ```sh
curl -fsSL https://raw.githubusercontent.com/karleinarb/lazysecrets/main/install.sh | sh
# Or
curl -fsSL https://git.kebcloud.no/keb/lazysecrets/raw/branch/main/install.sh | sh curl -fsSL https://git.kebcloud.no/keb/lazysecrets/raw/branch/main/install.sh | sh
``` ```
+1 -1
View File
@@ -287,7 +287,7 @@ Flags:
if start == "" { if start == "" {
start, _ = os.Getwd() start, _ = os.Getwd()
} }
roots := []string{"~/git"} roots := repo.DefaultRoots
if pc.Repos.Roots != nil { if pc.Repos.Roots != nil {
roots = *pc.Repos.Roots roots = *pc.Repos.Roots
} }
+1 -1
View File
@@ -55,7 +55,7 @@ func updateCmd(configPath string, args []string) error {
fmt.Printf("lazysecrets %s is the latest (%s)\n", current, r.Tag) fmt.Printf("lazysecrets %s is the latest (%s)\n", current, r.Tag)
return nil return nil
} }
fmt.Printf("lazysecrets %s is out (you have %s):\n\n%s\n\n", r.Tag, current, indent(r.Notes())) fmt.Printf("lazysecrets %s is out (you have %s):\n\n%s\n\n", r.Tag, current, indent(r.Notes(current)))
if check { if check {
fmt.Println("lazysecrets update installs it.") fmt.Println("lazysecrets update installs it.")
return nil return nil
+1 -1
View File
@@ -142,7 +142,7 @@ Releases are published on GitHub and on git.kebcloud.no, and it asks
them in that order, the next when one doesn't answer within 5 seconds; them in that order, the next when one doesn't answer within 5 seconds;
`releases` lists others, such as a mirror of your own. Offline, or with `releases` lists others, such as a mirror of your own. Offline, or with
every server down, it says nothing. A newer release shows in the footer, `v0.1.1 ↑ v0.2.0`; `U` (or every server down, it says nothing. A newer release shows in the footer, `v0.1.1 ↑ v0.2.0`; `U` (or
`lazysecrets update --check`) shows its changelog. `lazysecrets update --check`) shows what changed since your version, across the last 10 releases.
Updating (`u` there, or `lazysecrets update`) downloads the release for Updating (`u` there, or `lazysecrets update`) downloads the release for
your OS and CPU from the first server that has it, checks it against your OS and CPU from the first server that has it, checks it against
+6 -5
View File
@@ -7,10 +7,11 @@ lazysecrets shows git repositories under **Repos**: their
## Which repos ## Which repos
**Repos** lists every repo with secret files under the `roots` (`~/git` **Repos** lists every repo with secret files under the `roots` (by
by default; one or two levels down, `repo` or `org/repo`), any listed in default whichever of `~/git`, `~/repo`, `~/repos`, `~/projects`, `~/code`,
`paths`, and any you starred something in (the repo, a file or a key), `~/source` and `~/src` exist; one or two levels down, `repo` or
wherever it is. They're checked in the background. `org/repo`), any listed in `paths`, and any you starred something in
(the repo, a file or a key), wherever it is. They're checked in the background.
Started inside a repo, that repo gets a row of its own at the top, Started inside a repo, that repo gets a row of its own at the top,
`Repo · <name>`, and lazysecrets opens on it, with the files under the `Repo · <name>`, and lazysecrets opens on it, with the files under the
@@ -49,7 +50,7 @@ Folders of dependencies and caches are never looked in, tracked or not:
```yaml ```yaml
providers: providers:
repos: repos:
roots: [~/git] # folders of repos (the default; [] turns it off) roots: [~/git] # folders of repos (replaces the defaults; [] turns it off)
paths: [~/work/infra] # single repos elsewhere paths: [~/work/infra] # single repos elsewhere
include: ["secrets/**", "**/*.sops.yaml"] # globs from the repo root; ** for any depth include: ["secrets/**", "**/*.sops.yaml"] # globs from the repo root; ** for any depth
exclude: ["**/testdata/**"] exclude: ["**/testdata/**"]
+2 -2
View File
@@ -18,8 +18,8 @@ require (
github.com/atotto/clipboard v0.1.4 github.com/atotto/clipboard v0.1.4
github.com/charmbracelet/x/ansi v0.11.9 github.com/charmbracelet/x/ansi v0.11.9
github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f
github.com/databricks/databricks-sdk-go v0.187.0 github.com/databricks/databricks-sdk-go v0.188.0
github.com/junegunn/fzf v0.74.4 github.com/junegunn/fzf v0.74.5
go.yaml.in/yaml/v3 v3.0.5 go.yaml.in/yaml/v3 v3.0.5
golang.org/x/mod v0.42.0 golang.org/x/mod v0.42.0
golang.org/x/term v0.47.0 golang.org/x/term v0.47.0
+4 -4
View File
@@ -72,8 +72,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/databricks/databricks-sdk-go v0.187.0 h1:V1rGbEUaEdGS7tTZSyplDLnE2Vp5N1aIPiIh/LzjOC8= github.com/databricks/databricks-sdk-go v0.188.0 h1:1nxBqlctGnPzlReyziG199abHs9cR/KNjsCnSM+oDGA=
github.com/databricks/databricks-sdk-go v0.187.0/go.mod h1:udBw6nxkaqa0fgPQ7ZW1kGFkltQncYx+qFJeVFmxtz8= github.com/databricks/databricks-sdk-go v0.188.0/go.mod h1:udBw6nxkaqa0fgPQ7ZW1kGFkltQncYx+qFJeVFmxtz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -100,8 +100,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dq
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/junegunn/fzf v0.74.4 h1:DSqIW3ZxtkuuPzeLKrVnHdoWqT9VCIGRbjc/lXiGCso= github.com/junegunn/fzf v0.74.5 h1:mpJlCdgGuddKerHrdjvV3cnezEBgyjZAVn8KCo/4OMc=
github.com/junegunn/fzf v0.74.4/go.mod h1:TBRY54j3mGjAm4JcpDyiUIuDq38QlHvAzrB3GLKxQXM= github.com/junegunn/fzf v0.74.5/go.mod h1:TBRY54j3mGjAm4JcpDyiUIuDq38QlHvAzrB3GLKxQXM=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c= github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
+2 -2
View File
@@ -115,8 +115,8 @@ type Repos struct {
// Enabled defaults to true. // Enabled defaults to true.
Enabled *bool `yaml:"enabled"` Enabled *bool `yaml:"enabled"`
Paths []string `yaml:"paths"` Paths []string `yaml:"paths"`
// Roots are folders of repos. Defaults to ~/git; an empty list // Roots are folders of repos. Defaults to repo.DefaultRoots (~/git,
// turns it off. // ~/src, ~/code and the like); an empty list turns it off.
Roots *[]string `yaml:"roots"` Roots *[]string `yaml:"roots"`
// Include and Exclude are globs relative to a repo's root (** for any // Include and Exclude are globs relative to a repo's root (** for any
// depth), for repos without their own .lazysecrets.yaml. Without them, // depth), for repos without their own .lazysecrets.yaml. Without them,
+15 -3
View File
@@ -80,6 +80,10 @@ type repoInfo struct {
here bool // the repo lazysecrets started in here bool // the repo lazysecrets started in
} }
// DefaultRoots are the usual folders of repos, used when the config
// doesn't set roots. Ones that don't exist cost a stat each.
var DefaultRoots = []string{"~/git", "~/repo", "~/repos", "~/projects", "~/code", "~/source", "~/src"}
func New(s Settings) *Provider { func New(s Settings) *Provider {
if s.Sops == "" { if s.Sops == "" {
s.Sops = "sops" s.Sops = "sops"
@@ -116,10 +120,18 @@ func (p *Provider) load() {
} }
add(root) add(root)
} }
// Missing roots are skipped (most of the defaults are), and one
// that's a symlink to another is scanned once.
scanned := map[string]bool{}
for _, dir := range p.s.Roots { for _, dir := range p.s.Roots {
base, _ := filepath.EvalSymlinks(expandHome(dir)) base, err := filepath.EvalSymlinks(expandHome(dir))
for _, root := range reposIn(expandHome(dir), 2) { if err != nil || scanned[base] {
if name, err := filepath.Rel(base, root); err == nil && !strings.HasPrefix(name, "..") { continue
}
scanned[base] = true
for _, root := range reposIn(base, 2) {
// A repo under two roots keeps the first one's name.
if name, err := filepath.Rel(base, root); err == nil && !strings.HasPrefix(name, "..") && names[root] == "" {
names[root] = filepath.ToSlash(name) names[root] = filepath.ToSlash(name)
} }
add(root) add(root)
+24
View File
@@ -336,6 +336,30 @@ func TestRoots(t *testing.T) {
} }
} }
func TestRootsDeduped(t *testing.T) {
root, _ := filepath.EvalSymlinks(t.TempDir())
os.Rename(gitRepo(t, map[string]string{"secrets/a.yaml": sopsYAML}), filepath.Join(root, "infra"))
os.MkdirAll(filepath.Join(root, "acme"), 0o755)
os.Rename(gitRepo(t, map[string]string{"prod.env": sopsEnv}), filepath.Join(root, "acme", "billing"))
// ~/src → ~/git, and a root reaching one repo at another depth.
links := t.TempDir()
alias := filepath.Join(links, "src")
os.Symlink(root, alias)
missing := filepath.Join(links, "nope")
acme := filepath.Join(root, "acme")
srcs, err := New(Settings{Dir: links, Roots: []string{missing, root, alias, acme}}).Sources(context.Background())
if err != nil {
t.Fatal(err)
}
got := names(srcs, func(s provider.Source) string { return s.Name })
slices.Sort(got)
if !slices.Equal(got, []string{"acme/billing", "infra"}) {
t.Errorf("repos: %v, want each once, named from the first root", got)
}
}
func TestStarredReposAlongside(t *testing.T) { func TestStarredReposAlongside(t *testing.T) {
here := gitRepo(t, map[string]string{"secrets/a.yaml": sopsYAML}) here := gitRepo(t, map[string]string{"secrets/a.yaml": sopsYAML})
starred := gitRepo(t, map[string]string{"prod.env": sopsEnv}) starred := gitRepo(t, map[string]string{"prod.env": sopsEnv})
+8 -1
View File
@@ -109,15 +109,22 @@ func (m *Model) update() tea.Cmd {
func (m *Model) showUpdate() { func (m *Model) showUpdate() {
r := *m.newer r := *m.newer
lines := []string{m.st.Muted.Render("You have " + m.versionLabel() + ".")} lines := []string{m.st.Muted.Render("You have " + m.versionLabel() + ".")}
if notes := r.Notes(); notes != "" { if notes := r.Notes(m.version); notes != "" {
lines = append(lines, "") lines = append(lines, "")
for _, l := range strings.Split(notes, "\n") { for _, l := range strings.Split(notes, "\n") {
if h, ok := strings.CutPrefix(l, "### "); ok { if h, ok := strings.CutPrefix(l, "### "); ok {
l = m.st.Label.Render(h) l = m.st.Label.Render(h)
} else if h, ok := strings.CutPrefix(l, "## "); ok { // a release, when several are new
l = m.st.TitleFocused.Render(h)
} }
lines = append(lines, l) lines = append(lines, l)
} }
} }
// A popup doesn't scroll: several releases behind, the rest is on
// the release pages.
if most := max(6, m.h-12); len(lines) > most {
lines = append(lines[:most-1], m.st.Muted.Render("… and more on the release pages"))
}
opts := []option{{key: "u", label: "Update now", tip: "Download, check and replace this lazysecrets; restart to use it.", opts := []option{{key: "u", label: "Update now", tip: "Download, check and replace this lazysecrets; restart to use it.",
run: func(m *Model) tea.Cmd { return m.install(r) }}} run: func(m *Model) tea.Cmd { return m.install(r) }}}
if r.URL != "" { if r.URL != "" {
+52 -14
View File
@@ -28,6 +28,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"slices"
"strings" "strings"
"time" "time"
@@ -53,18 +54,44 @@ const maxDownload = 200 << 20
// Release is a published release. // Release is a published release.
type Release struct { type Release struct {
Tag string `json:"tag_name"` Tag string `json:"tag_name"`
Body string `json:"body"` // the changelog section, then install notes // Body is the changelog section, then install notes. From Latest,
URL string `json:"html_url"` // it's the changelogs of the recent releases, each under a "## v…"
// heading, so someone a few versions behind sees all they missed.
Body string `json:"body"`
URL string `json:"html_url"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
} }
// Notes is the release's changelog: its body without the install notes // Notes is the changelog since the version current: of each release
// every release ends with. // newer than it (just this one for a dev build), without the install
func (r Release) Notes() string { // notes every release ends with.
notes, _, _ := strings.Cut(r.Body, "## Install") func (r Release) Notes(current string) string {
body, _, _ := strings.Cut(r.Body, "## Install")
var keep, headings []string
in := true // before any heading: a single release's notes
for _, l := range strings.Split(body, "\n") {
if tag, ok := strings.CutPrefix(l, "## "); ok && semver.IsValid(tag) {
in = Newer(current, tag) || tag == r.Tag
if in {
headings = append(headings, l)
}
}
if in {
keep = append(keep, l)
}
}
notes := strings.Join(keep, "\n")
if len(headings) == 1 { // the title names it
notes = strings.Replace(notes, headings[0], "", 1)
}
return PlainLinks(strings.TrimSpace(notes)) return PlainLinks(strings.TrimSpace(notes))
} }
// recentReleases is how many releases Latest reads the changelogs of.
const recentReleases = 10
// PlainLinks shows Markdown links as their text, for a terminal. // PlainLinks shows Markdown links as their text, for a terminal.
func PlainLinks(s string) string { return mdLink.ReplaceAllString(s, "$1") } func PlainLinks(s string) string { return mdLink.ReplaceAllString(s, "$1") }
@@ -128,18 +155,29 @@ func (c *Client) latest(ctx context.Context, repo string) (Release, error) {
} }
ctx, cancel := context.WithTimeout(ctx, latestTimeout) ctx, cancel := context.WithTimeout(ctx, latestTimeout)
defer cancel() defer cancel()
b, err := c.get(ctx, api+"/releases/latest", 1<<20) // The recent releases in one request (per_page on GitHub, limit on
// Gitea), for their changelogs.
b, err := c.get(ctx, fmt.Sprintf("%s/releases?per_page=%d&limit=%[2]d", api, recentReleases), 1<<20)
if err != nil { if err != nil {
return Release{}, err return Release{}, err
} }
var r Release var all []Release
if err := json.Unmarshal(b, &r); err != nil { if err := json.Unmarshal(b, &all); err != nil {
return Release{}, fmt.Errorf("reading the latest release from %s: %w", repo, err) return Release{}, fmt.Errorf("reading the releases from %s: %w", repo, err)
} }
if !semver.IsValid(r.Tag) { all = slices.DeleteFunc(all, func(r Release) bool { return r.Draft || r.Prerelease || !semver.IsValid(r.Tag) })
return Release{}, fmt.Errorf("latest release %q on %s isn't a version", r.Tag, repo) if len(all) == 0 {
return Release{}, fmt.Errorf("no releases on %s", repo)
} }
return r, nil slices.SortFunc(all, func(a, b Release) int { return semver.Compare(b.Tag, a.Tag) })
var notes strings.Builder
for _, r := range all {
section, _, _ := strings.Cut(r.Body, "## Install")
fmt.Fprintf(&notes, "## %s\n\n%s\n\n", r.Tag, strings.TrimSpace(section))
}
latest := all[0]
latest.Body = strings.TrimSpace(notes.String())
return latest, nil
} }
// Newer reports whether latest is newer than current. A build that isn't // Newer reports whether latest is newer than current. A build that isn't
+35 -4
View File
@@ -39,11 +39,42 @@ func TestNewer(t *testing.T) {
func TestNotes(t *testing.T) { func TestNotes(t *testing.T) {
r := Release{Body: "### Added\n\n- Install with a one-liner (#7)\n\n## Install\n\nOn macOS…"} r := Release{Body: "### Added\n\n- Install with a one-liner (#7)\n\n## Install\n\nOn macOS…"}
if got := r.Notes(); got != "### Added\n\n- Install with a one-liner (#7)" { if got := r.Notes("0.1.1"); got != "### Added\n\n- Install with a one-liner (#7)" {
t.Errorf("notes %q", got) t.Errorf("notes %q", got)
} }
} }
// Latest reads the recent releases, so the notes cover every version
// since the one running.
func TestNotesSince(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/repos/keb/lazysecrets/releases" {
http.NotFound(w, r)
return
}
fmt.Fprint(w, `[
{"tag_name":"v0.4.0-rc1","body":"### Added\n\n- rc","prerelease":true},
{"tag_name":"v0.2.0","body":"### Added\n\n- two\n\n## Install\n\ncurl"},
{"tag_name":"v0.3.0","body":"### Fixed\n\n- three\n\n## Install\n\ncurl","html_url":"https://example.com/v0.3.0"},
{"tag_name":"v0.1.1","body":"### Added\n\n- one"},
{"tag_name":"v9.0.0","body":"draft","draft":true}]`)
}))
t.Cleanup(srv.Close)
r, err := (&Client{Servers: []string{srv.URL + "/keb/lazysecrets"}}).Latest(context.Background())
if err != nil || r.Tag != "v0.3.0" || r.URL != "https://example.com/v0.3.0" {
t.Fatalf("latest %+v %v", r, err)
}
for current, want := range map[string]string{
"0.1.1": "## v0.3.0\n\n### Fixed\n\n- three\n\n## v0.2.0\n\n### Added\n\n- two",
"0.2.0": "### Fixed\n\n- three",
"dev-2ba762c": "### Fixed\n\n- three",
} {
if got := r.Notes(current); got != want {
t.Errorf("notes since %s: %q, want %q", current, got, want)
}
}
}
func TestManaged(t *testing.T) { func TestManaged(t *testing.T) {
if Managed("/opt/homebrew/Cellar/lazysecrets/0.2.0/bin/lazysecrets") == "" || Managed("/nix/store/abc-lazysecrets/bin/lazysecrets") == "" { if Managed("/opt/homebrew/Cellar/lazysecrets/0.2.0/bin/lazysecrets") == "" || Managed("/nix/store/abc-lazysecrets/bin/lazysecrets") == "" {
t.Error("package managers not recognised") t.Error("package managers not recognised")
@@ -70,8 +101,8 @@ func server(t *testing.T, tamper bool) *httptest.Server {
sum[0] ^= 0xff sum[0] ^= 0xff
} }
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/api/v1/repos/keb/lazysecrets/releases/latest", func(w http.ResponseWriter, _ *http.Request) { mux.HandleFunc("/api/v1/repos/keb/lazysecrets/releases", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprint(w, `{"tag_name":"v0.2.0","body":"### Added\n\n- x\n\n## Install\n","html_url":"https://example.com/r"}`) fmt.Fprint(w, `[{"tag_name":"v0.2.0","body":"### Added\n\n- x\n\n## Install\n","html_url":"https://example.com/r"}]`)
}) })
mux.HandleFunc("/keb/lazysecrets/releases/download/v0.2.0/checksums.txt", func(w http.ResponseWriter, _ *http.Request) { mux.HandleFunc("/keb/lazysecrets/releases/download/v0.2.0/checksums.txt", func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintf(w, "%s %s.tar.gz\n", hex.EncodeToString(sum[:]), name) fmt.Fprintf(w, "%s %s.tar.gz\n", hex.EncodeToString(sum[:]), name)
@@ -173,7 +204,7 @@ func TestInstallRefusesATamperedArchive(t *testing.T) {
func TestNotesShowLinksAsText(t *testing.T) { func TestNotesShowLinksAsText(t *testing.T) {
r := Release{Body: "### Added\n\n- Search ([#11](https://git.example.com/pulls/11)) ([65dc2d7](https://git.example.com/commit/65dc2d7abc))\n\n## Install\n\ncurl …"} r := Release{Body: "### Added\n\n- Search ([#11](https://git.example.com/pulls/11)) ([65dc2d7](https://git.example.com/commit/65dc2d7abc))\n\n## Install\n\ncurl …"}
if got, want := r.Notes(), "### Added\n\n- Search (#11) (65dc2d7)"; got != want { if got, want := r.Notes("0.1.1"), "### Added\n\n- Search (#11) (65dc2d7)"; got != want {
t.Errorf("notes %q, want %q", got, want) t.Errorf("notes %q, want %q", got, want)
} }
} }