Detailed changes
@@ -12,18 +12,20 @@ require (
github.com/charmbracelet/wish v0.2.1-0.20220208182816-534842b53d2a
github.com/dustin/go-humanize v1.0.0
github.com/gliderlabs/ssh v0.3.3
- github.com/go-git/go-billy/v5 v5.3.1
- github.com/go-git/go-git/v5 v5.4.2
+ github.com/go-git/go-billy/v5 v5.3.1 // indirect
+ github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 // indirect
github.com/matryer/is v1.2.0
github.com/muesli/reflow v0.3.0
github.com/muesli/termenv v0.11.0
- github.com/sergi/go-diff v1.1.0 // indirect
+ github.com/sergi/go-diff v1.1.0
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/gobwas/glob v0.2.3
+ github.com/gogs/git-module v1.5.0
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/muesli/mango v0.1.0
github.com/muesli/roff v0.1.0
)
@@ -40,7 +42,6 @@ require (
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
- github.com/google/go-cmp v0.5.6 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@@ -48,6 +49,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
+ github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 // indirect
github.com/microcosm-cc/bluemonday v1.0.17 // indirect
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
@@ -58,10 +60,8 @@ require (
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/yuin/goldmark v1.4.4 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
- golang.org/x/net v0.0.0-20220111093109-d55c255bac03 // indirect
+ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
- golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
- gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
@@ -59,13 +59,17 @@ github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Ai
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8=
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
-github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
+github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4 h1:1RSUwVK7VjTeA82kcLIqz1EU70QRwFdZUlJW58gP4GY=
+github.com/go-git/go-git/v5 v5.4.3-0.20210630082519-b4368b2a2ca4/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gogs/git-module v1.5.0 h1:2aAO79c36R3L6TdKutbVJwr0YwSWfRbPNP456yxDXtk=
+github.com/gogs/git-module v1.5.0/go.mod h1:oN37FFStFjdnTJXsSbhIHKJXh2YeDsEcXPATVz/oeuQ=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
@@ -98,6 +102,8 @@ github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRC
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk=
+github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y=
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
@@ -160,8 +166,10 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220111093109-d55c255bac03 h1:0FB83qp0AzVJm+0wcIlauAjJ+tNdh7jLuacRYCIVv7s=
-golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -178,6 +186,7 @@ golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -189,7 +198,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -202,6 +210,5 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -3,6 +3,7 @@ package config
import (
"bytes"
"log"
+ "path/filepath"
"strings"
"sync"
"text/template"
@@ -15,8 +16,7 @@ import (
"github.com/charmbracelet/soft-serve/config"
"github.com/charmbracelet/soft-serve/internal/git"
- gg "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing/object"
+ gg "github.com/gogs/git-module"
)
// Config is the Soft Serve configuration.
@@ -30,7 +30,7 @@ type Config struct {
Repos []Repo `yaml:"repos"`
Source *git.RepoSource
Cfg *config.Config
- reloadMtx sync.Mutex
+ mtx sync.Mutex
}
// User contains user-level configuration for a repository.
@@ -106,13 +106,17 @@ func NewConfig(cfg *config.Config) (*Config, error) {
if err != nil {
return nil, err
}
+ err = c.Reload()
+ if err != nil {
+ return nil, err
+ }
return c, nil
}
// Reload reloads the configuration.
func (cfg *Config) Reload() error {
- cfg.reloadMtx.Lock()
- defer cfg.reloadMtx.Unlock()
+ cfg.mtx.Lock()
+ defer cfg.mtx.Unlock()
err := cfg.Source.LoadRepos()
if err != nil {
return err
@@ -121,7 +125,7 @@ func (cfg *Config) Reload() error {
if err != nil {
return err
}
- cs, err := cr.LatestFile("config.yaml")
+ cs, _, err := cr.LatestFile("config.yaml")
if err != nil {
return err
}
@@ -147,18 +151,8 @@ func (cfg *Config) Reload() error {
pat = rp
}
rm := ""
- f, err := r.FindLatestFile(pat)
- if err != nil && err != object.ErrFileNotFound {
- return err
- }
- if err == nil {
- fc, err := f.Contents()
- if err != nil {
- return err
- }
- rm = fc
- r.ReadmePath = f.Name
- }
+ fc, fp, _ := r.LatestFile(pat)
+ rm = fc
if name == "config" {
md, err := templatize(rm, cfg)
if err != nil {
@@ -166,7 +160,7 @@ func (cfg *Config) Reload() error {
}
rm = md
}
- r.Readme = rm
+ r.SetReadme(rm, fp)
}
return nil
}
@@ -187,21 +181,15 @@ func createFile(path string, content string) error {
func (cfg *Config) createDefaultConfigRepo(yaml string) error {
cn := "config"
rs := cfg.Source
- err := rs.LoadRepos()
- if err != nil {
- return err
- }
- _, err = rs.GetRepo(cn)
- if err == git.ErrMissingRepo {
- cr, err := rs.InitRepo(cn, true)
- if err != nil {
- return err
- }
- wt, err := cr.Repository().Worktree()
+ err := rs.LoadRepo(cn)
+ if os.IsNotExist(err) {
+ repo, err := rs.InitRepo(cn, true)
if err != nil {
return err
}
- rm, err := wt.Filesystem.Create("README.md")
+ wt := repo.Path()
+ defer os.RemoveAll(wt)
+ rm, err := os.Create(filepath.Join(wt, "README.md"))
if err != nil {
return err
}
@@ -209,7 +197,7 @@ func (cfg *Config) createDefaultConfigRepo(yaml string) error {
if err != nil {
return err
}
- cf, err := wt.Filesystem.Create("config.yaml")
+ cf, err := os.Create(filepath.Join(wt, "config.yaml"))
if err != nil {
return err
}
@@ -217,32 +205,25 @@ func (cfg *Config) createDefaultConfigRepo(yaml string) error {
if err != nil {
return err
}
- _, err = wt.Add("README.md")
+ err = gg.Add(wt, gg.AddOptions{All: true})
if err != nil {
return err
}
- _, err = wt.Add("config.yaml")
+ err = gg.CreateCommit(wt, &gg.Signature{
+ Name: "Soft Serve Server",
+ Email: "vt100@charm.sh",
+ }, "Default init")
if err != nil {
return err
}
- _, err = wt.Commit("Default init", &gg.CommitOptions{
- All: true,
- Author: &object.Signature{
- Name: "Soft Serve Server",
- Email: "vt100@charm.sh",
- },
- })
- if err != nil {
- return err
- }
- err = cr.Repository().Push(&gg.PushOptions{})
+ err = repo.Push("origin", "master")
if err != nil {
return err
}
} else if err != nil {
return err
}
- return cfg.Reload()
+ return nil
}
func (cfg *Config) isPrivate(repo string) bool {
@@ -1,24 +1,15 @@
package git
import (
- "context"
"errors"
"log"
"os"
- "os/exec"
"path/filepath"
- "sort"
"sync"
- gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
- "github.com/go-git/go-billy/v5/memfs"
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
- "github.com/go-git/go-git/v5/plumbing/storer"
- "github.com/go-git/go-git/v5/plumbing/transport"
- "github.com/go-git/go-git/v5/storage/memory"
+ "github.com/charmbracelet/soft-serve/pkg/git"
"github.com/gobwas/glob"
+ "github.com/golang/groupcache/lru"
)
// ErrMissingRepo indicates that the requested repository could not be found.
@@ -28,195 +19,126 @@ var ErrMissingRepo = errors.New("missing repo")
type Repo struct {
path string
repository *git.Repository
- Readme string
- ReadmePath string
- refCommits map[plumbing.Hash]gitypes.Commits
- head *plumbing.Reference
- refs []*plumbing.Reference
- trees map[plumbing.Hash]*object.Tree
- commits map[plumbing.Hash]*object.Commit
- patch map[plumbing.Hash]*object.Patch
+ readme string
+ readmePath string
+ head *git.Reference
+ refs []*git.Reference
+ patchCache *lru.Cache
}
-// GetName returns the name of the repository.
-func (r *Repo) Name() string {
- return filepath.Base(r.path)
-}
-
-// GetHEAD returns the reference for a repository.
-func (r *Repo) GetHEAD() *plumbing.Reference {
- return r.head
-}
-
-// SetHEAD sets the repository head reference.
-func (r *Repo) SetHEAD(ref *plumbing.Reference) error {
- r.head = ref
- return nil
-}
-
-// GetReferences returns the references for a repository.
-func (r *Repo) GetReferences() []*plumbing.Reference {
- return r.refs
-}
-
-// GetRepository returns the underlying go-git repository object.
-func (r *Repo) Repository() *git.Repository {
- return r.repository
-}
-
-// Tree returns the git tree for a given path.
-func (r *Repo) Tree(ref *plumbing.Reference, path string) (*object.Tree, error) {
- path = filepath.Clean(path)
- hash, err := r.targetHash(ref)
+// Open opens a Git repository.
+func (rs *RepoSource) Open(path string) (*Repo, error) {
+ rg, err := git.Open(path)
if err != nil {
return nil, err
}
- c, err := r.commitForHash(hash)
+ r := &Repo{
+ path: path,
+ repository: rg,
+ patchCache: lru.New(1000),
+ }
+ _, err = r.HEAD()
if err != nil {
return nil, err
}
- t, err := r.treeForHash(c.TreeHash)
+ _, err = r.References()
if err != nil {
return nil, err
}
- if path == "." {
- return t, nil
- }
- return t.Tree(path)
+ return r, nil
}
-func (r *Repo) treeForHash(treeHash plumbing.Hash) (*object.Tree, error) {
- var err error
- t, ok := r.trees[treeHash]
- if !ok {
- t, err = r.repository.TreeObject(treeHash)
- if err != nil {
- return nil, err
- }
- r.trees[treeHash] = t
- }
- return t, nil
+// Path returns the path to the repository.
+func (r *Repo) Path() string {
+ return r.path
}
-func (r *Repo) commitForHash(hash plumbing.Hash) (*object.Commit, error) {
- var err error
- co, ok := r.commits[hash]
- if !ok {
- co, err = r.repository.CommitObject(hash)
- if err != nil {
- return nil, err
- }
- r.commits[hash] = co
- }
- return co, nil
+// GetName returns the name of the repository.
+func (r *Repo) Name() string {
+ return filepath.Base(r.path)
}
-// PatchCtx returns the patch for a given commit.
-func (r *Repo) PatchCtx(ctx context.Context, commit *object.Commit) (*object.Patch, error) {
- hash := commit.Hash
- p, ok := r.patch[hash]
- if !ok {
- c, err := r.commitForHash(hash)
- if err != nil {
- return nil, err
- }
- // Using commit trees fixes the issue when generating diff for the first commit
- // https://github.com/go-git/go-git/issues/281
- tree, err := r.treeForHash(c.TreeHash)
- if err != nil {
- return nil, err
- }
- var parent *object.Commit
- parentTree := &object.Tree{}
- if c.NumParents() > 0 {
- parent, err = r.commitForHash(c.ParentHashes[0])
- if err != nil {
- return nil, err
- }
- parentTree, err = r.treeForHash(parent.TreeHash)
- if err != nil {
- return nil, err
- }
- }
- p, err = parentTree.PatchContext(ctx, tree)
- if err != nil {
- return nil, err
- }
- }
- return p, nil
+// Readme returns the readme and its path for the repository.
+func (r *Repo) Readme() (readme string, path string) {
+ return r.readme, r.readmePath
+}
+
+// SetReadme sets the readme for the repository.
+func (r *Repo) SetReadme(readme, path string) {
+ r.readme = readme
+ r.readmePath = path
}
-// GetCommits returns the commits for a repository.
-func (r *Repo) GetCommits(ref *plumbing.Reference) (gitypes.Commits, error) {
- hash, err := r.targetHash(ref)
+// HEAD returns the reference for a repository.
+func (r *Repo) HEAD() (*git.Reference, error) {
+ if r.head != nil {
+ return r.head, nil
+ }
+ h, err := r.repository.HEAD()
if err != nil {
return nil, err
}
- // return cached commits if available
- commits, ok := r.refCommits[hash]
- if ok {
- return commits, nil
+ r.head = h
+ return h, nil
+}
+
+// GetReferences returns the references for a repository.
+func (r *Repo) References() ([]*git.Reference, error) {
+ if r.refs != nil {
+ return r.refs, nil
}
- commits = gitypes.Commits{}
- co, err := r.commitForHash(hash)
+ refs, err := r.repository.References()
if err != nil {
return nil, err
}
- // traverse the commit tree to get all commits
- commits = append(commits, co)
- for co.NumParents() > 0 {
- co, err = r.commitForHash(co.ParentHashes[0])
- if err != nil {
- return nil, err
- }
- commits = append(commits, co)
+ r.refs = refs
+ return refs, nil
+}
+
+// Tree returns the git tree for a given path.
+func (r *Repo) Tree(ref *git.Reference, path string) (*git.Tree, error) {
+ return r.repository.TreePath(ref, path)
+}
+
+// Diff returns the diff for a given commit.
+func (r *Repo) Diff(commit *git.Commit) (*git.Diff, error) {
+ hash := commit.Hash.String()
+ c, ok := r.patchCache.Get(hash)
+ if ok {
+ return c.(*git.Diff), nil
}
+ diff, err := r.repository.Diff(commit)
if err != nil {
return nil, err
}
- sort.Sort(commits)
- // cache the commits in the repo
- r.refCommits[hash] = commits
- return commits, nil
+ r.patchCache.Add(hash, diff)
+ return diff, nil
}
-// targetHash returns the target hash for a given reference. If reference is an
-// annotated tag, find the target hash for that tag.
-func (r *Repo) targetHash(ref *plumbing.Reference) (plumbing.Hash, error) {
- hash := ref.Hash()
- if ref.Type() != plumbing.HashReference {
- return plumbing.ZeroHash, plumbing.ErrInvalidType
- }
- if ref.Name().IsTag() {
- to, err := r.repository.TagObject(hash)
- switch err {
- case nil:
- // annotated tag (object has a target hash)
- hash = to.Target
- case plumbing.ErrObjectNotFound:
- // lightweight tag (hash points to a commit)
- default:
- return plumbing.ZeroHash, err
- }
+// CountCommits returns the number of commits for a repository.
+func (r *Repo) CountCommits(ref *git.Reference) (int64, error) {
+ tc, err := r.repository.CountCommits(ref)
+ if err != nil {
+ return 0, err
}
- return hash, nil
+ return tc, nil
}
-// GetReadme returns the readme for a repository.
-func (r *Repo) GetReadme() string {
- return r.Readme
+// CommitsByPage returns the commits for a repository.
+func (r *Repo) CommitsByPage(ref *git.Reference, page, size int) (git.Commits, error) {
+ return r.repository.CommitsByPage(ref, page, size)
}
-// GetReadmePath returns the path to the readme for a repository.
-func (r *Repo) GetReadmePath() string {
- return r.ReadmePath
+// Push pushes the repository to the remote.
+func (r *Repo) Push(remote, branch string) error {
+ return r.repository.Push(remote, branch)
}
// RepoSource is a reference to an on-disk repositories.
type RepoSource struct {
Path string
mtx sync.Mutex
- repos []*Repo
+ repos map[string]*Repo
}
// NewRepoSource creates a new RepoSource.
@@ -226,6 +148,7 @@ func NewRepoSource(repoPath string) *RepoSource {
log.Fatal(err)
}
rs := &RepoSource{Path: repoPath}
+ rs.repos = make(map[string]*Repo, 0)
return rs
}
@@ -233,19 +156,22 @@ func NewRepoSource(repoPath string) *RepoSource {
func (rs *RepoSource) AllRepos() []*Repo {
rs.mtx.Lock()
defer rs.mtx.Unlock()
- return rs.repos
+ repos := make([]*Repo, 0, len(rs.repos))
+ for _, r := range rs.repos {
+ repos = append(repos, r)
+ }
+ return repos
}
// GetRepo returns a repository by name.
func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
rs.mtx.Lock()
defer rs.mtx.Unlock()
- for _, r := range rs.repos {
- if filepath.Base(r.path) == name {
- return r, nil
- }
+ r, ok := rs.repos[name]
+ if !ok {
+ return nil, ErrMissingRepo
}
- return nil, ErrMissingRepo
+ return r, nil
}
// InitRepo initializes a new Git repository.
@@ -253,142 +179,75 @@ func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
rs.mtx.Lock()
defer rs.mtx.Unlock()
rp := filepath.Join(rs.Path, name)
- rg, err := git.PlainInit(rp, bare)
+ rg, err := git.Init(rp, bare)
if err != nil {
return nil, err
}
- if bare {
- // Clone repo into memory storage
- ar, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
- URL: rp,
- })
- if err != nil && err != transport.ErrEmptyRemoteRepository {
- return nil, err
- }
- rg = ar
- }
r := &Repo{
path: rp,
repository: rg,
+ refs: []*git.Reference{
+ git.NewReference(rp, git.RefsHeads+"master"),
+ },
}
- rs.repos = append(rs.repos, r)
+ rs.repos[name] = r
return r, nil
}
-// LoadRepos opens Git repositories.
-func (rs *RepoSource) LoadRepos() error {
+// LoadRepo loads a repository from disk.
+func (rs *RepoSource) LoadRepo(name string) error {
rs.mtx.Lock()
defer rs.mtx.Unlock()
+ rp := filepath.Join(rs.Path, name)
+ r, err := rs.Open(rp)
+ if err != nil {
+ return err
+ }
+ rs.repos[name] = r
+ return nil
+}
+
+// LoadRepos opens Git repositories.
+func (rs *RepoSource) LoadRepos() error {
rd, err := os.ReadDir(rs.Path)
if err != nil {
return err
}
- rs.repos = make([]*Repo, 0)
for _, de := range rd {
- rp := filepath.Join(rs.Path, de.Name())
- rg, err := git.PlainOpen(rp)
+ err = rs.LoadRepo(de.Name())
if err != nil {
return err
}
- r, err := rs.loadRepo(rp, rg)
- if err != nil {
- return err
- }
- rs.repos = append(rs.repos, r)
}
return nil
}
-func (rs *RepoSource) loadRepo(path string, rg *git.Repository) (*Repo, error) {
- r := &Repo{
- path: path,
- repository: rg,
- patch: make(map[plumbing.Hash]*object.Patch),
- }
- r.commits = make(map[plumbing.Hash]*object.Commit)
- r.trees = make(map[plumbing.Hash]*object.Tree)
- r.refCommits = make(map[plumbing.Hash]gitypes.Commits)
- ref, err := rg.Head()
- if err != nil {
- return nil, err
- }
- r.head = ref
- l, err := r.repository.Log(&git.LogOptions{All: true})
- if err != nil {
- return nil, err
- }
- err = l.ForEach(func(c *object.Commit) error {
- r.commits[c.Hash] = c
- return nil
- })
- if err != nil {
- return nil, err
- }
- refs := make([]*plumbing.Reference, 0)
- ri, err := rg.References()
- if err != nil {
- return nil, err
- }
- ri.ForEach(func(r *plumbing.Reference) error {
- refs = append(refs, r)
- return nil
- })
- r.refs = refs
- return r, nil
-}
-
-// FindLatestFile returns the latest file for a given path.
-func (r *Repo) FindLatestFile(pattern string) (*object.File, error) {
- g, err := glob.Compile(pattern)
+// LatestFile returns the contents of the latest file at the specified path in the repository.
+func (r *Repo) LatestFile(pattern string) (string, string, error) {
+ g := glob.MustCompile(pattern)
+ dir := filepath.Dir(pattern)
+ t, err := r.repository.TreePath(r.head, dir)
if err != nil {
- return nil, err
+ return "", "", err
}
- c, err := r.commitForHash(r.head.Hash())
+ ents, err := t.Entries()
if err != nil {
- return nil, err
+ return "", "", err
}
- fi, err := c.Files()
- if err != nil {
- return nil, err
- }
- var f *object.File
- err = fi.ForEach(func(ff *object.File) error {
- if g.Match(ff.Name) {
- f = ff
- return storer.ErrStop
+ for _, e := range ents {
+ fp := filepath.Join(dir, e.Name())
+ if g.Match(fp) {
+ bts, err := e.Contents()
+ if err != nil {
+ return "", "", err
+ }
+ return string(bts), fp, nil
}
- return nil
- })
- if err != nil {
- return nil, err
- }
- if f == nil {
- return nil, object.ErrFileNotFound
}
- return f, nil
-}
-
-// LatestFile returns the contents of the latest file at the specified path in the repository.
-func (r *Repo) LatestFile(pattern string) (string, error) {
- f, err := r.FindLatestFile(pattern)
- if err != nil {
- return "", err
- }
- content, err := f.Contents()
- if err != nil {
- return "", err
- }
- return content, nil
-}
-
-// LatestTree returns the latest tree at the specified path in the repository.
-func (r *Repo) LatestTree(path string) (*object.Tree, error) {
- return r.Tree(r.head, path)
+ return "", "", git.ErrFileNotFound
}
// UpdateServerInfo updates the server info for the repository.
func (r *Repo) UpdateServerInfo() error {
- cmd := exec.Command("git", "update-server-info")
- cmd.Dir = r.path
- return cmd.Run()
+ return r.repository.UpdateServerInfo()
}
@@ -7,10 +7,10 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/soft-serve/internal/config"
- gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
"github.com/charmbracelet/soft-serve/internal/tui/bubbles/selection"
"github.com/charmbracelet/soft-serve/internal/tui/style"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
"github.com/gliderlabs/ssh"
)
@@ -157,19 +157,19 @@ func (b Bubble) headerView() string {
func (b Bubble) footerView() string {
w := &strings.Builder{}
- var h []gittypes.HelpEntry
+ var h []common.HelpEntry
if b.state != errorState {
- h = []gittypes.HelpEntry{
+ h = []common.HelpEntry{
{Key: "tab", Value: "section"},
}
- if box, ok := b.boxes[b.activeBox].(gittypes.BubbleHelper); ok {
+ if box, ok := b.boxes[b.activeBox].(common.BubbleHelper); ok {
help := box.Help()
for _, he := range help {
h = append(h, he)
}
}
}
- h = append(h, gittypes.HelpEntry{Key: "q", Value: "quit"})
+ h = append(h, common.HelpEntry{Key: "q", Value: "quit"})
for i, v := range h {
fmt.Fprint(w, helpEntryRender(v, b.styles))
if i != len(h)-1 {
@@ -178,13 +178,14 @@ func (b Bubble) footerView() string {
}
branch := ""
if b.state == loadedState {
- branch = b.boxes[1].(*repo.Bubble).Reference().Short()
+ ref := b.boxes[1].(*repo.Bubble).Reference()
+ branch = ref.Name().Short()
}
help := w.String()
branchMaxWidth := b.width - // bubble width
lipgloss.Width(help) - // help width
b.styles.App.GetHorizontalFrameSize() // App paddings
- branch = b.styles.Branch.Render(gittypes.TruncateString(branch, branchMaxWidth-1, "…"))
+ branch = b.styles.Branch.Render(common.TruncateString(branch, branchMaxWidth-1, "…"))
gap := lipgloss.NewStyle().
Width(b.width -
lipgloss.Width(help) -
@@ -228,6 +229,6 @@ func (b Bubble) View() string {
return b.styles.App.Render(s.String())
}
-func helpEntryRender(h gittypes.HelpEntry, s *style.Styles) string {
+func helpEntryRender(h common.HelpEntry, s *style.Styles) string {
return fmt.Sprintf("%s %s", s.HelpKey.Render(h.Key), s.HelpValue.Render(h.Value))
}
@@ -1,30 +0,0 @@
-package types
-
-import (
- "context"
-
- "github.com/go-git/go-git/v5"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
-)
-
-type Repo interface {
- Name() string
- GetHEAD() *plumbing.Reference
- SetHEAD(*plumbing.Reference) error
- GetReferences() []*plumbing.Reference
- GetReadme() string
- GetReadmePath() string
- GetCommits(*plumbing.Reference) (Commits, error)
- Repository() *git.Repository
- Tree(*plumbing.Reference, string) (*object.Tree, error)
- PatchCtx(context.Context, *object.Commit) (*object.Patch, error)
-}
-
-type Commits []*object.Commit
-
-func (cl Commits) Len() int { return len(cl) }
-func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
-func (cl Commits) Less(i, j int) bool {
- return cl[i].Author.When.After(cl[j].Author.When)
-}
@@ -6,10 +6,10 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- gitui "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git"
- gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/go-git/go-git/v5/plumbing"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ gitui "github.com/charmbracelet/soft-serve/pkg/tui"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
"github.com/muesli/reflow/truncate"
"github.com/muesli/reflow/wrap"
)
@@ -22,7 +22,7 @@ type Bubble struct {
name string
host string
port int
- repo gittypes.Repo
+ repo common.GitRepo
styles *style.Styles
width int
widthMargin int
@@ -33,7 +33,7 @@ type Bubble struct {
Active bool
}
-func NewBubble(repo gittypes.Repo, host string, port int, styles *style.Styles, width, wm, height, hm int) *Bubble {
+func NewBubble(repo common.GitRepo, host string, port int, styles *style.Styles, width, wm, height, hm int) *Bubble {
b := &Bubble{
name: repo.Name(),
host: host,
@@ -56,6 +56,9 @@ func (b *Bubble) Init() tea.Cmd {
func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
+ if msg.Width == b.width && msg.Height == b.height {
+ return b, nil
+ }
b.width = msg.Width
b.height = msg.Height
}
@@ -64,7 +67,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return b, cmd
}
-func (b *Bubble) Help() []gittypes.HelpEntry {
+func (b *Bubble) Help() []common.HelpEntry {
return b.box.Help()
}
@@ -95,7 +98,7 @@ func (b Bubble) headerView() string {
note = b.styles.RepoNote.Copy().Width(noteWidth).Render(note)
// Render borders on name and command
- height := gittypes.Max(lipgloss.Height(title), lipgloss.Height(note))
+ height := common.Max(lipgloss.Height(title), lipgloss.Height(note))
titleBoxStyle := b.styles.RepoTitleBox.Copy().Height(height)
noteBoxStyle := b.styles.RepoNoteBox.Copy().Height(height)
if b.Active {
@@ -121,7 +124,7 @@ func (b *Bubble) View() string {
return header + body
}
-func (b *Bubble) Reference() plumbing.ReferenceName {
+func (b *Bubble) Reference() *git.Reference {
return b.box.Reference()
}
@@ -5,8 +5,8 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- gittypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/charmbracelet/soft-serve/internal/tui/style"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
"github.com/muesli/reflow/truncate"
)
@@ -80,8 +80,8 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return b, tea.Batch(cmds...)
}
-func (b *Bubble) Help() []gittypes.HelpEntry {
- return []gittypes.HelpEntry{
+func (b *Bubble) Help() []common.HelpEntry {
+ return []common.HelpEntry{
{Key: "↑/↓", Value: "navigate"},
}
}
@@ -5,9 +5,9 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- gitypes "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/charmbracelet/soft-serve/internal/tui/bubbles/repo"
"github.com/charmbracelet/soft-serve/internal/tui/bubbles/selection"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
gm "github.com/charmbracelet/wish/git"
)
@@ -110,7 +110,7 @@ func (b *Bubble) newMenuEntry(name string, rn string) (MenuEntry, error) {
initCmd := rb.Init()
msg := initCmd()
switch msg := msg.(type) {
- case gitypes.ErrMsg:
+ case common.ErrMsg:
return me, fmt.Errorf("missing %s: %s", me.Repo, msg.Err.Error())
}
me.bubble = rb
@@ -0,0 +1,42 @@
+package git
+
+import (
+ "github.com/gogs/git-module"
+)
+
+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())
+}
+
+// Commit is a wrapper around git.Commit with helper methods.
+type Commit struct {
+ *git.Commit
+ Hash Hash
+}
+
+// Commits is a list of commits.
+type Commits []*Commit
+
+// Len implements sort.Interface.
+func (cl Commits) Len() int { return len(cl) }
+
+// Swap implements sort.Interface.
+func (cl Commits) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
+
+// Less implements sort.Interface.
+func (cl Commits) Less(i, j int) bool {
+ return cl[i].Author.When.After(cl[j].Author.When)
+}
@@ -0,0 +1,9 @@
+package git
+
+import "errors"
+
+var (
+ ErrFileNotFound = errors.New("file not found")
+ ErrDirectoryNotFound = errors.New("directory not found")
+ ErrReferenceNotFound = errors.New("reference not found")
+)
@@ -0,0 +1,329 @@
+package git
+
+import (
+ "bytes"
+ "fmt"
+ "math"
+ "strings"
+ "sync"
+
+ "github.com/dustin/go-humanize/english"
+ "github.com/gogs/git-module"
+ "github.com/sergi/go-diff/diffmatchpatch"
+)
+
+// DiffSection is a wrapper to git.DiffSection with helper methods.
+type DiffSection struct {
+ *git.DiffSection
+
+ initOnce sync.Once
+ dmp *diffmatchpatch.DiffMatchPatch
+}
+
+// diffFor computes inline diff for the given line.
+func (s *DiffSection) diffFor(line *git.DiffLine) string {
+ fallback := line.Content
+
+ // Find equivalent diff line, ignore when not found.
+ var diff1, diff2 string
+ switch line.Type {
+ case git.DiffLineAdd:
+ compareLine := s.Line(git.DiffLineDelete, line.RightLine)
+ if compareLine == nil {
+ return fallback
+ }
+
+ diff1 = compareLine.Content
+ diff2 = line.Content
+
+ case git.DiffLineDelete:
+ compareLine := s.Line(git.DiffLineAdd, line.LeftLine)
+ if compareLine == nil {
+ return fallback
+ }
+
+ diff1 = line.Content
+ diff2 = compareLine.Content
+
+ default:
+ return fallback
+ }
+
+ s.initOnce.Do(func() {
+ s.dmp = diffmatchpatch.New()
+ s.dmp.DiffEditCost = 100
+ })
+
+ diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)
+ diffs = s.dmp.DiffCleanupEfficiency(diffs)
+
+ return diffsToString(diffs, line.Type)
+}
+
+func diffsToString(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) string {
+ buf := bytes.NewBuffer(nil)
+
+ // Reproduce signs which are cutted for inline diff before.
+ switch lineType {
+ case git.DiffLineAdd:
+ buf.WriteByte('+')
+ case git.DiffLineDelete:
+ buf.WriteByte('-')
+ }
+
+ for i := range diffs {
+ switch {
+ case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
+ buf.WriteString(diffs[i].Text)
+ case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
+ buf.WriteString(diffs[i].Text)
+ case diffs[i].Type == diffmatchpatch.DiffEqual:
+ buf.WriteString(diffs[i].Text)
+ }
+ }
+
+ return string(buf.Bytes())
+}
+
+// DiffFile is a wrapper to git.DiffFile with helper methods.
+type DiffFile struct {
+ *git.DiffFile
+ Sections []*DiffSection
+}
+
+type DiffFileChange struct {
+ hash string
+ name string
+ mode git.EntryMode
+}
+
+func (f *DiffFileChange) Hash() string {
+ return f.hash
+}
+
+func (f *DiffFileChange) Name() string {
+ return f.name
+}
+
+func (f *DiffFileChange) Mode() git.EntryMode {
+ return f.mode
+}
+
+func (f *DiffFile) Files() (from *DiffFileChange, to *DiffFileChange) {
+ if f.OldIndex != ZeroHash.String() {
+ from = &DiffFileChange{
+ hash: f.OldIndex,
+ name: f.OldName(),
+ mode: f.OldMode(),
+ }
+ }
+ if f.Index != ZeroHash.String() {
+ to = &DiffFileChange{
+ hash: f.Index,
+ name: f.Name,
+ mode: f.Mode(),
+ }
+ }
+ return
+}
+
+// FileStats
+type FileStats []*DiffFile
+
+// String returns a string representation of file stats.
+func (fs FileStats) String() string {
+ return printStats(fs)
+}
+
+func printStats(stats FileStats) string {
+ padLength := float64(len(" "))
+ newlineLength := float64(len("\n"))
+ separatorLength := float64(len("|"))
+ // Soft line length limit. The text length calculation below excludes
+ // length of the change number. Adding that would take it closer to 80,
+ // but probably not more than 80, until it's a huge number.
+ lineLength := 72.0
+
+ // Get the longest filename and longest total change.
+ var longestLength float64
+ var longestTotalChange float64
+ for _, fs := range stats {
+ if int(longestLength) < len(fs.Name) {
+ longestLength = float64(len(fs.Name))
+ }
+ totalChange := fs.NumAdditions() + fs.NumDeletions()
+ if int(longestTotalChange) < totalChange {
+ longestTotalChange = float64(totalChange)
+ }
+ }
+
+ // Parts of the output:
+ // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
+ // example: " main.go | 10 +++++++--- "
+
+ // <pad><filename><pad>
+ leftTextLength := padLength + longestLength + padLength
+
+ // <pad><number><pad><+++++/-----><newline>
+ // Excluding number length here.
+ rightTextLength := padLength + padLength + newlineLength
+
+ totalTextArea := leftTextLength + separatorLength + rightTextLength
+ heightOfHistogram := lineLength - totalTextArea
+
+ // Scale the histogram.
+ var scaleFactor float64
+ if longestTotalChange > heightOfHistogram {
+ // Scale down to heightOfHistogram.
+ scaleFactor = longestTotalChange / heightOfHistogram
+ } else {
+ scaleFactor = 1.0
+ }
+
+ taddc := 0
+ tdelc := 0
+ output := strings.Builder{}
+ for _, fs := range stats {
+ taddc += fs.NumAdditions()
+ tdelc += fs.NumDeletions()
+ addn := float64(fs.NumAdditions())
+ deln := float64(fs.NumDeletions())
+ addc := int(math.Floor(addn / scaleFactor))
+ delc := int(math.Floor(deln / scaleFactor))
+ if addc < 0 {
+ addc = 0
+ }
+ if delc < 0 {
+ delc = 0
+ }
+ adds := strings.Repeat("+", addc)
+ dels := strings.Repeat("-", delc)
+ diffLines := fmt.Sprint(fs.NumAdditions() + fs.NumDeletions())
+ totalDiffLines := fmt.Sprint(int(longestTotalChange))
+ fmt.Fprintf(&output, "%s | %s %s%s\n",
+ fs.Name+strings.Repeat(" ", int(longestLength)-len(fs.Name)),
+ strings.Repeat(" ", len(totalDiffLines)-len(diffLines))+diffLines,
+ adds,
+ dels)
+ }
+ files := len(stats)
+ fc := fmt.Sprintf("%s changed", english.Plural(files, "file", ""))
+ ins := fmt.Sprintf("%s(+)", english.Plural(taddc, "insertion", ""))
+ dels := fmt.Sprintf("%s(-)", english.Plural(tdelc, "deletion", ""))
+ fmt.Fprint(&output, fc)
+ if taddc > 0 {
+ fmt.Fprintf(&output, ", %s", ins)
+ }
+ if tdelc > 0 {
+ fmt.Fprintf(&output, ", %s", dels)
+ }
+ fmt.Fprint(&output, "\n")
+
+ return output.String()
+}
+
+// Diff is a wrapper around git.Diff with helper methods.
+type Diff struct {
+ *git.Diff
+ Files []*DiffFile
+}
+
+// FileStats returns the diff file stats.
+func (d *Diff) Stats() FileStats {
+ return d.Files
+}
+
+const (
+ dstPrefix = "b/"
+ srcPrefix = "a/"
+)
+
+func appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {
+ if isBinary {
+ return append(lines,
+ fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),
+ )
+ }
+ return append(lines,
+ fmt.Sprintf("--- %s", fromPath),
+ fmt.Sprintf("+++ %s", toPath),
+ )
+}
+
+func writeFilePatchHeader(sb *strings.Builder, filePatch *DiffFile) {
+ from, to := filePatch.Files()
+ if from == nil && to == nil {
+ return
+ }
+ isBinary := filePatch.IsBinary()
+
+ var lines []string
+ switch {
+ case from != nil && to != nil:
+ hashEquals := from.Hash() == to.Hash()
+ lines = append(lines,
+ fmt.Sprintf("diff --git %s%s %s%s",
+ srcPrefix, from.Name(), dstPrefix, to.Name()),
+ )
+ if from.Mode() != to.Mode() {
+ lines = append(lines,
+ fmt.Sprintf("old mode %o", from.Mode()),
+ fmt.Sprintf("new mode %o", to.Mode()),
+ )
+ }
+ if from.Name() != to.Name() {
+ lines = append(lines,
+ fmt.Sprintf("rename from %s", from.Name()),
+ fmt.Sprintf("rename to %s", to.Name()),
+ )
+ }
+ if from.Mode() != to.Mode() && !hashEquals {
+ lines = append(lines,
+ fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),
+ )
+ } else if !hashEquals {
+ lines = append(lines,
+ fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),
+ )
+ }
+ if !hashEquals {
+ lines = appendPathLines(lines, srcPrefix+from.Name(), dstPrefix+to.Name(), isBinary)
+ }
+ case from == nil:
+ lines = append(lines,
+ fmt.Sprintf("diff --git %s %s", srcPrefix+to.Name(), dstPrefix+to.Name()),
+ fmt.Sprintf("new file mode %o", to.Mode()),
+ fmt.Sprintf("index %s..%s", ZeroHash, to.Hash()),
+ )
+ lines = appendPathLines(lines, "/dev/null", dstPrefix+to.Name(), isBinary)
+ case to == nil:
+ lines = append(lines,
+ fmt.Sprintf("diff --git %s %s", srcPrefix+from.Name(), dstPrefix+from.Name()),
+ fmt.Sprintf("deleted file mode %o", from.Mode()),
+ fmt.Sprintf("index %s..%s", from.Hash(), ZeroHash),
+ )
+ lines = appendPathLines(lines, srcPrefix+from.Name(), "/dev/null", isBinary)
+ }
+
+ sb.WriteString(lines[0])
+ for _, line := range lines[1:] {
+ sb.WriteByte('\n')
+ sb.WriteString(line)
+ }
+ sb.WriteByte('\n')
+}
+
+// Patch returns the diff as a patch.
+func (d *Diff) Patch() string {
+ var p strings.Builder
+ for _, f := range d.Files {
+ writeFilePatchHeader(&p, f)
+ for _, s := range f.Sections {
+ for _, l := range s.Lines {
+ p.WriteString(s.diffFor(l))
+ p.WriteString("\n")
+ }
+ }
+ }
+ return p.String()
+}
@@ -0,0 +1,76 @@
+package git
+
+import (
+ "strings"
+
+ "github.com/gogs/git-module"
+)
+
+const (
+ // HEAD represents the name of the HEAD reference.
+ HEAD = "HEAD"
+ // RefsHeads represents the prefix for branch references.
+ RefsHeads = git.RefsHeads
+ // RefsTags represents the prefix for tag references.
+ RefsTags = git.RefsTags
+)
+
+// Reference is a wrapper around git.Reference with helper methods.
+type Reference struct {
+ *git.Reference
+ Hash Hash
+ path string // repo path
+}
+
+// ReferenceName is a Refspec wrapper.
+type ReferenceName string
+
+// NewReference creates a new reference.
+func NewReference(rp, refspec string) *Reference {
+ return &Reference{
+ Reference: &git.Reference{
+ Refspec: refspec,
+ },
+ path: rp,
+ }
+}
+
+// String returns the reference name i.e. refs/heads/master.
+func (r ReferenceName) String() string {
+ return string(r)
+}
+
+// Short returns the short name of the reference i.e. master.
+func (r ReferenceName) Short() string {
+ s := strings.Split(r.String(), "/")
+ if len(s) > 0 {
+ return s[len(s)-1]
+ }
+ return r.String()
+}
+
+// Name returns the reference name i.e. refs/heads/master.
+func (r *Reference) Name() ReferenceName {
+ return ReferenceName(r.Refspec)
+}
+
+// IsBranch returns true if the reference is a branch.
+func (r *Reference) IsBranch() bool {
+ return strings.HasPrefix(r.Refspec, git.RefsHeads)
+}
+
+// IsTag returns true if the reference is a tag.
+func (r *Reference) IsTag() bool {
+ return strings.HasPrefix(r.Refspec, git.RefsTags)
+}
+
+// TargetHash returns the hash of the reference target.
+func (r *Reference) TargetHash() Hash {
+ if r.IsTag() {
+ id, err := git.ShowRefVerify(r.path, r.Refspec)
+ if err == nil {
+ return Hash(id)
+ }
+ }
+ return r.Hash
+}
@@ -0,0 +1,185 @@
+package git
+
+import (
+ "path/filepath"
+
+ "github.com/gogs/git-module"
+)
+
+var (
+ DiffMaxFiles = 1000
+ DiffMaxFileLines = 1000
+ DiffMaxLineChars = 1000
+)
+
+// Repository is a wrapper around git.Repository with helper methods.
+type Repository struct {
+ *git.Repository
+ Path string
+}
+
+// Clone clones a repository.
+func Clone(src, dst string, opts ...git.CloneOptions) error {
+ return git.Clone(src, dst, opts...)
+}
+
+// Init initializes and opens a new git repository.
+func Init(path string, bare bool) (*Repository, error) {
+ err := git.Init(path, git.InitOptions{Bare: bare})
+ if err != nil {
+ return nil, err
+ }
+ return Open(path)
+}
+
+// Open opens a git repository at the given path.
+func Open(path string) (*Repository, error) {
+ repo, err := git.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ return &Repository{
+ Repository: repo,
+ Path: path,
+ }, nil
+}
+
+// Name returns the name of the repository.
+func (r *Repository) Name() string {
+ return filepath.Base(r.Path)
+}
+
+// HEAD returns the HEAD reference for a repository.
+func (r *Repository) HEAD() (*Reference, error) {
+ rn, err := r.SymbolicRef()
+ if err != nil {
+ return nil, err
+ }
+ hash, err := r.ShowRefVerify(rn)
+ if err != nil {
+ return nil, err
+ }
+ return &Reference{
+ Reference: &git.Reference{
+ ID: hash,
+ Refspec: rn,
+ },
+ Hash: Hash(hash),
+ path: r.Path,
+ }, nil
+}
+
+// References returns the references for a repository.
+func (r *Repository) References() ([]*Reference, error) {
+ refs, err := r.ShowRef()
+ if err != nil {
+ return nil, err
+ }
+ rrefs := make([]*Reference, 0, len(refs))
+ for _, ref := range refs {
+ rrefs = append(rrefs, &Reference{
+ Reference: ref,
+ Hash: Hash(ref.ID),
+ path: r.Path,
+ })
+ }
+ return rrefs, nil
+}
+
+// Tree returns the tree for the given reference.
+func (r *Repository) Tree(ref *Reference) (*Tree, error) {
+ if ref == nil {
+ rref, err := r.HEAD()
+ if err != nil {
+ return nil, err
+ }
+ ref = rref
+ }
+ tree, err := r.LsTree(ref.Hash.String())
+ if err != nil {
+ return nil, err
+ }
+ return &Tree{
+ Tree: tree,
+ Path: "",
+ }, nil
+}
+
+// TreePath returns the tree for the given path.
+func (r *Repository) TreePath(ref *Reference, path string) (*Tree, error) {
+ path = filepath.Clean(path)
+ if path == "." {
+ path = ""
+ }
+ if path == "" {
+ return r.Tree(ref)
+ }
+ t, err := r.Tree(ref)
+ if err != nil {
+ return nil, err
+ }
+ return t.SubTree(path)
+}
+
+// Diff returns the diff for the given commit.
+func (r *Repository) Diff(commit *Commit) (*Diff, error) {
+ ddiff, err := r.Repository.Diff(commit.Hash.String(), DiffMaxFiles, DiffMaxFileLines, DiffMaxLineChars)
+ if err != nil {
+ return nil, err
+ }
+ files := make([]*DiffFile, 0, len(ddiff.Files))
+ for _, df := range ddiff.Files {
+ sections := make([]*DiffSection, 0, len(df.Sections))
+ for _, ds := range df.Sections {
+ sections = append(sections, &DiffSection{
+ DiffSection: ds,
+ })
+ }
+ files = append(files, &DiffFile{
+ DiffFile: df,
+ Sections: sections,
+ })
+ }
+ diff := &Diff{
+ Diff: ddiff,
+ Files: files,
+ }
+ return diff, nil
+}
+
+// Patch returns the patch for the given reference.
+func (r *Repository) Patch(commit *Commit) (string, error) {
+ diff, err := r.Diff(commit)
+ if err != nil {
+ return "", err
+ }
+ return diff.Patch(), err
+}
+
+// CountCommits returns the number of commits in the repository.
+func (r *Repository) CountCommits(ref *Reference) (int64, error) {
+ return r.Repository.RevListCount([]string{ref.Name().String()})
+}
+
+// CommitsByPage returns the commits for a given page and size.
+func (r *Repository) CommitsByPage(ref *Reference, page, size int) (Commits, error) {
+ cs, err := r.Repository.CommitsByPage(ref.Name().String(), page, size)
+ if err != nil {
+ return nil, err
+ }
+ commits := make(Commits, len(cs))
+ for i, c := range cs {
+ commits[i] = &Commit{
+ Commit: c,
+ Hash: Hash(c.ID.String()),
+ }
+ }
+ return commits, nil
+}
+
+// UpdateServerInfo updates the repository server info.
+func (r *Repository) UpdateServerInfo() error {
+ cmd := git.NewCommand("update-server-info")
+ _, err := cmd.RunInDir(r.Path)
+ return err
+}
@@ -0,0 +1,191 @@
+package git
+
+import (
+ "bufio"
+ "bytes"
+ "io"
+ "io/fs"
+ "path/filepath"
+ "sort"
+
+ "github.com/gogs/git-module"
+)
+
+// Tree is a wrapper around git.Tree with helper methods.
+type Tree struct {
+ *git.Tree
+ Path string
+}
+
+// TreeEntry is a wrapper around git.TreeEntry with helper methods.
+type TreeEntry struct {
+ *git.TreeEntry
+ // path is the full path of the file
+ path string
+}
+
+// Entries is a wrapper around git.Entries
+type Entries []*TreeEntry
+
+var sorters = []func(t1, t2 *TreeEntry) bool{
+ func(t1, t2 *TreeEntry) bool {
+ return (t1.IsTree() || t1.IsCommit()) && !t2.IsTree() && !t2.IsCommit()
+ },
+ func(t1, t2 *TreeEntry) bool {
+ return t1.Name() < t2.Name()
+ },
+}
+
+// Len implements sort.Interface.
+func (es Entries) Len() int { return len(es) }
+
+// Swap implements sort.Interface.
+func (es Entries) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
+
+// Less implements sort.Interface.
+func (es Entries) Less(i, j int) bool {
+ t1, t2 := es[i], es[j]
+ var k int
+ for k = 0; k < len(sorters)-1; k++ {
+ sorter := sorters[k]
+ switch {
+ case sorter(t1, t2):
+ return true
+ case sorter(t2, t1):
+ return false
+ }
+ }
+ return sorters[k](t1, t2)
+}
+
+// Sort sorts the entries in the tree.
+func (es Entries) Sort() {
+ sort.Sort(es)
+}
+
+// File is a wrapper around git.Blob with helper methods.
+type File struct {
+ *git.Blob
+ Entry *TreeEntry
+}
+
+// Name returns the name of the file.
+func (f *File) Name() string {
+ return f.Entry.Name()
+}
+
+// Path returns the full path of the file.
+func (f *File) Path() string {
+ return f.Entry.path
+}
+
+// SubTree returns the sub-tree at the given path.
+func (t *Tree) SubTree(path string) (*Tree, error) {
+ tree, err := t.Subtree(path)
+ if err != nil {
+ return nil, err
+ }
+ return &Tree{
+ Tree: tree,
+ Path: path,
+ }, nil
+}
+
+// Entries returns the entries in the tree.
+func (t *Tree) Entries() (Entries, error) {
+ entries, err := t.Tree.Entries()
+ if err != nil {
+ return nil, err
+ }
+ ret := make(Entries, len(entries))
+ for i, e := range entries {
+ ret[i] = &TreeEntry{
+ TreeEntry: e,
+ path: filepath.Join(t.Path, e.Name()),
+ }
+ }
+ return ret, nil
+}
+
+func (t *Tree) TreeEntry(path string) (*TreeEntry, error) {
+ entry, err := t.Tree.TreeEntry(path)
+ if err != nil {
+ return nil, err
+ }
+ return &TreeEntry{
+ TreeEntry: entry,
+ path: filepath.Join(t.Path, entry.Name()),
+ }, nil
+}
+
+const sniffLen = 8000
+
+// IsBinary detects if data is a binary value based on:
+// http://git.kernel.org/cgit/git/git.git/tree/xdiff-interface.c?id=HEAD#n198
+func IsBinary(r io.Reader) (bool, error) {
+ reader := bufio.NewReader(r)
+ c := 0
+ for {
+ if c == sniffLen {
+ break
+ }
+
+ b, err := reader.ReadByte()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return false, err
+ }
+
+ if b == byte(0) {
+ return true, nil
+ }
+
+ c++
+ }
+
+ return false, nil
+}
+
+// IsBinary returns true if the file is binary.
+func (f *File) IsBinary() (bool, error) {
+ stdout := new(bytes.Buffer)
+ stderr := new(bytes.Buffer)
+ err := f.Pipeline(stdout, stderr)
+ if err != nil {
+ return false, err
+ }
+ r := bufio.NewReader(stdout)
+ return IsBinary(r)
+}
+
+// Mode returns the mode of the file in fs.FileMode format.
+func (e *TreeEntry) Mode() fs.FileMode {
+ m := e.Blob().Mode()
+ switch m {
+ case git.EntryTree:
+ return fs.ModeDir | fs.ModePerm
+ default:
+ return fs.FileMode(m)
+ }
+}
+
+// File returns the file for the TreeEntry.
+func (e *TreeEntry) File() *File {
+ b := e.Blob()
+ return &File{
+ Blob: b,
+ Entry: e,
+ }
+}
+
+// Contents returns the contents of the file.
+func (e *TreeEntry) Contents() ([]byte, error) {
+ return e.File().Contents()
+}
+
+// Contents returns the contents of the file.
+func (f *File) Contents() ([]byte, error) {
+ return f.Blob.Bytes()
+}
@@ -3,26 +3,26 @@ package about
import (
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
- vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
"github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/go-git/go-git/v5/plumbing"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
+ vp "github.com/charmbracelet/soft-serve/pkg/tui/viewport"
"github.com/muesli/reflow/wrap"
)
type Bubble struct {
readmeViewport *vp.ViewportBubble
- repo types.Repo
+ repo common.GitRepo
styles *style.Styles
height int
heightMargin int
width int
widthMargin int
- ref *plumbing.Reference
+ ref *git.Reference
}
-func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
+func NewBubble(repo common.GitRepo, styles *style.Styles, width, wm, height, hm int) *Bubble {
b := &Bubble{
readmeViewport: &vp.ViewportBubble{
Viewport: &viewport.Model{},
@@ -31,13 +31,12 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
styles: styles,
widthMargin: wm,
heightMargin: hm,
- ref: repo.GetHEAD(),
}
b.SetSize(width, height)
return b
}
func (b *Bubble) Init() tea.Cmd {
- return b.setupCmd
+ return b.reset
}
func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -56,11 +55,11 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "R":
- b.GotoTop()
+ return b, b.reset
}
case refs.RefMsg:
b.ref = msg
- return b, b.setupCmd
+ return b, b.reset
}
rv, cmd := b.readmeViewport.Update(msg)
b.readmeViewport = rv.(*vp.ViewportBubble)
@@ -83,17 +82,17 @@ func (b *Bubble) View() string {
return b.readmeViewport.View()
}
-func (b *Bubble) Help() []types.HelpEntry {
+func (b *Bubble) Help() []common.HelpEntry {
return nil
}
func (b *Bubble) glamourize() (string, error) {
w := b.width - b.widthMargin - b.styles.RepoBody.GetHorizontalFrameSize()
- rm := b.repo.GetReadme()
+ rm, rp := b.repo.Readme()
if rm == "" {
return b.styles.AboutNoReadme.Render("No readme found."), nil
}
- f, err := types.RenderFile(b.repo.GetReadmePath(), rm, w)
+ f, err := common.RenderFile(rp, rm, w)
if err != nil {
return "", err
}
@@ -107,11 +106,16 @@ func (b *Bubble) glamourize() (string, error) {
return wrap.String(f, w), nil
}
-func (b *Bubble) setupCmd() tea.Msg {
+func (b *Bubble) reset() tea.Msg {
md, err := b.glamourize()
if err != nil {
- return types.ErrMsg{Err: err}
+ return common.ErrMsg{Err: err}
}
+ head, err := b.repo.HEAD()
+ if err != nil {
+ return common.ErrMsg{Err: err}
+ }
+ b.ref = head
b.readmeViewport.Viewport.SetContent(md)
b.GotoTop()
return nil
internal/tui/bubbles/git/bubble.go → pkg/tui/bubble.go
🔗
@@ -1,15 +1,15 @@
-package git
+package tui
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/about"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/log"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/tree"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/go-git/go-git/v5/plumbing"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ "github.com/charmbracelet/soft-serve/pkg/tui/about"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
+ "github.com/charmbracelet/soft-serve/pkg/tui/log"
+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
+ "github.com/charmbracelet/soft-serve/pkg/tui/tree"
)
const (
@@ -27,17 +27,17 @@ const (
type Bubble struct {
state state
- repo types.Repo
+ repo common.GitRepo
height int
heightMargin int
width int
widthMargin int
style *style.Styles
boxes []tea.Model
- ref *plumbing.Reference
+ ref *git.Reference
}
-func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int) *Bubble {
+func NewBubble(repo common.GitRepo, styles *style.Styles, width, wm, height, hm int) *Bubble {
b := &Bubble{
repo: repo,
state: aboutState,
@@ -47,7 +47,6 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, wm, height, hm int)
heightMargin: hm,
style: styles,
boxes: make([]tea.Model, 4),
- ref: repo.GetHEAD(),
}
heightMargin := hm + lipgloss.Height(b.headerView())
b.boxes[aboutState] = about.NewBubble(repo, b.style, b.width, wm, b.height, heightMargin)
@@ -106,20 +105,20 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return b, tea.Batch(cmds...)
}
-func (b *Bubble) Help() []types.HelpEntry {
- h := []types.HelpEntry{}
- h = append(h, b.boxes[b.state].(types.BubbleHelper).Help()...)
+func (b *Bubble) Help() []common.HelpEntry {
+ h := []common.HelpEntry{}
+ h = append(h, b.boxes[b.state].(common.BubbleHelper).Help()...)
if b.repo.Name() != "config" {
- h = append(h, types.HelpEntry{Key: "R", Value: "readme"})
- h = append(h, types.HelpEntry{Key: "F", Value: "files"})
- h = append(h, types.HelpEntry{Key: "C", Value: "commits"})
- h = append(h, types.HelpEntry{Key: "B", Value: "branches"})
+ h = append(h, common.HelpEntry{Key: "R", Value: "readme"})
+ h = append(h, common.HelpEntry{Key: "F", Value: "files"})
+ h = append(h, common.HelpEntry{Key: "C", Value: "commits"})
+ h = append(h, common.HelpEntry{Key: "B", Value: "branches"})
}
return h
}
-func (b *Bubble) Reference() plumbing.ReferenceName {
- return b.ref.Name()
+func (b *Bubble) Reference() *git.Reference {
+ return b.ref
}
func (b *Bubble) headerView() string {
@@ -133,6 +132,11 @@ func (b *Bubble) View() string {
}
func (b *Bubble) setupCmd() tea.Msg {
+ head, err := b.repo.HEAD()
+ if err != nil {
+ return common.ErrMsg{Err: err}
+ }
+ b.ref = head
cmds := make([]tea.Cmd, 0)
for _, bx := range b.boxes {
if bx != nil {
@@ -140,7 +144,7 @@ func (b *Bubble) setupCmd() tea.Msg {
if initCmd != nil {
msg := initCmd()
switch msg := msg.(type) {
- case types.ErrMsg:
+ case common.ErrMsg:
return msg
}
}
@@ -1,4 +1,4 @@
-package types
+package common
import (
"time"
@@ -1,4 +1,4 @@
-package types
+package common
import (
"errors"
@@ -1,4 +1,4 @@
-package types
+package common
import (
"strings"
@@ -0,0 +1,16 @@
+package common
+
+import (
+ "github.com/charmbracelet/soft-serve/pkg/git"
+)
+
+type GitRepo interface {
+ Name() string
+ Readme() (string, string)
+ HEAD() (*git.Reference, error)
+ CommitsByPage(*git.Reference, int, int) (git.Commits, error)
+ CountCommits(*git.Reference) (int64, error)
+ Diff(*git.Commit) (*git.Diff, error)
+ References() ([]*git.Reference, error)
+ Tree(*git.Reference, string) (*git.Tree, error)
+}
@@ -1,4 +1,4 @@
-package types
+package common
type BubbleHelper interface {
Help() []HelpEntry
@@ -1,4 +1,4 @@
-package types
+package common
import tea "github.com/charmbracelet/bubbletea"
@@ -1,4 +1,4 @@
-package types
+package common
import "github.com/muesli/reflow/truncate"
@@ -1,10 +1,8 @@
package log
import (
- "context"
"fmt"
"io"
- "math"
"strings"
"time"
@@ -13,13 +11,11 @@ import (
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
gansi "github.com/charmbracelet/glamour/ansi"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
- vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
"github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/dustin/go-humanize/english"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
+ vp "github.com/charmbracelet/soft-serve/pkg/tui/viewport"
)
var (
@@ -30,7 +26,7 @@ var (
waitBeforeLoading = time.Millisecond * 300
)
-type commitMsg *object.Commit
+type commitMsg *git.Commit
type sessionState int
@@ -42,13 +38,12 @@ const (
)
type item struct {
- *object.Commit
+ *git.Commit
}
func (i item) Title() string {
- lines := strings.Split(i.Message, "\n")
- if len(lines) > 0 {
- return lines[0]
+ if i.Commit != nil {
+ return strings.Split(i.Commit.Message, "\n")[0]
}
return ""
}
@@ -67,40 +62,45 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
if !ok {
return
}
+ if i.Commit == nil {
+ return
+ }
+ hash := i.ID.String()
leftMargin := d.style.LogItemSelector.GetMarginLeft() +
d.style.LogItemSelector.GetWidth() +
d.style.LogItemHash.GetMarginLeft() +
d.style.LogItemHash.GetWidth() +
d.style.LogItemInactive.GetMarginLeft()
- title := types.TruncateString(i.Title(), m.Width()-leftMargin, "…")
+ title := common.TruncateString(i.Title(), m.Width()-leftMargin, "…")
if index == m.Index() {
fmt.Fprint(w, d.style.LogItemSelector.Render(">")+
- d.style.LogItemHash.Bold(true).Render(i.Hash.String()[:7])+
+ d.style.LogItemHash.Bold(true).Render(hash[:7])+
d.style.LogItemActive.Render(title))
} else {
fmt.Fprint(w, d.style.LogItemSelector.Render(" ")+
- d.style.LogItemHash.Render(i.Hash.String()[:7])+
+ d.style.LogItemHash.Render(hash[:7])+
d.style.LogItemInactive.Render(title))
}
}
type Bubble struct {
- repo types.Repo
+ repo common.GitRepo
+ count int64
list list.Model
state sessionState
commitViewport *vp.ViewportBubble
- ref *plumbing.Reference
+ ref *git.Reference
style *style.Styles
width int
widthMargin int
height int
heightMargin int
- error types.ErrMsg
+ error common.ErrMsg
spinner spinner.Model
}
-func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
l.SetShowFilter(false)
l.SetShowHelp(false)
@@ -109,8 +109,8 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
l.SetShowTitle(false)
l.SetFilteringEnabled(false)
l.DisableQuitKeybindings()
- l.KeyMap.NextPage = types.NextPage
- l.KeyMap.PrevPage = types.PrevPage
+ l.KeyMap.NextPage = common.NextPage
+ l.KeyMap.PrevPage = common.PrevPage
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = styles.Spinner
@@ -126,7 +126,6 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
height: height,
heightMargin: heightMargin,
list: l,
- ref: repo.GetHEAD(),
spinner: s,
}
b.SetSize(width, height)
@@ -134,26 +133,49 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
}
func (b *Bubble) reset() tea.Cmd {
+ errMsg := func(err error) tea.Cmd {
+ return func() tea.Msg { return common.ErrMsg{Err: err} }
+ }
+ ref, err := b.repo.HEAD()
+ if err != nil {
+ return errMsg(err)
+ }
+ b.ref = ref
+ count, err := b.repo.CountCommits(ref)
+ if err != nil {
+ return errMsg(err)
+ }
+ b.count = count
b.state = logState
b.list.Select(0)
- cmd := b.updateItems()
b.SetSize(b.width, b.height)
+ cmd := b.updateItems()
return cmd
}
func (b *Bubble) updateItems() tea.Cmd {
- items := make([]list.Item, 0)
- cc, err := b.repo.GetCommits(b.ref)
+ count := b.count
+ items := make([]list.Item, count)
+ b.list.SetItems(items)
+ page := b.list.Paginator.Page
+ limit := b.list.Paginator.PerPage
+ skip := page * limit
+ cc, err := b.repo.CommitsByPage(b.ref, page+1, limit)
if err != nil {
- return func() tea.Msg { return types.ErrMsg{Err: err} }
+ return func() tea.Msg { return common.ErrMsg{Err: err} }
}
- for _, c := range cc {
- items = append(items, item{c})
+ for i, c := range cc {
+ idx := i + skip
+ if idx >= int(count) {
+ break
+ }
+ items[idx] = item{c}
}
- return b.list.SetItems(items)
+ cmd := b.list.SetItems(items)
+ return cmd
}
-func (b *Bubble) Help() []types.HelpEntry {
+func (b *Bubble) Help() []common.HelpEntry {
return nil
}
@@ -179,6 +201,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
b.SetSize(msg.Width, msg.Height)
+ cmds = append(cmds, b.updateItems())
case tea.KeyMsg:
switch msg.String() {
@@ -193,7 +216,22 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
b.state = logState
}
}
- case types.ErrMsg:
+ switch b.state {
+ case logState:
+ curPage := b.list.Paginator.Page
+ m, cmd := b.list.Update(msg)
+ b.list = m
+ if m.Paginator.Page != curPage {
+ cmds = append(cmds, b.updateItems())
+ }
+ cmds = append(cmds, cmd)
+ case commitState:
+ rv, cmd := b.commitViewport.Update(msg)
+ b.commitViewport = rv.(*vp.ViewportBubble)
+ cmds = append(cmds, cmd)
+ }
+ return b, tea.Batch(cmds...)
+ case common.ErrMsg:
b.error = msg
b.state = errorState
return b, nil
@@ -203,6 +241,11 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
case refs.RefMsg:
b.ref = msg
+ count, err := b.repo.CountCommits(msg)
+ if err != nil {
+ b.error = common.ErrMsg{Err: err}
+ }
+ b.count = count
case spinner.TickMsg:
if b.state == loadingState {
s, cmd := b.spinner.Update(msg)
@@ -213,42 +256,39 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
- switch b.state {
- case commitState:
- rv, cmd := b.commitViewport.Update(msg)
- b.commitViewport = rv.(*vp.ViewportBubble)
- cmds = append(cmds, cmd)
- case logState:
- l, cmd := b.list.Update(msg)
- b.list = l
- cmds = append(cmds, cmd)
- }
-
return b, tea.Batch(cmds...)
}
-func (b *Bubble) loadPatch(c *object.Commit) error {
+func (b *Bubble) loadPatch(c *git.Commit) error {
var patch strings.Builder
style := b.style.LogCommit.Copy().Width(b.width - b.widthMargin - b.style.LogCommit.GetHorizontalFrameSize())
- ctx, cancel := context.WithTimeout(context.TODO(), types.MaxPatchWait)
- defer cancel()
- p, err := b.repo.PatchCtx(ctx, c)
+ p, err := b.repo.Diff(c)
if err != nil {
return err
}
+ stats := strings.Split(p.Stats().String(), "\n")
+ for i, l := range stats {
+ ch := strings.Split(l, "|")
+ if len(ch) > 1 {
+ adddel := ch[len(ch)-1]
+ adddel = strings.ReplaceAll(adddel, "+", b.style.LogCommitStatsAdd.Render("+"))
+ adddel = strings.ReplaceAll(adddel, "-", b.style.LogCommitStatsDel.Render("-"))
+ stats[i] = strings.Join(ch[:len(ch)-1], "|") + "|" + adddel
+ }
+ }
patch.WriteString(b.renderCommit(c))
- fpl := len(p.FilePatches())
- if fpl > types.MaxDiffFiles {
- patch.WriteString("\n" + types.ErrDiffFilesTooLong.Error())
+ fpl := len(p.Files)
+ if fpl > common.MaxDiffFiles {
+ patch.WriteString("\n" + common.ErrDiffFilesTooLong.Error())
} else {
- patch.WriteString("\n" + b.renderStats(p.Stats()))
+ patch.WriteString("\n" + strings.Join(stats, "\n"))
}
- if fpl <= types.MaxDiffFiles {
- ps := p.String()
- if len(strings.Split(ps, "\n")) > types.MaxDiffLines {
- patch.WriteString("\n" + types.ErrDiffTooLong.Error())
+ if fpl <= common.MaxDiffFiles {
+ ps := ""
+ if len(strings.Split(ps, "\n")) > common.MaxDiffLines {
+ patch.WriteString("\n" + common.ErrDiffTooLong.Error())
} else {
- patch.WriteString("\n" + b.renderDiff(ps))
+ patch.WriteString("\n" + b.renderDiff(p))
}
}
content := style.Render(patch.String())
@@ -280,118 +320,31 @@ func (b *Bubble) loadCommit() tea.Cmd {
b.state = loadingState
}
if err != nil {
- return types.ErrMsg{Err: err}
+ return common.ErrMsg{Err: err}
}
return commitMsg(c.Commit)
}
}
-func (b *Bubble) renderCommit(c *object.Commit) string {
+func (b *Bubble) renderCommit(c *git.Commit) string {
s := strings.Builder{}
// FIXME: lipgloss prints empty lines when CRLF is used
// sanitize commit message from CRLF
msg := strings.ReplaceAll(c.Message, "\r\n", "\n")
s.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n",
- b.style.LogCommitHash.Render("commit "+c.Hash.String()),
- b.style.LogCommitAuthor.Render("Author: "+c.Author.String()),
+ b.style.LogCommitHash.Render("commit "+c.ID.String()),
+ b.style.LogCommitAuthor.Render(fmt.Sprintf("Author: %s <%s>", c.Author.Name, c.Author.Email)),
b.style.LogCommitDate.Render("Date: "+c.Committer.When.Format(time.UnixDate)),
b.style.LogCommitBody.Render(msg),
))
return s.String()
}
-func (b *Bubble) renderStats(fileStats object.FileStats) string {
- padLength := float64(len(" "))
- newlineLength := float64(len("\n"))
- separatorLength := float64(len("|"))
- // Soft line length limit. The text length calculation below excludes
- // length of the change number. Adding that would take it closer to 80,
- // but probably not more than 80, until it's a huge number.
- lineLength := 72.0
-
- // Get the longest filename and longest total change.
- var longestLength float64
- var longestTotalChange float64
- for _, fs := range fileStats {
- if int(longestLength) < len(fs.Name) {
- longestLength = float64(len(fs.Name))
- }
- totalChange := fs.Addition + fs.Deletion
- if int(longestTotalChange) < totalChange {
- longestTotalChange = float64(totalChange)
- }
- }
-
- // Parts of the output:
- // <pad><filename><pad>|<pad><changeNumber><pad><+++/---><newline>
- // example: " main.go | 10 +++++++--- "
-
- // <pad><filename><pad>
- leftTextLength := padLength + longestLength + padLength
-
- // <pad><number><pad><+++++/-----><newline>
- // Excluding number length here.
- rightTextLength := padLength + padLength + newlineLength
-
- totalTextArea := leftTextLength + separatorLength + rightTextLength
- heightOfHistogram := lineLength - totalTextArea
-
- // Scale the histogram.
- var scaleFactor float64
- if longestTotalChange > heightOfHistogram {
- // Scale down to heightOfHistogram.
- scaleFactor = longestTotalChange / heightOfHistogram
- } else {
- scaleFactor = 1.0
- }
-
- taddc := 0
- tdelc := 0
- output := strings.Builder{}
- for _, fs := range fileStats {
- taddc += fs.Addition
- tdelc += fs.Deletion
- addn := float64(fs.Addition)
- deln := float64(fs.Deletion)
- addc := int(math.Floor(addn / scaleFactor))
- delc := int(math.Floor(deln / scaleFactor))
- if addc < 0 {
- addc = 0
- }
- if delc < 0 {
- delc = 0
- }
- adds := strings.Repeat("+", addc)
- dels := strings.Repeat("-", delc)
- diffLines := fmt.Sprint(fs.Addition + fs.Deletion)
- totalDiffLines := fmt.Sprint(int(longestTotalChange))
- fmt.Fprintf(&output, "%s | %s %s%s\n",
- fs.Name+strings.Repeat(" ", int(longestLength)-len(fs.Name)),
- strings.Repeat(" ", len(totalDiffLines)-len(diffLines))+diffLines,
- b.style.LogCommitStatsAdd.Render(adds),
- b.style.LogCommitStatsDel.Render(dels))
- }
- files := len(fileStats)
- fc := fmt.Sprintf("%s changed", english.Plural(files, "file", ""))
- ins := fmt.Sprintf("%s(+)", english.Plural(taddc, "insertion", ""))
- dels := fmt.Sprintf("%s(-)", english.Plural(tdelc, "deletion", ""))
- fmt.Fprint(&output, fc)
- if taddc > 0 {
- fmt.Fprintf(&output, ", %s", ins)
- }
- if tdelc > 0 {
- fmt.Fprintf(&output, ", %s", dels)
- }
- fmt.Fprint(&output, "\n")
-
- return output.String()
-}
-
-func (b *Bubble) renderDiff(diff string) string {
+func (b *Bubble) renderDiff(diff *git.Diff) string {
var s strings.Builder
- pr := strings.Builder{}
- diffChroma.Code = diff
- err := diffChroma.Render(&pr, types.RenderCtx)
+ var pr strings.Builder
+ diffChroma.Code = diff.Patch()
+ err := diffChroma.Render(&pr, common.RenderCtx)
if err != nil {
s.WriteString(fmt.Sprintf("\n%s", err.Error()))
} else {
@@ -7,19 +7,19 @@ import (
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
"github.com/charmbracelet/soft-serve/internal/tui/style"
- "github.com/go-git/go-git/v5/plumbing"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
)
-type RefMsg = *plumbing.Reference
+type RefMsg = *git.Reference
type item struct {
- *plumbing.Reference
+ *git.Reference
}
func (i item) Short() string {
- return i.Name().Short()
+ return i.Reference.Name().Short()
}
func (i item) FilterValue() string { return i.Short() }
@@ -29,7 +29,7 @@ type items []item
func (cl items) Len() int { return len(cl) }
func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
func (cl items) Less(i, j int) bool {
- return cl[i].Name().Short() < cl[j].Name().Short()
+ return cl[i].Short() < cl[j].Short()
}
type itemDelegate struct {
@@ -47,7 +47,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
}
ref := i.Short()
- if i.Name().IsTag() {
+ if i.Reference.IsTag() {
ref = s.RefItemTag.Render(ref)
}
ref = s.RefItemBranch.Render(ref)
@@ -55,7 +55,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
s.RefItemSelector.GetMarginLeft() -
s.RefItemSelector.GetWidth() -
s.RefItemInactive.GetMarginLeft()
- ref = types.TruncateString(ref, refMaxWidth, "…")
+ ref = common.TruncateString(ref, refMaxWidth, "…")
if index == m.Index() {
fmt.Fprint(w, s.RefItemSelector.Render(">")+
s.RefItemActive.Render(ref))
@@ -66,17 +66,21 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
}
type Bubble struct {
- repo types.Repo
+ repo common.GitRepo
list list.Model
style *style.Styles
width int
widthMargin int
height int
heightMargin int
- ref *plumbing.Reference
+ ref *git.Reference
}
-func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+ head, err := repo.HEAD()
+ if err != nil {
+ return nil
+ }
l := list.NewModel([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
l.SetShowFilter(false)
l.SetShowHelp(false)
@@ -93,13 +97,13 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
widthMargin: widthMargin,
heightMargin: heightMargin,
list: l,
- ref: repo.GetHEAD(),
+ ref: head,
}
b.SetSize(width, height)
return b
}
-func (b *Bubble) SetBranch(ref *plumbing.Reference) (tea.Model, tea.Cmd) {
+func (b *Bubble) SetBranch(ref *git.Reference) (tea.Model, tea.Cmd) {
b.ref = ref
return b, func() tea.Msg {
return RefMsg(ref)
@@ -123,21 +127,21 @@ func (b *Bubble) SetSize(width, height int) {
b.list.Styles.PaginationStyle = b.style.RefPaginator.Copy().Width(width - b.widthMargin)
}
-func (b *Bubble) Help() []types.HelpEntry {
+func (b *Bubble) Help() []common.HelpEntry {
return nil
}
func (b *Bubble) updateItems() tea.Cmd {
its := make(items, 0)
tags := make(items, 0)
- for _, r := range b.repo.GetReferences() {
- if r.Type() != plumbing.HashReference {
- continue
- }
- n := r.Name()
- if n.IsTag() {
+ refs, err := b.repo.References()
+ if err != nil {
+ return func() tea.Msg { return common.ErrMsg{Err: err} }
+ }
+ for _, r := range refs {
+ if r.IsTag() {
tags = append(tags, item{r})
- } else if n.IsBranch() {
+ } else if r.IsBranch() {
its = append(its, item{r})
}
}
@@ -3,22 +3,20 @@ package tree
import (
"fmt"
"io"
+ "io/fs"
"path/filepath"
- "sort"
"strings"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/refs"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
- vp "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/viewport"
"github.com/charmbracelet/soft-serve/internal/tui/style"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
+ "github.com/charmbracelet/soft-serve/pkg/tui/refs"
+ vp "github.com/charmbracelet/soft-serve/pkg/tui/viewport"
"github.com/dustin/go-humanize"
- "github.com/go-git/go-git/v5/plumbing"
- "github.com/go-git/go-git/v5/plumbing/filemode"
- "github.com/go-git/go-git/v5/plumbing/object"
)
type fileMsg struct {
@@ -34,16 +32,15 @@ const (
)
type item struct {
- entry *object.TreeEntry
- file *object.File
+ entry *git.TreeEntry
}
func (i item) Name() string {
- return i.entry.Name
+ return i.entry.Name()
}
-func (i item) Mode() filemode.FileMode {
- return i.entry.Mode
+func (i item) Mode() fs.FileMode {
+ return i.entry.Mode()
}
func (i item) FilterValue() string { return i.Name() }
@@ -53,11 +50,11 @@ type items []item
func (cl items) Len() int { return len(cl) }
func (cl items) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
func (cl items) Less(i, j int) bool {
- if cl[i].Mode() == filemode.Dir && cl[j].Mode() == filemode.Dir {
+ if cl[i].entry.IsTree() && cl[j].entry.IsTree() {
return cl[i].Name() < cl[j].Name()
- } else if cl[i].Mode() == filemode.Dir {
+ } else if cl[i].entry.IsTree() {
return true
- } else if cl[j].Mode() == filemode.Dir {
+ } else if cl[j].entry.IsTree() {
return false
} else {
return cl[i].Name() < cl[j].Name()
@@ -79,15 +76,13 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
}
name := i.Name()
- if i.Mode() == filemode.Dir {
+ size := humanize.Bytes(uint64(i.entry.Size()))
+ if i.entry.IsTree() {
+ size = ""
name = s.TreeFileDir.Render(name)
}
- size := ""
- if i.file != nil {
- size = humanize.Bytes(uint64(i.file.Size))
- }
var cs lipgloss.Style
- mode, _ := i.Mode().ToOSFileMode()
+ mode := i.Mode()
if index == m.Index() {
cs = s.TreeItemActive
fmt.Fprint(w, s.TreeItemSelector.Render(">"))
@@ -101,7 +96,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
s.TreeFileMode.GetWidth() +
cs.GetMarginLeft()
rightMargin := s.TreeFileSize.GetMarginLeft() + lipgloss.Width(size)
- name = types.TruncateString(name, m.Width()-leftMargin-rightMargin, "…")
+ name = common.TruncateString(name, m.Width()-leftMargin-rightMargin, "…")
sizeStyle := s.TreeFileSize.Copy().
Width(m.Width() -
leftMargin -
@@ -114,7 +109,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
}
type Bubble struct {
- repo types.Repo
+ repo common.GitRepo
list list.Model
style *style.Styles
width int
@@ -123,13 +118,13 @@ type Bubble struct {
heightMargin int
path string
state sessionState
- error types.ErrMsg
+ error common.ErrMsg
fileViewport *vp.ViewportBubble
lastSelected []int
- ref *plumbing.Reference
+ ref *git.Reference
}
-func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
+func NewBubble(repo common.GitRepo, styles *style.Styles, width, widthMargin, height, heightMargin int) *Bubble {
l := list.New([]list.Item{}, itemDelegate{styles}, width-widthMargin, height-heightMargin)
l.SetShowFilter(false)
l.SetShowHelp(false)
@@ -138,8 +133,8 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
l.SetShowTitle(false)
l.SetFilteringEnabled(false)
l.DisableQuitKeybindings()
- l.KeyMap.NextPage = types.NextPage
- l.KeyMap.PrevPage = types.PrevPage
+ l.KeyMap.NextPage = common.NextPage
+ l.KeyMap.PrevPage = common.PrevPage
l.Styles.NoItems = styles.TreeNoItems
b := &Bubble{
fileViewport: &vp.ViewportBubble{
@@ -153,7 +148,6 @@ func NewBubble(repo types.Repo, styles *style.Styles, width, widthMargin, height
heightMargin: heightMargin,
list: l,
state: treeState,
- ref: repo.GetHEAD(),
}
b.SetSize(width, height)
return b
@@ -169,6 +163,13 @@ func (b *Bubble) reset() tea.Cmd {
}
func (b *Bubble) Init() tea.Cmd {
+ head, err := b.repo.HEAD()
+ if err != nil {
+ return func() tea.Msg {
+ return common.ErrMsg{Err: err}
+ }
+ }
+ b.ref = head
return nil
}
@@ -181,37 +182,30 @@ func (b *Bubble) SetSize(width, height int) {
b.list.Styles.PaginationStyle = b.style.LogPaginator.Copy().Width(width - b.widthMargin)
}
-func (b *Bubble) Help() []types.HelpEntry {
+func (b *Bubble) Help() []common.HelpEntry {
return nil
}
func (b *Bubble) updateItems() tea.Cmd {
- its := make(items, 0)
+ files := make([]list.Item, 0)
+ dirs := make([]list.Item, 0)
t, err := b.repo.Tree(b.ref, b.path)
if err != nil {
- return func() tea.Msg { return types.ErrMsg{Err: err} }
+ return func() tea.Msg { return common.ErrMsg{Err: err} }
}
- tw := object.NewTreeWalker(t, false, map[plumbing.Hash]bool{})
- defer tw.Close()
- for {
- _, e, err := tw.Next()
- if err != nil {
- break
- }
- i := item{entry: &e}
- if e.Mode.IsFile() {
- if f, err := t.TreeEntryFile(&e); err == nil {
- i.file = f
- }
- }
- its = append(its, i)
+ ents, err := t.Entries()
+ if err != nil {
+ return func() tea.Msg { return common.ErrMsg{Err: err} }
}
- sort.Sort(its)
- itt := make([]list.Item, len(its))
- for i, it := range its {
- itt[i] = it
+ ents.Sort()
+ for _, e := range ents {
+ if e.IsTree() {
+ dirs = append(dirs, item{e})
+ } else {
+ files = append(files, item{e})
+ }
}
- cmd := b.list.SetItems(itt)
+ cmd := b.list.SetItems(append(dirs, files...))
b.list.Select(0)
return cmd
}
@@ -224,7 +218,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
if b.state == errorState {
- ref := b.repo.GetHEAD()
+ ref, _ := b.repo.HEAD()
b.ref = ref
return b, tea.Batch(b.reset(), func() tea.Msg {
return ref
@@ -240,7 +234,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
item := b.list.SelectedItem().(item)
mode := item.Mode()
b.path = filepath.Join(b.path, item.Name())
- if mode == filemode.Dir {
+ if mode.IsDir() {
b.lastSelected = append(b.lastSelected, index)
cmds = append(cmds, b.updateItems())
} else {
@@ -267,7 +261,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
b.ref = msg
return b, b.reset()
- case types.ErrMsg:
+ case common.ErrMsg:
b.error = msg
b.state = errorState
return b, nil
@@ -308,22 +302,23 @@ func (b *Bubble) View() string {
func (b *Bubble) loadFile(i item) tea.Cmd {
return func() tea.Msg {
- if !i.Mode().IsFile() || i.file == nil {
- return types.ErrMsg{Err: types.ErrInvalidFile}
+ f := i.entry.File()
+ if i.Mode().IsDir() || f == nil {
+ return common.ErrMsg{Err: common.ErrInvalidFile}
}
- bin, err := i.file.IsBinary()
+ bin, err := f.IsBinary()
if err != nil {
- return types.ErrMsg{Err: err}
+ return common.ErrMsg{Err: err}
}
if bin {
- return types.ErrMsg{Err: types.ErrBinaryFile}
+ return common.ErrMsg{Err: common.ErrBinaryFile}
}
- c, err := i.file.Contents()
+ c, err := f.Bytes()
if err != nil {
- return types.ErrMsg{Err: err}
+ return common.ErrMsg{Err: err}
}
return fileMsg{
- content: c,
+ content: string(c),
}
}
}
@@ -331,11 +326,11 @@ func (b *Bubble) loadFile(i item) tea.Cmd {
func (b *Bubble) renderFile(m fileMsg) string {
s := strings.Builder{}
c := m.content
- if len(strings.Split(c, "\n")) > types.MaxDiffLines {
- s.WriteString(types.ErrFileTooLarge.Error())
+ if len(strings.Split(c, "\n")) > common.MaxDiffLines {
+ s.WriteString(b.style.TreeNoItems.Render(common.ErrFileTooLarge.Error()))
} else {
w := b.width - b.widthMargin - b.style.RepoBody.GetHorizontalFrameSize()
- f, err := types.RenderFile(b.path, m.content, w)
+ f, err := common.RenderFile(b.path, m.content, w)
if err != nil {
s.WriteString(err.Error())
} else {
@@ -3,19 +3,18 @@ package server
import (
"fmt"
"path/filepath"
- "sort"
"strings"
"github.com/alecthomas/chroma/lexers"
gansi "github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/lipgloss"
appCfg "github.com/charmbracelet/soft-serve/internal/config"
- "github.com/charmbracelet/soft-serve/internal/tui/bubbles/git/types"
+ "github.com/charmbracelet/soft-serve/pkg/git"
+ "github.com/charmbracelet/soft-serve/pkg/tui/common"
"github.com/charmbracelet/wish"
gitwish "github.com/charmbracelet/wish/git"
"github.com/gliderlabs/ssh"
- "github.com/go-git/go-git/v5/plumbing/filemode"
- "github.com/go-git/go-git/v5/plumbing/object"
+ ggit "github.com/gogs/git-module"
"github.com/muesli/termenv"
)
@@ -27,22 +26,6 @@ var (
filemodeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#777777"))
)
-type entries []object.TreeEntry
-
-func (cl entries) Len() int { return len(cl) }
-func (cl entries) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
-func (cl entries) Less(i, j int) bool {
- if cl[i].Mode == filemode.Dir && cl[j].Mode == filemode.Dir {
- return cl[i].Name < cl[j].Name
- } else if cl[i].Mode == filemode.Dir {
- return true
- } else if cl[j].Mode == filemode.Dir {
- return false
- } else {
- return cl[i].Name < cl[j].Name
- }
-}
-
// softServeMiddleware is a middleware that handles displaying files with the
// option of syntax highlighting and line numbers.
func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
@@ -90,15 +73,27 @@ func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
_ = s.Exit(1)
return
}
+ ref, err := rs.HEAD()
+ if err != nil {
+ _, _ = s.Write([]byte(err.Error()))
+ _ = s.Exit(1)
+ return
+ }
p := strings.Join(ps[1:], "/")
- t, err := rs.LatestTree(p)
- if err != nil && err != object.ErrDirectoryNotFound {
+ t, err := rs.Tree(ref, p)
+ if err != nil && err != ggit.ErrRevisionNotExist {
_, _ = s.Write([]byte(err.Error()))
_ = s.Exit(1)
return
}
- if err == object.ErrDirectoryNotFound {
- fc, err := rs.LatestFile(p)
+ if err == ggit.ErrRevisionNotExist {
+ _, _ = s.Write([]byte(git.ErrFileNotFound.Error()))
+ _ = s.Exit(1)
+ return
+ }
+ ents, err := t.Entries()
+ if err != nil {
+ fc, _, err := rs.LatestFile(p)
if err != nil {
_, _ = s.Write([]byte(err.Error()))
_ = s.Exit(1)
@@ -118,20 +113,19 @@ func softServeMiddleware(ac *appCfg.Config) wish.Middleware {
}
s.Write([]byte(fc))
} else {
- ents := entries(t.Entries)
- sort.Sort(ents)
+ ents.Sort()
for _, e := range ents {
- m, _ := e.Mode.ToOSFileMode()
+ m := e.Mode()
if m == 0 {
s.Write([]byte(strings.Repeat(" ", 10)))
} else {
s.Write([]byte(filemodeStyle.Render(m.String())))
}
s.Write([]byte(" "))
- if e.Mode.IsFile() {
- s.Write([]byte(filenameStyle.Render(e.Name)))
+ if !e.IsTree() {
+ s.Write([]byte(filenameStyle.Render(e.Name())))
} else {
- s.Write([]byte(dirnameStyle.Render(e.Name)))
+ s.Write([]byte(dirnameStyle.Render(e.Name())))
}
s.Write([]byte("\n"))
}
@@ -177,7 +171,7 @@ func withFormatting(p, c string) (string, error) {
Language: lang,
}
r := strings.Builder{}
- styles := types.DefaultStyles()
+ styles := common.DefaultStyles()
styles.CodeBlock.Margin = &zero
rctx := gansi.NewRenderContext(gansi.Options{
Styles: styles,
@@ -8,7 +8,6 @@ import (
"github.com/charmbracelet/soft-serve/config"
appCfg "github.com/charmbracelet/soft-serve/internal/config"
"github.com/charmbracelet/soft-serve/internal/tui"
-
"github.com/charmbracelet/wish"
bm "github.com/charmbracelet/wish/bubbletea"
gm "github.com/charmbracelet/wish/git"