68feddf0916b6b716c4654535360a38f0abdab0f

Author
Ayman Bagabas <ayman.bagabas@gmail.com>
Committer
Ayman Bagabas <ayman.bagabas@gmail.com>
Date

Message

feat: specify custom readme file

* Optionally specify per repo readme file path in config
* Default to "README*" glob and choose the first glob match
* Refactor code and move stuff around

Fixes: https://github.com/charmbracelet/soft-serve/issues/31

Diff

This diff is truncated to protect this page.

  1diff --git a/go.mod b/go.mod
  2index d5f7682a9923cb74f8bca15d843e3081c3d6d8f1..cf1b09a59d8632c429f7c463253b5f089fdeebf6 100644
  3--- a/go.mod
  4+++ b/go.mod
  5@@ -23,6 +23,7 @@ require (
  6 )
  7 
  8 require (
  9+	github.com/gobwas/glob v0.2.3
 10 	github.com/muesli/mango v0.1.0
 11 	github.com/muesli/roff v0.1.0
 12 )
 13diff --git a/go.sum b/go.sum
 14index cee667d313c1218357ce9eccd76647357f1d76e0..28b4b112f5b43f968ef2cc8e825ee12b8cdc241b 100644
 15--- a/go.sum
 16+++ b/go.sum
 17@@ -61,6 +61,8 @@ github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2Su
 18 github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
 19 github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
 20 github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
 21+github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 22+github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 23 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 24 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 25 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 26diff --git a/internal/config/config.go b/internal/config/config.go
 27index cf2f58f922f4003e57ef2dbdca28ec3aa0a26960..972fe1e4cc2efadea699d971c0bcb96df04828d2 100644
 28--- a/internal/config/config.go
 29+++ b/internal/config/config.go
 30@@ -1,10 +1,10 @@
 31 package config
 32 
 33 import (
 34-	"os/exec"
 35-	"path/filepath"
 36+	"bytes"
 37 	"strings"
 38 	"sync"
 39+	"text/template"
 40 
 41 	"golang.org/x/crypto/ssh"
 42 	"gopkg.in/yaml.v2"
 43@@ -46,6 +46,7 @@ type Repo struct {
 44 	Repo    string `yaml:"repo"`
 45 	Note    string `yaml:"note"`
 46 	Private bool   `yaml:"private"`
 47+	Readme  string `yaml:"readme"`
 48 }
 49 
 50 // NewConfig creates a new internal Config struct.
 51@@ -127,6 +128,41 @@ func (cfg *Config) Reload() error {
 52 	if err != nil {
 53 		return fmt.Errorf("bad yaml in config.yaml: %s", err)
 54 	}
 55+	for _, r := range cfg.Source.AllRepos() {
 56+		name := r.Name()
 57+		pat := "README*"
 58+		rp := ""
 59+		for _, rr := range cfg.Repos {
 60+			if name == rr.Repo {
 61+				rp = rr.Readme
 62+				break
 63+			}
 64+		}
 65+		if rp != "" {
 66+			pat = rp
 67+		}
 68+		rm := ""
 69+		f, err := r.FindLatestFile(pat)
 70+		if err != nil && err != object.ErrFileNotFound {
 71+			return err
 72+		}
 73+		if err == nil {
 74+			fc, err := f.Contents()
 75+			if err != nil {
 76+				return err
 77+			}
 78+			rm = fc
 79+			r.ReadmePath = f.Name
 80+		}
 81+		if name == "config" {
 82+			md, err := templatize(rm, cfg)
 83+			if err != nil {
 84+				return err
 85+			}
 86+			rm = md
 87+		}
 88+		r.Readme = rm
 89+	}
 90 	return nil
 91 }
 92 
 93@@ -150,7 +186,7 @@ func (cfg *Config) createDefaultConfigRepo(yaml string) error {
 94 	if err != nil {
 95 		return err
 96 	}
 97-	_, err = rs.GetRepo(cn)
 98+	r, err := rs.GetRepo(cn)
 99 	if err == git.ErrMissingRepo {
100 		cr, err := rs.InitRepo(cn, true)
101 		if err != nil {
102@@ -218,3 +254,16 @@ func (cfg *Config) isPrivate(repo string) bool {
103 	}
104 	return false
105 }
106+
107+func templatize(mdt string, tmpl interface{}) (string, error) {
108+	t, err := template.New("readme").Parse(mdt)
109+	if err != nil {
110+		return "", err
111+	}
112+	buf := &bytes.Buffer{}
113+	err = t.Execute(buf, tmpl)
114+	if err != nil {
115+		return "", err
116+	}
117+	return buf.String(), nil
118+}
119diff --git a/internal/config/defaults.go b/internal/config/defaults.go
120index 30ed4ec86f375ebd219cead9ae1c32fb93f4f4b1..60a93b36dfb3cccd4b8dad0c4502476ae83b5e3c 100644
121--- a/internal/config/defaults.go
122+++ b/internal/config/defaults.go
123@@ -19,13 +19,13 @@ anon-access: %s
124 # will be accepted.
125 allow-keyless: false
126 
127-# Customize repo display in the menu. Only repos in this list will appear in
128-# the TUI.
129+# Customize repo display in the menu.
130 repos:
131   - name: Home
132     repo: config
133     private: true
134     note: "Configuration and content repo for this server"
135+    readme: README.md
136 `
137 
138 const hasKeyUserConfig = `
139diff --git a/internal/git/git.go b/internal/git/git.go
140index 010e22f16014112e617eefa867f64b7d9fa9c002..5008477728b9e31f3d636212ca7879bc782910e4 100644
141--- a/internal/git/git.go
142+++ b/internal/git/git.go
143@@ -14,8 +14,10 @@ import (
144 	"github.com/go-git/go-git/v5"
145 	"github.com/go-git/go-git/v5/plumbing"
146 	"github.com/go-git/go-git/v5/plumbing/object"
147+	"github.com/go-git/go-git/v5/plumbing/storer"
148 	"github.com/go-git/go-git/v5/plumbing/transport"
149 	"github.com/go-git/go-git/v5/storage/memory"
150+	"github.com/gobwas/glob"
151 )
152 
153 // ErrMissingRepo indicates that the requested repository could not be found.
154@@ -26,6 +28,7 @@ type Repo struct {
155 	path       string
156 	repository *git.Repository
157 	Readme     string
158+	ReadmePath string
159 	refCommits map[plumbing.Hash]gitypes.Commits
160 	head       *plumbing.Reference
161 	refs       []*plumbing.Reference
162@@ -107,6 +110,7 @@ func (r *Repo) commitForHash(hash plumbing.Hash) (*object.Commit, error) {
163 	return co, nil
164 }
165 
166+// PatchCtx returns the patch for a given commit.
167 func (r *Repo) PatchCtx(ctx context.Context, commit *object.Commit) (*object.Patch, error) {
168 	hash := commit.Hash
169 	p, ok := r.patch[hash]
170@@ -199,14 +203,12 @@ func (r *Repo) targetHash(ref *plumbing.Reference) (plumbing.Hash, error) {
171 
172 // GetReadme returns the readme for a repository.
173 func (r *Repo) GetReadme() string {
174-	if r.Readme != "" {
175-		return r.Readme
176-	}
177-	md, err := r.LatestFile("README.md")
178-	if err != nil {
179-		return ""
180-	}
181-	return md
182+	return r.Readme
183+}
184+
185+// GetReadmePath returns the path to the readme for a repository.
186+func (r *Repo) GetReadmePath() string {
187+	return r.ReadmePath
188 }
189 
190 // RepoSource is a reference to an on-disk repositories.
191@@ -310,13 +312,6 @@ func (rs *RepoSource) loadRepo(path string, rg *git.Repository) (*Repo, error) {
192 		return nil, err
193 	}
194 	r.head = ref
195-	rm, err := r.LatestFile("README.md")
196-	if err == object.ErrFileNotFound {
197-		rm = ""
198-	} else if err != nil {
199-		return nil, err
200-	}
201-	r.Readme = rm
202 	l, err := r.repository.Log(&git.LogOptions{All: true})
203 	if err != nil {
204 		return nil, err
205@@ -341,13 +336,40 @@ func (rs *RepoSource) loadRepo(path string, rg *git.Repository) (*Repo, error) {
206 	return r, nil
207 }
208 
209-// LatestFile returns the latest file at the specified path in the repository.
210-func (r *Repo) LatestFile(path string) (string, error) {
211+// FindLatestFile returns the latest file for a given path.
212+func (r *Repo) FindLatestFile(pattern string) (*object.File, error) {
213+	g, err := glob.Compile(pattern)
214+	if err != nil {
215+		return nil, err
216+	}
217 	c, err := r.commitForHash(r.head.Hash())
218 	if err != nil {
219-		return "", err
220+		return nil, err
221 	}
222-	f, err := c.File(path)
223+	fi, err := c.Files()
224+	if err != nil {
225+		return nil, err
226+	}
227+	var f *object.File
228+	err = fi.ForEach(func(ff *object.File) error {
229+		if g.Match(ff.Name) {
230+			f = ff
231+			return storer.ErrStop
232+		}
233+		return nil
234+	})
235+	if err != nil {
236+		return nil, err
237+	}
238+	if f == nil {
239+		return nil, object.ErrFileNotFound
240+	}
241+	return f, nil
242+}
243diff --git a/internal/tui/bubbles/git/about/bubble.go b/internal/tui/bubbles/git/about/bubble.go
244index 4740246b670a001a93b59c4425a34cc4820877c2..2c957397d0bde01e0911261bb5dd5d4cfefc1efc 100644
245--- a/internal/tui/bubbles/git/about/bubble.go
246+++ b/internal/tui/bubbles/git/about/bubble.go
247@@ -8,6 +8,7 @@ import (
248 	vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
249 	"github.com/charmbracelet/soft-serve/internal/tui/style"
250 	"github.com/go-git/go-git/v5/plumbing"
251+	"github.com/muesli/reflow/wrap"
252 )
253 
254 type Bubble struct {
255@@ -92,7 +93,18 @@ func (b *Bubble) glamourize() (string, error) {
256 	if rm == "" {
257 		return b.styles.AboutNoReadme.Render("No readme found."), nil
258 	}
259-	return types.Glamourize(w, rm)
260+	f, err := types.RenderFile(b.repo.GetReadmePath(), rm, w)
261+	if err != nil {
262+		return "", err
263+	}
264+	// For now, hard-wrap long lines in Glamour that would otherwise break the
265+	// layout when wrapping. This may be due to #43 in Reflow, which has to do
266+	// with a bug in the way lines longer than the given width are wrapped.
267+	//
268+	//     https://github.com/muesli/reflow/issues/43
269+	//
270+	// TODO: solve this upstream in Glamour/Reflow.
271+	return wrap.String(f, w), nil
272 }
273 
274 func (b *Bubble) setupCmd() tea.Msg {
275diff --git a/internal/tui/bubbles/git/tree/bubble.go b/internal/tui/bubbles/git/tree/bubble.go
276index 1876df204b6901eeed52f690d665fa8ef69f4d85..e6c2a22d4698cb964352b1468a33569a80b7b775 100644
277--- a/internal/tui/bubbles/git/tree/bubble.go
278+++ b/internal/tui/bubbles/git/tree/bubble.go
279@@ -7,11 +7,9 @@ import (
280 	"sort"
281 	"strings"
282 
283-	"github.com/alecthomas/chroma/lexers"
284 	"github.com/charmbracelet/bubbles/list"
285 	"github.com/charmbracelet/bubbles/viewport"
286 	tea "github.com/charmbracelet/bubbletea"
287-	gansi "github.com/charmbracelet/glamour/ansi"
288 	"github.com/charmbracelet/lipgloss"
289 	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
290 	"github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
291@@ -336,31 +334,12 @@ func (b *Bubble) renderFile(m fileMsg) string {
292 	if len(strings.Split(c, "\n")) > types.MaxDiffLines {
293 		s.WriteString(types.ErrFileTooLarge.Error())
294 	} else {
295-		lexer := lexers.Match(b.path)
296-		lang := ""
297-		if lexer != nil && lexer.Config() != nil {
298-			lang = lexer.Config().Name
299-		}
300-		formatter := &gansi.CodeBlockElement{
301-			Code:     c,
302-			Language: lang,
303-		}
304-		if lang == "markdown" {
305-			w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize()
306-			md, err := types.Glamourize(w, c)
307-			if err != nil {
308-				s.WriteString(err.Error())
309-			} else {
310-				s.WriteString(md)
311-			}
312+		w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize()
313+		f, err := types.RenderFile(b.path, m.content, w)
314+		if err != nil {
315+			s.WriteString(err.Error())
316 		} else {
317-			r := strings.Builder{}
318-			err := formatter.Render(&r, types.RenderCtx)
319-			if err != nil {
320-				s.WriteString(err.Error())
321-			} else {
322-				s.WriteString(r.String())
323-			}
324+			s.WriteString(f)
325 		}
326 	}
327 	return b.style.TreeFileContent.Copy().Width(b.width - b.widthMargin).Render(s.String())
328diff --git a/internal/tui/bubbles/git/types/formatter.go b/internal/tui/bubbles/git/types/formatter.go
329index ea3197a22e08a7c687b6720e6efcf8817efb480d..a7ef850e834284ddb0a41807ccd059429260d4a0 100644
330--- a/internal/tui/bubbles/git/types/formatter.go
331+++ b/internal/tui/bubbles/git/types/formatter.go
332@@ -1,9 +1,11 @@
333 package types
334 
335 import (
336+	"strings"
337+
338+	"github.com/alecthomas/chroma/lexers"
339 	"github.com/charmbracelet/glamour"
340 	gansi "github.com/charmbracelet/glamour/ansi"
341-	"github.com/muesli/reflow/wrap"
342 	"github.com/muesli/termenv"
343 )
344 
345@@ -52,12 +54,35 @@ func Glamourize(w int, md string) (string, error) {
346 	if err != nil {
347 		return "", err
348 	}
349-	// For now, hard-wrap long lines in Glamour that would otherwise break the
350-	// layout when wrapping. This may be due to #43 in Reflow, which has to do
351-	// with a bug in the way lines longer than the given width are wrapped.
352-	//
353-	//     https://github.com/muesli/reflow/issues/43
354-	//
355-	// TODO: solve this upstream in Glamour/Reflow.
356-	return wrap.String(mdt, w), nil
357+	return mdt, nil
358+}
359+
360+func RenderFile(path, content string, width int) (string, error) {
361+	lexer := lexers.Fallback
362+	if path == "" {
363+		lexer = lexers.Analyse(content)
364+	} else {
365+		lexer = lexers.Match(path)
366+	}
367+	lang := ""
368+	if lexer != nil && lexer.Config() != nil {
369+		lang = lexer.Config().Name
370+	}
371+	formatter := &gansi.CodeBlockElement{
372+		Code:     content,
373+		Language: lang,
374+	}
375+	if lang == "markdown" {
376+		md, err := Glamourize(width, content)
377+		if err != nil {
378+			return "", err
379+		}
380+		return md, nil
381+	}
382+	r := strings.Builder{}
383+	err := formatter.Render(&r, RenderCtx)
384+	if err != nil {
385+		return "", err
386+	}
387+	return r.String(), nil
388 }
389diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go
390index 05a1c26114c178ee6e08286c3687c7a187c6b95c..90e5436e98a21a06413dd177dd5f085c59eac91b 100644
391--- a/internal/tui/bubbles/git/types/git.go
392+++ b/internal/tui/bubbles/git/types/git.go
393@@ -14,6 +14,7 @@ type Repo interface {
394 	SetHEAD(*plumbing.Reference) error
395 	GetReferences() []*plumbing.Reference
396 	GetReadme() string
397+	GetReadmePath() string
398 	GetCommits(*plumbing.Reference) (Commits, error)
399 	Repository() *git.Repository
400 	Tree(*plumbing.Reference, string) (*object.Tree, error)
401diff --git a/internal/tui/commands.go b/internal/tui/commands.go
402index 20d3b16aaa4a541033f7af8c01d1f0d686d5907f..2008ae811db8f59d58152979d133c03aea521fd4 100644
403--- a/internal/tui/commands.go
404+++ b/internal/tui/commands.go
405@@ -1,9 +1,7 @@
406 package tui
407 
408 import (
409-	"bytes"
410 	"fmt"
411-	"text/template"
412 
413 	tea "github.com/charmbracelet/bubbletea"
414 	"github.com/charmbracelet/lipgloss"
415@@ -102,13 +100,6 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
416 	if err != nil {
417 		return me, err
418 	}
419-	if rn == "config" {
420-		md, err := templatize(r.Readme, b.config)
421-		if err != nil {
422-			return me, err
423-		}
424-		r.Readme = md
425-	}
426 	boxLeftWidth := b.styles.Menu.GetWidth() + b.styles.Menu.GetHorizontalFrameSize()
427 	// TODO: also send this along with a tea.WindowSizeMsg
428 	var heightMargin = lipgloss.Height(b.headerView()) +
429@@ -125,16 +116,3 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
430 	me.bubble = rb
431 	return me, nil
432 }
433-
434-func templatize(mdt string, tmpl interface{}) (string, error) {
435-	t, err := template.New("readme").Parse(mdt)
436-	if err != nil {
437-		return "", err
438-	}
439-	buf := &bytes.Buffer{}
440-	err = t.Execute(buf, tmpl)
441-	if err != nil {
442-		return "", err
443-	}
444-	return buf.String(), nil
445-}