1package main
2
3import (
4 "encoding/xml"
5 "fmt"
6 "log"
7 "net/http"
8 "sort"
9 "strconv"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/go-git/go-git/v5"
15 "github.com/go-git/go-git/v5/plumbing"
16 "github.com/go-git/go-git/v5/plumbing/object"
17)
18
19func repoHandler(w http.ResponseWriter, r *http.Request) {
20 ctx, err := getRepoContext(w, r)
21 if err != nil {
22 if err.Error() == "repository not found" {
23 http.NotFound(w, r)
24 } else {
25 http.Error(w, err.Error(), http.StatusInternalServerError)
26 }
27 return
28 }
29
30 page, _ := strconv.Atoi(r.URL.Query().Get("page"))
31 if page < 1 {
32 page = 1
33 }
34 pageSize := 30
35
36 totalCommitsKey := ctx.Repo.Name + ":" + ctx.Hash.String()
37 var totalCommits int
38 if val, ok := repoMetadataCache.Load(totalCommitsKey); ok {
39 totalCommits = val.(RepoMetadata).TotalCommits
40 } else {
41 cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash})
42 if err != nil {
43 http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError)
44 return
45 }
46 _ = cIter.ForEach(func(c *object.Commit) error {
47 totalCommits++
48 return nil
49 })
50 repoMetadataCache.Store(totalCommitsKey, RepoMetadata{
51 TotalCommits: totalCommits,
52 Branches: ctx.Branches,
53 Tags: ctx.Tags,
54 ReadmeName: ctx.ReadmeName,
55 LicenseName: ctx.LicenseName,
56 Version: CurrentMetadataVersion,
57 })
58 NotifySave()
59 }
60
61 totalPages := (totalCommits + pageSize - 1) / pageSize
62
63 cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash})
64 if err != nil {
65 http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError)
66 return
67 }
68
69 var commits []Commit
70 var wg sync.WaitGroup
71 count := 0
72
73 var commitsToProcess []*object.Commit
74 err = cIter.ForEach(func(c *object.Commit) error {
75 if count < (page-1)*pageSize {
76 count++
77 return nil
78 }
79 if len(commitsToProcess) >= pageSize {
80 return fmt.Errorf("limit reached")
81 }
82 commitsToProcess = append(commitsToProcess, c)
83 count++
84 return nil
85 })
86
87 if err != nil && err.Error() != "limit reached" {
88 http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError)
89 return
90 }
91
92 commits = make([]Commit, len(commitsToProcess))
93 for i, c := range commitsToProcess {
94 wg.Add(1)
95 go func(idx int, commit *object.Commit) {
96 defer wg.Done()
97 hashStr := commit.Hash.String()
98
99 var adds, dels int
100 if val, ok := commitStatsCache.Load(hashStr); ok {
101 s := val.(CommitStat)
102 adds, dels = s.Additions, s.Deletions
103 } else {
104 stats, err := commit.Stats()
105 if err == nil {
106 for _, st := range stats {
107 adds += st.Addition
108 dels += st.Deletion
109 }
110 commitStatsCache.Store(hashStr, CommitStat{Additions: adds, Deletions: dels})
111 NotifySave()
112 }
113 }
114
115 commits[idx] = Commit{
116 Hash: hashStr,
117 AuthorName: commit.Author.Name,
118 AuthorEmail: commit.Author.Email,
119 AuthorDate: commit.Author.When,
120 CommitterName: commit.Committer.Name,
121 CommitterEmail: commit.Committer.Email,
122 CommitterDate: commit.Committer.When,
123 Message: commit.Message,
124 Additions: adds,
125 Deletions: dels,
126 }
127 }(i, c)
128 }
129 wg.Wait()
130
131 // Calculate page range (show up to 8 pages around current page)
132 startPage := page - 4
133 if startPage < 1 {
134 startPage = 1
135 }
136 endPage := startPage + 7
137 if endPage > totalPages {
138 endPage = totalPages
139 startPage = endPage - 7
140 if startPage < 1 {
141 startPage = 1
142 }
143 }
144
145 var pages []int
146 for i := startPage; i <= endPage; i++ {
147 pages = append(pages, i)
148 }
149
150 commit, _ := ctx.GitRepo.CommitObject(ctx.Hash)
151 tree, _ := commit.Tree()
152 langStats, _ := getLanguageStats(ctx.Repo.Name, ctx.Hash.String(), tree)
153
154 data := struct {
155 *RepoContext
156 Commits []Commit
157 Languages []LanguageStat
158 View string
159 Page int
160 TotalPages int
161 Pages []int
162 PrevPage int
163 NextPage int
164 }{
165 RepoContext: ctx,
166 Commits: commits,
167 Languages: langStats,
168 View: "commits",
169 Page: page,
170 TotalPages: totalPages,
171 Pages: pages,
172 PrevPage: page - 1,
173 }
174 if page < totalPages {
175 data.NextPage = page + 1
176 }
177
178 err = templates.ExecuteTemplate(w, "repository.html", data)
179 if err != nil {
180 http.Error(w, err.Error(), http.StatusInternalServerError)
181 }
182}
183
184func repoCommitsRSSHandler(w http.ResponseWriter, r *http.Request) {
185 ctx, err := getRepoContext(w, r)
186 if err != nil {
187 http.Error(w, err.Error(), http.StatusInternalServerError)
188 return
189 }
190
191 cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash})
192 if err != nil {
193 http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError)
194 return
195 }
196
197 scheme := "http"
198 if r.TLS != nil {
199 scheme = "https"
200 }
201 baseURL := fmt.Sprintf("%s://%s", scheme, r.Host)
202
203 rss := RSS{
204 Version: "2.0",
205 Channel: Channel{
206 Title: fmt.Sprintf("%s - Commits", ctx.Repo.Name),
207 Link: fmt.Sprintf("%s/r/%s?ref=%s", baseURL, ctx.Repo.Name, ctx.CurrentRef),
208 Description: fmt.Sprintf("Commit history for %s (%s)", ctx.Repo.Name, ctx.CurrentRef),
209 },
210 }
211
212 count := 0
213 err = cIter.ForEach(func(c *object.Commit) error {
214 if count >= 20 {
215 return fmt.Errorf("limit reached")
216 }
217
218 hash := c.Hash.String()
219 item := RSSItem{
220 Title: strings.Split(c.Message, "\n")[0],
221 Link: fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash),
222 Description: c.Message,
223 PubDate: c.Author.When.Format(time.RFC1123Z),
224 GUID: fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash),
225 }
226 rss.Channel.Items = append(rss.Channel.Items, item)
227 count++
228 return nil
229 })
230
231 if err != nil && err.Error() != "limit reached" {
232 http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError)
233 return
234 }
235
236 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
237 fmt.Fprint(w, xml.Header)
238 enc := xml.NewEncoder(w)
239 enc.Indent("", " ")
240 if err := enc.Encode(rss); err != nil {
241 log.Printf("Error encoding RSS: %v", err)
242 }
243}
244
245func repoTagsRSSHandler(w http.ResponseWriter, r *http.Request) {
246 name := r.PathValue("name")
247 config := GlobalConfig
248
249 var repo *Repository
250 for _, repoItem := range config.Repositories {
251 if repoItem.Name == name {
252 repo = &repoItem
253 break
254 }
255 }
256
257 if repo == nil {
258 http.NotFound(w, r)
259 return
260 }
261
262 gitRepo, err := git.PlainOpen(repo.Path)
263 if err != nil {
264 http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError)
265 return
266 }
267
268 tIter, err := gitRepo.Tags()
269 if err != nil {
270 http.Error(w, fmt.Sprintf("Error getting tags: %v", err), http.StatusInternalServerError)
271 return
272 }
273
274 type tagInfo struct {
275 Name string
276 Date time.Time
277 Hash string
278 }
279 var tags []tagInfo
280
281 err = tIter.ForEach(func(ref *plumbing.Reference) error {
282 obj, err := gitRepo.TagObject(ref.Hash())
283 if err != nil {
284 // Lightweight tag
285 commit, err := gitRepo.CommitObject(ref.Hash())
286 if err == nil {
287 tags = append(tags, tagInfo{
288 Name: ref.Name().Short(),
289 Date: commit.Author.When,
290 Hash: ref.Hash().String(),
291 })
292 }
293 } else {
294 // Annotated tag
295 if _, err := obj.Commit(); err == nil {
296 tags = append(tags, tagInfo{
297 Name: ref.Name().Short(),
298 Date: obj.Tagger.When,
299 Hash: ref.Hash().String(),
300 })
301 } else {
302 tags = append(tags, tagInfo{
303 Name: ref.Name().Short(),
304 Date: obj.Tagger.When,
305 Hash: obj.Target.String(),
306 })
307 }
308 }
309 return nil
310 })
311
312 sort.Slice(tags, func(i, j int) bool {
313 return tags[i].Date.After(tags[j].Date)
314 })
315
316 if len(tags) > 20 {
317 tags = tags[:20]
318 }
319
320 scheme := "http"
321 if r.TLS != nil {
322 scheme = "https"
323 }
324 baseURL := fmt.Sprintf("%s://%s", scheme, r.Host)
325
326 rss := RSS{
327 Version: "2.0",
328 Channel: Channel{
329 Title: fmt.Sprintf("%s - Tags", repo.Name),
330 Link: fmt.Sprintf("%s/r/%s", baseURL, repo.Name),
331 Description: fmt.Sprintf("Tags for %s", repo.Name),
332 },
333 }
334
335 for _, t := range tags {
336 item := RSSItem{
337 Title: t.Name,
338 Link: fmt.Sprintf("%s/r/%s?ref=%s", baseURL, repo.Name, t.Name),
339 Description: fmt.Sprintf("Tag %s at %s", t.Name, t.Hash),
340 PubDate: t.Date.Format(time.RFC1123Z),
341 GUID: fmt.Sprintf("%s/r/%s/tags/%s", baseURL, repo.Name, t.Name),
342 }
343 rss.Channel.Items = append(rss.Channel.Items, item)
344 }
345
346 w.Header().Set("Content-Type", "application/xml; charset=utf-8")
347 fmt.Fprint(w, xml.Header)
348 enc := xml.NewEncoder(w)
349 enc.Indent("", " ")
350 if err := enc.Encode(rss); err != nil {
351 log.Printf("Error encoding RSS: %v", err)
352 }
353}