Detailed changes
@@ -1,20 +1,56 @@
package common
import (
+ "context"
+
"github.com/aymanbagabas/go-osc52"
+ "github.com/charmbracelet/soft-serve/server/config"
+ "github.com/charmbracelet/soft-serve/ui/git"
"github.com/charmbracelet/soft-serve/ui/keymap"
"github.com/charmbracelet/soft-serve/ui/styles"
+ "github.com/gliderlabs/ssh"
zone "github.com/lrstanley/bubblezone"
)
+type contextKey struct {
+ name string
+}
+
+// Keys to use for context.Context.
+var (
+ ConfigKey = &contextKey{"config"}
+ RepoKey = &contextKey{"repo"}
+)
+
// Common is a struct all components should embed.
type Common struct {
- Copy *osc52.Output
- Styles *styles.Styles
- KeyMap *keymap.KeyMap
- Width int
- Height int
- Zone *zone.Manager
+ ctx context.Context
+ Width, Height int
+ Styles *styles.Styles
+ KeyMap *keymap.KeyMap
+ Copy *osc52.Output
+ Zone *zone.Manager
+}
+
+// NewCommon returns a new Common struct.
+func NewCommon(ctx context.Context, copy *osc52.Output, width, height int) Common {
+ if ctx == nil {
+ ctx = context.TODO()
+ }
+ return Common{
+ ctx: ctx,
+ Width: width,
+ Height: height,
+ Copy: copy,
+ Styles: styles.DefaultStyles(),
+ KeyMap: keymap.DefaultKeyMap(),
+ Zone: zone.New(),
+ }
+}
+
+// SetValue sets a value in the context.
+func (c *Common) SetValue(key, value interface{}) {
+ c.ctx = context.WithValue(c.ctx, key, value)
}
// SetSize sets the width and height of the common struct.
@@ -22,3 +58,30 @@ func (c *Common) SetSize(width, height int) {
c.Width = width
c.Height = height
}
+
+// Config returns the server config.
+func (c *Common) Config() *config.Config {
+ v := c.ctx.Value(ConfigKey)
+ if cfg, ok := v.(*config.Config); ok {
+ return cfg
+ }
+ return nil
+}
+
+// Repo returns the repository.
+func (c *Common) Repo() *git.Repository {
+ v := c.ctx.Value(RepoKey)
+ if r, ok := v.(*git.Repository); ok {
+ return r
+ }
+ return nil
+}
+
+// PublicKey returns the public key.
+func (c *Common) PublicKey() ssh.PublicKey {
+ v := c.ctx.Value(ssh.ContextKeyPublicKey)
+ if p, ok := v.(ssh.PublicKey); ok {
+ return p
+ }
+ return nil
+}
@@ -1,25 +0,0 @@
-package ui
-
-import (
- "github.com/charmbracelet/soft-serve/config"
- "github.com/charmbracelet/soft-serve/ui/git"
-)
-
-// source is a wrapper around config.RepoSource that implements git.GitRepoSource.
-type source struct {
- *config.RepoSource
-}
-
-// GetRepo implements git.GitRepoSource.
-func (s *source) GetRepo(name string) (git.GitRepo, error) {
- return s.RepoSource.GetRepo(name)
-}
-
-// AllRepos implements git.GitRepoSource.
-func (s *source) AllRepos() []git.GitRepo {
- rs := make([]git.GitRepo, 0)
- for _, r := range s.RepoSource.AllRepos() {
- rs = append(rs, r)
- }
- return rs
-}
@@ -4,32 +4,28 @@ import (
"errors"
"fmt"
- "github.com/charmbracelet/soft-serve/git"
+ "github.com/charmbracelet/soft-serve/proto"
)
// ErrMissingRepo indicates that the requested repository could not be found.
var ErrMissingRepo = errors.New("missing repo")
-// GitRepo is an interface for Git repositories.
-type GitRepo interface {
- Repo() string
- Name() string
- Description() string
- Readme() (string, string)
- HEAD() (*git.Reference, error)
- Commit(string) (*git.Commit, error)
- CommitsByPage(*git.Reference, int, int) (git.Commits, error)
- CountCommits(*git.Reference) (int64, error)
- Diff(*git.Commit) (*git.Diff, error)
- References() ([]*git.Reference, error)
- Tree(*git.Reference, string) (*git.Tree, error)
- IsPrivate() bool
+// Repository is a Git repository with its metadata.
+type Repository struct {
+ Repo proto.Repository
+ Info proto.Metadata
}
-// GitRepoSource is an interface for Git repository factory.
-type GitRepoSource interface {
- GetRepo(string) (GitRepo, error)
- AllRepos() []GitRepo
+// Readme returns the repository's README.
+func (r *Repository) Readme() (readme string, path string) {
+ readme, path, _ = r.LatestFile("README*")
+ return
+}
+
+// LatestFile returns the contents of the latest file at the specified path in
+// the repository and its file path.
+func (r *Repository) LatestFile(pattern string) (string, string, error) {
+ return proto.LatestFile(r.Repo, pattern)
}
// RepoURL returns the URL of the repository.
@@ -3,6 +3,7 @@ package repo
import (
"errors"
"fmt"
+ "log"
"path/filepath"
"github.com/alecthomas/chroma/lexers"
@@ -51,7 +52,7 @@ type Files struct {
selector *selector.Selector
ref *ggit.Reference
activeView filesView
- repo git.GitRepo
+ repo *git.Repository
code *code.Code
path string
currentItem *FileItem
@@ -200,8 +201,7 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case RepoMsg:
- f.repo = git.GitRepo(msg)
- cmds = append(cmds, f.Init())
+ f.repo = msg
case RefMsg:
f.ref = msg
cmds = append(cmds, f.Init())
@@ -320,14 +320,17 @@ func (f *Files) updateFilesCmd() tea.Msg {
files := make([]selector.IdentifiableItem, 0)
dirs := make([]selector.IdentifiableItem, 0)
if f.ref == nil {
+ log.Printf("ui: files: ref is nil")
return common.ErrorMsg(errNoRef)
}
- t, err := f.repo.Tree(f.ref, f.path)
+ t, err := f.repo.Repo.Repository().TreePath(f.ref, f.path)
if err != nil {
+ log.Printf("ui: files: error getting tree %v", err)
return common.ErrorMsg(err)
}
ents, err := t.Entries()
if err != nil {
+ log.Printf("ui: files: error listing files %v", err)
return common.ErrorMsg(err)
}
ents.Sort()
@@ -347,6 +350,7 @@ func (f *Files) selectTreeCmd() tea.Msg {
f.selector.Select(0)
return f.updateFilesCmd()
}
+ log.Printf("ui: files: current item is not a tree")
return common.ErrorMsg(errNoFileSelected)
}
@@ -355,25 +359,30 @@ func (f *Files) selectFileCmd() tea.Msg {
if i != nil && !i.entry.IsTree() {
fi := i.entry.File()
if i.Mode().IsDir() || f == nil {
+ log.Printf("ui: files: current item is not a file")
return common.ErrorMsg(errInvalidFile)
}
bin, err := fi.IsBinary()
if err != nil {
f.path = filepath.Dir(f.path)
+ log.Printf("ui: files: error checking if file is binary %v", err)
return common.ErrorMsg(err)
}
if bin {
f.path = filepath.Dir(f.path)
+ log.Printf("ui: files: file is binary")
return common.ErrorMsg(errBinaryFile)
}
c, err := fi.Bytes()
if err != nil {
f.path = filepath.Dir(f.path)
+ log.Printf("ui: files: error reading file %v", err)
return common.ErrorMsg(err)
}
f.lastSelected = append(f.lastSelected, f.selector.Index())
return FileContentMsg{string(c), i.entry.Name()}
}
+ log.Printf("ui: files: current item is not a file")
return common.ErrorMsg(errNoFileSelected)
}
@@ -2,6 +2,7 @@ package repo
import (
"fmt"
+ "log"
"strings"
"time"
@@ -47,7 +48,7 @@ type Log struct {
selector *selector.Selector
vp *viewport.Viewport
activeView logView
- repo git.GitRepo
+ repo *git.Repository
ref *ggit.Reference
count int64
nextPage int
@@ -77,9 +78,8 @@ func NewLog(common common.Common) *Log {
selector.KeyMap.NextPage = common.KeyMap.NextPage
selector.KeyMap.PrevPage = common.KeyMap.PrevPage
l.selector = selector
- s := spinner.New()
- s.Spinner = spinner.Dot
- s.Style = common.Styles.Spinner
+ s := spinner.New(spinner.WithSpinner(spinner.Dot),
+ spinner.WithStyle(common.Styles.Spinner))
l.spinner = s
return l
}
@@ -189,8 +189,7 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case RepoMsg:
- l.repo = git.GitRepo(msg)
- cmds = append(cmds, l.Init())
+ l.repo = msg
case RefMsg:
l.ref = msg
cmds = append(cmds, l.Init())
@@ -245,6 +244,7 @@ func (l *Log) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if l.activeView == logViewDiff {
l.activeView = logViewCommits
l.selectedCommit = nil
+ cmds = append(cmds, updateStatusBarCmd)
}
case selector.ActiveMsg:
switch sel := msg.IdentifiableItem.(type) {
@@ -326,7 +326,9 @@ func (l *Log) View() string {
msg += "s"
}
msg += "β¦"
- return msg
+ return l.common.Styles.SpinnerContainer.Copy().
+ Height(l.common.Height).
+ Render(msg)
}
switch l.activeView {
case logViewCommits:
@@ -374,10 +376,12 @@ func (l *Log) StatusBarInfo() string {
func (l *Log) countCommitsCmd() tea.Msg {
if l.ref == nil {
+ log.Printf("ui: log: ref is nil")
return common.ErrorMsg(errNoRef)
}
- count, err := l.repo.CountCommits(l.ref)
+ count, err := l.repo.Repo.Repository().CountCommits(l.ref)
if err != nil {
+ log.Printf("ui: error counting commits: %v", err)
return common.ErrorMsg(err)
}
return LogCountMsg(count)
@@ -394,6 +398,7 @@ func (l *Log) updateCommitsCmd() tea.Msg {
}
}
if l.ref == nil {
+ log.Printf("ui: log: ref is nil")
return common.ErrorMsg(errNoRef)
}
items := make([]selector.IdentifiableItem, count)
@@ -401,8 +406,9 @@ func (l *Log) updateCommitsCmd() tea.Msg {
limit := l.selector.PerPage()
skip := page * limit
// CommitsByPage pages start at 1
- cc, err := l.repo.CommitsByPage(l.ref, page+1, limit)
+ cc, err := l.repo.Repo.Repository().CommitsByPage(l.ref, page+1, limit)
if err != nil {
+ log.Printf("ui: error loading commits: %v", err)
return common.ErrorMsg(err)
}
for i, c := range cc {
@@ -422,8 +428,9 @@ func (l *Log) selectCommitCmd(commit *ggit.Commit) tea.Cmd {
}
func (l *Log) loadDiffCmd() tea.Msg {
- diff, err := l.repo.Diff(l.selectedCommit)
+ diff, err := l.repo.Repo.Repository().Diff(l.selectedCommit)
if err != nil {
+ log.Printf("ui: error loading diff: %v", err)
return common.ErrorMsg(err)
}
return LogDiffMsg(diff)
@@ -26,6 +26,7 @@ func (i LogItem) ID() string {
return i.Hash()
}
+// Hash returns the commit hash.
func (i LogItem) Hash() string {
return i.Commit.ID.String()
}
@@ -10,14 +10,17 @@ import (
"github.com/charmbracelet/soft-serve/ui/git"
)
-type ReadmeMsg struct{}
+// ReadmeMsg is a message sent when the readme is loaded.
+type ReadmeMsg struct {
+ Msg tea.Msg
+}
// Readme is the readme component page.
type Readme struct {
common common.Common
code *code.Code
ref RefMsg
- repo git.GitRepo
+ repo *git.Repository
}
// NewReadme creates a new readme model.
@@ -64,15 +67,7 @@ func (r *Readme) FullHelp() [][]key.Binding {
// Init implements tea.Model.
func (r *Readme) Init() tea.Cmd {
- if r.repo == nil {
- return common.ErrorCmd(git.ErrMissingRepo)
- }
- rm, rp := r.repo.Readme()
- r.code.GotoTop()
- return tea.Batch(
- r.code.SetContent(rm, rp),
- r.updateReadmeCmd,
- )
+ return r.updateReadmeCmd
}
// Update implements tea.Model.
@@ -80,8 +75,7 @@ func (r *Readme) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case RepoMsg:
- r.repo = git.GitRepo(msg)
- cmds = append(cmds, r.Init())
+ r.repo = msg
case RefMsg:
r.ref = msg
cmds = append(cmds, r.Init())
@@ -110,5 +104,15 @@ func (r *Readme) StatusBarInfo() string {
}
func (r *Readme) updateReadmeCmd() tea.Msg {
- return ReadmeMsg{}
+ m := ReadmeMsg{}
+ if r.repo == nil {
+ return common.ErrorCmd(git.ErrMissingRepo)
+ }
+ rm, rp := r.repo.Readme()
+ r.code.GotoTop()
+ cmd := r.code.SetContent(rm, rp)
+ if cmd != nil {
+ m.Msg = cmd()
+ }
+ return m
}
@@ -3,6 +3,7 @@ package repo
import (
"errors"
"fmt"
+ "log"
"sort"
"strings"
@@ -19,6 +20,9 @@ var (
errNoRef = errors.New("no reference specified")
)
+// RefMsg is a message that contains a git.Reference.
+type RefMsg *ggit.Reference
+
// RefItemsMsg is a message that contains a list of RefItem.
type RefItemsMsg struct {
prefix string
@@ -29,7 +33,7 @@ type RefItemsMsg struct {
type Refs struct {
common common.Common
selector *selector.Selector
- repo git.GitRepo
+ repo *git.Repository
ref *ggit.Reference
activeRef *ggit.Reference
refPrefix string
@@ -104,8 +108,7 @@ func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case RepoMsg:
r.selector.Select(0)
- r.repo = git.GitRepo(msg)
- cmds = append(cmds, r.Init())
+ r.repo = msg
case RefMsg:
r.ref = msg
cmds = append(cmds, r.Init())
@@ -169,8 +172,9 @@ func (r *Refs) StatusBarInfo() string {
func (r *Refs) updateItemsCmd() tea.Msg {
its := make(RefItems, 0)
- refs, err := r.repo.References()
+ refs, err := r.repo.Repo.Repository().References()
if err != nil {
+ log.Printf("ui: error getting references: %v", err)
return common.ErrorMsg(err)
}
for _, ref := range refs {
@@ -194,3 +198,16 @@ func switchRefCmd(ref *ggit.Reference) tea.Cmd {
return RefMsg(ref)
}
}
+
+// UpdateRefCmd gets the repository's HEAD reference and sends a RefMsg.
+func UpdateRefCmd(repo *git.Repository) tea.Cmd {
+ return func() tea.Msg {
+ ref, err := repo.Repo.Repository().HEAD()
+ if err != nil {
+ log.Printf("ui: error getting HEAD reference: %v", err)
+ return common.ErrorMsg(err)
+ }
+ log.Printf("HEAD: %s", ref.Name())
+ return RefMsg(ref)
+ }
+}
@@ -9,7 +9,6 @@ import (
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/config"
ggit "github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/components/footer"
@@ -56,10 +55,7 @@ type ResetURLMsg struct{}
type UpdateStatusBarMsg struct{}
// RepoMsg is a message that contains a git.Repository.
-type RepoMsg git.GitRepo
-
-// RefMsg is a message that contains a git.Reference.
-type RefMsg *ggit.Reference
+type RepoMsg *git.Repository
// BackMsg is a message to go back to the previous view.
type BackMsg struct{}
@@ -67,18 +63,20 @@ type BackMsg struct{}
// Repo is a view for a git repository.
type Repo struct {
common common.Common
- cfg *config.Config
- selectedRepo git.GitRepo
+ selectedRepo *git.Repository
activeTab tab
tabs *tabs.Tabs
statusbar *statusbar.StatusBar
panes []common.Component
ref *ggit.Reference
copyURL time.Time
+ state state
+ spinner spinner.Model
+ panesReady [lastTab]bool
}
// New returns a new Repo.
-func New(cfg *config.Config, c common.Common) *Repo {
+func New(c common.Common) *Repo {
sb := statusbar.New(c)
ts := make([]string, lastTab)
// Tabs must match the order of tab constants above.
@@ -99,12 +97,15 @@ func New(cfg *config.Config, c common.Common) *Repo {
branches,
tags,
}
+ s := spinner.New(spinner.WithSpinner(spinner.Dot),
+ spinner.WithStyle(c.Styles.Spinner))
r := &Repo{
- cfg: cfg,
common: c,
tabs: tb,
statusbar: sb,
panes: panes,
+ state: loadingState,
+ spinner: s,
}
return r
}
@@ -162,16 +163,20 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case RepoMsg:
+ // Set the state to loading when we get a new repository.
+ r.state = loadingState
+ r.panesReady = [lastTab]bool{}
r.activeTab = 0
- r.selectedRepo = git.GitRepo(msg)
+ r.selectedRepo = msg
cmds = append(cmds,
r.tabs.Init(),
- r.updateRefCmd,
+ // This will set the selected repo in each pane's model.
r.updateModels(msg),
)
case RefMsg:
r.ref = msg
for _, p := range r.panes {
+ // Init will initiate each pane's model with its contents.
cmds = append(cmds, p.Init())
}
cmds = append(cmds,
@@ -200,7 +205,7 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
if r.selectedRepo != nil {
cmds = append(cmds, r.updateStatusBarCmd)
- urlID := fmt.Sprintf("%s-url", r.selectedRepo.Repo())
+ urlID := fmt.Sprintf("%s-url", r.selectedRepo.Info.Name())
if msg, ok := msg.(tea.MouseMsg); ok && r.common.Zone.Get(urlID).InBounds(msg) {
cmds = append(cmds, r.copyURLCmd())
}
@@ -221,41 +226,32 @@ func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
case CopyURLMsg:
- r.common.Copy.Copy(
- git.RepoURL(r.cfg.Host, r.cfg.Port, r.selectedRepo.Repo()),
- )
+ if cfg := r.common.Config(); cfg != nil {
+ host := cfg.Host
+ port := cfg.SSH.Port
+ r.common.Copy.Copy(
+ git.RepoURL(host, port, r.selectedRepo.Info.Name()),
+ )
+ }
case ResetURLMsg:
r.copyURL = time.Time{}
- case ReadmeMsg:
- case FileItemsMsg:
- f, cmd := r.panes[filesTab].Update(msg)
- r.panes[filesTab] = f.(*Files)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- // The Log bubble is the only bubble that uses a spinner, so this is fine
- // for now. We need to pass the TickMsg to the Log bubble when the Log is
- // loading but not the current selected tab so that the spinner works.
- case LogCountMsg, LogItemsMsg, spinner.TickMsg:
- l, cmd := r.panes[commitsTab].Update(msg)
- r.panes[commitsTab] = l.(*Log)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- case RefItemsMsg:
- switch msg.prefix {
- case ggit.RefsHeads:
- b, cmd := r.panes[branchesTab].Update(msg)
- r.panes[branchesTab] = b.(*Refs)
- if cmd != nil {
- cmds = append(cmds, cmd)
- }
- case ggit.RefsTags:
- t, cmd := r.panes[tagsTab].Update(msg)
- r.panes[tagsTab] = t.(*Refs)
- if cmd != nil {
- cmds = append(cmds, cmd)
+ case ReadmeMsg, FileItemsMsg, LogCountMsg, LogItemsMsg, RefItemsMsg:
+ cmds = append(cmds, r.updateRepo(msg))
+ // We have two spinners, one is used to when loading the repository and the
+ // other is used when loading the log.
+ // Check if the spinner ID matches the spinner model.
+ case spinner.TickMsg:
+ switch msg.ID {
+ case r.spinner.ID():
+ if r.state == loadingState {
+ s, cmd := r.spinner.Update(msg)
+ r.spinner = s
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
}
+ default:
+ cmds = append(cmds, r.updateRepo(msg))
}
case UpdateStatusBarMsg:
cmds = append(cmds, r.updateStatusBarCmd)
@@ -289,15 +285,24 @@ func (r *Repo) View() string {
r.common.Styles.Tabs.GetVerticalFrameSize()
mainStyle := repoBodyStyle.
Height(r.common.Height - hm)
- main := r.common.Zone.Mark(
+ var main string
+ var statusbar string
+ switch r.state {
+ case loadingState:
+ main = fmt.Sprintf("%s loadingβ¦", r.spinner.View())
+ case loadedState:
+ main = r.panes[r.activeTab].View()
+ statusbar = r.statusbar.View()
+ }
+ main = r.common.Zone.Mark(
"repo-main",
- mainStyle.Render(r.panes[r.activeTab].View()),
+ mainStyle.Render(main),
)
view := lipgloss.JoinVertical(lipgloss.Top,
r.headerView(),
r.tabs.View(),
main,
- r.statusbar.View(),
+ statusbar,
)
return s.Render(view)
}
@@ -306,10 +311,9 @@ func (r *Repo) headerView() string {
if r.selectedRepo == nil {
return ""
}
- cfg := r.cfg
truncate := lipgloss.NewStyle().MaxWidth(r.common.Width)
- name := r.common.Styles.Repo.HeaderName.Render(r.selectedRepo.Name())
- desc := r.selectedRepo.Description()
+ name := r.common.Styles.Repo.HeaderName.Render(r.selectedRepo.Info.Name())
+ desc := r.selectedRepo.Info.Description()
if desc == "" {
desc = name
name = ""
@@ -319,13 +323,16 @@ func (r *Repo) headerView() string {
urlStyle := r.common.Styles.URLStyle.Copy().
Width(r.common.Width - lipgloss.Width(desc) - 1).
Align(lipgloss.Right)
- url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
+ var url string
+ if cfg := r.common.Config(); cfg != nil {
+ url = git.RepoURL(cfg.Host, cfg.SSH.Port, r.selectedRepo.Info.Name())
+ }
if !r.copyURL.IsZero() && r.copyURL.Add(time.Second).After(time.Now()) {
url = "copied!"
}
url = common.TruncateString(url, r.common.Width-lipgloss.Width(desc)-1)
url = r.common.Zone.Mark(
- fmt.Sprintf("%s-url", r.selectedRepo.Repo()),
+ fmt.Sprintf("%s-url", r.selectedRepo.Info.Name()),
urlStyle.Render(url),
)
style := r.common.Styles.Repo.Header.Copy().Width(r.common.Width)
@@ -351,24 +358,13 @@ func (r *Repo) updateStatusBarCmd() tea.Msg {
ref = r.ref.Name().Short()
}
return statusbar.StatusBarMsg{
- Key: r.selectedRepo.Repo(),
+ Key: r.selectedRepo.Info.Name(),
Value: value,
Info: info,
Branch: fmt.Sprintf("* %s", ref),
}
}
-func (r *Repo) updateRefCmd() tea.Msg {
- if r.selectedRepo == nil {
- return nil
- }
- head, err := r.selectedRepo.HEAD()
- if err != nil {
- return common.ErrorMsg(err)
- }
- return RefMsg(head)
-}
-
func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
cmds := make([]tea.Cmd, 0)
for i, b := range r.panes {
@@ -381,6 +377,67 @@ func (r *Repo) updateModels(msg tea.Msg) tea.Cmd {
return tea.Batch(cmds...)
}
+func (r *Repo) updateRepo(msg tea.Msg) tea.Cmd {
+ cmds := make([]tea.Cmd, 0)
+ switch msg := msg.(type) {
+ case LogCountMsg, LogItemsMsg, spinner.TickMsg:
+ switch msg.(type) {
+ case LogItemsMsg:
+ r.panesReady[commitsTab] = true
+ }
+ l, cmd := r.panes[commitsTab].Update(msg)
+ r.panes[commitsTab] = l.(*Log)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ case FileItemsMsg:
+ r.panesReady[filesTab] = true
+ f, cmd := r.panes[filesTab].Update(msg)
+ r.panes[filesTab] = f.(*Files)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ case RefItemsMsg:
+ switch msg.prefix {
+ case ggit.RefsHeads:
+ r.panesReady[branchesTab] = true
+ b, cmd := r.panes[branchesTab].Update(msg)
+ r.panes[branchesTab] = b.(*Refs)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ case ggit.RefsTags:
+ r.panesReady[tagsTab] = true
+ t, cmd := r.panes[tagsTab].Update(msg)
+ r.panes[tagsTab] = t.(*Refs)
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ }
+ case ReadmeMsg:
+ r.panesReady[readmeTab] = true
+ }
+ if r.isReady() {
+ r.state = loadedState
+ }
+ return tea.Batch(cmds...)
+}
+
+func (r *Repo) isReady() bool {
+ ready := true
+ // We purposely ignore the log pane here because it has its own spinner.
+ for _, b := range []bool{
+ r.panesReady[filesTab], r.panesReady[branchesTab],
+ r.panesReady[tagsTab], r.panesReady[readmeTab],
+ } {
+ if !b {
+ ready = false
+ break
+ }
+ }
+ return ready
+}
+
func (r *Repo) copyURLCmd() tea.Cmd {
r.copyURL = time.Now()
return tea.Batch(
@@ -3,6 +3,8 @@ package selection
import (
"fmt"
"io"
+ "log"
+ "sort"
"strings"
"time"
@@ -10,29 +12,77 @@ import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/soft-serve/proto"
+ "github.com/charmbracelet/soft-serve/server/config"
"github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/git"
"github.com/dustin/go-humanize"
)
+var _ sort.Interface = Items{}
+
+// Items is a list of Item.
+type Items []Item
+
+// Len implements sort.Interface.
+func (it Items) Len() int {
+ return len(it)
+}
+
+// Less implements sort.Interface.
+func (it Items) Less(i int, j int) bool {
+ return it[i].lastUpdate.After(it[j].lastUpdate)
+}
+
+// Swap implements sort.Interface.
+func (it Items) Swap(i int, j int) {
+ it[i], it[j] = it[j], it[i]
+}
+
// Item represents a single item in the selector.
type Item struct {
- repo git.GitRepo
+ repo proto.Repository
+ info proto.Metadata
lastUpdate time.Time
cmd string
copied time.Time
}
+// New creates a new Item.
+func NewItem(info proto.Metadata, cfg *config.Config) (Item, error) {
+ repo, err := info.Open()
+ if err != nil {
+ log.Printf("error opening repo: %v", err)
+ return Item{}, err
+ }
+ lu, err := repo.Repository().LatestCommitTime()
+ if err != nil {
+ log.Printf("error getting latest commit time: %v", err)
+ return Item{}, err
+ }
+ return Item{
+ repo: repo,
+ info: info,
+ lastUpdate: lu,
+ cmd: git.RepoURL(cfg.Host, cfg.SSH.Port, info.Name()),
+ }, nil
+}
+
// ID implements selector.IdentifiableItem.
func (i Item) ID() string {
- return i.repo.Repo()
+ return i.info.Name()
}
// Title returns the item title. Implements list.DefaultItem.
-func (i Item) Title() string { return i.repo.Name() }
+func (i Item) Title() string {
+ if pn := i.info.ProjectName(); pn != "" {
+ return pn
+ }
+ return i.info.Name()
+}
// Description returns the item description. Implements list.DefaultItem.
-func (i Item) Description() string { return i.repo.Description() }
+func (i Item) Description() string { return i.info.Description() }
// FilterValue implements list.Item.
func (i Item) FilterValue() string { return i.Title() }
@@ -101,7 +151,7 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
title := i.Title()
title = common.TruncateString(title, m.Width()-styles.Base.GetHorizontalFrameSize())
- if i.repo.IsPrivate() {
+ if i.info.IsPrivate() {
title += " π"
}
if isSelected {
@@ -2,20 +2,18 @@ package selection
import (
"fmt"
- "strings"
+ "log"
+ "sort"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/config"
"github.com/charmbracelet/soft-serve/proto"
"github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/components/code"
"github.com/charmbracelet/soft-serve/ui/components/selector"
"github.com/charmbracelet/soft-serve/ui/components/tabs"
- "github.com/charmbracelet/soft-serve/ui/git"
- "github.com/gliderlabs/ssh"
)
type pane int
@@ -35,8 +33,6 @@ func (p pane) String() string {
// Selection is the model for the selection screen/page.
type Selection struct {
- cfg *config.Config
- pk ssh.PublicKey
common common.Common
readme *code.Code
readmeHeight int
@@ -46,7 +42,7 @@ type Selection struct {
}
// New creates a new selection model.
-func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection {
+func New(common common.Common) *Selection {
ts := make([]string, lastPane)
for i, b := range []pane{selectorPane, readmePane} {
ts[i] = b.String()
@@ -58,8 +54,6 @@ func New(cfg *config.Config, pk ssh.PublicKey, common common.Common) *Selection
t.TabDot = common.Styles.TopLevelActiveTabDot.Copy()
t.UseDot = true
sel := &Selection{
- cfg: cfg,
- pk: pk,
common: common,
activePane: selectorPane, // start with the selector focused
tabs: t,
@@ -184,59 +178,34 @@ func (s *Selection) FullHelp() [][]key.Binding {
// Init implements tea.Model.
func (s *Selection) Init() tea.Cmd {
var readmeCmd tea.Cmd
- items := make([]selector.IdentifiableItem, 0)
- cfg := s.cfg
- pk := s.pk
+ cfg := s.common.Config()
+ pk := s.common.PublicKey()
+ if cfg == nil || pk == nil {
+ return nil
+ }
+ repos, err := cfg.ListRepos()
+ if err != nil {
+ return common.ErrorCmd(err)
+ }
+ sortedItems := make(Items, 0)
// Put configured repos first
- for _, r := range cfg.Repos {
- acc := cfg.AuthRepo(r.Repo, pk)
- if r.Private && acc < proto.ReadOnlyAccess {
+ for _, r := range repos {
+ log.Printf("adding configured repo %s", r.Name())
+ acc := cfg.AuthRepo(r.Name(), pk)
+ if r.IsPrivate() && acc < proto.ReadOnlyAccess {
continue
}
- repo, err := cfg.Source.GetRepo(r.Repo)
+ item, err := NewItem(r, cfg)
if err != nil {
+ log.Printf("ui: failed to create item for %s: %v", r.Name(), err)
continue
}
- items = append(items, Item{
- repo: repo,
- cmd: git.RepoURL(cfg.Host, cfg.Port, r.Repo),
- })
+ sortedItems = append(sortedItems, item)
}
- for _, r := range cfg.Source.AllRepos() {
- if r.Repo() == "config" {
- rm, rp := r.Readme()
- s.readmeHeight = strings.Count(rm, "\n")
- readmeCmd = s.readme.SetContent(rm, rp)
- }
- acc := cfg.AuthRepo(r.Repo(), pk)
- if r.IsPrivate() && acc < proto.ReadOnlyAccess {
- continue
- }
- exists := false
- lc, err := r.Commit("HEAD")
- if err != nil {
- return common.ErrorCmd(err)
- }
- lastUpdate := lc.Committer.When
- if lastUpdate.IsZero() {
- lastUpdate = lc.Author.When
- }
- for i, item := range items {
- item := item.(Item)
- if item.repo.Repo() == r.Repo() {
- exists = true
- item.lastUpdate = lastUpdate
- items[i] = item
- break
- }
- }
- if !exists {
- items = append(items, Item{
- repo: r,
- lastUpdate: lastUpdate,
- cmd: git.RepoURL(cfg.Host, cfg.Port, r.Name()),
- })
- }
+ sort.Sort(sortedItems)
+ items := make([]selector.IdentifiableItem, len(sortedItems))
+ for i, it := range sortedItems {
+ items[i] = it
}
return tea.Batch(
s.selector.Init(),
@@ -122,7 +122,8 @@ type Styles struct {
NoItems lipgloss.Style
}
- Spinner lipgloss.Style
+ Spinner lipgloss.Style
+ SpinnerContainer lipgloss.Style
CodeNoContent lipgloss.Style
@@ -409,6 +410,8 @@ func DefaultStyles() *Styles {
MarginLeft(2).
Foreground(lipgloss.Color("205"))
+ s.SpinnerContainer = lipgloss.NewStyle()
+
s.CodeNoContent = lipgloss.NewStyle().
SetString("No Content.").
MarginTop(1).
@@ -1,11 +1,13 @@
package ui
import (
+ "errors"
+ "log"
+
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/config"
"github.com/charmbracelet/soft-serve/ui/common"
"github.com/charmbracelet/soft-serve/ui/components/footer"
"github.com/charmbracelet/soft-serve/ui/components/header"
@@ -13,7 +15,6 @@ import (
"github.com/charmbracelet/soft-serve/ui/git"
"github.com/charmbracelet/soft-serve/ui/pages/repo"
"github.com/charmbracelet/soft-serve/ui/pages/selection"
- "github.com/gliderlabs/ssh"
)
type page int
@@ -33,9 +34,7 @@ const (
// UI is the main UI model.
type UI struct {
- cfg *config.Config
- session ssh.Session
- rs git.GitRepoSource
+ serverName string
initialRepo string
common common.Common
pages []common.Component
@@ -48,13 +47,14 @@ type UI struct {
}
// New returns a new UI model.
-func New(cfg *config.Config, s ssh.Session, c common.Common, initialRepo string) *UI {
- src := &source{cfg.Source}
- h := header.New(c, cfg.Name)
+func New(c common.Common, initialRepo string) *UI {
+ var serverName string
+ if cfg := c.Config(); cfg != nil {
+ serverName = cfg.ServerName
+ }
+ h := header.New(c, serverName)
ui := &UI{
- cfg: cfg,
- session: s,
- rs: src,
+ serverName: serverName,
common: c,
pages: make([]common.Component, 2), // selection & repo
activePage: selectionPage,
@@ -136,15 +136,8 @@ func (ui *UI) SetSize(width, height int) {
// Init implements tea.Model.
func (ui *UI) Init() tea.Cmd {
- ui.pages[selectionPage] = selection.New(
- ui.cfg,
- ui.session.PublicKey(),
- ui.common,
- )
- ui.pages[repoPage] = repo.New(
- ui.cfg,
- ui.common,
- )
+ ui.pages[selectionPage] = selection.New(ui.common)
+ ui.pages[repoPage] = repo.New(ui.common)
ui.SetSize(ui.common.Width, ui.common.Height)
cmds := make([]tea.Cmd, 0)
cmds = append(cmds,
@@ -171,6 +164,7 @@ func (ui *UI) IsFiltering() bool {
// Update implements tea.Model.
func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ log.Printf("msg received: %T", msg)
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
@@ -220,9 +214,11 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
ui.showFooter = !ui.showFooter
}
case repo.RepoMsg:
+ ui.common.SetValue(common.RepoKey, msg)
ui.activePage = repoPage
// Show the footer on repo page if show all is set.
ui.showFooter = ui.footer.ShowAll()
+ cmds = append(cmds, repo.UpdateRefCmd(msg))
case common.ErrorMsg:
ui.error = msg
ui.state = errorState
@@ -292,24 +288,48 @@ func (ui *UI) View() string {
)
}
+func (ui *UI) openRepo(rn string) (*git.Repository, error) {
+ cfg := ui.common.Config()
+ if cfg == nil {
+ return nil, errors.New("config is nil")
+ }
+ repos, err := cfg.ListRepos()
+ if err != nil {
+ log.Printf("ui: failed to list repos: %v", err)
+ return nil, err
+ }
+ for _, r := range repos {
+ if r.Name() == rn {
+ re, err := cfg.Open(rn)
+ if err != nil {
+ log.Printf("ui: failed to open repo: %v", err)
+ return nil, err
+ }
+ return &git.Repository{
+ Info: r,
+ Repo: re,
+ }, nil
+ }
+ }
+ return nil, git.ErrMissingRepo
+}
+
func (ui *UI) setRepoCmd(rn string) tea.Cmd {
return func() tea.Msg {
- for _, r := range ui.rs.AllRepos() {
- if r.Repo() == rn {
- return repo.RepoMsg(r)
- }
+ r, err := ui.openRepo(rn)
+ if err != nil {
+ return common.ErrorMsg(err)
}
- return common.ErrorMsg(git.ErrMissingRepo)
+ return repo.RepoMsg(r)
}
}
func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
return func() tea.Msg {
- for _, r := range ui.rs.AllRepos() {
- if r.Repo() == rn {
- return repo.RepoMsg(r)
- }
+ r, err := ui.openRepo(rn)
+ if err != nil {
+ return nil
}
- return nil
+ return repo.RepoMsg(r)
}
}