Skip to content

Commit 73ebf36

Browse files
committed
Added Forks Timeline
1 parent 38d80bf commit 73ebf36

6 files changed

Lines changed: 933 additions & 4 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.21.6
44

55
require (
66
github.com/Code-Hex/go-generics-cache v1.5.1
7-
github.com/emanuelef/github-repo-activity-stats v0.2.29
7+
github.com/emanuelef/github-repo-activity-stats v0.2.30
88
github.com/gofiber/contrib/otelfiber v1.0.10
99
github.com/gofiber/fiber/v2 v2.52.5
1010
github.com/joho/godotenv v1.5.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
66
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
77
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
88
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9-
github.com/emanuelef/github-repo-activity-stats v0.2.29 h1:zJNiNWnBv8gBMIxakzy9SimJpGYz9pOJUA6CvK5b2oY=
10-
github.com/emanuelef/github-repo-activity-stats v0.2.29/go.mod h1:Z6q8Ahan1HhKCArQovdXyjZAhHswriqTFGJ14fu7MKA=
9+
github.com/emanuelef/github-repo-activity-stats v0.2.30 h1:Q6/nV+CsV6XhCI648t6VNhbq7/3oygIuZiaWNmgSK7A=
10+
github.com/emanuelef/github-repo-activity-stats v0.2.30/go.mod h1:Z6q8Ahan1HhKCArQovdXyjZAhHswriqTFGJ14fu7MKA=
1111
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
1212
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
1313
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=

main.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ type IssuesWithStatsResponse struct {
5555
Issues []stats.IssuesPerDay `json:"issues"`
5656
}
5757

58+
type ForksWithStatsResponse struct {
59+
Forks []stats.ForksPerDay `json:"forks"`
60+
}
61+
5862
func getEnv(key, fallback string) string {
5963
value, exists := os.LookupEnv(key)
6064
if !exists {
@@ -106,9 +110,11 @@ func main() {
106110
cacheOverall := cache.New[string, *stats.RepoStats]()
107111
cacheStars := cache.New[string, StarsWithStatsResponse]()
108112
cacheIssues := cache.New[string, IssuesWithStatsResponse]()
113+
cacheForks := cache.New[string, ForksWithStatsResponse]()
109114

110115
onGoingStars := make(map[string]bool)
111116
onGoingIssues := make(map[string]bool)
117+
onGoingForks := make(map[string]bool)
112118

113119
ghStatClients := make(map[string]*repostats.ClientGQL)
114120

@@ -454,6 +460,110 @@ func main() {
454460
return c.JSON(res)
455461
})
456462

463+
app.Get("/allForks", func(c *fiber.Ctx) error {
464+
param := c.Query("repo")
465+
randomIndex := rand.Intn(len(maps.Keys(ghStatClients)))
466+
clientKey := c.Query("client", maps.Keys(ghStatClients)[randomIndex])
467+
forceRefetch := c.Query("forceRefetch", "false") == "true"
468+
469+
client, ok := ghStatClients[clientKey]
470+
if !ok {
471+
return c.Status(404).SendString("Resource not found")
472+
}
473+
474+
repo, err := url.QueryUnescape(param)
475+
if err != nil {
476+
return err
477+
}
478+
479+
// needed because c.Query cannot be used as a map key
480+
repo = fmt.Sprintf("%s", repo)
481+
repo = strings.ToLower(repo)
482+
483+
ip := c.Get("X-Forwarded-For")
484+
485+
// If X-Forwarded-For is empty, fallback to RemoteIP
486+
if ip == "" {
487+
ip = c.IP()
488+
}
489+
490+
userAgent := c.Get("User-Agent")
491+
log.Printf("Forks Request from IP: %s, Repo: %s User-Agent: %s\n", ip, repo, userAgent)
492+
493+
if strings.Contains(userAgent, "python-requests") {
494+
return c.Status(404).SendString("Custom 404 Error: Resource not found")
495+
}
496+
497+
span := trace.SpanFromContext(c.UserContext())
498+
span.SetAttributes(attribute.String("github.repo", repo))
499+
span.SetAttributes(attribute.String("caller.ip", ip))
500+
501+
if forceRefetch {
502+
cacheForks.Delete(repo)
503+
}
504+
505+
if res, hit := cacheForks.Get(repo); hit {
506+
return c.JSON(res)
507+
}
508+
509+
// if another request is already getting the data, skip and rely on SSE updates
510+
if _, hit := onGoingForks[repo]; hit {
511+
return c.SendStatus(fiber.StatusNoContent)
512+
}
513+
514+
onGoingForks[repo] = true
515+
516+
updateChannel := make(chan int)
517+
var allForks []stats.ForksPerDay
518+
519+
eg, ctx := errgroup.WithContext(ctx)
520+
521+
eg.Go(func() error {
522+
allForks, err = client.GetAllForksHistory(ctx, repo, updateChannel)
523+
if err != nil {
524+
return err
525+
}
526+
return nil
527+
})
528+
529+
for progress := range updateChannel {
530+
// fmt.Printf("Progress: %d\n", progress)
531+
532+
wg := &sync.WaitGroup{}
533+
534+
for _, s := range currentSessions.Sessions {
535+
wg.Add(1)
536+
go func(cs *session.Session) {
537+
defer wg.Done()
538+
if cs.Repo == repo {
539+
cs.StateChannel <- progress
540+
}
541+
}(s)
542+
}
543+
wg.Wait()
544+
}
545+
546+
if err := eg.Wait(); err != nil {
547+
delete(onGoingForks, repo)
548+
return err
549+
}
550+
551+
// defer close(updateChannel)
552+
553+
res := ForksWithStatsResponse{
554+
Forks: allForks,
555+
}
556+
557+
now := time.Now()
558+
nextDay := now.UTC().Truncate(24 * time.Hour).Add(DAY_CACHED * 24 * time.Hour)
559+
durationUntilEndOfDay := nextDay.Sub(now)
560+
561+
cacheForks.Set(repo, res, cache.WithExpiration(durationUntilEndOfDay))
562+
delete(onGoingForks, repo)
563+
564+
return c.JSON(res)
565+
})
566+
457567
app.Get("/limits", func(c *fiber.Ctx) error {
458568
client, ok := ghStatClients["PAT"]
459569
if !ok {

website/src/App.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import MainPage from "./MainPage";
55
import TimeSeriesChart from "./TimeSeriesChart";
66
import CompareChart from "./CompareChart";
77
import IssuesTimeSeriesChart from "./IssuesTimeSeriesChart";
8+
import ForksTimeSeriesChart from "./ForksTimeSeriesChart";
89
import CalendarChart from "./CalendarChart";
910
import InfoPage from "./InfoPage";
1011

@@ -18,6 +19,7 @@ import QueryStatsRoundedIcon from "@mui/icons-material/QueryStatsRounded";
1819
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
1920
import SsidChartRoundedIcon from "@mui/icons-material/SsidChartRounded";
2021
import BugReportRoundedIcon from "@mui/icons-material/BugReportRounded";
22+
import AltRouteOutlinedIcon from "@mui/icons-material/AltRouteOutlined";
2123

2224
import { ThemeProvider, createTheme } from "@mui/material/styles";
2325
import CssBaseline from "@mui/material/CssBaseline";
@@ -81,7 +83,8 @@ function App() {
8183
useLocation().pathname.includes("/table") ||
8284
useLocation().pathname.includes("/calendar") ||
8385
useLocation().pathname.includes("/info") ||
84-
useLocation().pathname.includes("/issues")
86+
useLocation().pathname.includes("/issues") ||
87+
useLocation().pathname.includes("/forks")
8588
)
8689
}
8790
>
@@ -109,6 +112,17 @@ function App() {
109112
>
110113
Issues
111114
</MenuItem>
115+
<MenuItem
116+
component={<Link to="/forks" className="link" />}
117+
icon={
118+
<Tooltip title="Forks Timeline" placement="right">
119+
<AltRouteOutlinedIcon />
120+
</Tooltip>
121+
}
122+
active={useLocation().pathname.includes("/forks")}
123+
>
124+
Forks
125+
</MenuItem>
112126
<MenuItem
113127
component={<Link to="/table" className="link" />}
114128
icon={
@@ -158,6 +172,7 @@ function App() {
158172
<Route path="/calendar" element={<CalendarChart />} />
159173
<Route path="/info" element={<InfoPage />} />
160174
<Route path="/issues" element={<IssuesTimeSeriesChart />} />
175+
<Route path="/forks" element={<ForksTimeSeriesChart />} />
161176
</Routes>
162177
</section>
163178
</div>

0 commit comments

Comments
 (0)