1package repo
2
3import (
4 "errors"
5 "fmt"
6 "log"
7 "sort"
8 "strings"
9
10 "github.com/charmbracelet/bubbles/key"
11 tea "github.com/charmbracelet/bubbletea"
12 ggit "github.com/charmbracelet/soft-serve/git"
13 "github.com/charmbracelet/soft-serve/ui/common"
14 "github.com/charmbracelet/soft-serve/ui/components/selector"
15 "github.com/charmbracelet/soft-serve/ui/components/tabs"
16 "github.com/charmbracelet/soft-serve/ui/git"
17)
18
19var (
20 errNoRef = errors.New("no reference specified")
21)
22
23// RefMsg is a message that contains a git.Reference.
24type RefMsg *ggit.Reference
25
26// RefItemsMsg is a message that contains a list of RefItem.
27type RefItemsMsg struct {
28 prefix string
29 items []selector.IdentifiableItem
30}
31
32// Refs is a component that displays a list of references.
33type Refs struct {
34 common common.Common
35 selector *selector.Selector
36 repo *git.Repository
37 ref *ggit.Reference
38 activeRef *ggit.Reference
39 refPrefix string
40}
41
42// NewRefs creates a new Refs component.
43func NewRefs(common common.Common, refPrefix string) *Refs {
44 r := &Refs{
45 common: common,
46 refPrefix: refPrefix,
47 }
48 s := selector.New(common, []selector.IdentifiableItem{}, RefItemDelegate{&common})
49 s.SetShowFilter(false)
50 s.SetShowHelp(false)
51 s.SetShowPagination(false)
52 s.SetShowStatusBar(false)
53 s.SetShowTitle(false)
54 s.SetFilteringEnabled(false)
55 s.DisableQuitKeybindings()
56 r.selector = s
57 return r
58}
59
60// SetSize implements common.Component.
61func (r *Refs) SetSize(width, height int) {
62 r.common.SetSize(width, height)
63 r.selector.SetSize(width, height)
64}
65
66// ShortHelp implements help.KeyMap.
67func (r *Refs) ShortHelp() []key.Binding {
68 copyKey := r.common.KeyMap.Copy
69 copyKey.SetHelp("c", "copy ref")
70 k := r.selector.KeyMap
71 return []key.Binding{
72 r.common.KeyMap.SelectItem,
73 k.CursorUp,
74 k.CursorDown,
75 copyKey,
76 }
77}
78
79// FullHelp implements help.KeyMap.
80func (r *Refs) FullHelp() [][]key.Binding {
81 copyKey := r.common.KeyMap.Copy
82 copyKey.SetHelp("c", "copy ref")
83 k := r.selector.KeyMap
84 return [][]key.Binding{
85 {r.common.KeyMap.SelectItem},
86 {
87 k.CursorUp,
88 k.CursorDown,
89 k.NextPage,
90 k.PrevPage,
91 },
92 {
93 k.GoToStart,
94 k.GoToEnd,
95 copyKey,
96 },
97 }
98}
99
100// Init implements tea.Model.
101func (r *Refs) Init() tea.Cmd {
102 return r.updateItemsCmd
103}
104
105// Update implements tea.Model.
106func (r *Refs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107 cmds := make([]tea.Cmd, 0)
108 switch msg := msg.(type) {
109 case RepoMsg:
110 r.selector.Select(0)
111 r.repo = msg
112 case RefMsg:
113 r.ref = msg
114 cmds = append(cmds, r.Init())
115 case RefItemsMsg:
116 if r.refPrefix == msg.prefix {
117 cmds = append(cmds, r.selector.SetItems(msg.items))
118 i := r.selector.SelectedItem()
119 if i != nil {
120 r.activeRef = i.(RefItem).Reference
121 }
122 }
123 case selector.ActiveMsg:
124 switch sel := msg.IdentifiableItem.(type) {
125 case RefItem:
126 r.activeRef = sel.Reference
127 }
128 cmds = append(cmds, updateStatusBarCmd)
129 case selector.SelectMsg:
130 switch i := msg.IdentifiableItem.(type) {
131 case RefItem:
132 cmds = append(cmds,
133 switchRefCmd(i.Reference),
134 tabs.SelectTabCmd(int(filesTab)),
135 )
136 }
137 case tea.KeyMsg:
138 switch {
139 case key.Matches(msg, r.common.KeyMap.SelectItem):
140 cmds = append(cmds, r.selector.SelectItem)
141 }
142 }
143 m, cmd := r.selector.Update(msg)
144 r.selector = m.(*selector.Selector)
145 if cmd != nil {
146 cmds = append(cmds, cmd)
147 }
148 return r, tea.Batch(cmds...)
149}
150
151// View implements tea.Model.
152func (r *Refs) View() string {
153 return r.selector.View()
154}
155
156// StatusBarValue implements statusbar.StatusBar.
157func (r *Refs) StatusBarValue() string {
158 if r.activeRef == nil {
159 return ""
160 }
161 return r.activeRef.Name().String()
162}
163
164// StatusBarInfo implements statusbar.StatusBar.
165func (r *Refs) StatusBarInfo() string {
166 totalPages := r.selector.TotalPages()
167 if totalPages > 1 {
168 return fmt.Sprintf("p. %d/%d", r.selector.Page()+1, totalPages)
169 }
170 return ""
171}
172
173func (r *Refs) updateItemsCmd() tea.Msg {
174 its := make(RefItems, 0)
175 refs, err := r.repo.Repo.Repository().References()
176 if err != nil {
177 log.Printf("ui: error getting references: %v", err)
178 return common.ErrorMsg(err)
179 }
180 for _, ref := range refs {
181 if strings.HasPrefix(ref.Name().String(), r.refPrefix) {
182 its = append(its, RefItem{Reference: ref})
183 }
184 }
185 sort.Sort(its)
186 items := make([]selector.IdentifiableItem, len(its))
187 for i, it := range its {
188 items[i] = it
189 }
190 return RefItemsMsg{
191 items: items,
192 prefix: r.refPrefix,
193 }
194}
195
196func switchRefCmd(ref *ggit.Reference) tea.Cmd {
197 return func() tea.Msg {
198 return RefMsg(ref)
199 }
200}
201
202// UpdateRefCmd gets the repository's HEAD reference and sends a RefMsg.
203func UpdateRefCmd(repo *git.Repository) tea.Cmd {
204 return func() tea.Msg {
205 ref, err := repo.Repo.Repository().HEAD()
206 if err != nil {
207 if bs, err := repo.Repo.Repository().Branches(); err != nil && len(bs) == 0 {
208 return EmptyRepoMsg{}
209 }
210 log.Printf("ui: error getting HEAD reference: %v", err)
211 return common.ErrorMsg(err)
212 }
213 log.Printf("HEAD: %s", ref.Name())
214 return RefMsg(ref)
215 }
216}