Skip to content

Commit a11fb69

Browse files
committed
feat: add contributors api
1 parent d020c78 commit a11fb69

3 files changed

Lines changed: 262 additions & 0 deletions

File tree

internal/handlers/contributors.go

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"sort"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/gofiber/fiber/v2"
14+
)
15+
16+
// Contributor represents a GitHub contributor
17+
type Contributor struct {
18+
Login string `json:"login"`
19+
ID int `json:"id"`
20+
AvatarURL string `json:"avatar_url"`
21+
HTMLURL string `json:"html_url"`
22+
Contributions int `json:"contributions"`
23+
Type string `json:"type,omitempty"`
24+
}
25+
26+
// ContributorsHandler handles contributor-related requests
27+
type ContributorsHandler struct {
28+
githubToken string
29+
cache []Contributor
30+
cacheMutex sync.RWMutex
31+
cacheTime time.Time
32+
cacheTTL time.Duration
33+
}
34+
35+
var contributorsHandler *ContributorsHandler
36+
37+
// InitContributorsHandler initializes the contributors handler
38+
func InitContributorsHandler(githubToken string) {
39+
contributorsHandler = &ContributorsHandler{
40+
githubToken: githubToken,
41+
cacheTTL: 24 * time.Hour, // Cache for 24 hours
42+
cache: []Contributor{},
43+
}
44+
}
45+
46+
// GetContributorsHandler is the exported handler function for getting contributors
47+
func GetContributorsHandler(c *fiber.Ctx) error {
48+
if contributorsHandler == nil {
49+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
50+
"error": "Contributors handler not initialized",
51+
})
52+
}
53+
return contributorsHandler.GetContributors(c)
54+
}
55+
56+
// GetContributorsStatsHandler is the exported handler function for getting stats
57+
func GetContributorsStatsHandler(c *fiber.Ctx) error {
58+
if contributorsHandler == nil {
59+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
60+
"error": "Contributors handler not initialized",
61+
})
62+
}
63+
return contributorsHandler.GetContributorStats(c)
64+
}
65+
66+
// GetContributors retrieves contributors from both repositories
67+
func (h *ContributorsHandler) GetContributors(c *fiber.Ctx) error {
68+
// Check cache
69+
h.cacheMutex.RLock()
70+
if len(h.cache) > 0 && time.Since(h.cacheTime) < h.cacheTTL {
71+
defer h.cacheMutex.RUnlock()
72+
return c.JSON(h.cache)
73+
}
74+
h.cacheMutex.RUnlock()
75+
76+
// Fetch from both repos
77+
fixfxContribs := h.fetchRepoContributors("CodeMeAPixel", "FixFX")
78+
coreContribs := h.fetchRepoContributors("CodeMeAPixel", "FixFX-Core")
79+
80+
// Merge contributors
81+
merged := h.mergeContributors(fixfxContribs, coreContribs)
82+
83+
// Sort by contributions (descending)
84+
sort.Slice(merged, func(i, j int) bool {
85+
return merged[i].Contributions > merged[j].Contributions
86+
})
87+
88+
// Apply limit if specified
89+
limit := c.QueryInt("limit", 0)
90+
if limit > 0 && limit < len(merged) {
91+
merged = merged[:limit]
92+
}
93+
94+
// Update cache
95+
h.cacheMutex.Lock()
96+
h.cache = merged
97+
h.cacheTime = time.Now()
98+
h.cacheMutex.Unlock()
99+
100+
return c.JSON(merged)
101+
}
102+
103+
// GetContributorStats returns statistics about contributors
104+
func (h *ContributorsHandler) GetContributorStats(c *fiber.Ctx) error {
105+
contributors := h.getOrFetchContributors()
106+
107+
stats := fiber.Map{
108+
"total": len(contributors),
109+
"topContributor": nil,
110+
"totalContributions": 0,
111+
"byRepository": fiber.Map{},
112+
}
113+
114+
totalContrib := 0
115+
for _, contrib := range contributors {
116+
totalContrib += contrib.Contributions
117+
}
118+
stats["totalContributions"] = totalContrib
119+
120+
if len(contributors) > 0 {
121+
stats["topContributor"] = contributors[0]
122+
}
123+
124+
// Break down by repo
125+
fixfxContribs := h.fetchRepoContributors("CodeMeAPixel", "FixFX")
126+
coreContribs := h.fetchRepoContributors("CodeMeAPixel", "FixFX-Core")
127+
128+
stats["byRepository"] = fiber.Map{
129+
"FixFX": len(fixfxContribs),
130+
"FixFX-Core": len(coreContribs),
131+
}
132+
133+
return c.JSON(stats)
134+
}
135+
136+
// fetchRepoContributors fetches contributors from a specific GitHub repository
137+
func (h *ContributorsHandler) fetchRepoContributors(owner, repo string) []Contributor {
138+
var contributors []Contributor
139+
page := 1
140+
perPage := 100
141+
142+
for {
143+
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contributors?per_page=%d&page=%d", owner, repo, perPage, page)
144+
145+
req, err := http.NewRequest("GET", url, nil)
146+
if err != nil {
147+
continue
148+
}
149+
150+
// Add authentication header if token is available
151+
if h.githubToken != "" {
152+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", h.githubToken))
153+
}
154+
155+
req.Header.Set("Accept", "application/vnd.github.v3+json")
156+
req.Header.Set("User-Agent", "FixFX-API")
157+
158+
client := &http.Client{Timeout: 10 * time.Second}
159+
resp, err := client.Do(req)
160+
if err != nil {
161+
break
162+
}
163+
defer resp.Body.Close()
164+
165+
if resp.StatusCode != http.StatusOK {
166+
break
167+
}
168+
169+
body, err := io.ReadAll(resp.Body)
170+
if err != nil {
171+
break
172+
}
173+
174+
var pageContribs []Contributor
175+
if err := json.Unmarshal(body, &pageContribs); err != nil {
176+
break
177+
}
178+
179+
if len(pageContribs) == 0 {
180+
break
181+
}
182+
183+
contributors = append(contributors, pageContribs...)
184+
185+
if len(pageContribs) < perPage {
186+
break
187+
}
188+
189+
page++
190+
}
191+
192+
return contributors
193+
}
194+
195+
// mergeContributors merges contributors from multiple repos, combining their contributions
196+
func (h *ContributorsHandler) mergeContributors(repos ...[]Contributor) []Contributor {
197+
mergedMap := make(map[string]*Contributor)
198+
199+
// Merge all contributors
200+
for _, repo := range repos {
201+
for _, contrib := range repo {
202+
if existing, found := mergedMap[strings.ToLower(contrib.Login)]; found {
203+
existing.Contributions += contrib.Contributions
204+
} else {
205+
contrib := contrib // Create a copy
206+
mergedMap[strings.ToLower(contrib.Login)] = &contrib
207+
}
208+
}
209+
}
210+
211+
// Convert back to slice
212+
result := make([]Contributor, 0, len(mergedMap))
213+
for _, contrib := range mergedMap {
214+
result = append(result, *contrib)
215+
}
216+
217+
return result
218+
}
219+
220+
// getOrFetchContributors gets contributors from cache or fetches them
221+
func (h *ContributorsHandler) getOrFetchContributors() []Contributor {
222+
h.cacheMutex.RLock()
223+
if len(h.cache) > 0 && time.Since(h.cacheTime) < h.cacheTTL {
224+
defer h.cacheMutex.RUnlock()
225+
return h.cache
226+
}
227+
h.cacheMutex.RUnlock()
228+
229+
fixfxContribs := h.fetchRepoContributors("CodeMeAPixel", "FixFX")
230+
coreContribs := h.fetchRepoContributors("CodeMeAPixel", "FixFX-Core")
231+
merged := h.mergeContributors(fixfxContribs, coreContribs)
232+
233+
sort.Slice(merged, func(i, j int) bool {
234+
return merged[i].Contributions > merged[j].Contributions
235+
})
236+
237+
h.cacheMutex.Lock()
238+
h.cache = merged
239+
h.cacheTime = time.Now()
240+
h.cacheMutex.Unlock()
241+
242+
return merged
243+
}

