Skip to content

Commit

Permalink
feat: browse local repositories (#369)
Browse files Browse the repository at this point in the history
* feat: browse local repositories

* refactor(ui): resolve race conditions and clean up code

* feat(ui): update readme based on selected reference

* feat(ui): add branch/tag commit date and hash

Fixes: #382

* fix(ui): clean up statusbar logic

* fix(ui): cleanup statusbar and misc msgs

* fix(ui): preserve header line when no description is available

* fix(ui): match readme and list missing items styles

* fix(ui): header height calculation

Fixes: 43b4331 (fix(ui): preserve header line when no description is available)

* fix(ui): truncate refitem msg

Fixes: fd02409 (feat(ui): add branch/tag commit date and hash)

* fix(ui): dry code line number and highlight formatting

* feat(ui): add blame file view

Fixes: #149

* fix: lint errors

* fix(ui): NaN floats, analyse file content, and start spinner

* feat(ui): use right mouse click to go back in the files tab

* fix(ui): display "1" when there's only one page to display

* feat(ui): add stash view

Display repository stashed items

* feat: run browse on "soft"
  • Loading branch information
aymanbagabas authored Oct 25, 2023
1 parent 5df79ee commit 02e1617
Show file tree
Hide file tree
Showing 36 changed files with 1,844 additions and 798 deletions.
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

0 comments on commit 02e1617

Please sign in to comment.