github.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. package feed
  2. import (
  3. "fmt"
  4. "log/slog"
  5. "net/http"
  6. "strings"
  7. "sync"
  8. "time"
  9. )
  10. type githubReleaseResponseJson struct {
  11. TagName string `json:"tag_name"`
  12. PublishedAt string `json:"published_at"`
  13. HtmlUrl string `json:"html_url"`
  14. Draft bool `json:"draft"`
  15. PreRelease bool `json:"prerelease"`
  16. Reactions struct {
  17. Downvotes int `json:"-1"`
  18. } `json:"reactions"`
  19. }
  20. func parseGithubTime(t string) time.Time {
  21. parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
  22. if err != nil {
  23. return time.Now()
  24. }
  25. return parsedTime
  26. }
  27. func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
  28. appReleases := make(AppReleases, 0, len(repositories))
  29. if len(repositories) == 0 {
  30. return appReleases, nil
  31. }
  32. requests := make([]*http.Request, len(repositories))
  33. for i, repository := range repositories {
  34. request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=10", repository), nil)
  35. if token != "" {
  36. request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
  37. }
  38. requests[i] = request
  39. }
  40. task := decodeJsonFromRequestTask[[]githubReleaseResponseJson](defaultClient)
  41. job := newJob(task, requests).withWorkers(15)
  42. responses, errs, err := workerPoolDo(job)
  43. if err != nil {
  44. return nil, err
  45. }
  46. var failed int
  47. for i := range responses {
  48. if errs[i] != nil {
  49. failed++
  50. slog.Error("Failed to fetch or parse github release", "error", errs[i], "url", requests[i].URL)
  51. continue
  52. }
  53. releases := responses[i]
  54. if len(releases) < 1 {
  55. failed++
  56. slog.Error("No releases found", "repository", repositories[i], "url", requests[i].URL)
  57. continue
  58. }
  59. var liveRelease *githubReleaseResponseJson
  60. for i := range releases {
  61. release := &releases[i]
  62. if !release.Draft && !release.PreRelease {
  63. liveRelease = release
  64. break
  65. }
  66. }
  67. if liveRelease == nil {
  68. slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
  69. continue
  70. }
  71. version := liveRelease.TagName
  72. if version[0] != 'v' {
  73. version = "v" + version
  74. }
  75. appReleases = append(appReleases, AppRelease{
  76. Name: repositories[i],
  77. Version: version,
  78. NotesUrl: liveRelease.HtmlUrl,
  79. TimeReleased: parseGithubTime(liveRelease.PublishedAt),
  80. Downvotes: liveRelease.Reactions.Downvotes,
  81. })
  82. }
  83. if len(appReleases) == 0 {
  84. return nil, ErrNoContent
  85. }
  86. appReleases.SortByNewest()
  87. if failed > 0 {
  88. return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
  89. }
  90. return appReleases, nil
  91. }
  92. type GithubTicket struct {
  93. Number int
  94. CreatedAt time.Time
  95. Title string
  96. }
  97. type RepositoryDetails struct {
  98. Name string
  99. Stars int
  100. Forks int
  101. OpenPullRequests int
  102. PullRequests []GithubTicket
  103. OpenIssues int
  104. Issues []GithubTicket
  105. LastCommits int
  106. Commits []CommitsDetails
  107. }
  108. type githubRepositoryDetailsResponseJson struct {
  109. Name string `json:"full_name"`
  110. Stars int `json:"stargazers_count"`
  111. Forks int `json:"forks_count"`
  112. }
  113. type githubTicketResponseJson struct {
  114. Count int `json:"total_count"`
  115. Tickets []struct {
  116. Number int `json:"number"`
  117. CreatedAt string `json:"created_at"`
  118. Title string `json:"title"`
  119. } `json:"items"`
  120. }
  121. type CommitsDetails struct {
  122. Sha string
  123. Author string
  124. Email string
  125. Date time.Time
  126. Message string
  127. }
  128. type Author struct {
  129. Name string `json:"name"`
  130. Email string `json:"email"`
  131. Date string `json:"date"`
  132. }
  133. type Commit struct {
  134. Sha string `json:"sha"`
  135. Commit struct {
  136. Author Author `json:"author"`
  137. Message string `json:"message"`
  138. } `json:"commit"`
  139. }
  140. type githubCommitsResponseJson []Commit
  141. func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int, maxCommits int) (RepositoryDetails, error) {
  142. repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
  143. if err != nil {
  144. return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
  145. }
  146. 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)
  147. 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)
  148. CommitsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/commits?per_page=%d", repository, maxCommits), nil)
  149. if token != "" {
  150. token = fmt.Sprintf("Bearer %s", token)
  151. repositoryRequest.Header.Add("Authorization", token)
  152. PRsRequest.Header.Add("Authorization", token)
  153. issuesRequest.Header.Add("Authorization", token)
  154. CommitsRequest.Header.Add("Authorization", token)
  155. }
  156. var detailsResponse githubRepositoryDetailsResponseJson
  157. var detailsErr error
  158. var PRsResponse githubTicketResponseJson
  159. var PRsErr error
  160. var issuesResponse githubTicketResponseJson
  161. var issuesErr error
  162. var CommitsResponse githubCommitsResponseJson
  163. var CommitsErr error
  164. var wg sync.WaitGroup
  165. wg.Add(1)
  166. go (func() {
  167. defer wg.Done()
  168. detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
  169. })()
  170. if maxPRs > 0 {
  171. wg.Add(1)
  172. go (func() {
  173. defer wg.Done()
  174. PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
  175. })()
  176. }
  177. if maxIssues > 0 {
  178. wg.Add(1)
  179. go (func() {
  180. defer wg.Done()
  181. issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
  182. })()
  183. }
  184. if maxCommits > 0 {
  185. wg.Add(1)
  186. go (func() {
  187. defer wg.Done()
  188. CommitsResponse, CommitsErr = decodeJsonFromRequest[githubCommitsResponseJson](defaultClient, CommitsRequest)
  189. })()
  190. }
  191. wg.Wait()
  192. if detailsErr != nil {
  193. return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
  194. }
  195. details := RepositoryDetails{
  196. Name: detailsResponse.Name,
  197. Stars: detailsResponse.Stars,
  198. Forks: detailsResponse.Forks,
  199. PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
  200. Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
  201. Commits: make([]CommitsDetails, 0, len(CommitsResponse)),
  202. }
  203. err = nil
  204. if maxPRs > 0 {
  205. if PRsErr != nil {
  206. err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
  207. } else {
  208. details.OpenPullRequests = PRsResponse.Count
  209. for i := range PRsResponse.Tickets {
  210. details.PullRequests = append(details.PullRequests, GithubTicket{
  211. Number: PRsResponse.Tickets[i].Number,
  212. CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
  213. Title: PRsResponse.Tickets[i].Title,
  214. })
  215. }
  216. }
  217. }
  218. if maxIssues > 0 {
  219. if issuesErr != nil {
  220. // TODO: fix, overwriting the previous error
  221. err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
  222. } else {
  223. details.OpenIssues = issuesResponse.Count
  224. for i := range issuesResponse.Tickets {
  225. details.Issues = append(details.Issues, GithubTicket{
  226. Number: issuesResponse.Tickets[i].Number,
  227. CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
  228. Title: issuesResponse.Tickets[i].Title,
  229. })
  230. }
  231. }
  232. }
  233. if maxCommits > 0 {
  234. if CommitsErr != nil {
  235. err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, CommitsErr)
  236. } else {
  237. for _, commit := range CommitsResponse {
  238. details.LastCommits++
  239. details.Commits = append(details.Commits, CommitsDetails{
  240. Sha: commit.Sha,
  241. Author: commit.Commit.Author.Name,
  242. Email: commit.Commit.Author.Email,
  243. Date: parseGithubTime(commit.Commit.Author.Date),
  244. Message: strings.SplitN(commit.Commit.Message, "\n\n", 2)[0],
  245. })
  246. }
  247. }
  248. }
  249. return details, err
  250. }