internal/routes/contributors.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package routes
2+
3+
import (
4+
"github.com/CodeMeAPixel/FixFX-Core/internal/handlers"
5+
"github.com/gofiber/fiber/v2"
6+
)
7+
8+
// RegisterContributorsRoutes registers all contributors-related routes
9+
func RegisterContributorsRoutes(api fiber.Router) {
10+
contributors := api.Group("/contributors")
11+
12+
// GET /api/contributors - Get list of contributors
13+
contributors.Get("", handlers.GetContributorsHandler)
14+
15+
// GET /api/contributors/stats - Get contributor statistics
16+
contributors.Get("/stats", handlers.GetContributorsStatsHandler)
17+
}

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func main() {
6161
handlers.InitArtifactsHandler(githubToken)
6262
handlers.InitNativesHandler()
6363
handlers.InitSourceHandler(".", nil) // Use current directory as base path
64+
handlers.InitContributorsHandler(githubToken)
6465

6566
// Health check
6667
app.Get("/health", healthCheck)
@@ -83,6 +84,7 @@ func main() {
8384
routes.RegisterNativesRoutes(api)
8485
routes.RegisterSourceRoutes(api)
8586
routes.RegisterSearchRoutes(api)
87+
routes.RegisterContributorsRoutes(api)
8688

8789
// Start server
8890
port := os.Getenv("PORT")

0 commit comments

Comments
 (0)