diff --git a/go.mod b/go.mod index d5f7682a9923cb74f8bca15d843e3081c3d6d8f1..cf1b09a59d8632c429f7c463253b5f089fdeebf6 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( ) require ( + github.com/gobwas/glob v0.2.3 github.com/muesli/mango v0.1.0 github.com/muesli/roff v0.1.0 ) diff --git a/go.sum b/go.sum index cee667d313c1218357ce9eccd76647357f1d76e0..28b4b112f5b43f968ef2cc8e825ee12b8cdc241b 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,8 @@ github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2Su github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= diff --git a/internal/config/config.go b/internal/config/config.go index cf2f58f922f4003e57ef2dbdca28ec3aa0a26960..972fe1e4cc2efadea699d971c0bcb96df04828d2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,10 +1,10 @@ package config import ( - "os/exec" - "path/filepath" + "bytes" "strings" "sync" + "text/template" "golang.org/x/crypto/ssh" "gopkg.in/yaml.v2" @@ -46,6 +46,7 @@ type Repo struct { Repo string `yaml:"repo"` Note string `yaml:"note"` Private bool `yaml:"private"` + Readme string `yaml:"readme"` } // NewConfig creates a new internal Config struct. @@ -127,6 +128,41 @@ func (cfg *Config) Reload() error { if err != nil { return fmt.Errorf("bad yaml in config.yaml: %s", err) } + for _, r := range cfg.Source.AllRepos() { + name := r.Name() + pat := "README*" + rp := "" + for _, rr := range cfg.Repos { + if name == rr.Repo { + rp = rr.Readme + break + } + } + if rp != "" { + pat = rp + } + rm := "" + f, err := r.FindLatestFile(pat) + if err != nil && err != object.ErrFileNotFound { + return err + } + if err == nil { + fc, err := f.Contents() + if err != nil { + return err + } + rm = fc + r.ReadmePath = f.Name + } + if name == "config" { + md, err := templatize(rm, cfg) + if err != nil { + return err + } + rm = md + } + r.Readme = rm + } return nil } @@ -150,7 +186,7 @@ func (cfg *Config) createDefaultConfigRepo(yaml string) error { if err != nil { return err } - _, err = rs.GetRepo(cn) + r, err := rs.GetRepo(cn) if err == git.ErrMissingRepo { cr, err := rs.InitRepo(cn, true) if err != nil { @@ -218,3 +254,16 @@ func (cfg *Config) isPrivate(repo string) bool { } return false } + +func templatize(mdt string, tmpl interface{}) (string, error) { + t, err := template.New("readme").Parse(mdt) + if err != nil { + return "", err + } + buf := &bytes.Buffer{} + err = t.Execute(buf, tmpl) + if err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 30ed4ec86f375ebd219cead9ae1c32fb93f4f4b1..60a93b36dfb3cccd4b8dad0c4502476ae83b5e3c 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -19,13 +19,13 @@ anon-access: %s # will be accepted. allow-keyless: false -# Customize repo display in the menu. Only repos in this list will appear in -# the TUI. +# Customize repo display in the menu. repos: - name: Home repo: config private: true note: "Configuration and content repo for this server" + readme: README.md ` const hasKeyUserConfig = ` diff --git a/internal/git/git.go b/internal/git/git.go index 010e22f16014112e617eefa867f64b7d9fa9c002..5008477728b9e31f3d636212ca7879bc782910e4 100644 --- a/internal/git/git.go +++ b/internal/git/git.go @@ -14,8 +14,10 @@ import ( "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/storer" "github.com/go-git/go-git/v5/plumbing/transport" "github.com/go-git/go-git/v5/storage/memory" + "github.com/gobwas/glob" ) // ErrMissingRepo indicates that the requested repository could not be found. @@ -26,6 +28,7 @@ type Repo struct { path string repository *git.Repository Readme string + ReadmePath string refCommits map[plumbing.Hash]gitypes.Commits head *plumbing.Reference refs []*plumbing.Reference @@ -107,6 +110,7 @@ func (r *Repo) commitForHash(hash plumbing.Hash) (*object.Commit, error) { return co, nil } +// PatchCtx returns the patch for a given commit. func (r *Repo) PatchCtx(ctx context.Context, commit *object.Commit) (*object.Patch, error) { hash := commit.Hash p, ok := r.patch[hash] @@ -199,14 +203,12 @@ func (r *Repo) targetHash(ref *plumbing.Reference) (plumbing.Hash, error) { // GetReadme returns the readme for a repository. func (r *Repo) GetReadme() string { - if r.Readme != "" { - return r.Readme - } - md, err := r.LatestFile("README.md") - if err != nil { - return "" - } - return md + return r.Readme +} + +// GetReadmePath returns the path to the readme for a repository. +func (r *Repo) GetReadmePath() string { + return r.ReadmePath } // RepoSource is a reference to an on-disk repositories. @@ -310,13 +312,6 @@ func (rs *RepoSource) loadRepo(path string, rg *git.Repository) (*Repo, error) { return nil, err } r.head = ref - rm, err := r.LatestFile("README.md") - if err == object.ErrFileNotFound { - rm = "" - } else if err != nil { - return nil, err - } - r.Readme = rm l, err := r.repository.Log(&git.LogOptions{All: true}) if err != nil { return nil, err @@ -341,13 +336,40 @@ func (rs *RepoSource) loadRepo(path string, rg *git.Repository) (*Repo, error) { return r, nil } -// LatestFile returns the latest file at the specified path in the repository. -func (r *Repo) LatestFile(path string) (string, error) { +// FindLatestFile returns the latest file for a given path. +func (r *Repo) FindLatestFile(pattern string) (*object.File, error) { + g, err := glob.Compile(pattern) + if err != nil { + return nil, err + } c, err := r.commitForHash(r.head.Hash()) if err != nil { - return "", err + return nil, err } - f, err := c.File(path) + fi, err := c.Files() + if err != nil { + return nil, err + } + var f *object.File + err = fi.ForEach(func(ff *object.File) error { + if g.Match(ff.Name) { + f = ff + return storer.ErrStop + } + return nil + }) + if err != nil { + return nil, err + } + if f == nil { + return nil, object.ErrFileNotFound + } + return f, nil +} + +// LatestFile returns the contents of the latest file at the specified path in the repository. +func (r *Repo) LatestFile(pattern string) (string, error) { + f, err := r.FindLatestFile(pattern) if err != nil { return "", err } diff --git a/internal/tui/bubbles/git/about/bubble.go b/internal/tui/bubbles/git/about/bubble.go index 4740246b670a001a93b59c4425a34cc4820877c2..2c957397d0bde01e0911261bb5dd5d4cfefc1efc 100644 --- a/internal/tui/bubbles/git/about/bubble.go +++ b/internal/tui/bubbles/git/about/bubble.go @@ -8,6 +8,7 @@ import ( vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport" "github.com/charmbracelet/soft-serve/internal/tui/style" "github.com/go-git/go-git/v5/plumbing" + "github.com/muesli/reflow/wrap" ) type Bubble struct { @@ -92,7 +93,18 @@ func (b *Bubble) glamourize() (string, error) { if rm == "" { return b.styles.AboutNoReadme.Render("No readme found."), nil } - return types.Glamourize(w, rm) + f, err := types.RenderFile(b.repo.GetReadmePath(), rm, w) + if err != nil { + return "", err + } + // For now, hard-wrap long lines in Glamour that would otherwise break the + // layout when wrapping. This may be due to #43 in Reflow, which has to do + // with a bug in the way lines longer than the given width are wrapped. + // + // https://github.com/muesli/reflow/issues/43 + // + // TODO: solve this upstream in Glamour/Reflow. + return wrap.String(f, w), nil } func (b *Bubble) setupCmd() tea.Msg { diff --git a/internal/tui/bubbles/git/tree/bubble.go b/internal/tui/bubbles/git/tree/bubble.go index 1876df204b6901eeed52f690d665fa8ef69f4d85..e6c2a22d4698cb964352b1468a33569a80b7b775 100644 --- a/internal/tui/bubbles/git/tree/bubble.go +++ b/internal/tui/bubbles/git/tree/bubble.go @@ -7,11 +7,9 @@ import ( "sort" "strings" - "github.com/alecthomas/chroma/lexers" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" - gansi "github.com/charmbracelet/glamour/ansi" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs" "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types" @@ -336,31 +334,12 @@ func (b *Bubble) renderFile(m fileMsg) string { if len(strings.Split(c, "\n")) > types.MaxDiffLines { s.WriteString(types.ErrFileTooLarge.Error()) } else { - lexer := lexers.Match(b.path) - lang := "" - if lexer != nil && lexer.Config() != nil { - lang = lexer.Config().Name - } - formatter := &gansi.CodeBlockElement{ - Code: c, - Language: lang, - } - if lang == "markdown" { - w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize() - md, err := types.Glamourize(w, c) - if err != nil { - s.WriteString(err.Error()) - } else { - s.WriteString(md) - } + w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize() + f, err := types.RenderFile(b.path, m.content, w) + if err != nil { + s.WriteString(err.Error()) } else { - r := strings.Builder{} - err := formatter.Render(&r, types.RenderCtx) - if err != nil { - s.WriteString(err.Error()) - } else { - s.WriteString(r.String()) - } + s.WriteString(f) } } return b.style.TreeFileContent.Copy().Width(b.width - b.widthMargin).Render(s.String()) diff --git a/internal/tui/bubbles/git/types/formatter.go b/internal/tui/bubbles/git/types/formatter.go index ea3197a22e08a7c687b6720e6efcf8817efb480d..a7ef850e834284ddb0a41807ccd059429260d4a0 100644 --- a/internal/tui/bubbles/git/types/formatter.go +++ b/internal/tui/bubbles/git/types/formatter.go @@ -1,9 +1,11 @@ package types import ( + "strings" + + "github.com/alecthomas/chroma/lexers" "github.com/charmbracelet/glamour" gansi "github.com/charmbracelet/glamour/ansi" - "github.com/muesli/reflow/wrap" "github.com/muesli/termenv" ) @@ -52,12 +54,35 @@ func Glamourize(w int, md string) (string, error) { if err != nil { return "", err } - // For now, hard-wrap long lines in Glamour that would otherwise break the - // layout when wrapping. This may be due to #43 in Reflow, which has to do - // with a bug in the way lines longer than the given width are wrapped. - // - // https://github.com/muesli/reflow/issues/43 - // - // TODO: solve this upstream in Glamour/Reflow. - return wrap.String(mdt, w), nil + return mdt, nil +} + +func RenderFile(path, content string, width int) (string, error) { + lexer := lexers.Fallback + if path == "" { + lexer = lexers.Analyse(content) + } else { + lexer = lexers.Match(path) + } + lang := "" + if lexer != nil && lexer.Config() != nil { + lang = lexer.Config().Name + } + formatter := &gansi.CodeBlockElement{ + Code: content, + Language: lang, + } + if lang == "markdown" { + md, err := Glamourize(width, content) + if err != nil { + return "", err + } + return md, nil + } + r := strings.Builder{} + err := formatter.Render(&r, RenderCtx) + if err != nil { + return "", err + } + return r.String(), nil } diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go index 05a1c26114c178ee6e08286c3687c7a187c6b95c..90e5436e98a21a06413dd177dd5f085c59eac91b 100644 --- a/internal/tui/bubbles/git/types/git.go +++ b/internal/tui/bubbles/git/types/git.go @@ -14,6 +14,7 @@ type Repo interface { SetHEAD(*plumbing.Reference) error GetReferences() []*plumbing.Reference GetReadme() string + GetReadmePath() string GetCommits(*plumbing.Reference) (Commits, error) Repository() *git.Repository Tree(*plumbing.Reference, string) (*object.Tree, error) diff --git a/internal/tui/commands.go b/internal/tui/commands.go index 20d3b16aaa4a541033f7af8c01d1f0d686d5907f..2008ae811db8f59d58152979d133c03aea521fd4 100644 --- a/internal/tui/commands.go +++ b/internal/tui/commands.go @@ -1,9 +1,7 @@ package tui import ( - "bytes" "fmt" - "text/template" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" @@ -102,13 +100,6 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) { if err != nil { return me, err } - if rn == "config" { - md, err := templatize(r.Readme, b.config) - if err != nil { - return me, err - } - r.Readme = md - } boxLeftWidth := b.styles.Menu.GetWidth() + b.styles.Menu.GetHorizontalFrameSize() // TODO: also send this along with a tea.WindowSizeMsg var heightMargin = lipgloss.Height(b.headerView()) + @@ -125,16 +116,3 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) { me.bubble = rb return me, nil } - -func templatize(mdt string, tmpl interface{}) (string, error) { - t, err := template.New("readme").Parse(mdt) - if err != nil { - return "", err - } - buf := &bytes.Buffer{} - err = t.Execute(buf, tmpl) - if err != nil { - return "", err - } - return buf.String(), nil -}