api: move gitea api to subpackage

This commit is contained in:
mappu 2022-12-31 14:06:43 +13:00
parent 45ed36b327
commit cb454938cc
8 changed files with 446 additions and 401 deletions

372
api.go
View File

@ -1,372 +0,0 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
)
type Repo struct {
Name string `json:"name"`
Description string `json:"description"`
GiteaCreated time.Time `json:"created_at"`
GiteaUpdated time.Time `json:"updated_at"`
newestCommit time.Time
topics []string
}
func (this *Application) populateCommitInfo(ctx context.Context, rr *Repo) {
// The most recent commit will be the head of one of the branches (easy to find)
brs, err := this.branches(ctx, rr.Name)
if err != nil {
log.Printf("loading branches for '%s': %s", rr.Name, err)
rr.newestCommit = rr.GiteaUpdated // best guess
} else {
newestCommit := time.Unix(0, 0) // sentinel
for _, br := range brs {
if br.Commit.Timestamp.After(newestCommit) {
newestCommit = br.Commit.Timestamp
}
}
if !newestCommit.Equal(time.Unix(0, 0)) {
rr.newestCommit = newestCommit // replace it
}
}
}
type ContentsResponse struct {
Content []byte `json:"content"` // Assume base64 "encoding" parameter in Gitea response, and use Go's auto decode
}
type TopicsResponse struct {
Topics []string `json:"topics"`
}
type ReaddirEntry struct {
Name string `json:"name"`
Path string `json:"path"`
Size int `json:"size"`
RawURL string `json:"download_url"`
}
func (rde ReaddirEntry) isImage() bool {
return strings.HasSuffix(rde.Name, `.png`) || strings.HasSuffix(rde.Name, `.jpg`) || strings.HasSuffix(rde.Name, `.jpeg`)
}
type MarkdownRequest struct {
Context string
Mode string
Text string
Wiki bool
}
type BranchCommit struct {
ID string `json:"id"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
type Branch struct {
Name string `json:"name"`
Commit BranchCommit `json:"commit"`
}
type AuthorInfo struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
}
type CommitListEntryCommit struct {
Message string `json:"message"`
Author AuthorInfo `json:"author"`
Committer AuthorInfo `json:"committer"`
}
type CommitListEntry struct {
ID string `json:"sha"`
Commit CommitListEntryCommit `json:"commit"`
}
func (this *Application) apiRequest(ctx context.Context, endpoint string, target interface{}) error {
err := this.apiSem.Acquire(ctx, 1)
if err != nil {
return err // e.g. ctx closed
}
defer this.apiSem.Release(1)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.cfg.Gitea.URL+endpoint, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
err = json.NewDecoder(resp.Body).Decode(target)
if err != nil {
return err
}
return nil
}
func (this *Application) branches(ctx context.Context, repo string) ([]Branch, error) {
var branches []Branch
err := this.apiRequest(ctx, `api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/branches`, &branches)
if err != nil {
return nil, err // e.g. ctx closed
}
return branches, nil
}
func (this *Application) commitsPage(ctx context.Context, repo, ref string, page, limit int) ([]CommitListEntry, error) {
var ret []CommitListEntry
err := this.apiRequest(ctx, fmt.Sprintf(`api/v1/repos/%s/%s/commits?page=%d&limit=%d`, url.PathEscape(this.cfg.Gitea.Org), url.PathEscape(repo), page, limit), &ret)
if err != nil {
return nil, err // e.g. ctx closed
}
return ret, nil
}
func (this *Application) commits(ctx context.Context, repo, ref string) ([]CommitListEntry, error) {
var ret []CommitListEntry
nextPage := 1 // Counting starts at 1
for {
page, err := this.commitsPage(ctx, repo, ref, nextPage, 300)
if err != nil {
return nil, err
}
if len(page) == 0 && len(ret) > 0 {
break // Found enough already
}
ret = append(ret, page...)
nextPage += 1
}
if len(ret) == 0 {
return nil, fmt.Errorf("no commits found")
}
return ret, nil
}
// reposPage gets a single page of the list of Git repositories in this organisation.
func (this *Application) reposPage(ctx context.Context, page, limit int) ([]Repo, error) {
var repos []Repo
err := this.apiRequest(ctx, `api/v1/orgs/`+url.PathEscape(this.cfg.Gitea.Org)+fmt.Sprintf(`/repos?page=%d&limit=%d`, page, limit), &repos)
if err != nil {
return nil, err
}
return repos, nil
}
// repos gets a list of Git repositories in this organisation. It may have to
// make multiple network requests.
func (this *Application) repos(ctx context.Context) ([]Repo, error) {
// Seems like gitea-1.13.0-rc1 returns 30 results by default, and supports up to a limit of 100
// Make a much larger request
ret := make([]Repo, 0)
nextPage := 1 // Counting starts at 1
for {
page, err := this.reposPage(ctx, nextPage, 300)
if err != nil {
return nil, err
}
if len(page) == 0 && len(ret) > 0 {
break // Found enough already
}
ret = append(ret, page...)
nextPage += 1
}
return ret, nil
}
// repoFile gets a single file from the default branch of the git repository
// Usually the default branch is `master`.
func (this *Application) repoFile(ctx context.Context, repo, filename string) ([]byte, error) {
var cr ContentsResponse
err := this.apiRequest(ctx, `api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/contents/`+url.PathEscape(filename), &cr)
if err != nil {
return nil, err
}
return cr.Content, nil
}
func (this *Application) filesInDirectory(ctx context.Context, repo, dir string) ([]ReaddirEntry, error) {
err := this.apiSem.Acquire(ctx, 1)
if err != nil {
return nil, err // e.g. ctx closed
}
defer this.apiSem.Release(1)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, this.cfg.Gitea.URL+`api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/contents/`+dir, nil) // n.b. $dir param not escaped
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
// "No files found" happens with a HTTP 500/404 error depending on Gitea version. Catch this special case
if resp.StatusCode == 500 || resp.StatusCode == 404 {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if strings.Contains(string(b), `does not exist`) {
return []ReaddirEntry{}, nil // no files found
}
}
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
var ret []ReaddirEntry
err = json.NewDecoder(resp.Body).Decode(&ret)
if err != nil {
return nil, err
}
return ret, nil
}
// imageFilesForRepo finds documentation images for the repository.
// It searches the top-level directory and the dist/ and doc/ subdirectories.
func (this *Application) imageFilesForRepo(ctx context.Context, repo string) ([]ReaddirEntry, error) {
ret := []ReaddirEntry{}
for _, dirName := range []string{``, `dist`, `doc`} {
files, err := this.filesInDirectory(ctx, repo, dirName)
if err != nil {
return nil, fmt.Errorf("readdir(%s): %w", dirName, err)
}
for _, f := range files {
if f.isImage() {
ret = append(ret, f)
}
}
}
return ret, nil
}
func (this *Application) topicsForRepo(ctx context.Context, repo string) ([]string, error) {
var tr TopicsResponse
err := this.apiRequest(ctx, `api/v1/repos/`+url.PathEscape(this.cfg.Gitea.Org)+`/`+url.PathEscape(repo)+`/topics`, &tr)
if err != nil {
return nil, err
}
return tr.Topics, nil
}
// renderMarkdown calls the remote Gitea server's own markdown renderer.
func (this *Application) renderMarkdown(ctx context.Context, repoName string, body string) ([]byte, error) {
err := this.apiSem.Acquire(ctx, 1)
if err != nil {
return nil, err // e.g. ctx closed
}
defer this.apiSem.Release(1)
jb, err := json.Marshal(MarkdownRequest{
Context: this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName) + `/src/branch/master`,
Mode: "gfm", // magic constant - Github Flavoured Markdown
Text: body,
})
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, this.cfg.Gitea.URL+`api/v1/markdown/`, bytes.NewReader(jb))
if err != nil {
return nil, err
}
req.Header.Set(`Content-Type`, `application/json`)
req.Header.Set(`Content-Length`, fmt.Sprintf("%d", len(jb)))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}
// renderMarkdownRaw calls the remote Gitea server's own markdown renderer.
func (this *Application) renderMarkdownRaw(ctx context.Context, body []byte) ([]byte, error) {
err := this.apiSem.Acquire(ctx, 1)
if err != nil {
return nil, err // e.g. ctx closed
}
defer this.apiSem.Release(1)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, this.cfg.Gitea.URL+`api/v1/markdown/raw`, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set(`Content-Type`, `text/plain`)
req.Header.Set(`Content-Length`, fmt.Sprintf("%d", len(body)))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}

348
gitea/apiclient.go Normal file
View File

@ -0,0 +1,348 @@
package gitea
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/sync/semaphore"
)
type APIClient struct {
urlBase string
orgName string
apiSem *semaphore.Weighted
}
// NewAPIClient creates a new Gitea API client for a single Gitea organization.
// Set maxConnections to 0 for unlimited concurrent API calls.
func NewAPIClient(urlBase string, orgName string, maxConnections int64) *APIClient {
if !strings.HasSuffix(urlBase, `/`) {
urlBase += `/`
}
ret := &APIClient{
urlBase: urlBase,
orgName: orgName,
}
if maxConnections == 0 { // unlimited
ret.apiSem = semaphore.NewWeighted(99999)
} else {
ret.apiSem = semaphore.NewWeighted(maxConnections)
}
return ret
}
func (ac *APIClient) PopulateCommitInfo(ctx context.Context, rr *Repo) error {
// The most recent commit will be the head of one of the branches (easy to find)
brs, err := ac.Branches(ctx, rr.Name)
if err != nil {
return err
} else {
NewestCommit := time.Unix(0, 0) // sentinel
for _, br := range brs {
if br.Commit.Timestamp.After(NewestCommit) {
NewestCommit = br.Commit.Timestamp
}
}
if !NewestCommit.Equal(time.Unix(0, 0)) {
rr.NewestCommit = NewestCommit // replace it
}
}
return nil
}
func (ac *APIClient) PopulateTopics(ctx context.Context, rr *Repo) error {
t, err := ac.topicsForRepo(ctx, rr.Name)
if err != nil {
return err
}
rr.Topics = t
return nil
}
func (ac *APIClient) apiRequest(ctx context.Context, endpoint string, target interface{}) error {
err := ac.apiSem.Acquire(ctx, 1)
if err != nil {
return err // e.g. ctx closed
}
defer ac.apiSem.Release(1)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ac.urlBase+endpoint, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("HTTP %d", resp.StatusCode)
}
err = json.NewDecoder(resp.Body).Decode(target)
if err != nil {
return err
}
return nil
}
func (ac *APIClient) Branches(ctx context.Context, repo string) ([]Branch, error) {
var branches []Branch
err := ac.apiRequest(ctx, `api/v1/repos/`+url.PathEscape(ac.orgName)+`/`+url.PathEscape(repo)+`/branches`, &branches)
if err != nil {
return nil, err // e.g. ctx closed
}
return branches, nil
}
func (ac *APIClient) commitsPage(ctx context.Context, repo, ref string, page, limit int) ([]CommitListEntry, error) {
var ret []CommitListEntry
err := ac.apiRequest(ctx, fmt.Sprintf(`api/v1/repos/%s/%s/commits?page=%d&limit=%d`, url.PathEscape(ac.orgName), url.PathEscape(repo), page, limit), &ret)
if err != nil {
return nil, err // e.g. ctx closed
}
return ret, nil
}
func (ac *APIClient) Commits(ctx context.Context, repo, ref string) ([]CommitListEntry, error) {
var ret []CommitListEntry
nextPage := 1 // Counting starts at 1
for {
page, err := ac.commitsPage(ctx, repo, ref, nextPage, 300)
if err != nil {
return nil, err
}
if len(page) == 0 && len(ret) > 0 {
break // Found enough already
}
ret = append(ret, page...)
nextPage += 1
}
if len(ret) == 0 {
return nil, fmt.Errorf("no commits found")
}
return ret, nil
}
// reposPage gets a single page of the list of Git repositories in this organisation.
func (ac *APIClient) reposPage(ctx context.Context, page, limit int) ([]Repo, error) {
var repos []Repo
err := ac.apiRequest(ctx, `api/v1/orgs/`+url.PathEscape(ac.orgName)+fmt.Sprintf(`/repos?page=%d&limit=%d`, page, limit), &repos)
if err != nil {
return nil, err
}
return repos, nil
}
// repos gets a list of Git repositories in this organisation. It may have to
// make multiple network requests.
func (ac *APIClient) Repos(ctx context.Context) ([]Repo, error) {
// Seems like gitea-1.13.0-rc1 returns 30 results by default, and supports up to a limit of 100
// Make a much larger request
ret := make([]Repo, 0)
nextPage := 1 // Counting starts at 1
for {
page, err := ac.reposPage(ctx, nextPage, 300)
if err != nil {
return nil, err
}
if len(page) == 0 && len(ret) > 0 {
break // Found enough already
}
ret = append(ret, page...)
nextPage += 1
}
return ret, nil
}
// repoFile gets a single file from the default branch of the git repository
// Usually the default branch is `master`.
func (ac *APIClient) RepoFile(ctx context.Context, repo, filename string) ([]byte, error) {
var cr ContentsResponse
err := ac.apiRequest(ctx, `api/v1/repos/`+url.PathEscape(ac.orgName)+`/`+url.PathEscape(repo)+`/contents/`+url.PathEscape(filename), &cr)
if err != nil {
return nil, err
}
return cr.Content, nil
}
func (ac *APIClient) filesInDirectory(ctx context.Context, repo, dir string) ([]ReaddirEntry, error) {
err := ac.apiSem.Acquire(ctx, 1)
if err != nil {
return nil, err // e.g. ctx closed
}
defer ac.apiSem.Release(1)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, ac.urlBase+`api/v1/repos/`+url.PathEscape(ac.orgName)+`/`+url.PathEscape(repo)+`/contents/`+dir, nil) // n.b. $dir param not escaped
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
// "No files found" happens with a HTTP 500/404 error depending on Gitea version. Catch this special case
if resp.StatusCode == 500 || resp.StatusCode == 404 {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if strings.Contains(string(b), `does not exist`) {
return []ReaddirEntry{}, nil // no files found
}
}
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
var ret []ReaddirEntry
err = json.NewDecoder(resp.Body).Decode(&ret)
if err != nil {
return nil, err
}
return ret, nil
}
// imageFilesForRepo finds documentation images for the repository.
// It searches the top-level directory and the dist/ and doc/ subdirectories.
func (ac *APIClient) ImageFilesForRepo(ctx context.Context, repo string) ([]ReaddirEntry, error) {
ret := []ReaddirEntry{}
for _, dirName := range []string{``, `dist`, `doc`} {
files, err := ac.filesInDirectory(ctx, repo, dirName)
if err != nil {
return nil, fmt.Errorf("readdir(%s): %w", dirName, err)
}
for _, f := range files {
if f.isImage() {
ret = append(ret, f)
}
}
}
return ret, nil
}
func (ac *APIClient) topicsForRepo(ctx context.Context, repo string) ([]string, error) {
var tr TopicsResponse
err := ac.apiRequest(ctx, `api/v1/repos/`+url.PathEscape(ac.orgName)+`/`+url.PathEscape(repo)+`/topics`, &tr)
if err != nil {
return nil, err
}
return tr.Topics, nil
}
// renderMarkdown calls the remote Gitea server's own markdown renderer.
func (ac *APIClient) RenderMarkdown(ctx context.Context, repoName string, body string) ([]byte, error) {
err := ac.apiSem.Acquire(ctx, 1)
if err != nil {
return nil, err // e.g. ctx closed
}
defer ac.apiSem.Release(1)
jb, err := json.Marshal(MarkdownRequest{
Context: ac.urlBase + url.PathEscape(ac.orgName) + `/` + url.PathEscape(repoName) + `/src/branch/master`,
Mode: "gfm", // magic constant - Github Flavoured Markdown
Text: body,
})
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, ac.urlBase+`api/v1/markdown/`, bytes.NewReader(jb))
if err != nil {
return nil, err
}
req.Header.Set(`Content-Type`, `application/json`)
req.Header.Set(`Content-Length`, fmt.Sprintf("%d", len(jb)))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}
// renderMarkdownRaw calls the remote Gitea server's own markdown renderer.
func (ac *APIClient) renderMarkdownRaw(ctx context.Context, body []byte) ([]byte, error) {
err := ac.apiSem.Acquire(ctx, 1)
if err != nil {
return nil, err // e.g. ctx closed
}
defer ac.apiSem.Release(1)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, ac.urlBase+`api/v1/markdown/raw`, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set(`Content-Type`, `text/plain`)
req.Header.Set(`Content-Length`, fmt.Sprintf("%d", len(body)))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}

72
gitea/types.go Normal file
View File

@ -0,0 +1,72 @@
package gitea
import (
"strings"
"time"
)
type Repo struct {
Name string `json:"name"`
Description string `json:"description"`
GiteaCreated time.Time `json:"created_at"`
GiteaUpdated time.Time `json:"updated_at"`
// NewestCommit is populated via PopulateCommitInfo().
NewestCommit time.Time
// Topics is populated via topicsForRepo().
Topics []string
}
type ContentsResponse struct {
Content []byte `json:"content"` // Assume base64 "encoding" parameter in Gitea response, and use Go's auto decode
}
type TopicsResponse struct {
Topics []string `json:"topics"`
}
type ReaddirEntry struct {
Name string `json:"name"`
Path string `json:"path"`
Size int `json:"size"`
RawURL string `json:"download_url"`
}
func (rde ReaddirEntry) isImage() bool {
return strings.HasSuffix(rde.Name, `.png`) || strings.HasSuffix(rde.Name, `.jpg`) || strings.HasSuffix(rde.Name, `.jpeg`)
}
type MarkdownRequest struct {
Context string
Mode string
Text string
Wiki bool
}
type BranchCommit struct {
ID string `json:"id"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
type Branch struct {
Name string `json:"name"`
Commit BranchCommit `json:"commit"`
}
type AuthorInfo struct {
Name string `json:"name"`
Email string `json:"email"`
Date time.Time `json:"date"`
}
type CommitListEntryCommit struct {
Message string `json:"message"`
Author AuthorInfo `json:"author"`
Committer AuthorInfo `json:"committer"`
}
type CommitListEntry struct {
ID string `json:"sha"`
Commit CommitListEntryCommit `json:"commit"`
}

21
main.go
View File

@ -6,11 +6,10 @@ import (
"log" "log"
"net/http" "net/http"
"regexp" "regexp"
"strings"
"sync" "sync"
"teafolio/gitea"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"golang.org/x/sync/semaphore"
) )
type Config struct { type Config struct {
@ -31,10 +30,11 @@ type Application struct {
cfg Config cfg Config
rxRepoPage, rxRepoImage *regexp.Regexp rxRepoPage, rxRepoImage *regexp.Regexp
apiSem *semaphore.Weighted
gitea *gitea.APIClient
reposMut sync.RWMutex reposMut sync.RWMutex
reposCache []Repo // Sorted by recently-created-first reposCache []gitea.Repo // Sorted by recently-created-first
reposAlphabeticalOrder map[string]int reposAlphabeticalOrder map[string]int
} }
@ -52,17 +52,8 @@ func main() {
log.Fatalf("toml.DecodeFile: %s", err.Error()) log.Fatalf("toml.DecodeFile: %s", err.Error())
} }
// Assert Gitea URL always has trailing slash // Create Gitea API client
if !strings.HasSuffix(app.cfg.Gitea.URL, `/`) { app.gitea = gitea.NewAPIClient(app.cfg.Gitea.URL, app.cfg.Gitea.Org, app.cfg.Gitea.MaxConnections)
app.cfg.Gitea.URL += `/`
}
// Create semaphore
if app.cfg.Gitea.MaxConnections == 0 { // unlimited
app.apiSem = semaphore.NewWeighted(99999)
} else {
app.apiSem = semaphore.NewWeighted(app.cfg.Gitea.MaxConnections)
}
// Sync worker // Sync worker
go app.syncWorker(context.Background()) go app.syncWorker(context.Background())

