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}