Compare commits
4
Commits
v0.1.1
...
4e4014a043
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e4014a043
|
||
|
|
5cd6d90555 | ||
|
|
dc99779885 | ||
|
|
47ef86293d |
No files matched your search
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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/**"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|||||||
@@ -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
@@ -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(¬es, "## %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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user