a869926848e4a71e27a205bcb94f1a1ac6d7bbf8
- Author
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Committer
- Ayman Bagabas <ayman.bagabas@gmail.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/go.mod b/go.mod
2old mode 100644
3new mode 100755
4index cf1b09a59d8632c429f7c463253b5f089fdeebf6..707bafd46d611de2489bb2f0ca43985bcd68ccf4
5--- a/go.mod
6+++ b/go.mod
7@@ -12,18 +12,20 @@ require (
8 github.com/charmbracelet/wish v0.2.1-0.20220208182816-534842b53d2a
9 github.com/dustin/go-humanize v1.0.0
10 github.com/gliderlabs/ssh v0.3.3
11- github.com/go-git/go-billy/v5 v5.3.1
12- github.com/go-git/go-git/v5 v5.4.2
13+ github.com/go-git/go-billy/v5 v5.3.1 // indirect
14+ github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 // indirect
15 github.com/matryer/is v1.2.0
16 github.com/muesli/reflow v0.3.0
17 github.com/muesli/termenv v0.11.0
18- github.com/sergi/go-diff v1.1.0 // indirect
19+ github.com/sergi/go-diff v1.1.0
20 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
21 gopkg.in/yaml.v2 v2.4.0
22 )
23
24 require (
25 github.com/gobwas/glob v0.2.3
26+ github.com/gogs/git-module v1.5.0
27+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
28 github.com/muesli/mango v0.1.0
29 github.com/muesli/roff v0.1.0
30 )
31@@ -40,7 +42,6 @@ require (
32 github.com/dlclark/regexp2 v1.4.0 // indirect
33 github.com/emirpasic/gods v1.12.0 // indirect
34 github.com/go-git/gcfg v1.5.0 // indirect
35- github.com/google/go-cmp v0.5.6 // indirect
36 github.com/gorilla/css v1.0.0 // indirect
37 github.com/imdario/mergo v0.3.12 // indirect
38 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
39@@ -48,6 +49,7 @@ require (
40 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
41 github.com/mattn/go-isatty v0.0.14 // indirect
42 github.com/mattn/go-runewidth v0.0.13 // indirect
43+ github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 // indirect
44 github.com/microcosm-cc/bluemonday v1.0.17 // indirect
45 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
46 github.com/mitchellh/go-homedir v1.1.0 // indirect
47@@ -58,10 +60,8 @@ require (
48 github.com/xanzy/ssh-agent v0.3.1 // indirect
49 github.com/yuin/goldmark v1.4.4 // indirect
50 github.com/yuin/goldmark-emoji v1.0.1 // indirect
51- golang.org/x/net v0.0.0-20220111093109-d55c255bac03 // indirect
52+ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
53 golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
54 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
55- golang.org/x/text v0.3.7 // indirect
56 gopkg.in/warnings.v0 v0.1.2 // indirect
57- gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
58 )
59diff --git a/go.sum b/go.sum
60index 28b4b112f5b43f968ef2cc8e825ee12b8cdc241b..9607f28d3fbd3526e1af37d2f18e85dfd95de631 100644
61--- a/go.sum
62+++ b/go.sum
63@@ -59,13 +59,17 @@ github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Ai
64 github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
65 github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
66 github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
67-github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
68 github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
69+github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 h1:1RSUwVK7VjTeA82kcLIqz1EU70QRwFdZUlJW58gP4GY=
70+github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
71 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
72 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
73+github.com/gogs/git-module v1.5.0 h1:2aAO79c36R3L6TdKutbVJwr0YwSWfRbPNP456yxDXtk=
74+github.com/gogs/git-module v1.5.0/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
75+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
76+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
77+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
78 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
79-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
80-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
81 github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
82 github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
83 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
84@@ -98,6 +102,8 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
85 github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
86 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
87 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
88+github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk=
89+github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
90 github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
91 github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
92 github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
93@@ -160,8 +166,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
94 golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
95 golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
96 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
97-golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s=
98-golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
99+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
100+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
101+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
102+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
103 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
104 golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
105 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
106@@ -178,6 +186,7 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w
107 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
108 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
109 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
110+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
111 golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
112 golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
113 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
114@@ -189,7 +198,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
115 golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
116 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
117 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
118-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
119 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
120 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
121 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
122@@ -202,6 +210,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
123 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
124 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
125 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
126+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
127 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
128-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
129-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
130diff --git a/internal/config/config.go b/internal/config/config.go
131index 10c9ed3607f220c601d8f585c5df1aeff94733d2..b779a7344f0d3a883d5623b37da71f8dbb298b1b 100644
132--- a/internal/config/config.go
133+++ b/internal/config/config.go
134@@ -3,6 +3,7 @@ package config
135 import (
136 "bytes"
137 "log"
138+ "path/filepath"
139 "strings"
140 "sync"
141 "text/template"
142@@ -15,8 +16,7 @@ import (
143
144 "github.com/charmbracelet/soft-serve/config"
145 "github.com/charmbracelet/soft-serve/internal/git"
146- gg "github.com/go-git/go-git/v5"
147- "github.com/go-git/go-git/v5/plumbing/object"
148+ gg "github.com/gogs/git-module"
149 )
150
151 // Config is the Soft Serve configuration.
152@@ -30,7 +30,7 @@ type Config struct {
153 Repos []Repo `yaml:"repos"`
154 Source *git.RepoSource
155 Cfg *config.Config
156- reloadMtx sync.Mutex
157+ mtx sync.Mutex
158 }
159
160 // User contains user-level configuration for a repository.
161@@ -106,13 +106,17 @@ func NewConfig(cfg *config.Config) (*Config, error) {
162 if err != nil {
163 return nil, err
164 }
165+ err = c.Reload()
166+ if err != nil {
167+ return nil, err
168+ }
169 return c, nil
170 }
171
172 // Reload reloads the configuration.
173 func (cfg *Config) Reload() error {
174- cfg.reloadMtx.Lock()
175- defer cfg.reloadMtx.Unlock()
176+ cfg.mtx.Lock()
177+ defer cfg.mtx.Unlock()
178 err := cfg.Source.LoadRepos()
179 if err != nil {
180 return err
181@@ -121,7 +125,7 @@ func (cfg *Config) Reload() error {
182 if err != nil {
183 return err
184 }
185- cs, err := cr.LatestFile("config.yaml")
186+ cs, _, err := cr.LatestFile("config.yaml")
187 if err != nil {
188 return err
189 }
190@@ -147,18 +151,8 @@ func (cfg *Config) Reload() error {
191 pat = rp
192 }
193 rm := ""
194- f, err := r.FindLatestFile(pat)
195- if err != nil && err != object.ErrFileNotFound {
196- return err
197- }
198- if err == nil {
199- fc, err := f.Contents()
200- if err != nil {
201- return err
202- }
203- rm = fc
204- r.ReadmePath = f.Name
205- }
206+ fc, fp, _ := r.LatestFile(pat)
207+ rm = fc
208 if name == "config" {
209 md, err := templatize(rm, cfg)
210 if err != nil {
211@@ -166,7 +160,7 @@ func (cfg *Config) Reload() error {
212 }
213 rm = md
214 }
215- r.Readme = rm
216+ r.SetReadme(rm, fp)
217 }
218 return nil
219 }
220@@ -187,21 +181,15 @@ func createFile(path string, content string) error {
221 func (cfg *Config) createDefaultConfigRepo(yaml string) error {
222 cn := "config"
223 rs := cfg.Source
224- err := rs.LoadRepos()
225- if err != nil {
226- return err
227- }
228- _, err = rs.GetRepo(cn)
229- if err == git.ErrMissingRepo {
230- cr, err := rs.InitRepo(cn, true)
231- if err != nil {
232- return err
233- }
234diff --git a/internal/git/git.go b/internal/git/git.go
235index 1762d57d7dec95520b44d1871c4c611c7dd56173..87c84ac9f91d39ec939ae62c248a2e7ac2b79bc1 100644
236--- a/internal/git/git.go
237+++ b/internal/git/git.go
238@@ -1,24 +1,15 @@
239 package git
240
241 import (
242- "context"
243 "errors"
244 "log"
245 "os"
246- "os/exec"
247 "path/filepath"
248- "sort"
249 "sync"
250
251- gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
252- "github.com/go-git/go-billy/v5/memfs"
253- "github.com/go-git/go-git/v5"
254- "github.com/go-git/go-git/v5/plumbing"
255- "github.com/go-git/go-git/v5/plumbing/object"
256- "github.com/go-git/go-git/v5/plumbing/storer"
257- "github.com/go-git/go-git/v5/plumbing/transport"
258- "github.com/go-git/go-git/v5/storage/memory"
259+ "github.com/charmbracelet/soft-serve/pkg/git"
260 "github.com/gobwas/glob"
261+ "github.com/golang/groupcache/lru"
262 )
263
264 // ErrMissingRepo indicates that the requested repository could not be found.
265@@ -28,195 +19,126 @@ var ErrMissingRepo = errors.New("missing repo")
266 type Repo struct {
267 path string
268 repository *git.Repository
269- Readme string
270- ReadmePath string
271- refCommits map[plumbing.Hash]gitypes.Commits
272- head *plumbing.Reference
273- refs []*plumbing.Reference
274- trees map[plumbing.Hash]*object.Tree
275- commits map[plumbing.Hash]*object.Commit
276- patch map[plumbing.Hash]*object.Patch
277+ readme string
278+ readmePath string
279+ head *git.Reference
280+ refs []*git.Reference
281+ patchCache *lru.Cache
282 }
283
284-// GetName returns the name of the repository.
285-func (r *Repo) Name() string {
286- return filepath.Base(r.path)
287-}
288-
289-// GetHEAD returns the reference for a repository.
290-func (r *Repo) GetHEAD() *plumbing.Reference {
291- return r.head
292-}
293-
294-// SetHEAD sets the repository head reference.
295-func (r *Repo) SetHEAD(ref *plumbing.Reference) error {
296- r.head = ref
297- return nil
298-}
299-
300-// GetReferences returns the references for a repository.
301-func (r *Repo) GetReferences() []*plumbing.Reference {
302- return r.refs
303-}
304-
305-// GetRepository returns the underlying go-git repository object.
306-func (r *Repo) Repository() *git.Repository {
307- return r.repository
308-}
309-
310-// Tree returns the git tree for a given path.
311-func (r *Repo) Tree(ref *plumbing.Reference, path string) (*object.Tree, error) {
312- path = filepath.Clean(path)
313- hash, err := r.targetHash(ref)
314+// Open opens a Git repository.
315+func (rs *RepoSource) Open(path string) (*Repo, error) {
316+ rg, err := git.Open(path)
317 if err != nil {
318 return nil, err
319 }
320- c, err := r.commitForHash(hash)
321+ r := &Repo{
322+ path: path,
323+ repository: rg,
324+ patchCache: lru.New(1000),
325+ }
326+ _, err = r.HEAD()
327 if err != nil {
328 return nil, err
329 }
330- t, err := r.treeForHash(c.TreeHash)
331+ _, err = r.References()
332 if err != nil {
333 return nil, err
334 }
335- if path == "." {
336- return t, nil
337- }
338diff --git a/internal/tui/bubble.go b/internal/tui/bubble.go
339index 321b0fe86a5c3879ff253aecd3fec4fd3a5e4f8e..bacc3de883538dd8cace59bbd1cce185b0b783bf 100644
340--- a/internal/tui/bubble.go
341+++ b/internal/tui/bubble.go
342@@ -7,10 +7,10 @@ import (
343 tea "github.com/charmbracelet/bubbletea"
344 "github.com/charmbracelet/lipgloss"
345 "github.com/charmbracelet/soft-serve/internal/config"
346- gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
347 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
348 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/selection"
349 "github.com/charmbracelet/soft-serve/internal/tui/style"
350+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
351 "github.com/gliderlabs/ssh"
352 )
353
354@@ -157,19 +157,19 @@ func (b Bubble) headerView() string {
355
356 func (b Bubble) footerView() string {
357 w := &strings.Builder{}
358- var h []gittypes.HelpEntry
359+ var h []common.HelpEntry
360 if b.state != errorState {
361- h = []gittypes.HelpEntry{
362+ h = []common.HelpEntry{
363 {Key: "tab", Value: "section"},
364 }
365- if box, ok := b.boxes[b.activeBox].(gittypes.BubbleHelper); ok {
366+ if box, ok := b.boxes[b.activeBox].(common.BubbleHelper); ok {
367 help := box.Help()
368 for _, he := range help {
369 h = append(h, he)
370 }
371 }
372 }
373- h = append(h, gittypes.HelpEntry{Key: "q", Value: "quit"})
374+ h = append(h, common.HelpEntry{Key: "q", Value: "quit"})
375 for i, v := range h {
376 fmt.Fprint(w, helpEntryRender(v, b.styles))
377 if i != len(h)-1 {
378@@ -178,13 +178,14 @@ func (b Bubble) footerView() string {
379 }
380 branch := ""
381 if b.state == loadedState {
382- branch = b.boxes[1].(*repo.Bubble).Reference().Short()
383+ ref := b.boxes[1].(*repo.Bubble).Reference()
384+ branch = ref.Name().Short()
385 }
386 help := w.String()
387 branchMaxWidth := b.width - // bubble width
388 lipgloss.Width(help) - // help width
389 b.styles.App.GetHorizontalFrameSize() // App paddings
390- branch = b.styles.Branch.Render(gittypes.TruncateString(branch, branchMaxWidth-1, "…"))
391+ branch = b.styles.Branch.Render(common.TruncateString(branch, branchMaxWidth-1, "…"))
392 gap := lipgloss.NewStyle().
393 Width(b.width -
394 lipgloss.Width(help) -
395@@ -228,6 +229,6 @@ func (b Bubble) View() string {
396 return b.styles.App.Render(s.String())
397 }
398
399-func helpEntryRender(h gittypes.HelpEntry, s *style.Styles) string {
400+func helpEntryRender(h common.HelpEntry, s *style.Styles) string {
401 return fmt.Sprintf("%s %s", s.HelpKey.Render(h.Key), s.HelpValue.Render(h.Value))
402 }
403diff --git a/internal/tui/bubbles/git/types/git.go b/internal/tui/bubbles/git/types/git.go
404deleted file mode 100644
405index 90e5436e98a21a06413dd177dd5f085c59eac91b..0000000000000000000000000000000000000000
406--- a/internal/tui/bubbles/git/types/git.go
407+++ /dev/null
408@@ -1,30 +0,0 @@
409-package types
410-
411-import (
412- "context"
413-
414- "github.com/go-git/go-git/v5"
415- "github.com/go-git/go-git/v5/plumbing"
416- "github.com/go-git/go-git/v5/plumbing/object"
417-)
418-
419-type Repo interface {
420- Name() string
421- GetHEAD() *plumbing.Reference
422- SetHEAD(*plumbing.Reference) error
423- GetReferences() []*plumbing.Reference
424- GetReadme() string
425- GetReadmePath() string
426- GetCommits(*plumbing.Reference) (Commits, error)
427- Repository() *git.Repository
428- Tree(*plumbing.Reference, string) (*object.Tree, error)
429- PatchCtx(context.Context, *object.Commit) (*object.Patch, error)
430-}
431-
432-type Commits []*object.Commit
433-
434-func (cl Commits) Len() int { return len(cl) }
435-func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
436-func (cl Commits) Less(i, j int) bool {
437- return cl[i].Author.When.After(cl[j].Author.When)
438-}
439diff --git a/internal/tui/bubbles/repo/bubble.go b/internal/tui/bubbles/repo/bubble.go
440index 5ba6f8fcacea26ea8af02c61007f1911802ab550..b8cdd0e2d7e689c671fa4d1f8576faf65cb8d14f 100644
441--- a/internal/tui/bubbles/repo/bubble.go
442+++ b/internal/tui/bubbles/repo/bubble.go
443@@ -6,10 +6,10 @@ import (
444
445 tea "github.com/charmbracelet/bubbletea"
446 "github.com/charmbracelet/lipgloss"
447- gitui "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git"
448- gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
449 "github.com/charmbracelet/soft-serve/internal/tui/style"
450- "github.com/go-git/go-git/v5/plumbing"
451+ "github.com/charmbracelet/soft-serve/pkg/git"
452+ gitui "github.com/charmbracelet/soft-serve/pkg/tui"
453+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
454 "github.com/muesli/reflow/truncate"
455 "github.com/muesli/reflow/wrap"
456 )
457@@ -22,7 +22,7 @@ type Bubble struct {
458 name string
459 host string
460 port int
461- repo gittypes.Repo
462+ repo common.GitRepo
463 styles *style.Styles
464 width int
465 widthMargin int
466@@ -33,7 +33,7 @@ type Bubble struct {
467 Active bool
468 }
469
470-func NewBubble(repo gittypes.Repo, host string, port int, styles *style.Styles, width, wm, height, hm int) *Bubble {
471+func NewBubble(repo common.GitRepo, host string, port int, styles *style.Styles, width, wm, height, hm int) *Bubble {
472 b := &Bubble{
473 name: repo.Name(),
474 host: host,
475@@ -56,6 +56,9 @@ func (b *Bubble) Init() tea.Cmd {
476 func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
477 switch msg := msg.(type) {
478 case tea.WindowSizeMsg:
479+ if msg.Width == b.width && msg.Height == b.height {
480+ return b, nil
481+ }
482 b.width = msg.Width
483 b.height = msg.Height
484 }
485@@ -64,7 +67,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
486 return b, cmd
487 }
488
489-func (b *Bubble) Help() []gittypes.HelpEntry {
490+func (b *Bubble) Help() []common.HelpEntry {
491 return b.box.Help()
492 }
493
494@@ -95,7 +98,7 @@ func (b Bubble) headerView() string {
495 note = b.styles.RepoNote.Copy().Width(noteWidth).Render(note)
496
497 // Render borders on name and command
498- height := gittypes.Max(lipgloss.Height(title), lipgloss.Height(note))
499+ height := common.Max(lipgloss.Height(title), lipgloss.Height(note))
500 titleBoxStyle := b.styles.RepoTitleBox.Copy().Height(height)
501 noteBoxStyle := b.styles.RepoNoteBox.Copy().Height(height)
502 if b.Active {
503@@ -121,7 +124,7 @@ func (b *Bubble) View() string {
504 return header + body
505 }
506
507-func (b *Bubble) Reference() plumbing.ReferenceName {
508+func (b *Bubble) Reference() *git.Reference {
509 return b.box.Reference()
510 }
511
512diff --git a/internal/tui/bubbles/selection/bubble.go b/internal/tui/bubbles/selection/bubble.go
513index eb84b5d6817ac5174b92a1c187ca3eae14546d94..866de2db81e189426cfad9bc9f1f89f02140c89c 100644
514--- a/internal/tui/bubbles/selection/bubble.go
515+++ b/internal/tui/bubbles/selection/bubble.go
516@@ -5,8 +5,8 @@ import (
517
518 tea "github.com/charmbracelet/bubbletea"
519 "github.com/charmbracelet/lipgloss"
520- gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
521 "github.com/charmbracelet/soft-serve/internal/tui/style"
522+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
523 "github.com/muesli/reflow/truncate"
524 )
525
526@@ -80,8 +80,8 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
527 return b, tea.Batch(cmds...)
528 }
529
530-func (b *Bubble) Help() []gittypes.HelpEntry {
531- return []gittypes.HelpEntry{
532+func (b *Bubble) Help() []common.HelpEntry {
533+ return []common.HelpEntry{
534 {Key: "↑/↓", Value: "navigate"},
535 }
536 }
537diff --git a/internal/tui/commands.go b/internal/tui/commands.go
538index 2008ae811db8f59d58152979d133c03aea521fd4..e95bfc3dcbd6eba275a4e26c91e11fdef1779b3c 100644
539--- a/internal/tui/commands.go
540+++ b/internal/tui/commands.go
541@@ -5,9 +5,9 @@ import (
542
543 tea "github.com/charmbracelet/bubbletea"
544 "github.com/charmbracelet/lipgloss"
545- gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
546 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
547 "github.com/charmbracelet/soft-serve/internal/tui/bubbles/selection"
548+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
549 gm "github.com/charmbracelet/wish/git"
550 )
551
552@@ -110,7 +110,7 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
553 initCmd := rb.Init()
554 msg := initCmd()
555 switch msg := msg.(type) {
556- case gitypes.ErrMsg:
557+ case common.ErrMsg:
558 return me, fmt.Errorf("missing %s: %s", me.Repo, msg.Err.Error())
559 }
560 me.bubble = rb
561diff --git a/pkg/git/commit.go b/pkg/git/commit.go
562new file mode 100644
563index 0000000000000000000000000000000000000000..6a7d2778064dd35923e92788eab19f75048a201c
564--- /dev/null
565+++ b/pkg/git/commit.go
566@@ -0,0 +1,42 @@
567+package git
568+
569+import (
570+ "github.com/gogs/git-module"
571+)
572+
573+var (
574+ ZeroHash Hash = git.EmptyID
575+)
576+
577+// Hash represents a git hash.
578+type Hash string
579+
580+// String returns the string representation of a hash as a string.
581+func (h Hash) String() string {
582+ return string(h)
583+}
584+
585+// SHA1 represents the hash as a SHA1.
586+func (h Hash) SHA1() *git.SHA1 {
587+ return git.MustIDFromString(h.String())
588+}
589+
590+// Commit is a wrapper around git.Commit with helper methods.
591+type Commit struct {
592+ *git.Commit
593+ Hash Hash
594+}
595+
596+// Commits is a list of commits.
597+type Commits []*Commit
598+
599+// Len implements sort.Interface.
600+func (cl Commits) Len() int { return len(cl) }
601+
602+// Swap implements sort.Interface.
603+func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
604+
605+// Less implements sort.Interface.
606+func (cl Commits) Less(i, j int) bool {
607+ return cl[i].Author.When.After(cl[j].Author.When)
608+}
609diff --git a/pkg/git/errors.go b/pkg/git/errors.go
610new file mode 100644
611index 0000000000000000000000000000000000000000..9f8b705d77c864e5cdae5adc760c12ce1955ccd9
612--- /dev/null
613+++ b/pkg/git/errors.go
614@@ -0,0 +1,9 @@
615+package git
616+
617+import "errors"
618+
619+var (
620+ ErrFileNotFound = errors.New("file not found")
621+ ErrDirectoryNotFound = errors.New("directory not found")
622+ ErrReferenceNotFound = errors.New("reference not found")
623+)
624diff --git a/pkg/git/patch.go b/pkg/git/patch.go
625new file mode 100644
626index 0000000000000000000000000000000000000000..f2f3e1cc4b3008a7402e5f6759fe5c88747b8311
627--- /dev/null
628+++ b/pkg/git/patch.go
629@@ -0,0 +1,329 @@
630+package git
631+
632+import (
633+ "bytes"
634+ "fmt"
635+ "math"
636+ "strings"
637+ "sync"
638+
639+ "github.com/dustin/go-humanize/english"
640+ "github.com/gogs/git-module"
641+ "github.com/sergi/go-diff/diffmatchpatch"
642+)
643+
644+// DiffSection is a wrapper to git.DiffSection with helper methods.
645+type DiffSection struct {
646+ *git.DiffSection
647+
648+ initOnce sync.Once
649+ dmp *diffmatchpatch.DiffMatchPatch
650+}
651+
652+// diffFor computes inline diff for the given line.
653+func (s *DiffSection) diffFor(line *git.DiffLine) string {
654+ fallback := line.Content
655+
656+ // Find equivalent diff line, ignore when not found.
657+ var diff1, diff2 string
658+ switch line.Type {
659+ case git.DiffLineAdd:
660+ compareLine := s.Line(git.DiffLineDelete, line.RightLine)
661+ if compareLine == nil {
662+ return fallback
663+ }
664+
665+ diff1 = compareLine.Content
666+ diff2 = line.Content
667+
668+ case git.DiffLineDelete:
669+ compareLine := s.Line(git.DiffLineAdd, line.LeftLine)
670+ if compareLine == nil {
671+ return fallback
672+ }
673+
674+ diff1 = line.Content
675+ diff2 = compareLine.Content
676+
677+ default:
678+ return fallback
679+ }
680+
681+ s.initOnce.Do(func() {
682+ s.dmp = diffmatchpatch.New()
683+ s.dmp.DiffEditCost = 100
684+ })
685+
686+ diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)
687+ diffs = s.dmp.DiffCleanupEfficiency(diffs)
688+
689+ return diffsToString(diffs, line.Type)
690+}
691+
692+func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) string {
693+ buf := bytes.NewBuffer(nil)
694+
695+ // Reproduce signs which are cutted for inline diff before.
696+ switch lineType {
697+ case git.DiffLineAdd:
698+ buf.WriteByte('+')
699+ case git.DiffLineDelete:
700+ buf.WriteByte('-')
701+ }
702+
703+ for i := range diffs {
704+ switch {
705+ case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
706+ buf.WriteString(diffs[i].Text)
707+ case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
708+ buf.WriteString(diffs[i].Text)
709+ case diffs[i].Type == diffmatchpatch.DiffEqual:
710+ buf.WriteString(diffs[i].Text)
711+ }
712+ }
713+
714+ return string(buf.Bytes())
715+}
716+
717+// DiffFile is a wrapper to git.DiffFile with helper methods.
718+type DiffFile struct {
719+ *git.DiffFile
720+ Sections []*DiffSection
721+}
722+
723+type DiffFileChange struct {
724+ hash string
725+ name string
726+ mode git.EntryMode
727+}
728+
729diff --git a/pkg/git/reference.go b/pkg/git/reference.go
730new file mode 100644
731index 0000000000000000000000000000000000000000..ec01a53fdc8c70f52283107c33029ae5fa57577d
732--- /dev/null
733+++ b/pkg/git/reference.go
734@@ -0,0 +1,76 @@
735+package git
736+
737+import (
738+ "strings"
739+
740+ "github.com/gogs/git-module"
741+)
742+
743+const (
744+ // HEAD represents the name of the HEAD reference.
745+ HEAD = "HEAD"
746+ // RefsHeads represents the prefix for branch references.
747+ RefsHeads = git.RefsHeads
748+ // RefsTags represents the prefix for tag references.
749+ RefsTags = git.RefsTags
750+)
751+
752+// Reference is a wrapper around git.Reference with helper methods.
753+type Reference struct {
754+ *git.Reference
755+ Hash Hash
756+ path string // repo path
757+}
758+
759+// ReferenceName is a Refspec wrapper.
760+type ReferenceName string
761+
762+// NewReference creates a new reference.
763+func NewReference(rp, refspec string) *Reference {
764+ return &Reference{
765+ Reference: &git.Reference{
766+ Refspec: refspec,
767+ },
768+ path: rp,
769+ }
770+}
771+
772+// String returns the reference name i.e. refs/heads/master.
773+func (r ReferenceName) String() string {
774+ return string(r)
775+}
776+
777+// Short returns the short name of the reference i.e. master.
778+func (r ReferenceName) Short() string {
779+ s := strings.Split(r.String(), "/")
780+ if len(s) > 0 {
781+ return s[len(s)-1]
782+ }
783+ return r.String()
784+}
785+
786+// Name returns the reference name i.e. refs/heads/master.
787+func (r *Reference) Name() ReferenceName {
788+ return ReferenceName(r.Refspec)
789+}
790+
791+// IsBranch returns true if the reference is a branch.
792+func (r *Reference) IsBranch() bool {
793+ return strings.HasPrefix(r.Refspec, git.RefsHeads)
794+}
795+
796+// IsTag returns true if the reference is a tag.
797+func (r *Reference) IsTag() bool {
798+ return strings.HasPrefix(r.Refspec, git.RefsTags)
799+}
800+
801+// TargetHash returns the hash of the reference target.
802+func (r *Reference) TargetHash() Hash {
803+ if r.IsTag() {
804+ id, err := git.ShowRefVerify(r.path, r.Refspec)
805+ if err == nil {
806+ return Hash(id)
807+ }
808+ }
809+ return r.Hash
810+}
811diff --git a/pkg/git/repo.go b/pkg/git/repo.go
812new file mode 100644
813index 0000000000000000000000000000000000000000..dc749c797dc1383e7851d46257b1184343dc1646
814--- /dev/null
815+++ b/pkg/git/repo.go
816@@ -0,0 +1,185 @@
817+package git
818+
819+import (
820+ "path/filepath"
821+
822+ "github.com/gogs/git-module"
823+)
824+
825+var (
826+ DiffMaxFiles = 1000
827+ DiffMaxFileLines = 1000
828+ DiffMaxLineChars = 1000
829+)
830+
831+// Repository is a wrapper around git.Repository with helper methods.
832+type Repository struct {
833+ *git.Repository
834+ Path string
835+}
836+
837+// Clone clones a repository.
838+func Clone(src, dst string, opts ...git.CloneOptions) error {
839+ return git.Clone(src, dst, opts...)
840+}
841+
842+// Init initializes and opens a new git repository.
843+func Init(path string, bare bool) (*Repository, error) {
844+ err := git.Init(path, git.InitOptions{Bare: bare})
845+ if err != nil {
846+ return nil, err
847+ }
848+ return Open(path)
849+}
850+
851+// Open opens a git repository at the given path.
852+func Open(path string) (*Repository, error) {
853+ repo, err := git.Open(path)
854+ if err != nil {
855+ return nil, err
856+ }
857+ return &Repository{
858+ Repository: repo,
859+ Path: path,
860+ }, nil
861+}
862+
863+// Name returns the name of the repository.
864+func (r *Repository) Name() string {
865+ return filepath.Base(r.Path)
866+}
867+
868+// HEAD returns the HEAD reference for a repository.
869+func (r *Repository) HEAD() (*Reference, error) {
870+ rn, err := r.SymbolicRef()
871+ if err != nil {
872+ return nil, err
873+ }
874+ hash, err := r.ShowRefVerify(rn)
875+ if err != nil {
876+ return nil, err
877+ }
878+ return &Reference{
879+ Reference: &git.Reference{
880+ ID: hash,
881+ Refspec: rn,
882+ },
883+ Hash: Hash(hash),
884+ path: r.Path,
885+ }, nil
886+}
887+
888+// References returns the references for a repository.
889+func (r *Repository) References() ([]*Reference, error) {
890+ refs, err := r.ShowRef()
891+ if err != nil {
892+ return nil, err
893+ }
894+ rrefs := make([]*Reference, 0, len(refs))
895+ for _, ref := range refs {
896+ rrefs = append(rrefs, &Reference{
897+ Reference: ref,
898+ Hash: Hash(ref.ID),
899+ path: r.Path,
900+ })
901+ }
902+ return rrefs, nil
903+}
904+
905+// Tree returns the tree for the given reference.
906+func (r *Repository) Tree(ref *Reference) (*Tree, error) {
907+ if ref == nil {
908+ rref, err := r.HEAD()
909+ if err != nil {
910+ return nil, err
911+ }
912+ ref = rref
913+ }
914+ tree, err := r.LsTree(ref.Hash.String())
915+ if err != nil {
916diff --git a/pkg/git/tree.go b/pkg/git/tree.go
917new file mode 100644
918index 0000000000000000000000000000000000000000..517c9effb9ea6df86b1c80b7003837a52a190365
919--- /dev/null
920+++ b/pkg/git/tree.go
921@@ -0,0 +1,191 @@
922+package git
923+
924+import (
925+ "bufio"
926+ "bytes"
927+ "io"
928+ "io/fs"
929+ "path/filepath"
930+ "sort"
931+
932+ "github.com/gogs/git-module"
933+)
934+
935+// Tree is a wrapper around git.Tree with helper methods.
936+type Tree struct {
937+ *git.Tree
938+ Path string
939+}
940+
941+// TreeEntry is a wrapper around git.TreeEntry with helper methods.
942+type TreeEntry struct {
943+ *git.TreeEntry
944+ // path is the full path of the file
945+ path string
946+}
947+
948+// Entries is a wrapper around git.Entries
949+type Entries []*TreeEntry
950+
951+var sorters = []func(t1, t2 *TreeEntry) bool{
952+ func(t1, t2 *TreeEntry) bool {
953+ return (t1.IsTree() || t1.IsCommit()) && !t2.IsTree() && !t2.IsCommit()
954+ },
955+ func(t1, t2 *TreeEntry) bool {
956+ return t1.Name() < t2.Name()
957+ },
958+}
959+
960+// Len implements sort.Interface.
961+func (es Entries) Len() int { return len(es) }
962+
963+// Swap implements sort.Interface.
964+func (es Entries) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
965+
966+// Less implements sort.Interface.
967+func (es Entries) Less(i, j int) bool {
968+ t1, t2 := es[i], es[j]
969+ var k int
970+ for k = 0; k < len(sorters)-1; k++ {
971+ sorter := sorters[k]
972+ switch {
973+ case sorter(t1, t2):
974+ return true
975+ case sorter(t2, t1):
976+ return false
977+ }
978+ }
979+ return sorters[k](t1, t2)
980+}
981+
982+// Sort sorts the entries in the tree.
983+func (es Entries) Sort() {
984+ sort.Sort(es)
985+}
986+
987+// File is a wrapper around git.Blob with helper methods.
988+type File struct {
989+ *git.Blob
990+ Entry *TreeEntry
991+}
992+
993+// Name returns the name of the file.
994+func (f *File) Name() string {
995+ return f.Entry.Name()
996+}
997+
998+// Path returns the full path of the file.
999+func (f *File) Path() string {
1000+ return f.Entry.path
1001+}
1002+
1003+// SubTree returns the sub-tree at the given path.
1004+func (t *Tree) SubTree(path string) (*Tree, error) {
1005+ tree, err := t.Subtree(path)
1006+ if err != nil {
1007+ return nil, err
1008+ }
1009+ return &Tree{
1010+ Tree: tree,
1011+ Path: path,
1012+ }, nil
1013+}
1014+
1015+// Entries returns the entries in the tree.
1016+func (t *Tree) Entries() (Entries, error) {
1017+ entries, err := t.Tree.Entries()
1018+ if err != nil {
1019+ return nil, err
1020+ }
1021diff --git a/internal/tui/bubbles/git/about/bubble.go b/pkg/tui/about/bubble.go
1022rename from internal/tui/bubbles/git/about/bubble.go
1023rename to pkg/tui/about/bubble.go
1024index 2c957397d0bde01e0911261bb5dd5d4cfefc1efc..bc819af6ab6d267e9f4e6afde4f23f12e25d7018 100644
1025--- a/internal/tui/bubbles/git/about/bubble.go
1026+++ b/pkg/tui/about/bubble.go
1027@@ -3,26 +3,26 @@ package about
1028 import (
1029 "github.com/charmbracelet/bubbles/viewport"
1030 tea "github.com/charmbracelet/bubbletea"
1031- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
1032- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1033- vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
1034 "github.com/charmbracelet/soft-serve/internal/tui/style"
1035- "github.com/go-git/go-git/v5/plumbing"
1036+ "github.com/charmbracelet/soft-serve/pkg/git"
1037+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
1038+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
1039+ vp "github.com/charmbracelet/soft-serve/pkg/tui/viewport"
1040 "github.com/muesli/reflow/wrap"
1041 )
1042
1043 type Bubble struct {
1044 readmeViewport *vp.ViewportBubble
1045- repo types.Repo
1046+ repo common.GitRepo
1047 styles *style.Styles
1048 height int
1049 heightMargin int
1050 width int
1051 widthMargin int
1052- ref *plumbing.Reference
1053+ ref *git.Reference
1054 }
1055
1056-func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
1057+func NewBubble(repo common.GitRepo, styles *style.Styles, width, wm, height, hm int) *Bubble {
1058 b := &Bubble{
1059 readmeViewport: &vp.ViewportBubble{
1060 Viewport: &viewport.Model{},
1061@@ -31,13 +31,12 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
1062 styles: styles,
1063 widthMargin: wm,
1064 heightMargin: hm,
1065- ref: repo.GetHEAD(),
1066 }
1067 b.SetSize(width, height)
1068 return b
1069 }
1070 func (b *Bubble) Init() tea.Cmd {
1071- return b.setupCmd
1072+ return b.reset
1073 }
1074
1075 func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1076@@ -56,11 +55,11 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1077 case tea.KeyMsg:
1078 switch msg.String() {
1079 case "R":
1080- b.GotoTop()
1081+ return b, b.reset
1082 }
1083 case refs.RefMsg:
1084 b.ref = msg
1085- return b, b.setupCmd
1086+ return b, b.reset
1087 }
1088 rv, cmd := b.readmeViewport.Update(msg)
1089 b.readmeViewport = rv.(*vp.ViewportBubble)
1090@@ -83,17 +82,17 @@ func (b *Bubble) View() string {
1091 return b.readmeViewport.View()
1092 }
1093
1094-func (b *Bubble) Help() []types.HelpEntry {
1095+func (b *Bubble) Help() []common.HelpEntry {
1096 return nil
1097 }
1098
1099 func (b *Bubble) glamourize() (string, error) {
1100 w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
1101- rm := b.repo.GetReadme()
1102+ rm, rp := b.repo.Readme()
1103 if rm == "" {
1104 return b.styles.AboutNoReadme.Render("No readme found."), nil
1105 }
1106- f, err := types.RenderFile(b.repo.GetReadmePath(), rm, w)
1107+ f, err := common.RenderFile(rp, rm, w)
1108 if err != nil {
1109 return "", err
1110 }
1111@@ -107,11 +106,16 @@ func (b *Bubble) glamourize() (string, error) {
1112 return wrap.String(f, w), nil
1113 }
1114
1115-func (b *Bubble) setupCmd() tea.Msg {
1116+func (b *Bubble) reset() tea.Msg {
1117 md, err := b.glamourize()
1118 if err != nil {
1119- return types.ErrMsg{Err: err}
1120+ return common.ErrMsg{Err: err}
1121 }
1122+ head, err := b.repo.HEAD()
1123+ if err != nil {
1124+ return common.ErrMsg{Err: err}
1125+ }
1126+ b.ref = head
1127diff --git a/internal/tui/bubbles/git/bubble.go b/pkg/tui/bubble.go
1128rename from internal/tui/bubbles/git/bubble.go
1129rename to pkg/tui/bubble.go
1130index 6b5ea700c42478615b56e36c06f7a554e3cc4e0f..40a3ace61c52730d7fa208dbf2dbcd5c47420a49 100644
1131--- a/internal/tui/bubbles/git/bubble.go
1132+++ b/pkg/tui/bubble.go
1133@@ -1,15 +1,15 @@
1134-package git
1135+package tui
1136
1137 import (
1138 tea "github.com/charmbracelet/bubbletea"
1139 "github.com/charmbracelet/lipgloss"
1140- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/about"
1141- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/log"
1142- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
1143- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/tree"
1144- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1145 "github.com/charmbracelet/soft-serve/internal/tui/style"
1146- "github.com/go-git/go-git/v5/plumbing"
1147+ "github.com/charmbracelet/soft-serve/pkg/git"
1148+ "github.com/charmbracelet/soft-serve/pkg/tui/about"
1149+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
1150+ "github.com/charmbracelet/soft-serve/pkg/tui/log"
1151+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
1152+ "github.com/charmbracelet/soft-serve/pkg/tui/tree"
1153 )
1154
1155 const (
1156@@ -27,17 +27,17 @@ const (
1157
1158 type Bubble struct {
1159 state state
1160- repo types.Repo
1161+ repo common.GitRepo
1162 height int
1163 heightMargin int
1164 width int
1165 widthMargin int
1166 style *style.Styles
1167 boxes []tea.Model
1168- ref *plumbing.Reference
1169+ ref *git.Reference
1170 }
1171
1172-func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
1173+func NewBubble(repo common.GitRepo, styles *style.Styles, width, wm, height, hm int) *Bubble {
1174 b := &Bubble{
1175 repo: repo,
1176 state: aboutState,
1177@@ -47,7 +47,6 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
1178 heightMargin: hm,
1179 style: styles,
1180 boxes: make([]tea.Model, 4),
1181- ref: repo.GetHEAD(),
1182 }
1183 heightMargin := hm + lipgloss.Height(b.headerView())
1184 b.boxes[aboutState] = about.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
1185@@ -106,20 +105,20 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
1186 return b, tea.Batch(cmds...)
1187 }
1188
1189-func (b *Bubble) Help() []types.HelpEntry {
1190- h := []types.HelpEntry{}
1191- h = append(h, b.boxes[b.state].(types.BubbleHelper).Help()...)
1192+func (b *Bubble) Help() []common.HelpEntry {
1193+ h := []common.HelpEntry{}
1194+ h = append(h, b.boxes[b.state].(common.BubbleHelper).Help()...)
1195 if b.repo.Name() != "config" {
1196- h = append(h, types.HelpEntry{Key: "R", Value: "readme"})
1197- h = append(h, types.HelpEntry{Key: "F", Value: "files"})
1198- h = append(h, types.HelpEntry{Key: "C", Value: "commits"})
1199- h = append(h, types.HelpEntry{Key: "B", Value: "branches"})
1200+ h = append(h, common.HelpEntry{Key: "R", Value: "readme"})
1201+ h = append(h, common.HelpEntry{Key: "F", Value: "files"})
1202+ h = append(h, common.HelpEntry{Key: "C", Value: "commits"})
1203+ h = append(h, common.HelpEntry{Key: "B", Value: "branches"})
1204 }
1205 return h
1206 }
1207
1208-func (b *Bubble) Reference() plumbing.ReferenceName {
1209- return b.ref.Name()
1210+func (b *Bubble) Reference() *git.Reference {
1211+ return b.ref
1212 }
1213
1214 func (b *Bubble) headerView() string {
1215@@ -133,6 +132,11 @@ func (b *Bubble) View() string {
1216 }
1217
1218 func (b *Bubble) setupCmd() tea.Msg {
1219+ head, err := b.repo.HEAD()
1220+ if err != nil {
1221+ return common.ErrMsg{Err: err}
1222+ }
1223+ b.ref = head
1224 cmds := make([]tea.Cmd, 0)
1225 for _, bx := range b.boxes {
1226 if bx != nil {
1227@@ -140,7 +144,7 @@ func (b *Bubble) setupCmd() tea.Msg {
1228 if initCmd != nil {
1229 msg := initCmd()
1230 switch msg := msg.(type) {
1231- case types.ErrMsg:
1232+ case common.ErrMsg:
1233diff --git a/internal/tui/bubbles/git/types/consts.go b/pkg/tui/common/consts.go
1234rename from internal/tui/bubbles/git/types/consts.go
1235rename to pkg/tui/common/consts.go
1236index 940938638aa3082404ce339b8cdfbad01c52f7a2..c915df177fbfe6ca0c6c67c58ae246b39d633f6a 100644
1237--- a/internal/tui/bubbles/git/types/consts.go
1238+++ b/pkg/tui/common/consts.go
1239@@ -1,4 +1,4 @@
1240-package types
1241+package common
1242
1243 import (
1244 "time"
1245diff --git a/internal/tui/bubbles/git/types/error.go b/pkg/tui/common/error.go
1246rename from internal/tui/bubbles/git/types/error.go
1247rename to pkg/tui/common/error.go
1248index 0e2cf1e4621a00626bba67d2eda23e5ab231c158..b9ecc9594b04ed7a793fee776af2f3835ffa6715 100644
1249--- a/internal/tui/bubbles/git/types/error.go
1250+++ b/pkg/tui/common/error.go
1251@@ -1,4 +1,4 @@
1252-package types
1253+package common
1254
1255 import (
1256 "errors"
1257diff --git a/internal/tui/bubbles/git/types/formatter.go b/pkg/tui/common/formatter.go
1258rename from internal/tui/bubbles/git/types/formatter.go
1259rename to pkg/tui/common/formatter.go
1260index a7ef850e834284ddb0a41807ccd059429260d4a0..11143ec48a2694437cf63ae9e7728c2ad61198dc 100644
1261--- a/internal/tui/bubbles/git/types/formatter.go
1262+++ b/pkg/tui/common/formatter.go
1263@@ -1,4 +1,4 @@
1264-package types
1265+package common
1266
1267 import (
1268 "strings"
1269diff --git a/pkg/tui/common/git.go b/pkg/tui/common/git.go
1270new file mode 100644
1271index 0000000000000000000000000000000000000000..64a371de644feef6adb5d41c00dda4ed59c28e2f
1272--- /dev/null
1273+++ b/pkg/tui/common/git.go
1274@@ -0,0 +1,16 @@
1275+package common
1276+
1277+import (
1278+ "github.com/charmbracelet/soft-serve/pkg/git"
1279+)
1280+
1281+type GitRepo interface {
1282+ Name() string
1283+ Readme() (string, string)
1284+ HEAD() (*git.Reference, error)
1285+ CommitsByPage(*git.Reference, int, int) (git.Commits, error)
1286+ CountCommits(*git.Reference) (int64, error)
1287+ Diff(*git.Commit) (*git.Diff, error)
1288+ References() ([]*git.Reference, error)
1289+ Tree(*git.Reference, string) (*git.Tree, error)
1290+}
1291diff --git a/internal/tui/bubbles/git/types/help.go b/pkg/tui/common/help.go
1292rename from internal/tui/bubbles/git/types/help.go
1293rename to pkg/tui/common/help.go
1294index c42fa527270b56b469b5d029f933add804d2de99..5bc1a9a8df577879b048fa0bceee2dad4089676d 100644
1295--- a/internal/tui/bubbles/git/types/help.go
1296+++ b/pkg/tui/common/help.go
1297@@ -1,4 +1,4 @@
1298-package types
1299+package common
1300
1301 type BubbleHelper interface {
1302 Help() []HelpEntry
1303diff --git a/internal/tui/bubbles/git/types/reset.go b/pkg/tui/common/reset.go
1304rename from internal/tui/bubbles/git/types/reset.go
1305rename to pkg/tui/common/reset.go
1306index 919ab3d896bcc1dfa7d677f7e5df94d9a6ae6a4e..fe92b47154c9fe0850563dea7e9f87550c281c36 100644
1307--- a/internal/tui/bubbles/git/types/reset.go
1308+++ b/pkg/tui/common/reset.go
1309@@ -1,4 +1,4 @@
1310-package types
1311+package common
1312
1313 import tea "github.com/charmbracelet/bubbletea"
1314
1315diff --git a/internal/tui/bubbles/git/types/utils.go b/pkg/tui/common/utils.go
1316rename from internal/tui/bubbles/git/types/utils.go
1317rename to pkg/tui/common/utils.go
1318index c1758097d235186a24d120676b42b667604f8fff..2cafee64e60735a350363506c578dcf2bb9cb193 100644
1319--- a/internal/tui/bubbles/git/types/utils.go
1320+++ b/pkg/tui/common/utils.go
1321@@ -1,4 +1,4 @@
1322-package types
1323+package common
1324
1325 import "github.com/muesli/reflow/truncate"
1326
1327diff --git a/internal/tui/bubbles/git/log/bubble.go b/pkg/tui/log/bubble.go
1328rename from internal/tui/bubbles/git/log/bubble.go
1329rename to pkg/tui/log/bubble.go
1330index 579876737765bc3ab00251642fb2076600478cae..43128d2bafbbcc3a6541f5f94f35e2e518a59418 100644
1331--- a/internal/tui/bubbles/git/log/bubble.go
1332+++ b/pkg/tui/log/bubble.go
1333@@ -1,10 +1,8 @@
1334 package log
1335
1336 import (
1337- "context"
1338 "fmt"
1339 "io"
1340- "math"
1341 "strings"
1342 "time"
1343
1344@@ -13,13 +11,11 @@ import (
1345 "github.com/charmbracelet/bubbles/viewport"
1346 tea "github.com/charmbracelet/bubbletea"
1347 gansi "github.com/charmbracelet/glamour/ansi"
1348- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
1349- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1350- vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
1351 "github.com/charmbracelet/soft-serve/internal/tui/style"
1352- "github.com/dustin/go-humanize/english"
1353- "github.com/go-git/go-git/v5/plumbing"
1354- "github.com/go-git/go-git/v5/plumbing/object"
1355+ "github.com/charmbracelet/soft-serve/pkg/git"
1356+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
1357+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
1358+ vp "github.com/charmbracelet/soft-serve/pkg/tui/viewport"
1359 )
1360
1361 var (
1362@@ -30,7 +26,7 @@ var (
1363 waitBeforeLoading = time.Millisecond * 300
1364 )
1365
1366-type commitMsg *object.Commit
1367+type commitMsg *git.Commit
1368
1369 type sessionState int
1370
1371@@ -42,13 +38,12 @@ const (
1372 )
1373
1374 type item struct {
1375- *object.Commit
1376+ *git.Commit
1377 }
1378
1379 func (i item) Title() string {
1380- lines := strings.Split(i.Message, "\n")
1381- if len(lines) > 0 {
1382- return lines[0]
1383+ if i.Commit != nil {
1384+ return strings.Split(i.Commit.Message, "\n")[0]
1385 }
1386 return ""
1387 }
1388@@ -67,40 +62,45 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1389 if !ok {
1390 return
1391 }
1392+ if i.Commit == nil {
1393+ return
1394+ }
1395
1396+ hash := i.ID.String()
1397 leftMargin := d.style.LogItemSelector.GetMarginLeft() +
1398 d.style.LogItemSelector.GetWidth() +
1399 d.style.LogItemHash.GetMarginLeft() +
1400 d.style.LogItemHash.GetWidth() +
1401 d.style.LogItemInactive.GetMarginLeft()
1402- title := types.TruncateString(i.Title(), m.Width()-leftMargin, "…")
1403+ title := common.TruncateString(i.Title(), m.Width()-leftMargin, "…")
1404 if index == m.Index() {
1405 fmt.Fprint(w, d.style.LogItemSelector.Render(">")+
1406- d.style.LogItemHash.Bold(true).Render(i.Hash.String()[:7])+
1407+ d.style.LogItemHash.Bold(true).Render(hash[:7])+
1408 d.style.LogItemActive.Render(title))
1409 } else {
1410 fmt.Fprint(w, d.style.LogItemSelector.Render(" ")+
1411- d.style.LogItemHash.Render(i.Hash.String()[:7])+
1412+ d.style.LogItemHash.Render(hash[:7])+
1413 d.style.LogItemInactive.Render(title))
1414 }
1415 }
1416
1417 type Bubble struct {
1418- repo types.Repo
1419+ repo common.GitRepo
1420+ count int64
1421 list list.Model
1422 state sessionState
1423 commitViewport *vp.ViewportBubble
1424- ref *plumbing.Reference
1425+ ref *git.Reference
1426 style *style.Styles
1427 width int
1428 widthMargin int
1429 height int
1430 heightMargin int
1431- error types.ErrMsg
1432+ error common.ErrMsg
1433diff --git a/internal/tui/bubbles/git/refs/bubble.go b/pkg/tui/refs/bubble.go
1434rename from internal/tui/bubbles/git/refs/bubble.go
1435rename to pkg/tui/refs/bubble.go
1436index 0c99b4cbb7bd26a5d8f381de779285955c60f7e3..f31f12aaceb6c2855c93b886788f169411d96588 100644
1437--- a/internal/tui/bubbles/git/refs/bubble.go
1438+++ b/pkg/tui/refs/bubble.go
1439@@ -7,19 +7,19 @@ import (
1440
1441 "github.com/charmbracelet/bubbles/list"
1442 tea "github.com/charmbracelet/bubbletea"
1443- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1444 "github.com/charmbracelet/soft-serve/internal/tui/style"
1445- "github.com/go-git/go-git/v5/plumbing"
1446+ "github.com/charmbracelet/soft-serve/pkg/git"
1447+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
1448 )
1449
1450-type RefMsg = *plumbing.Reference
1451+type RefMsg = *git.Reference
1452
1453 type item struct {
1454- *plumbing.Reference
1455+ *git.Reference
1456 }
1457
1458 func (i item) Short() string {
1459- return i.Name().Short()
1460+ return i.Reference.Name().Short()
1461 }
1462
1463 func (i item) FilterValue() string { return i.Short() }
1464@@ -29,7 +29,7 @@ type items []item
1465 func (cl items) Len() int { return len(cl) }
1466 func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
1467 func (cl items) Less(i, j int) bool {
1468- return cl[i].Name().Short() < cl[j].Name().Short()
1469+ return cl[i].Short() < cl[j].Short()
1470 }
1471
1472 type itemDelegate struct {
1473@@ -47,7 +47,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1474 }
1475
1476 ref := i.Short()
1477- if i.Name().IsTag() {
1478+ if i.Reference.IsTag() {
1479 ref = s.RefItemTag.Render(ref)
1480 }
1481 ref = s.RefItemBranch.Render(ref)
1482@@ -55,7 +55,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1483 s.RefItemSelector.GetMarginLeft() -
1484 s.RefItemSelector.GetWidth() -
1485 s.RefItemInactive.GetMarginLeft()
1486- ref = types.TruncateString(ref, refMaxWidth, "…")
1487+ ref = common.TruncateString(ref, refMaxWidth, "…")
1488 if index == m.Index() {
1489 fmt.Fprint(w, s.RefItemSelector.Render(">")+
1490 s.RefItemActive.Render(ref))
1491@@ -66,17 +66,21 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1492 }
1493
1494 type Bubble struct {
1495- repo types.Repo
1496+ repo common.GitRepo
1497 list list.Model
1498 style *style.Styles
1499 width int
1500 widthMargin int
1501 height int
1502 heightMargin int
1503- ref *plumbing.Reference
1504+ ref *git.Reference
1505 }
1506
1507-func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
1508+func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
1509+ head, err := repo.HEAD()
1510+ if err != nil {
1511+ return nil
1512+ }
1513 l := list.NewModel([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
1514 l.SetShowFilter(false)
1515 l.SetShowHelp(false)
1516@@ -93,13 +97,13 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
1517 widthMargin: widthMargin,
1518 heightMargin: heightMargin,
1519 list: l,
1520- ref: repo.GetHEAD(),
1521+ ref: head,
1522 }
1523 b.SetSize(width, height)
1524 return b
1525 }
1526
1527-func (b *Bubble) SetBranch(ref *plumbing.Reference) (tea.Model, tea.Cmd) {
1528+func (b *Bubble) SetBranch(ref *git.Reference) (tea.Model, tea.Cmd) {
1529 b.ref = ref
1530 return b, func() tea.Msg {
1531 return RefMsg(ref)
1532@@ -123,21 +127,21 @@ func (b *Bubble) SetSize(width, height int) {
1533 b.list.Styles.PaginationStyle = b.style.RefPaginator.Copy().Width(width - b.widthMargin)
1534 }
1535
1536-func (b *Bubble) Help() []types.HelpEntry {
1537+func (b *Bubble) Help() []common.HelpEntry {
1538 return nil
1539diff --git a/internal/tui/bubbles/git/tree/bubble.go b/pkg/tui/tree/bubble.go
1540rename from internal/tui/bubbles/git/tree/bubble.go
1541rename to pkg/tui/tree/bubble.go
1542index e6c2a22d4698cb964352b1468a33569a80b7b775..c0e735c2f7f375c9aeb77daeb04198ebd4c107b0 100644
1543--- a/internal/tui/bubbles/git/tree/bubble.go
1544+++ b/pkg/tui/tree/bubble.go
1545@@ -3,22 +3,20 @@ package tree
1546 import (
1547 "fmt"
1548 "io"
1549+ "io/fs"
1550 "path/filepath"
1551- "sort"
1552 "strings"
1553
1554 "github.com/charmbracelet/bubbles/list"
1555 "github.com/charmbracelet/bubbles/viewport"
1556 tea "github.com/charmbracelet/bubbletea"
1557 "github.com/charmbracelet/lipgloss"
1558- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
1559- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1560- vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
1561 "github.com/charmbracelet/soft-serve/internal/tui/style"
1562+ "github.com/charmbracelet/soft-serve/pkg/git"
1563+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
1564+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
1565+ vp "github.com/charmbracelet/soft-serve/pkg/tui/viewport"
1566 "github.com/dustin/go-humanize"
1567- "github.com/go-git/go-git/v5/plumbing"
1568- "github.com/go-git/go-git/v5/plumbing/filemode"
1569- "github.com/go-git/go-git/v5/plumbing/object"
1570 )
1571
1572 type fileMsg struct {
1573@@ -34,16 +32,15 @@ const (
1574 )
1575
1576 type item struct {
1577- entry *object.TreeEntry
1578- file *object.File
1579+ entry *git.TreeEntry
1580 }
1581
1582 func (i item) Name() string {
1583- return i.entry.Name
1584+ return i.entry.Name()
1585 }
1586
1587-func (i item) Mode() filemode.FileMode {
1588- return i.entry.Mode
1589+func (i item) Mode() fs.FileMode {
1590+ return i.entry.Mode()
1591 }
1592
1593 func (i item) FilterValue() string { return i.Name() }
1594@@ -53,11 +50,11 @@ type items []item
1595 func (cl items) Len() int { return len(cl) }
1596 func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
1597 func (cl items) Less(i, j int) bool {
1598- if cl[i].Mode() == filemode.Dir && cl[j].Mode() == filemode.Dir {
1599+ if cl[i].entry.IsTree() && cl[j].entry.IsTree() {
1600 return cl[i].Name() < cl[j].Name()
1601- } else if cl[i].Mode() == filemode.Dir {
1602+ } else if cl[i].entry.IsTree() {
1603 return true
1604- } else if cl[j].Mode() == filemode.Dir {
1605+ } else if cl[j].entry.IsTree() {
1606 return false
1607 } else {
1608 return cl[i].Name() < cl[j].Name()
1609@@ -79,15 +76,13 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1610 }
1611
1612 name := i.Name()
1613- if i.Mode() == filemode.Dir {
1614+ size := humanize.Bytes(uint64(i.entry.Size()))
1615+ if i.entry.IsTree() {
1616+ size = ""
1617 name = s.TreeFileDir.Render(name)
1618 }
1619- size := ""
1620- if i.file != nil {
1621- size = humanize.Bytes(uint64(i.file.Size))
1622- }
1623 var cs lipgloss.Style
1624- mode, _ := i.Mode().ToOSFileMode()
1625+ mode := i.Mode()
1626 if index == m.Index() {
1627 cs = s.TreeItemActive
1628 fmt.Fprint(w, s.TreeItemSelector.Render(">"))
1629@@ -101,7 +96,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1630 s.TreeFileMode.GetWidth() +
1631 cs.GetMarginLeft()
1632 rightMargin := s.TreeFileSize.GetMarginLeft() + lipgloss.Width(size)
1633- name = types.TruncateString(name, m.Width()-leftMargin-rightMargin, "…")
1634+ name = common.TruncateString(name, m.Width()-leftMargin-rightMargin, "…")
1635 sizeStyle := s.TreeFileSize.Copy().
1636 Width(m.Width() -
1637 leftMargin -
1638@@ -114,7 +109,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
1639 }
1640
1641 type Bubble struct {
1642- repo types.Repo
1643+ repo common.GitRepo
1644 list list.Model
1645diff --git a/internal/tui/bubbles/git/viewport/viewport_patch.go b/pkg/tui/viewport/viewport_patch.go
1646rename from internal/tui/bubbles/git/viewport/viewport_patch.go
1647rename to pkg/tui/viewport/viewport_patch.go
1648diff --git a/server/middleware.go b/server/middleware.go
1649index e8b86b8c568cf6ac28bf923cd6630ef2cb76174d..a87ae492deaebd1b22e8152109fb5275aae4dbe7 100644
1650--- a/server/middleware.go
1651+++ b/server/middleware.go
1652@@ -3,19 +3,18 @@ package server
1653 import (
1654 "fmt"
1655 "path/filepath"
1656- "sort"
1657 "strings"
1658
1659 "github.com/alecthomas/chroma/lexers"
1660 gansi "github.com/charmbracelet/glamour/ansi"
1661 "github.com/charmbracelet/lipgloss"
1662 appCfg "github.com/charmbracelet/soft-serve/internal/config"
1663- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
1664+ "github.com/charmbracelet/soft-serve/pkg/git"
1665+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
1666 "github.com/charmbracelet/wish"
1667 gitwish "github.com/charmbracelet/wish/git"
1668 "github.com/gliderlabs/ssh"
1669- "github.com/go-git/go-git/v5/plumbing/filemode"
1670- "github.com/go-git/go-git/v5/plumbing/object"
1671+ ggit "github.com/gogs/git-module"
1672 "github.com/muesli/termenv"
1673 )
1674
1675@@ -27,22 +26,6 @@ var (
1676 filemodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
1677 )
1678
1679-type entries []object.TreeEntry
1680-
1681-func (cl entries) Len() int { return len(cl) }
1682-func (cl entries) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
1683-func (cl entries) Less(i, j int) bool {
1684- if cl[i].Mode == filemode.Dir && cl[j].Mode == filemode.Dir {
1685- return cl[i].Name < cl[j].Name
1686- } else if cl[i].Mode == filemode.Dir {
1687- return true
1688- } else if cl[j].Mode == filemode.Dir {
1689- return false
1690- } else {
1691- return cl[i].Name < cl[j].Name
1692- }
1693-}
1694-
1695 // softServeMiddleware is a middleware that handles displaying files with the
1696 // option of syntax highlighting and line numbers.
1697 func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
1698@@ -90,15 +73,27 @@ func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
1699 _ = s.Exit(1)
1700 return
1701 }
1702+ ref, err := rs.HEAD()
1703+ if err != nil {
1704+ _, _ = s.Write([]byte(err.Error()))
1705+ _ = s.Exit(1)
1706+ return
1707+ }
1708 p := strings.Join(ps[1:], "/")
1709- t, err := rs.LatestTree(p)
1710- if err != nil && err != object.ErrDirectoryNotFound {
1711+ t, err := rs.Tree(ref, p)
1712+ if err != nil && err != ggit.ErrRevisionNotExist {
1713 _, _ = s.Write([]byte(err.Error()))
1714 _ = s.Exit(1)
1715 return
1716 }
1717- if err == object.ErrDirectoryNotFound {
1718- fc, err := rs.LatestFile(p)
1719+ if err == ggit.ErrRevisionNotExist {
1720+ _, _ = s.Write([]byte(git.ErrFileNotFound.Error()))
1721+ _ = s.Exit(1)
1722+ return
1723+ }
1724+ ents, err := t.Entries()
1725+ if err != nil {
1726+ fc, _, err := rs.LatestFile(p)
1727 if err != nil {
1728 _, _ = s.Write([]byte(err.Error()))
1729 _ = s.Exit(1)
1730@@ -118,20 +113,19 @@ func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
1731 }
1732 s.Write([]byte(fc))
1733 } else {
1734- ents := entries(t.Entries)
1735- sort.Sort(ents)
1736+ ents.Sort()
1737 for _, e := range ents {
1738- m, _ := e.Mode.ToOSFileMode()
1739+ m := e.Mode()
1740 if m == 0 {
1741 s.Write([]byte(strings.Repeat(" ", 10)))
1742 } else {
1743 s.Write([]byte(filemodeStyle.Render(m.String())))
1744 }
1745 s.Write([]byte(" "))
1746- if e.Mode.IsFile() {
1747- s.Write([]byte(filenameStyle.Render(e.Name)))
1748+ if !e.IsTree() {
1749+ s.Write([]byte(filenameStyle.Render(e.Name())))
1750 } else {
1751- s.Write([]byte(dirnameStyle.Render(e.Name)))
1752diff --git a/server/server.go b/server/server.go
1753index 7b72f6ca3705571f3c31248994dbde0e91a636b8..3f6a56ccbfe198da90c8985c25c6c3938729465c 100644
1754--- a/server/server.go
1755+++ b/server/server.go
1756@@ -8,7 +8,6 @@ import (
1757 "github.com/charmbracelet/soft-serve/config"
1758 appCfg "github.com/charmbracelet/soft-serve/internal/config"
1759 "github.com/charmbracelet/soft-serve/internal/tui"
1760-
1761 "github.com/charmbracelet/wish"
1762 bm "github.com/charmbracelet/wish/bubbletea"
1763 gm "github.com/charmbracelet/wish/git"