View File

@ -53,7 +53,7 @@ func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
} }
rowClass := "" rowClass := ""
for _, topic := range repo.topics { for _, topic := range repo.Topics {
rowClass += `taggedWith-` + topic + ` ` rowClass += `taggedWith-` + topic + ` `
} }
@ -61,9 +61,9 @@ func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
<tr <tr
class="`+html.EscapeString(rowClass)+`" class="`+html.EscapeString(rowClass)+`"
data-sort-al="`+fmt.Sprintf("-%d", this.reposAlphabeticalOrder[repo.Name])+`" data-sort-al="`+fmt.Sprintf("-%d", this.reposAlphabeticalOrder[repo.Name])+`"
data-sort-ls="`+fmt.Sprintf("%.0f", repo.newestCommit.Sub(repo.GiteaCreated).Seconds())+`" data-sort-ls="`+fmt.Sprintf("%.0f", repo.NewestCommit.Sub(repo.GiteaCreated).Seconds())+`"
data-sort-ct="`+fmt.Sprintf("%d", repo.GiteaCreated.Unix())+`" data-sort-ct="`+fmt.Sprintf("%d", repo.GiteaCreated.Unix())+`"
data-sort-mt="`+fmt.Sprintf("%d", repo.newestCommit.Unix())+`" data-sort-mt="`+fmt.Sprintf("%d", repo.NewestCommit.Unix())+`"
> >
<td> <td>
<a href="`+pageHref+`"><img class="homeimage" loading="lazy" src="`+html.EscapeString(`/:banner/`+url.PathEscape(repo.Name))+`"></div></a> <a href="`+pageHref+`"><img class="homeimage" loading="lazy" src="`+html.EscapeString(`/:banner/`+url.PathEscape(repo.Name))+`"></div></a>
@ -74,7 +74,7 @@ func (this *Application) Homepage(w http.ResponseWriter, r *http.Request) {
<br> <br>
<small> <small>
`) `)
for _, topic := range repo.topics { for _, topic := range repo.Topics {
fmt.Fprint(w, `<a class="tag tag-link" data-tag="`+html.EscapeString(topic)+`">`+html.EscapeString(topic)+`</a> `) fmt.Fprint(w, `<a class="tag tag-link" data-tag="`+html.EscapeString(topic)+`">`+html.EscapeString(topic)+`</a> `)
} }
fmt.Fprint(w, ` fmt.Fprint(w, `

View File

@ -8,7 +8,7 @@ import (
func (this *Application) Bannerpage(w http.ResponseWriter, r *http.Request, repoName string) { func (this *Application) Bannerpage(w http.ResponseWriter, r *http.Request, repoName string) {
ctx := r.Context() ctx := r.Context()
images, err := this.imageFilesForRepo(ctx, repoName) images, err := this.gitea.ImageFilesForRepo(ctx, repoName)
if err != nil { if err != nil {
this.internalError(w, r, fmt.Errorf("listing images: %w", err)) this.internalError(w, r, fmt.Errorf("listing images: %w", err))
return return

View File

@ -14,7 +14,7 @@ func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoNa
repoURL := this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName) repoURL := this.cfg.Gitea.URL + url.PathEscape(this.cfg.Gitea.Org) + `/` + url.PathEscape(repoName)
extraHead := "" extraHead := ""
readme, err := this.repoFile(ctx, repoName, `README.md`) readme, err := this.gitea.RepoFile(ctx, repoName, `README.md`)
if err != nil { if err != nil {
this.internalError(w, r, fmt.Errorf("loading README.md: %w", err)) this.internalError(w, r, fmt.Errorf("loading README.md: %w", err))
return return
@ -29,7 +29,7 @@ func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoNa
if rr.Name != repoName { if rr.Name != repoName {
continue continue
} }
for _, topic := range rr.topics { for _, topic := range rr.Topics {
if topic == "article" { if topic == "article" {
hasArticleTag = true hasArticleTag = true
break break
@ -53,7 +53,7 @@ func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoNa
lines = append([]string{lines[0], lines[1], extraBadgesMd, ""}, lines[2:]...) lines = append([]string{lines[0], lines[1], extraBadgesMd, ""}, lines[2:]...)
} }
readmeHtml, err := this.renderMarkdown(ctx, repoName, strings.Join(lines, "\n")) readmeHtml, err := this.gitea.RenderMarkdown(ctx, repoName, strings.Join(lines, "\n"))
if err != nil { if err != nil {
this.internalError(w, r, fmt.Errorf("rendering markdown: %w", err)) this.internalError(w, r, fmt.Errorf("rendering markdown: %w", err))
return return
@ -61,14 +61,14 @@ func (this *Application) Repopage(w http.ResponseWriter, r *http.Request, repoNa
readmeHtml = []byte(strings.Replace(string(readmeHtml), `%%REPLACEME__BADGE%%`, `<img src="/static/build_success_brightgreen.svg" style="width:90px;height:20px;">`, 1)) readmeHtml = []byte(strings.Replace(string(readmeHtml), `%%REPLACEME__BADGE%%`, `<img src="/static/build_success_brightgreen.svg" style="width:90px;height:20px;">`, 1))
images, err := this.imageFilesForRepo(ctx, repoName) images, err := this.gitea.ImageFilesForRepo(ctx, repoName)
if err != nil { if err != nil {
this.internalError(w, r, fmt.Errorf("listing images: %w", err)) this.internalError(w, r, fmt.Errorf("listing images: %w", err))
return return
} }
// If the Git repository contains a top-level go.mod file, allow vanity imports // If the Git repository contains a top-level go.mod file, allow vanity imports
if goMod, err := this.repoFile(ctx, repoName, `go.mod`); err == nil { if goMod, err := this.gitea.RepoFile(ctx, repoName, `go.mod`); err == nil {
// Check the first line should be `module MODULENAME\n` // Check the first line should be `module MODULENAME\n`
firstLine := bytes.SplitN(goMod, []byte("\n"), 2)[0] firstLine := bytes.SplitN(goMod, []byte("\n"), 2)[0]

14
sync.go
View File

@ -4,13 +4,14 @@ import (
"context" "context"
"log" "log"
"sort" "sort"
"teafolio/gitea"
"time" "time"
) )
func (this *Application) sync(ctx context.Context) (bool, error) { func (this *Application) sync(ctx context.Context) (bool, error) {
// List repositories on Gitea // List repositories on Gitea
repos, err := this.repos(ctx) repos, err := this.gitea.Repos(ctx)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -39,11 +40,16 @@ func (this *Application) sync(ctx context.Context) (bool, error) {
anyChanges = true anyChanges = true
// Refresh timestamps // Refresh timestamps
this.populateCommitInfo(ctx, &rr) err := this.gitea.PopulateCommitInfo(ctx, &rr)
if err != nil {
log.Printf("loading branches for '%s': %s", rr.Name, err)
rr.NewestCommit = rr.GiteaUpdated // best guess
}
// Refresh topics // Refresh topics
if t, err := this.topicsForRepo(ctx, rr.Name); err == nil { err = this.gitea.PopulateTopics(ctx, &rr)
rr.topics = t if err != nil {
log.Printf("loading topics for '%s': %s", rr.Name, err)
} }
// Save // Save