Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: browse local repositories #369

Merged
merged 18 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
301 changes: 301 additions & 0 deletions cmd/soft/browse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
package main

import (
"fmt"
"path/filepath"
"time"

"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/git"
"github.com/charmbracelet/soft-serve/server/proto"
"github.com/charmbracelet/soft-serve/server/ui/common"
"github.com/charmbracelet/soft-serve/server/ui/components/footer"
"github.com/charmbracelet/soft-serve/server/ui/pages/repo"
"github.com/muesli/termenv"
"github.com/spf13/cobra"
)

var browseCmd = &cobra.Command{
Use: "browse PATH",
Short: "Browse a repository",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
rp := "."
if len(args) > 0 {
rp = args[0]
}

abs, err := filepath.Abs(rp)
if err != nil {
return err
}

r, err := git.Open(abs)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}

// Bubble Tea uses Termenv default output so we have to use the same
// thing here.
output := termenv.DefaultOutput()
ctx := cmd.Context()
c := common.NewCommon(ctx, output, 0, 0)
comps := []common.TabComponent{
repo.NewReadme(c),
repo.NewFiles(c),
repo.NewLog(c),
}
if !r.IsBare {
comps = append(comps, repo.NewStash(c))
}
comps = append(comps, repo.NewRefs(c, git.RefsHeads), repo.NewRefs(c, git.RefsTags))
m := &model{
model: repo.New(c, comps...),
repo: repository{r},
common: c,
}

m.footer = footer.New(c, m)
p := tea.NewProgram(m,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)

_, err = p.Run()
return err
},
}

func init() {
// HACK: This is a hack to hide the clone url
// TODO: Make this configurable
common.CloneCmd = func(publicURL, name string) string { return "" }
rootCmd.AddCommand(browseCmd)
}

type state int

const (
startState state = iota
errorState
)

type model struct {
model *repo.Repo
footer *footer.Footer
repo proto.Repository
common common.Common
state state
showFooter bool
error error
}

var _ tea.Model = &model{}

func (m *model) SetSize(w, h int) {
m.common.SetSize(w, h)
style := m.common.Styles.App.Copy()
wm := style.GetHorizontalFrameSize()
hm := style.GetVerticalFrameSize()
if m.showFooter {
hm += m.footer.Height()
}

m.footer.SetSize(w-wm, h-hm)
m.model.SetSize(w-wm, h-hm)
}

// ShortHelp implements help.KeyMap.
func (m model) ShortHelp() []key.Binding {
switch m.state {
case errorState:
return []key.Binding{
m.common.KeyMap.Back,
m.common.KeyMap.Quit,
m.common.KeyMap.Help,
}
default:
return m.model.ShortHelp()
}
}

// FullHelp implements help.KeyMap.
func (m model) FullHelp() [][]key.Binding {
switch m.state {
case errorState:
return [][]key.Binding{
{
m.common.KeyMap.Back,
},
{
m.common.KeyMap.Quit,
m.common.KeyMap.Help,
},
}
default:
return m.model.FullHelp()
}
}

// Init implements tea.Model.
func (m *model) Init() tea.Cmd {
return tea.Batch(
m.model.Init(),
m.footer.Init(),
func() tea.Msg {
return repo.RepoMsg(m.repo)
},
repo.UpdateRefCmd(m.repo),
)
}

// Update implements tea.Model.
func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.common.Logger.Debugf("msg received: %T", msg)
cmds := make([]tea.Cmd, 0)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
switch {
case key.Matches(msg, m.common.KeyMap.Back) && m.error != nil:
m.error = nil
m.state = startState
// Always show the footer on error.
m.showFooter = m.footer.ShowAll()
case key.Matches(msg, m.common.KeyMap.Help):
cmds = append(cmds, footer.ToggleFooterCmd)
case key.Matches(msg, m.common.KeyMap.Quit):
// Stop bubblezone background workers.
m.common.Zone.Close()
return m, tea.Quit
}
case tea.MouseMsg:
switch msg.Type {
case tea.MouseLeft:
switch {
case m.common.Zone.Get("footer").InBounds(msg):
cmds = append(cmds, footer.ToggleFooterCmd)
}
}
case footer.ToggleFooterMsg:
m.footer.SetShowAll(!m.footer.ShowAll())
m.showFooter = !m.showFooter
case common.ErrorMsg:
m.error = msg
m.state = errorState
m.showFooter = true
}

