1package sqlite
  2
  3import (
  4	"bufio"
  5	"context"
  6	"os"
  7	"path/filepath"
  8	"sync"
  9	"time"
 10
 11	"github.com/charmbracelet/soft-serve/git"
 12	"github.com/charmbracelet/soft-serve/server/backend"
 13	"github.com/jmoiron/sqlx"
 14)
 15
 16var _ backend.Repository = (*Repo)(nil)
 17
 18// Repo is a Git repository with metadata stored in a SQLite database.
 19type Repo struct {
 20	name string
 21	path string
 22	db   *sqlx.DB
 23
 24	// cache
 25	// updatedAt is cached in "last-modified" file.
 26	mu          sync.Mutex
 27	desc        *string
 28	projectName *string
 29	isMirror    *bool
 30	isPrivate   *bool
 31	isHidden    *bool
 32}
 33
 34// Description returns the repository's description.
 35//
 36// It implements backend.Repository.
 37func (r *Repo) Description() string {
 38	r.mu.Lock()
 39	defer r.mu.Unlock()
 40	if r.desc != nil {
 41		return *r.desc
 42	}
 43
 44	var desc string
 45	if err := wrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
 46		return tx.Get(&desc, "SELECT description FROM repo WHERE name = ?", r.name)
 47	}); err != nil {
 48		return ""
 49	}
 50
 51	r.desc = &desc
 52	return desc
 53}
 54
 55// IsMirror returns whether the repository is a mirror.
 56//
 57// It implements backend.Repository.
 58func (r *Repo) IsMirror() bool {
 59	r.mu.Lock()
 60	defer r.mu.Unlock()
 61	if r.isMirror != nil {
 62		return *r.isMirror
 63	}
 64
 65	var mirror bool
 66	if err := wrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
 67		return tx.Get(&mirror, "SELECT mirror FROM repo WHERE name = ?", r.name)
 68	}); err != nil {
 69		return false
 70	}
 71
 72	r.isMirror = &mirror
 73	return mirror
 74}
 75
 76// IsPrivate returns whether the repository is private.
 77//
 78// It implements backend.Repository.
 79func (r *Repo) IsPrivate() bool {
 80	r.mu.Lock()
 81	defer r.mu.Unlock()
 82	if r.isPrivate != nil {
 83		return *r.isPrivate
 84	}
 85
 86	var private bool
 87	if err := wrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
 88		return tx.Get(&private, "SELECT private FROM repo WHERE name = ?", r.name)
 89	}); err != nil {
 90		return false
 91	}
 92
 93	r.isPrivate = &private
 94	return private
 95}
 96
 97// Name returns the repository's name.
 98//
 99// It implements backend.Repository.
100func (r *Repo) Name() string {
101	return r.name
102}
103
104// Open opens the repository.
105//
106// It implements backend.Repository.
107func (r *Repo) Open() (*git.Repository, error) {
108	return git.Open(r.path)
109}
110
111// ProjectName returns the repository's project name.
112//
113// It implements backend.Repository.
114func (r *Repo) ProjectName() string {
115	r.mu.Lock()
116	defer r.mu.Unlock()
117	if r.projectName != nil {
118		return *r.projectName
119	}
120
121	var name string
122	if err := wrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
123		return tx.Get(&name, "SELECT project_name FROM repo WHERE name = ?", r.name)
124	}); err != nil {
125		return ""
126	}
127
128	r.projectName = &name
129	return name
130}
131
132// IsHidden returns whether the repository is hidden.
133//
134// It implements backend.Repository.
135func (r *Repo) IsHidden() bool {
136	r.mu.Lock()
137	defer r.mu.Unlock()
138	if r.isHidden != nil {
139		return *r.isHidden
140	}
141
142	var hidden bool
143	if err := wrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
144		return tx.Get(&hidden, "SELECT hidden FROM repo WHERE name = ?", r.name)
145	}); err != nil {
146		return false
147	}
148
149	r.isHidden = &hidden
150	return hidden
151}
152
153// UpdatedAt returns the repository's last update time.
154func (r *Repo) UpdatedAt() time.Time {
155	var updatedAt time.Time
156
157	// Try to read the last modified time from the info directory.
158	if t, err := readOneline(filepath.Join(r.path, "info", "last-modified")); err == nil {
159		if t, err := time.Parse(time.RFC3339, t); err == nil {
160			return t
161		}
162	}
163
164	rr, err := git.Open(r.path)
165	if err == nil {
166		t, err := rr.LatestCommitTime()
167		if err == nil {
168			updatedAt = t
169		}
170	}
171
172	if updatedAt.IsZero() {
173		if err := wrapTx(r.db, context.Background(), func(tx *sqlx.Tx) error {
174			return tx.Get(&updatedAt, "SELECT updated_at FROM repo WHERE name = ?", r.name)
175		}); err != nil {
176			return time.Time{}
177		}
178	}
179
180	return updatedAt
181}
182
183func (r *Repo) writeLastModified(t time.Time) error {
184	fp := filepath.Join(r.path, "info", "last-modified")
185	if err := os.MkdirAll(filepath.Dir(fp), os.ModePerm); err != nil {
186		return err
187	}
188
189	return os.WriteFile(fp, []byte(t.Format(time.RFC3339)), os.ModePerm)
190}
191
192func readOneline(path string) (string, error) {
193	f, err := os.Open(path)
194	if err != nil {
195		return "", err
196	}
197
198	defer f.Close() // nolint: errcheck
199	s := bufio.NewScanner(f)
200	s.Scan()
201	return s.Text(), s.Err()
202}