1package repo
  2
  3import (
  4	"fmt"
  5	"sort"
  6	"strings"
  7
  8	"github.com/charmbracelet/bubbles/v2/key"
  9	"github.com/charmbracelet/bubbles/v2/spinner"
 10	tea "github.com/charmbracelet/bubbletea/v2"
 11	"github.com/charmbracelet/soft-serve/git"
 12	"github.com/charmbracelet/soft-serve/pkg/proto"
 13	"github.com/charmbracelet/soft-serve/pkg/ui/common"
 14	"github.com/charmbracelet/soft-serve/pkg/ui/components/selector"
 15)
 16
 17// RefMsg is a message that contains a git.Reference.
 18type RefMsg *git.Reference
 19
 20// RefItemsMsg is a message that contains a list of RefItem.
 21type RefItemsMsg struct {
 22	prefix string
 23	items  []selector.IdentifiableItem
 24}
 25
 26// Refs is a component that displays a list of references.
 27type Refs struct {
 28	common    common.Common
 29	selector  *selector.Selector
 30	repo      proto.Repository
 31	ref       *git.Reference
 32	activeRef *git.Reference
 33	refPrefix string
 34	spinner   spinner.Model
 35	isLoading bool
 36}
 37
 38// NewRefs creates a new Refs component.
 39func NewRefs(common common.Common, refPrefix string) *Refs {
 40	r := &Refs{
 41		common:    common,
 42		refPrefix: refPrefix,
 43		isLoading: true,
 44	}
 45	s := selector.New(common, []selector.IdentifiableItem{}, RefItemDelegate{&common})
 46	s.SetShowFilter(false)
 47	s.SetShowHelp(false)
 48	s.SetShowPagination(false)
 49	s.SetShowStatusBar(false)
 50	s.SetShowTitle(false)
 51	s.SetFilteringEnabled(false)
 52	s.DisableQuitKeybindings()
 53	r.selector = s
 54	sp := spinner.New(spinner.WithSpinner(spinner.Dot),
 55		spinner.WithStyle(common.Styles.Spinner))
 56	r.spinner = sp
 57	return r
 58}
 59
 60// Path implements common.TabComponent.
 61func (r *Refs) Path() string {
 62	return ""
 63}
 64
 65// TabName returns the name of the tab.
 66func (r *Refs) TabName() string {
 67	if r.refPrefix == git.RefsHeads {
 68		return "Branches"
 69	} else if r.refPrefix == git.RefsTags {
 70		return "Tags"
 71	}
 72	return "Refs"
 73}
 74
 75// SetSize implements common.Component.
 76func (r *Refs) SetSize(width, height int) {
 77	r.common.SetSize(width, height)
 78	r.selector.SetSize(width, height)
 79}
 80
 81// ShortHelp implements help.KeyMap.
 82func (r *Refs) ShortHelp() []key.Binding {
 83	copyKey := r.common.KeyMap.Copy
 84	copyKey.SetHelp("c", "copy ref")
 85	k := r.selector.KeyMap
 86	return []key.Binding{
 87		r.common.KeyMap.SelectItem,
 88		k.CursorUp,
 89		k.CursorDown,
 90		copyKey,
 91	}
 92}
 93
 94// FullHelp implements help.KeyMap.
 95func (r *Refs) FullHelp() [][]key.Binding {
 96	copyKey := r.common.KeyMap.Copy
 97	copyKey.SetHelp("c", "copy ref")
 98	k := r.selector.KeyMap
 99	return [][]key.Binding{
100		{r.common.KeyMap.SelectItem},
101		{
102			k.CursorUp,
103			k.CursorDown,
104			k.NextPage,
105			k.PrevPage,
106		},
107		{
108			k.GoToStart,
109			k.GoToEnd,
110			copyKey,
111		},
112	}
113}
114
115// Init implements tea.Model.
116func (r *Refs) Init() tea.Cmd {
117	r.isLoading = true
118	return tea.Batch(r.spinner.Tick, r.updateItemsCmd)
119}
120
121// Update implements tea.Model.
122func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
123	cmds := make([]tea.Cmd, 0)
124	switch msg := msg.(type) {
125	case RepoMsg:
126		r.selector.Select(0)
127		r.repo = msg
128	case RefMsg:
129		r.ref = msg
130		cmds = append(cmds, r.Init())
131	case tea.WindowSizeMsg:
132		r.SetSize(msg.Width, msg.Height)
133	case RefItemsMsg:
134		if r.refPrefix == msg.prefix {
135			cmds = append(cmds, r.selector.SetItems(msg.items))
136			i := r.selector.SelectedItem()
137			if i != nil {
138				r.activeRef = i.(RefItem).Reference
139			}
140			r.isLoading = false
141		}
142	case selector.ActiveMsg:
143		switch sel := msg.IdentifiableItem.(type) {
144		case RefItem:
145			r.activeRef = sel.Reference
146		}
147	case selector.SelectMsg:
148		switch i := msg.IdentifiableItem.(type) {
149		case RefItem:
150			cmds = append(cmds,
151				switchRefCmd(i.Reference),
152				switchTabCmd(&Files{}),
153			)
154		}
155	case tea.KeyPressMsg:
156		switch {
157		case key.Matches(msg, r.common.KeyMap.SelectItem):
158			cmds = append(cmds, r.selector.SelectItemCmd)
159		}
160	case EmptyRepoMsg:
161		r.ref = nil
162		cmds = append(cmds, r.setItems([]selector.IdentifiableItem{}))
163	case spinner.TickMsg:
164		if r.isLoading && r.spinner.ID() == msg.ID {
165			s, cmd := r.spinner.Update(msg)
166			if cmd != nil {
167				cmds = append(cmds, cmd)
168			}
169			r.spinner = s
170		}
171	}
172	m, cmd := r.selector.Update(msg)
173	r.selector = m.(*selector.Selector)
174	if cmd != nil {
175		cmds = append(cmds, cmd)
176	}
177	return r, tea.Batch(cmds...)
178}
179
180// View implements tea.Model.
181func (r *Refs) View() string {
182	if r.isLoading {
183		return renderLoading(r.common, r.spinner)
184	}
185	return r.selector.View()
186}
187
188// SpinnerID implements common.TabComponent.
189func (r *Refs) SpinnerID() int {
190	return r.spinner.ID()
191}
192
193// StatusBarValue implements statusbar.StatusBar.
194func (r *Refs) StatusBarValue() string {
195	if r.activeRef == nil {
196		return ""
197	}
198	return r.activeRef.Name().String()
199}
200
201// StatusBarInfo implements statusbar.StatusBar.
202func (r *Refs) StatusBarInfo() string {
203	totalPages := r.selector.TotalPages()
204	if totalPages <= 1 {
205		return "p. 1/1"
206	}
207	return fmt.Sprintf("p. %d/%d", r.selector.Page()+1, totalPages)
208}
209
210func (r *Refs) updateItemsCmd() tea.Msg {
211	its := make(RefItems, 0)
212	rr, err := r.repo.Open()
213	if err != nil {
214		return common.ErrorMsg(err)
215	}
216	refs, err := rr.References()
217	if err != nil {
218		r.common.Logger.Debugf("ui: error getting references: %v", err)
219		return common.ErrorMsg(err)
220	}
221	for _, ref := range refs {
222		if strings.HasPrefix(ref.Name().String(), r.refPrefix) {
223			refItem := RefItem{
224				Reference: ref,
225			}
226
227			if ref.IsTag() {
228				refItem.Tag, _ = rr.Tag(ref.Name().Short())
229				if refItem.Tag != nil {
230					refItem.Commit, _ = refItem.Tag.Commit()
231				}
232			} else {
233				refItem.Commit, _ = rr.CatFileCommit(ref.ID)
234			}
235			its = append(its, refItem)
236		}
237	}
238	sort.Sort(its)
239	items := make([]selector.IdentifiableItem, len(its))
240	for i, it := range its {
241		items[i] = it
242	}
243	return RefItemsMsg{
244		items:  items,
245		prefix: r.refPrefix,
246	}
247}
248
249func (r *Refs) setItems(items []selector.IdentifiableItem) tea.Cmd {
250	return func() tea.Msg {
251		return RefItemsMsg{
252			items:  items,
253			prefix: r.refPrefix,
254		}
255	}
256}
257
258func switchRefCmd(ref *git.Reference) tea.Cmd {
259	return func() tea.Msg {
260		return RefMsg(ref)
261	}
262}
263
264// UpdateRefCmd gets the repository's HEAD reference and sends a RefMsg.
265func UpdateRefCmd(repo proto.Repository) tea.Cmd {
266	return func() tea.Msg {
267		r, err := repo.Open()
268		if err != nil {
269			return common.ErrorMsg(err)
270		}
271		bs, _ := r.Branches()
272		if len(bs) == 0 {
273			return EmptyRepoMsg{}
274		}
275		ref, err := r.HEAD()
276		if err != nil {
277			return common.ErrorMsg(err)
278		}
279		return RefMsg(ref)
280	}
281}