f, cmd := m.footer.Update(msg)
m.footer = f.(*footer.Footer)
if cmd != nil {
cmds = append(cmds, cmd)
}

r, cmd := m.model.Update(msg)
m.model = r.(*repo.Repo)
if cmd != nil {
cmds = append(cmds, cmd)
}

// This fixes determining the height margin of the footer.
m.SetSize(m.common.Width, m.common.Height)

return m, tea.Batch(cmds...)
}

// View implements tea.Model.
func (m *model) View() string {
style := m.common.Styles.App.Copy()
wm, hm := style.GetHorizontalFrameSize(), style.GetVerticalFrameSize()
if m.showFooter {
hm += m.footer.Height()
}

var view string
switch m.state {
case startState:
view = m.model.View()
case errorState:
err := m.common.Styles.ErrorTitle.Render("Bummer")
err += m.common.Styles.ErrorBody.Render(m.error.Error())
view = m.common.Styles.Error.Copy().
Width(m.common.Width -
wm -
m.common.Styles.ErrorBody.GetHorizontalFrameSize()).
Height(m.common.Height -
hm -
m.common.Styles.Error.GetVerticalFrameSize()).
Render(err)
}

if m.showFooter {
view = lipgloss.JoinVertical(lipgloss.Top, view, m.footer.View())
}

return m.common.Zone.Scan(style.Render(view))
}

type repository struct {
r *git.Repository
}

var _ proto.Repository = repository{}

// Description implements proto.Repository.
func (r repository) Description() string {
return ""
}

// ID implements proto.Repository.
func (r repository) ID() int64 {
return 0
}

// IsHidden implements proto.Repository.
func (repository) IsHidden() bool {
return false
}

// IsMirror implements proto.Repository.
func (repository) IsMirror() bool {
return false
}

// IsPrivate implements proto.Repository.
func (repository) IsPrivate() bool {
return false
}

// Name implements proto.Repository.
func (r repository) Name() string {
return filepath.Base(r.r.Path)
}

// Open implements proto.Repository.
func (r repository) Open() (*git.Repository, error) {
return r.r, nil
}

// ProjectName implements proto.Repository.
func (r repository) ProjectName() string {
return r.Name()
}

// UpdatedAt implements proto.Repository.
func (r repository) UpdatedAt() time.Time {
t, err := r.r.LatestCommitTime()
if err != nil {
return time.Time{}
}

return t
}

// UserID implements proto.Repository.
func (r repository) UserID() int64 {
return 0
}
2 changes: 1 addition & 1 deletion cmd/soft/migrate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ var migrateConfig = &cobra.Command{
}
}

readme, readmePath, err := git.LatestFile(r, "README*")
readme, readmePath, err := git.LatestFile(r, nil, "README*")
hasReadme := err == nil

// Set server name
Expand Down
3 changes: 3 additions & 0 deletions cmd/soft/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ var (
Short: "A self-hostable Git server for the command line",
Long: "Soft Serve is a self-hostable Git server for the command line.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return browseCmd.RunE(cmd, args)
},
}
)

Expand Down
22 changes: 3 additions & 19 deletions git/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,11 @@ import (
"github.com/gogs/git-module"
)

// ZeroHash is the zero hash.
var ZeroHash Hash = git.EmptyID

// Hash represents a git hash.
type Hash string

// String returns the string representation of a hash as a string.
func (h Hash) String() string {
return string(h)
}

// SHA1 represents the hash as a SHA1.
func (h Hash) SHA1() *git.SHA1 {
return git.MustIDFromString(h.String())
}
// ZeroID is the zero hash.
const ZeroID = git.EmptyID

// Commit is a wrapper around git.Commit with helper methods.
type Commit struct {
*git.Commit
Hash Hash
}
type Commit = git.Commit

// Commits is a list of commits.
type Commits []*Commit
Expand Down
Loading
Loading