2024-04-27 21:10:24 +02:00
|
|
|
package feed
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log/slog"
|
|
|
|
"net/http"
|
2024-05-10 12:05:42 +02:00
|
|
|
"sync"
|
2024-04-27 21:10:24 +02:00
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
type githubReleaseResponseJson struct {
|
|
|
|
TagName string `json:"tag_name"`
|
|
|
|
PublishedAt string `json:"published_at"`
|
|
|
|
HtmlUrl string `json:"html_url"`
|
|
|
|
Draft bool `json:"draft"`
|
|
|
|
PreRelease bool `json:"prerelease"`
|
|
|
|
Reactions struct {
|
|
|
|
Downvotes int `json:"-1"`
|
|
|
|
} `json:"reactions"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseGithubTime(t string) time.Time {
|
|
|
|
parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return time.Now()
|
|
|
|
}
|
|
|
|
|
|
|
|
return parsedTime
|
|
|
|
}
|
|
|
|
|
|
|
|
func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
|
|
|
|
appReleases := make(AppReleases, 0, len(repositories))
|
|
|
|
|
|
|
|
if len(repositories) == 0 {
|
|
|
|
return appReleases, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
requests := make([]*http.Request, len(repositories))
|
|
|
|
|
|
|
|
for i, repository := range repositories {
|
|
|
|
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=10", repository), nil)
|
|
|
|
|
|
|
|
if token != "" {
|
|
|
|
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
|
|
}
|
|
|
|
|
|
|
|
requests[i] = request
|
|
|
|
}
|
|
|
|
|
|
|
|
task := decodeJsonFromRequestTask[[]githubReleaseResponseJson](defaultClient)
|
|
|
|
job := newJob(task, requests).withWorkers(15)
|
|
|
|
responses, errs, err := workerPoolDo(job)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var failed int
|
|
|
|
|
|
|
|
for i := range responses {
|
|
|
|
if errs[i] != nil {
|
|
|
|
failed++
|
|
|
|
slog.Error("Failed to fetch or parse github release", "error", errs[i], "url", requests[i].URL)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
releases := responses[i]
|
|
|
|
|
|
|
|
if len(releases) < 1 {
|
|
|
|
failed++
|
|
|
|
slog.Error("No releases found", "repository", repositories[i], "url", requests[i].URL)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var liveRelease *githubReleaseResponseJson
|
|
|
|
|
|
|
|
for i := range releases {
|
|
|
|
release := &releases[i]
|
|
|
|
|
|
|
|
if !release.Draft && !release.PreRelease {
|
|
|
|
liveRelease = release
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if liveRelease == nil {
|
|
|
|
slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
version := liveRelease.TagName
|
|
|
|
|
|
|
|
if version[0] != 'v' {
|
|
|
|
version = "v" + version
|
|
|
|
}
|
|
|
|
|
|
|
|
appReleases = append(appReleases, AppRelease{
|
|
|
|
Name: repositories[i],
|
|
|
|
Version: version,
|
|
|
|
NotesUrl: liveRelease.HtmlUrl,
|
|
|
|
TimeReleased: parseGithubTime(liveRelease.PublishedAt),
|
|
|
|
Downvotes: liveRelease.Reactions.Downvotes,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(appReleases) == 0 {
|
|
|
|
return nil, ErrNoContent
|
|
|
|
}
|
|
|
|
|
|
|
|
appReleases.SortByNewest()
|
|
|
|
|
|
|
|
if failed > 0 {
|
|
|
|
return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
|
|
|
|
}
|
|
|
|
|
|
|
|
return appReleases, nil
|
|
|
|
}
|
2024-05-10 12:05:42 +02:00
|
|
|
|
|
|
|
type GithubTicket struct {
|
|
|
|
Number int
|
|
|
|
CreatedAt time.Time
|
|
|
|
Title string
|
|
|
|
}
|
|
|
|
|
|
|
|
type RepositoryDetails struct {
|
|
|
|
Name string
|
|
|
|
Stars int
|
|
|
|
Forks int
|
|
|
|
OpenPullRequests int
|
|
|
|
PullRequests []GithubTicket
|
|
|
|
OpenIssues int
|
|
|
|
Issues []GithubTicket
|
|
|
|
}
|
|
|
|
|
|
|
|
type githubRepositoryDetailsResponseJson struct {
|
|
|
|
Name string `json:"full_name"`
|
|
|
|
Stars int `json:"stargazers_count"`
|
|
|
|
Forks int `json:"forks_count"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type githubTicketResponseJson struct {
|
|
|
|
Count int `json:"total_count"`
|
|
|
|
Tickets []struct {
|
|
|
|
Number int `json:"number"`
|
|
|
|
CreatedAt string `json:"created_at"`
|
|
|
|
Title string `json:"title"`
|
|
|
|
} `json:"items"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) {
|
|
|
|
repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
PRsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:pr+is:open+repo:%s&per_page=%d", repository, maxPRs), nil)
|
|
|
|
issuesRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:issue+is:open+repo:%s&per_page=%d", repository, maxIssues), nil)
|
|
|
|
|
|
|
|
if token != "" {
|
|
|
|
token = fmt.Sprintf("Bearer %s", token)
|
|
|
|
repositoryRequest.Header.Add("Authorization", token)
|
|
|
|
PRsRequest.Header.Add("Authorization", token)
|
|
|
|
issuesRequest.Header.Add("Authorization", token)
|
|
|
|
}
|
|
|
|
|
|
|
|
var detailsResponse githubRepositoryDetailsResponseJson
|
|
|
|
var detailsErr error
|
|
|
|
var PRsResponse githubTicketResponseJson
|
|
|
|
var PRsErr error
|
|
|
|
var issuesResponse githubTicketResponseJson
|
|
|
|
var issuesErr error
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go (func() {
|
|
|
|
defer wg.Done()
|
|
|
|
detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
|
|
|
|
})()
|
|
|
|
|
|
|
|
if maxPRs > 0 {
|
|
|
|
wg.Add(1)
|
|
|
|
go (func() {
|
|
|
|
defer wg.Done()
|
|
|
|
PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
|
|
|
|
})()
|
|
|
|
}
|
|
|
|
|
|
|
|
if maxIssues > 0 {
|
|
|
|
wg.Add(1)
|
|
|
|
go (func() {
|
|
|
|
defer wg.Done()
|
|
|
|
issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
|
|
|
|
})()
|
|
|
|
}
|
|
|
|
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
if detailsErr != nil {
|
|
|
|
return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
|
|
|
|
}
|
|
|
|
|
|
|
|
details := RepositoryDetails{
|
|
|
|
Name: detailsResponse.Name,
|
|
|
|
Stars: detailsResponse.Stars,
|
|
|
|
Forks: detailsResponse.Forks,
|
|
|
|
PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
|
|
|
|
Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
|
|
|
|
}
|
|
|
|
|
|
|
|
err = nil
|
|
|
|
|
|
|
|
if maxPRs > 0 {
|
|
|
|
if PRsErr != nil {
|
|
|
|
err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
|
|
|
|
} else {
|
|
|
|
details.OpenPullRequests = PRsResponse.Count
|
|
|
|
|
|
|
|
for i := range PRsResponse.Tickets {
|
|
|
|
details.PullRequests = append(details.PullRequests, GithubTicket{
|
|
|
|
Number: PRsResponse.Tickets[i].Number,
|
|
|
|
CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
|
|
|
|
Title: PRsResponse.Tickets[i].Title,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if maxIssues > 0 {
|
|
|
|
if issuesErr != nil {
|
|
|
|
// TODO: fix, overwriting the previous error
|
|
|
|
err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
|
|
|
|
} else {
|
|
|
|
details.OpenIssues = issuesResponse.Count
|
|
|
|
|
|
|
|
for i := range issuesResponse.Tickets {
|
|
|
|
details.Issues = append(details.Issues, GithubTicket{
|
|
|
|
Number: issuesResponse.Tickets[i].Number,
|
|
|
|
CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
|
|
|
|
Title: issuesResponse.Tickets[i].Title,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return details, err
|
|
|
|
}
|