fix: initial repo

Ayman Bagabas created

Change summary

config/config.go                |  2 +
config/git.go                   | 33 ++++++++++++----
server/git.go                   |  2 
server/session.go               |  5 ++
ui/git/git.go                   | 15 +++++++
ui/pages/repo/repo.go           | 40 ++++++--------------
ui/pages/selection/item.go      | 17 ++++----
ui/pages/selection/selection.go | 34 ++++++----------
ui/session/session.go           |  2 +
ui/ui.go                        | 68 +++++++++++++++++++---------------
10 files changed, 119 insertions(+), 99 deletions(-)

Detailed changes

config/config.go 🔗

@@ -152,6 +152,8 @@ func (cfg *Config) Reload() error {
 		for _, rr := range cfg.Repos {
 			if name == rr.Repo {
 				rp = rr.Readme
+				r.name = rr.Name
+				r.description = rr.Note
 				break
 			}
 		}

config/git.go 🔗

@@ -17,13 +17,15 @@ var ErrMissingRepo = errors.New("missing repo")
 
 // Repo represents a Git repository.
 type Repo struct {
-	path       string
-	repository *git.Repository
-	readme     string
-	readmePath string
-	head       *git.Reference
-	refs       []*git.Reference
-	patchCache *lru.Cache
+	name        string
+	description string
+	path        string
+	repository  *git.Repository
+	readme      string
+	readmePath  string
+	head        *git.Reference
+	refs        []*git.Reference
+	patchCache  *lru.Cache
 }
 
 // open opens a Git repository.
@@ -53,11 +55,24 @@ func (r *Repo) Path() string {
 	return r.path
 }
 
-// GetName returns the name of the repository.
-func (r *Repo) Name() string {
+// Repo returns the repository directory name.
+func (r *Repo) Repo() string {
 	return filepath.Base(r.path)
 }
 
+// Name returns the name of the repository.
+func (r *Repo) Name() string {
+	if r.name == "" {
+		return r.Repo()
+	}
+	return r.name
+}
+
+// Description returns the description for a repository.
+func (r *Repo) Description() string {
+	return r.description
+}
+
 // Readme returns the readme and its path for the repository.
 func (r *Repo) Readme() (readme string, path string) {
 	return r.readme, r.readmePath

ui/git.go → server/git.go 🔗

@@ -1,4 +1,4 @@
-package ui
+package server
 
 import (
 	"github.com/charmbracelet/soft-serve/config"

server/session.go 🔗

@@ -7,6 +7,7 @@ import (
 	appCfg "github.com/charmbracelet/soft-serve/config"
 	"github.com/charmbracelet/soft-serve/ui"
 	"github.com/charmbracelet/soft-serve/ui/common"
+	"github.com/charmbracelet/soft-serve/ui/git"
 	"github.com/charmbracelet/soft-serve/ui/keymap"
 	"github.com/charmbracelet/soft-serve/ui/styles"
 	bm "github.com/charmbracelet/wish/bubbletea"
@@ -36,6 +37,10 @@ func (s *Session) Session() ssh.Session {
 	return s.session
 }
 
+func (s *Session) Source() git.GitRepoSource {
+	return &source{s.Cfg.Source}
+}
+
 func SessionHandler(ac *appCfg.Config) bm.ProgramHandler {
 	return func(s ssh.Session) *tea.Program {
 		pty, _, active := s.Pty()

ui/git/git.go 🔗

@@ -2,14 +2,19 @@ package git
 
 import (
 	"errors"
+	"fmt"
 
 	"github.com/charmbracelet/soft-serve/git"
 )
 
+// 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)
 	CommitsByPage(*git.Reference, int, int) (git.Commits, error)
@@ -19,7 +24,17 @@ type GitRepo interface {
 	Tree(*git.Reference, string) (*git.Tree, error)
 }
 
+// GitRepoSource is an interface for Git repository factory.
 type GitRepoSource interface {
 	GetRepo(string) (GitRepo, error)
 	AllRepos() []GitRepo
 }
+
+// RepoURL returns the URL of the repository.
+func RepoURL(host string, port int, name string) string {
+	p := ""
+	if port != 22 {
+		p += fmt.Sprintf(":%d", port)
+	}
+	return fmt.Sprintf("git clone ssh://%s/%s", host+p, name)
+}

ui/pages/repo/repo.go 🔗

@@ -10,11 +10,10 @@ import (
 	ggit "github.com/charmbracelet/soft-serve/git"
 	"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/statusbar"
 	"github.com/charmbracelet/soft-serve/ui/components/tabs"
 	"github.com/charmbracelet/soft-serve/ui/git"
-	"github.com/charmbracelet/soft-serve/ui/pages/selection"
+	"github.com/charmbracelet/soft-serve/ui/session"
 )
 
 type tab int
@@ -38,10 +37,10 @@ type RefMsg *ggit.Reference
 
 // Repo is a view for a git repository.
 type Repo struct {
+	s            session.Session
 	common       common.Common
 	rs           git.GitRepoSource
 	selectedRepo git.GitRepo
-	selectedItem selection.Item
 	activeTab    tab
 	tabs         *tabs.Tabs
 	statusbar    *statusbar.StatusBar
@@ -50,7 +49,7 @@ type Repo struct {
 }
 
 // New returns a new Repo.
-func New(c common.Common, rs git.GitRepoSource) *Repo {
+func New(s session.Session, c common.Common) *Repo {
 	sb := statusbar.New(c)
 	// Tabs must match the order of tab constants above.
 	tb := tabs.New(c, []string{"Readme", "Files", "Commits", "Branches", "Tags"})
@@ -69,8 +68,9 @@ func New(c common.Common, rs git.GitRepoSource) *Repo {
 		tags,
 	}
 	r := &Repo{
+		s:         s,
 		common:    c,
-		rs:        rs,
+		rs:        s.Source(),
 		tabs:      tb,
 		statusbar: sb,
 		boxes:     boxes,
@@ -146,23 +146,16 @@ func (r *Repo) FullHelp() [][]key.Binding {
 
 // Init implements tea.View.
 func (r *Repo) Init() tea.Cmd {
-	cmds := make([]tea.Cmd, 0)
-	cmds = append(cmds,
+	return tea.Batch(
 		r.tabs.Init(),
 		r.statusbar.Init(),
 	)
-	return tea.Batch(cmds...)
 }
 
 // Update implements tea.Model.
 func (r *Repo) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	cmds := make([]tea.Cmd, 0)
 	switch msg := msg.(type) {
-	case selector.SelectMsg:
-		switch msg.IdentifiableItem.(type) {
-		case selection.Item:
-			r.selectedItem = msg.IdentifiableItem.(selection.Item)
-		}
 	case RepoMsg:
 		r.activeTab = 0
 		r.selectedRepo = git.GitRepo(msg)
@@ -281,14 +274,16 @@ func (r *Repo) headerView() string {
 	if r.selectedRepo == nil {
 		return ""
 	}
-	name := r.common.Styles.RepoHeaderName.Render(r.selectedItem.Title())
+	cfg := r.s.Config()
+	name := r.common.Styles.RepoHeaderName.Render(r.selectedRepo.Name())
+	url := git.RepoURL(cfg.Host, cfg.Port, r.selectedRepo.Repo())
 	// TODO move this into a style.
-	url := lipgloss.NewStyle().
+	url = lipgloss.NewStyle().
 		MarginLeft(1).
 		Width(r.common.Width - lipgloss.Width(name) - 1).
 		Align(lipgloss.Right).
-		Render(r.selectedItem.URL())
-	desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedItem.Description())
+		Render(url)
+	desc := r.common.Styles.RepoHeaderDesc.Render(r.selectedRepo.Description())
 	style := r.common.Styles.RepoHeader.Copy().Width(r.common.Width)
 	return style.Render(
 		lipgloss.JoinVertical(lipgloss.Top,
@@ -301,17 +296,6 @@ func (r *Repo) headerView() string {
 	)
 }
 
-func (r *Repo) setRepoCmd(repo string) tea.Cmd {
-	return func() tea.Msg {
-		for _, r := range r.rs.AllRepos() {
-			if r.Name() == repo {
-				return RepoMsg(r)
-			}
-		}
-		return common.ErrorMsg(git.ErrMissingRepo)
-	}
-}
-
 func (r *Repo) updateStatusBarCmd() tea.Msg {
 	var info, value string
 	switch r.activeTab {

ui/pages/selection/item.go 🔗

@@ -10,32 +10,31 @@ import (
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/soft-serve/ui/components/yankable"
+	"github.com/charmbracelet/soft-serve/ui/git"
 	"github.com/charmbracelet/soft-serve/ui/styles"
 	"github.com/dustin/go-humanize"
 )
 
 // Item represents a single item in the selector.
 type Item struct {
-	name       string
-	repo       string
-	desc       string
+	repo       git.GitRepo
 	lastUpdate time.Time
 	url        *yankable.Yankable
 }
 
 // ID implements selector.IdentifiableItem.
 func (i Item) ID() string {
-	return i.repo
+	return i.repo.Repo()
 }
 
 // Title returns the item title. Implements list.DefaultItem.
-func (i Item) Title() string { return i.name }
+func (i Item) Title() string { return i.repo.Name() }
 
 // Description returns the item description. Implements list.DefaultItem.
-func (i Item) Description() string { return i.desc }
+func (i Item) Description() string { return i.repo.Description() }
 
 // FilterValue implements list.Item.
-func (i Item) FilterValue() string { return i.name }
+func (i Item) FilterValue() string { return i.Title() }
 
 // URL returns the item URL view.
 func (i Item) URL() string {
@@ -119,7 +118,7 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 		}
 	}
 
-	title := i.name
+	title := i.Title()
 	updatedStr := fmt.Sprintf(" Updated %s", humanize.Time(i.lastUpdate))
 	updated := d.styles.MenuLastUpdate.
 		Copy().
@@ -143,7 +142,7 @@ func (d ItemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 
 	s.WriteString(lipgloss.JoinHorizontal(lipgloss.Bottom, title, updated))
 	s.WriteString("\n")
-	s.WriteString(i.desc)
+	s.WriteString(i.Description())
 	s.WriteString("\n\n")
 	s.WriteString(i.url.View())
 	w.Write([]byte(itemStyle.Render(s.String())))

ui/pages/selection/selection.go 🔗

@@ -1,18 +1,17 @@
 package selection
 
 import (
-	"errors"
 	"fmt"
 	"strings"
 
 	"github.com/charmbracelet/bubbles/key"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/charmbracelet/lipgloss"
-	appCfg "github.com/charmbracelet/soft-serve/config"
 	"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/yankable"
+	"github.com/charmbracelet/soft-serve/ui/git"
 	"github.com/charmbracelet/soft-serve/ui/session"
 )
 
@@ -165,11 +164,13 @@ func (s *Selection) Init() tea.Cmd {
 	}
 	// Put configured repos first
 	for _, r := range cfg.Repos {
+		repo, err := cfg.Source.GetRepo(r.Repo)
+		if err != nil {
+			continue
+		}
 		items = append(items, Item{
-			name: r.Name,
-			repo: r.Repo,
-			desc: r.Note,
-			url:  yank(repoUrl(cfg, r.Repo)),
+			repo: repo,
+			url:  yank(git.RepoURL(cfg.Host, cfg.Port, r.Repo)),
 		})
 	}
 	for _, r := range cfg.Source.AllRepos() {
@@ -185,7 +186,7 @@ func (s *Selection) Init() tea.Cmd {
 		lastUpdate := lc[0].Committer.When
 		for _, item := range items {
 			item := item.(Item)
-			if item.repo == r.Name() {
+			if item.repo.Repo() == r.Repo() {
 				exists = true
 				item.lastUpdate = lastUpdate
 				break
@@ -193,11 +194,9 @@ func (s *Selection) Init() tea.Cmd {
 		}
 		if !exists {
 			items = append(items, Item{
-				name:       r.Name(),
-				repo:       r.Name(),
-				desc:       "",
+				repo:       r,
 				lastUpdate: lastUpdate,
-				url:        yank(repoUrl(cfg, r.Name())),
+				url:        yank(git.RepoURL(cfg.Host, cfg.Port, r.Name())),
 			})
 		}
 	}
@@ -230,8 +229,9 @@ func (s *Selection) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		switch {
 		case key.Matches(msg, s.common.KeyMap.Section):
 			s.activeBox = (s.activeBox + 1) % 2
-		case msg.String() == "a":
-			cmds = append(cmds, common.ErrorCmd(errors.New("not implemented")))
+		case key.Matches(msg, s.common.KeyMap.Back):
+			s.selector.Select(0)
+			cmds = append(cmds, s.selector.Init())
 		}
 	}
 	switch s.activeBox {
@@ -280,11 +280,3 @@ func (s *Selection) changeActive(msg selector.ActiveMsg) tea.Cmd {
 	rm, rp := r.Readme()
 	return s.readme.SetContent(rm, rp)
 }
-
-func repoUrl(cfg *appCfg.Config, name string) string {
-	port := ""
-	if cfg.Port != 22 {
-		port += fmt.Sprintf(":%d", cfg.Port)
-	}
-	return fmt.Sprintf("git clone ssh://%s/%s", cfg.Host+port, name)
-}

ui/session/session.go 🔗

@@ -3,6 +3,7 @@ package session
 import (
 	tea "github.com/charmbracelet/bubbletea"
 	appCfg "github.com/charmbracelet/soft-serve/config"
+	"github.com/charmbracelet/soft-serve/ui/git"
 	"github.com/gliderlabs/ssh"
 )
 
@@ -15,4 +16,5 @@ type Session interface {
 	// PublicKey returns the public key of the user.
 	PublicKey() ssh.PublicKey
 	Session() ssh.Session
+	Source() git.GitRepoSource
 }

ui/ui.go 🔗

@@ -33,26 +33,28 @@ const (
 
 // UI is the main UI model.
 type UI struct {
-	s          session.Session
-	common     common.Common
-	pages      []common.Page
-	activePage page
-	state      sessionState
-	header     *header.Header
-	footer     *footer.Footer
-	error      error
+	s           session.Session
+	initialRepo string
+	common      common.Common
+	pages       []common.Page
+	activePage  page
+	state       sessionState
+	header      *header.Header
+	footer      *footer.Footer
+	error       error
 }
 
 // New returns a new UI model.
 func New(s session.Session, c common.Common, initialRepo string) *UI {
 	h := header.New(c, s.Config().Name)
 	ui := &UI{
-		s:          s,
-		common:     c,
-		pages:      make([]common.Page, 2), // selection & repo
-		activePage: selectionPage,
-		state:      startState,
-		header:     h,
+		s:           s,
+		common:      c,
+		pages:       make([]common.Page, 2), // selection & repo
+		activePage:  selectionPage,
+		state:       startState,
+		header:      h,
+		initialRepo: initialRepo,
 	}
 	ui.footer = footer.New(c, ui)
 	ui.SetSize(c.Width, c.Height)
@@ -114,28 +116,19 @@ func (ui *UI) SetSize(width, height int) {
 
 // Init implements tea.Model.
 func (ui *UI) Init() tea.Cmd {
-	cfg := ui.s.Config()
 	ui.pages[selectionPage] = selection.New(ui.s, ui.common)
-	ui.pages[repoPage] = repo.New(ui.common, &source{cfg.Source})
+	ui.pages[repoPage] = repo.New(ui.s, ui.common)
 	ui.SetSize(ui.common.Width, ui.common.Height)
-	cmd := tea.Batch(
+	cmds := make([]tea.Cmd, 0)
+	cmds = append(cmds,
 		ui.pages[selectionPage].Init(),
 		ui.pages[repoPage].Init(),
 	)
-	var msg tea.Msg
-	if cmd != nil {
-		msg = cmd()
-		switch msg := msg.(type) {
-		case common.ErrorMsg:
-			ui.state = errorState
-			return common.ErrorCmd(msg)
-		}
+	if ui.initialRepo != "" {
+		cmds = append(cmds, ui.initialRepoCmd(ui.initialRepo))
 	}
 	ui.state = loadedState
-	if msg != nil {
-		return func() tea.Msg { return msg }
-	}
-	return nil
+	return tea.Batch(cmds...)
 }
 
 // Update implements tea.Model.
@@ -175,7 +168,6 @@ func (ui *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		switch msg.IdentifiableItem.(type) {
 		case selection.Item:
 			if ui.activePage == selectionPage {
-				ui.activePage = repoPage
 				cmds = append(cmds, ui.setRepoCmd(msg.ID()))
 			}
 		}
@@ -237,13 +229,27 @@ func (ui *UI) View() string {
 }
 
 func (ui *UI) setRepoCmd(rn string) tea.Cmd {
-	rs := ui.s.Config().Source
+	rs := ui.s.Source()
 	return func() tea.Msg {
 		for _, r := range rs.AllRepos() {
 			if r.Name() == rn {
+				ui.activePage = repoPage
 				return repo.RepoMsg(r)
 			}
 		}
 		return common.ErrorMsg(git.ErrMissingRepo)
 	}
 }
+
+func (ui *UI) initialRepoCmd(rn string) tea.Cmd {
+	rs := ui.s.Source()
+	return func() tea.Msg {
+		for _, r := range rs.AllRepos() {
+			if r.Name() == rn {
+				ui.activePage = repoPage
+				return repo.RepoMsg(r)
+			}
+		}
+		return nil
+	}
+}