repo.go

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