Refactor handlers into own files

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-08 04:40:38 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-08 04:40:38 +0200
Commit 82b3c0bc11cf2ca9fb794af9ae9c9e74311121a8 (patch)
-rw-r--r-- cache.go 1
-rw-r--r-- gitutils.go 3
-rw-r--r-- handlers.go 992
-rw-r--r-- hcommit.go 273
-rw-r--r-- hhome.go 39
-rw-r--r-- hinfo.go 122
-rw-r--r-- hrepo.go 353
-rw-r--r-- htree.go 239
-rw-r--r-- languages.go 796
-rw-r--r-- mdalerts.go 3
-rw-r--r-- models.go 2
-rw-r--r-- static/style.css 4
12 files changed, 1428 insertions, 1399 deletions
diff --git a/cache.go b/cache.go
...
24
  
24
  
25
const CurrentMetadataVersion = 1
25
const CurrentMetadataVersion = 1
26
  
26
  
27
  
  
28
type LangCacheKey struct {
27
type LangCacheKey struct {
29
	RepoName   string
28
	RepoName   string
30
	CommitHash string
29
	CommitHash string
...
diff --git a/gitutils.go b/gitutils.go
...
157
		hash = *h
157
		hash = *h
158
	}
158
	}
159
  
159
  
160
	// Cache keys
  
161
	metadataKey := name + ":" + hash.String()
160
	metadataKey := name + ":" + hash.String()
162
  
161
  
163
	if val, ok := repoMetadataCache.Load(metadataKey); ok {
162
	if val, ok := repoMetadataCache.Load(metadataKey); ok {
...
227
		}
226
		}
228
	}
227
	}
229
  
228
  
230
	// Note: totalCommits will be updated in repoHandler if needed, 
229
	// Note: totalCommits will be updated in repoHandler if needed,
231
	// for now we store what we have.
230
	// for now we store what we have.
232
	repoMetadataCache.Store(metadataKey, RepoMetadata{
231
	repoMetadataCache.Store(metadataKey, RepoMetadata{
233
		Branches:    branches,
232
		Branches:    branches,
...
diff --git a/handlers.go b/handlers.go
1
package main
  
2
  
  
3
import (
  
4
	"archive/tar"
  
5
	"compress/gzip"
  
6
	"encoding/xml"
  
7
	"fmt"
  
8
	"html/template"
  
9
	"io"
  
10
	"log"
  
11
	"net/http"
  
12
	"path"
  
13
	"sort"
  
14
	"strconv"
  
15
	"strings"
  
16
	"sync"
  
17
	"time"
  
18
  
  
19
	"github.com/go-git/go-git/v5"
  
20
	"github.com/go-git/go-git/v5/plumbing"
  
21
	"github.com/go-git/go-git/v5/plumbing/format/diff"
  
22
	"github.com/go-git/go-git/v5/plumbing/object"
  
23
)
  
24
  
  
25
func homeHandler(w http.ResponseWriter, r *http.Request) {
  
26
	config := GlobalConfig
  
27
  
  
28
	groupsMap := make(map[string][]Repository)
  
29
	var groupOrder []string
  
30
  
  
31
	for _, repo := range config.Repositories {
  
32
		if _, ok := groupsMap[repo.Group]; !ok {
  
33
			groupOrder = append(groupOrder, repo.Group)
  
34
		}
  
35
		groupsMap[repo.Group] = append(groupsMap[repo.Group], repo)
  
36
	}
  
37
  
  
38
	var grouped []GroupedRepositories
  
39
	for _, groupName := range groupOrder {
  
40
		grouped = append(grouped, GroupedRepositories{
  
41
			Name:         groupName,
  
42
			Repositories: groupsMap[groupName],
  
43
		})
  
44
	}
  
45
  
  
46
	err := templates.ExecuteTemplate(w, "repositories.html", struct {
  
47
		Groups []GroupedRepositories
  
48
		Repo   *Repository
  
49
	}{
  
50
		Groups: grouped,
  
51
		Repo:   nil,
  
52
	})
  
53
  
  
54
	if err != nil {
  
55
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
56
	}
  
57
}
  
58
  
  
59
func repoHandler(w http.ResponseWriter, r *http.Request) {
  
60
	ctx, err := getRepoContext(w, r)
  
61
	if err != nil {
  
62
		if err.Error() == "repository not found" {
  
63
			http.NotFound(w, r)
  
64
		} else {
  
65
			http.Error(w, err.Error(), http.StatusInternalServerError)
  
66
		}
  
67
		return
  
68
	}
  
69
  
  
70
	page, _ := strconv.Atoi(r.URL.Query().Get("page"))
  
71
	if page < 1 {
  
72
		page = 1
  
73
	}
  
74
	pageSize := 30
  
75
  
  
76
	totalCommitsKey := ctx.Repo.Name + ":" + ctx.Hash.String()
  
77
	var totalCommits int
  
78
	if val, ok := repoMetadataCache.Load(totalCommitsKey); ok {
  
79
		totalCommits = val.(RepoMetadata).TotalCommits
  
80
	} else {
  
81
		cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash})
  
82
		if err != nil {
  
83
			http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError)
  
84
			return
  
85
		}
  
86
		_ = cIter.ForEach(func(c *object.Commit) error {
  
87
			totalCommits++
  
88
			return nil
  
89
		})
  
90
		repoMetadataCache.Store(totalCommitsKey, RepoMetadata{
  
91
			TotalCommits: totalCommits,
  
92
			Branches:     ctx.Branches,
  
93
			Tags:         ctx.Tags,
  
94
			ReadmeName:   ctx.ReadmeName,
  
95
			LicenseName:  ctx.LicenseName,
  
96
			Version:      CurrentMetadataVersion,
  
97
		})
  
98
		NotifySave()
  
99
	}
  
100
  
  
101
	totalPages := (totalCommits + pageSize - 1) / pageSize
  
102
  
  
103
	cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash})
  
104
	if err != nil {
  
105
		http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError)
  
106
		return
  
107
	}
  
108
  
  
109
	var commits []Commit
  
110
	var wg sync.WaitGroup
  
111
	count := 0
  
112
  
  
113
	// Collect commits for the current page
  
114
	var commitsToProcess []*object.Commit
  
115
	err = cIter.ForEach(func(c *object.Commit) error {
  
116
		if count < (page-1)*pageSize {
  
117
			count++
  
118
			return nil
  
119
		}
  
120
		if len(commitsToProcess) >= pageSize {
  
121
			return fmt.Errorf("limit reached")
  
122
		}
  
123
		commitsToProcess = append(commitsToProcess, c)
  
124
		count++
  
125
		return nil
  
126
	})
  
127
  
  
128
	if err != nil && err.Error() != "limit reached" {
  
129
		http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError)
  
130
		return
  
131
	}
  
132
  
  
133
	commits = make([]Commit, len(commitsToProcess))
  
134
	for i, c := range commitsToProcess {
  
135
		wg.Add(1)
  
136
		go func(idx int, commit *object.Commit) {
  
137
			defer wg.Done()
  
138
			hashStr := commit.Hash.String()
  
139
  
  
140
			var adds, dels int
  
141
			if val, ok := commitStatsCache.Load(hashStr); ok {
  
142
				s := val.(CommitStat)
  
143
				adds, dels = s.Additions, s.Deletions
  
144
			} else {
  
145
				stats, err := commit.Stats()
  
146
				if err == nil {
  
147
					for _, st := range stats {
  
148
						adds += st.Addition
  
149
						dels += st.Deletion
  
150
					}
  
151
					commitStatsCache.Store(hashStr, CommitStat{Additions: adds, Deletions: dels})
  
152
					NotifySave()
  
153
				}
  
154
			}
  
155
  
  
156
			commits[idx] = Commit{
  
157
				Hash:           hashStr,
  
158
				AuthorName:     commit.Author.Name,
  
159
				AuthorEmail:    commit.Author.Email,
  
160
				AuthorDate:     commit.Author.When,
  
161
				CommitterName:  commit.Committer.Name,
  
162
				CommitterEmail: commit.Committer.Email,
  
163
				CommitterDate:  commit.Committer.When,
  
164
				Message:        commit.Message,
  
165
				Additions:      adds,
  
166
				Deletions:      dels,
  
167
			}
  
168
		}(i, c)
  
169
	}
  
170
	wg.Wait()
  
171
  
  
172
	// Calculate page range (show up to 8 pages around current page)
  
173
	startPage := page - 4
  
174
	if startPage < 1 {
  
175
		startPage = 1
  
176
	}
  
177
	endPage := startPage + 7
  
178
	if endPage > totalPages {
  
179
		endPage = totalPages
  
180
		startPage = endPage - 7
  
181
		if startPage < 1 {
  
182
			startPage = 1
  
183
		}
  
184
	}
  
185
  
  
186
	var pages []int
  
187
	for i := startPage; i <= endPage; i++ {
  
188
		pages = append(pages, i)
  
189
	}
  
190
  
  
191
	commit, _ := ctx.GitRepo.CommitObject(ctx.Hash)
  
192
	tree, _ := commit.Tree()
  
193
	langStats, _ := getLanguageStats(ctx.Repo.Name, ctx.Hash.String(), tree)
  
194
  
  
195
	data := struct {
  
196
		*RepoContext
  
197
		Commits    []Commit
  
198
		Languages  []LanguageStat
  
199
		View       string
  
200
		Page       int
  
201
		TotalPages int
  
202
		Pages      []int
  
203
		PrevPage   int
  
204
		NextPage   int
  
205
	}{
  
206
		RepoContext: ctx,
  
207
		Commits:     commits,
  
208
		Languages:   langStats,
  
209
		View:        "commits",
  
210
		Page:        page,
  
211
		TotalPages:  totalPages,
  
212
		Pages:       pages,
  
213
		PrevPage:    page - 1,
  
214
	}
  
215
	if page < totalPages {
  
216
		data.NextPage = page + 1
  
217
	}
  
218
  
  
219
	err = templates.ExecuteTemplate(w, "repository.html", data)
  
220
	if err != nil {
  
221
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
222
	}
  
223
}
  
224
  
  
225
func treeHandler(w http.ResponseWriter, r *http.Request) {
  
226
	ctx, err := getRepoContext(w, r)
  
227
	if err != nil {
  
228
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
229
		return
  
230
	}
  
231
  
  
232
	path := r.PathValue("path")
  
233
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
234
	if err != nil {
  
235
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
236
		return
  
237
	}
  
238
  
  
239
	tree, err := commit.Tree()
  
240
	if err != nil {
  
241
		http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
  
242
		return
  
243
	}
  
244
  
  
245
	if path != "" {
  
246
		tree, err = tree.Tree(path)
  
247
		if err != nil {
  
248
			http.NotFound(w, r)
  
249
			return
  
250
		}
  
251
	}
  
252
  
  
253
	var entries []TreeEntry
  
254
	for _, entry := range tree.Entries {
  
255
		fullPath := entry.Name
  
256
		if path != "" {
  
257
			fullPath = path + "/" + entry.Name
  
258
		}
  
259
  
  
260
		isDir := entry.Mode.IsFile() == false
  
261
  
  
262
		var size int64
  
263
		if !isDir {
  
264
			obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, entry.Hash)
  
265
			if blob, ok := obj.(*object.Blob); ok {
  
266
				size = blob.Size
  
267
			}
  
268
		}
  
269
  
  
270
		entries = append(entries, TreeEntry{
  
271
			Name:  entry.Name,
  
272
			Path:  fullPath,
  
273
			IsDir: isDir,
  
274
			Size:  size,
  
275
			Mode:  entry.Mode.String(),
  
276
		})
  
277
	}
  
278
  
  
279
	sort.Slice(entries, func(i, j int) bool {
  
280
		if entries[i].IsDir != entries[j].IsDir {
  
281
			return entries[i].IsDir
  
282
		}
  
283
		return entries[i].Name < entries[j].Name
  
284
	})
  
285
  
  
286
	data := struct {
  
287
		*RepoContext
  
288
		Entries []TreeEntry
  
289
		Path    string
  
290
		View    string
  
291
	}{
  
292
		RepoContext: ctx,
  
293
		Entries:     entries,
  
294
		Path:        path,
  
295
		View:        "tree",
  
296
	}
  
297
  
  
298
	err = templates.ExecuteTemplate(w, "tree.html", data)
  
299
	if err != nil {
  
300
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
301
	}
  
302
}
  
303
  
  
304
func blobHandler(w http.ResponseWriter, r *http.Request) {
  
305
	ctx, err := getRepoContext(w, r)
  
306
	if err != nil {
  
307
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
308
		return
  
309
	}
  
310
  
  
311
	path := r.PathValue("path")
  
312
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
313
	if err != nil {
  
314
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
315
		return
  
316
	}
  
317
  
  
318
	file, err := commit.File(path)
  
319
	if err != nil {
  
320
		http.NotFound(w, r)
  
321
		return
  
322
	}
  
323
  
  
324
	content, err := file.Contents()
  
325
	if err != nil {
  
326
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
327
		return
  
328
	}
  
329
  
  
330
	data := struct {
  
331
		*RepoContext
  
332
		Path    string
  
333
		Content template.HTML
  
334
	}{
  
335
		RepoContext: ctx,
  
336
		Path:        path,
  
337
		Content:     highlight(path, content),
  
338
	}
  
339
  
  
340
	err = templates.ExecuteTemplate(w, "blob.html", data)
  
341
	if err != nil {
  
342
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
343
	}
  
344
}
  
345
  
  
346
func rawHandler(w http.ResponseWriter, r *http.Request) {
  
347
	ctx, err := getRepoContext(w, r)
  
348
	if err != nil {
  
349
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
350
		return
  
351
	}
  
352
  
  
353
	path := r.PathValue("path")
  
354
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
355
	if err != nil {
  
356
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
357
		return
  
358
	}
  
359
  
  
360
	file, err := commit.File(path)
  
361
	if err != nil {
  
362
		http.NotFound(w, r)
  
363
		return
  
364
	}
  
365
  
  
366
	reader, err := file.Reader()
  
367
	if err != nil {
  
368
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
369
		return
  
370
	}
  
371
	defer reader.Close()
  
372
  
  
373
	w.Header().Set("Content-Type", "application/octet-stream")
  
374
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", path))
  
375
	io.Copy(w, reader)
  
376
}
  
377
  
  
378
func archiveHandler(w http.ResponseWriter, r *http.Request) {
  
379
	ctx, err := getRepoContext(w, r)
  
380
	if err != nil {
  
381
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
382
		return
  
383
	}
  
384
  
  
385
	pathValue := r.PathValue("path")
  
386
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
387
	if err != nil {
  
388
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
389
		return
  
390
	}
  
391
  
  
392
	tree, err := commit.Tree()
  
393
	if err != nil {
  
394
		http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
  
395
		return
  
396
	}
  
397
  
  
398
	if pathValue != "" {
  
399
		tree, err = tree.Tree(pathValue)
  
400
		if err != nil {
  
401
			http.NotFound(w, r)
  
402
			return
  
403
		}
  
404
	}
  
405
  
  
406
	filename := ctx.Repo.Name
  
407
	if pathValue != "" {
  
408
		filename = path.Base(pathValue)
  
409
	}
  
410
	filename = fmt.Sprintf("%s-%s.tar.gz", filename, ctx.CurrentRef)
  
411
  
  
412
	w.Header().Set("Content-Type", "application/gzip")
  
413
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
  
414
  
  
415
	gw := gzip.NewWriter(w)
  
416
	defer gw.Close()
  
417
  
  
418
	tw := tar.NewWriter(gw)
  
419
	defer tw.Close()
  
420
  
  
421
	err = tree.Files().ForEach(func(f *object.File) error {
  
422
		hdr := &tar.Header{
  
423
			Name: strings.TrimPrefix(strings.TrimPrefix(f.Name, pathValue), "/"),
  
424
			Mode: int64(f.Mode),
  
425
			Size: f.Size,
  
426
		}
  
427
  
  
428
		if err := tw.WriteHeader(hdr); err != nil {
  
429
			return err
  
430
		}
  
431
  
  
432
		reader, err := f.Reader()
  
433
		if err != nil {
  
434
			return err
  
435
		}
  
436
		defer reader.Close()
  
437
  
  
438
		_, err = io.Copy(tw, reader)
  
439
		return err
  
440
	})
  
441
  
  
442
	if err != nil {
  
443
		log.Printf("Error creating archive: %v", err)
  
444
	}
  
445
}
  
446
  
  
447
func commitHandler(w http.ResponseWriter, r *http.Request) {
  
448
	ctx, err := getRepoContext(w, r)
  
449
	if err != nil {
  
450
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
451
		return
  
452
	}
  
453
  
  
454
	commitHash := r.PathValue("hash")
  
455
	hash := plumbing.NewHash(commitHash)
  
456
	commit, err := ctx.GitRepo.CommitObject(hash)
  
457
	if err != nil {
  
458
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
459
		return
  
460
	}
  
461
  
  
462
	var fileDiffs []FileDiff
  
463
	maxChanges := 0
  
464
	if commit.NumParents() > 0 {
  
465
		parent, _ := commit.Parent(0)
  
466
		patch, err := parent.Patch(commit)
  
467
		if err == nil {
  
468
			for _, fp := range patch.FilePatches() {
  
469
				from, to := fp.Files()
  
470
				name := ""
  
471
				mode := ""
  
472
				if to != nil {
  
473
					name = to.Path()
  
474
					mode = formatMode(to.Mode())
  
475
				} else if from != nil {
  
476
					name = from.Path()
  
477
					mode = formatMode(from.Mode())
  
478
				}
  
479
  
  
480
				fileAdd, fileDel := 0, 0
  
481
				isBinary := fp.IsBinary()
  
482
				deleted := to == nil
  
483
				var oldSize, newSize int64
  
484
  
  
485
				if isBinary {
  
486
					if from != nil {
  
487
						obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, from.Hash())
  
488
						if blob, ok := obj.(*object.Blob); ok {
  
489
							oldSize = blob.Size
  
490
						}
  
491
					}
  
492
					if to != nil {
  
493
						obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, to.Hash())
  
494
						if blob, ok := obj.(*object.Blob); ok {
  
495
							newSize = blob.Size
  
496
						}
  
497
					}
  
498
				}
  
499
  
  
500
				var diffLines []DiffLine
  
501
				leftNo, rightNo := 1, 1
  
502
  
  
503
				var delLines []string
  
504
				var addLines []string
  
505
  
  
506
				flush := func() {
  
507
					max := len(delLines)
  
508
					if len(addLines) > max {
  
509
						max = len(addLines)
  
510
					}
  
511
					for i := 0; i < max; i++ {
  
512
						line := DiffLine{}
  
513
						if i < len(delLines) && i < len(addLines) {
  
514
							line.LeftNo = fmt.Sprintf("%d", leftNo)
  
515
							line.Left = delLines[i]
  
516
							line.RightNo = fmt.Sprintf("%d", rightNo)
  
517
							line.Right = addLines[i]
  
518
							line.Type = "mod"
  
519
							leftNo++
  
520
							rightNo++
  
521
							fileAdd++
  
522
							fileDel++
  
523
						} else if i < len(delLines) {
  
524
							line.LeftNo = fmt.Sprintf("%d", leftNo)
  
525
							line.Left = delLines[i]
  
526
							line.Type = "del"
  
527
							leftNo++
  
528
							fileDel++
  
529
						} else if i < len(addLines) {
  
530
							line.RightNo = fmt.Sprintf("%d", rightNo)
  
531
							line.Right = addLines[i]
  
532
							line.Type = "add"
  
533
							rightNo++
  
534
							fileAdd++
  
535
						}
  
536
						diffLines = append(diffLines, line)
  
537
					}
  
538
					delLines = nil
  
539
					addLines = nil
  
540
				}
  
541
  
  
542
				for _, chunk := range fp.Chunks() {
  
543
					lines := strings.Split(strings.TrimSuffix(chunk.Content(), "\n"), "\n")
  
544
					switch chunk.Type() {
  
545
					case diff.Equal:
  
546
						flush()
  
547
						for _, line := range lines {
  
548
							diffLines = append(diffLines, DiffLine{
  
549
								LeftNo:  fmt.Sprintf("%d", leftNo),
  
550
								Left:    line,
  
551
								RightNo: fmt.Sprintf("%d", rightNo),
  
552
								Right:   line,
  
553
								Type:    "eq",
  
554
							})
  
555
							leftNo++
  
556
							rightNo++
  
557
						}
  
558
					case diff.Delete:
  
559
						delLines = append(delLines, lines...)
  
560
					case diff.Add:
  
561
						addLines = append(addLines, lines...)
  
562
					}
  
563
				}
  
564
				flush()
  
565
  
  
566
				if fileAdd+fileDel > maxChanges {
  
567
					maxChanges = fileAdd + fileDel
  
568
				}
  
569
  
  
570
				visible := make([]bool, len(diffLines))
  
571
				for i, line := range diffLines {
  
572
					if line.Type == "add" || line.Type == "del" || line.Type == "mod" {
  
573
						for j := i - 3; j <= i+3; j++ {
  
574
							if j >= 0 && j < len(diffLines) {
  
575
								visible[j] = true
  
576
							}
  
577
						}
  
578
					}
  
579
				}
  
580
  
  
581
				var filteredLines []DiffLine
  
582
				lastWasGap := false
  
583
				for i, isVisible := range visible {
  
584
					if isVisible {
  
585
						filteredLines = append(filteredLines, diffLines[i])
  
586
						lastWasGap = false
  
587
					} else {
  
588
						if !lastWasGap {
  
589
							filteredLines = append(filteredLines, DiffLine{Type: "gap"})
  
590
							lastWasGap = true
  
591
						}
  
592
					}
  
593
				}
  
594
  
  
595
				fileDiffs = append(fileDiffs, FileDiff{
  
596
					Name:     name,
  
597
					Lines:    filteredLines,
  
598
					Addition: fileAdd,
  
599
					Deletion: fileDel,
  
600
					IsBinary: isBinary,
  
601
					Mode:     mode,
  
602
					OldSize:  oldSize,
  
603
					NewSize:  newSize,
  
604
					Deleted:  deleted,
  
605
				})
  
606
			}
  
607
		}
  
608
	}
  
609
  
  
610
	stats, _ := commit.Stats()
  
611
	adds, dels := 0, 0
  
612
	for _, s := range stats {
  
613
		adds += s.Addition
  
614
		dels += s.Deletion
  
615
	}
  
616
  
  
617
	data := struct {
  
618
		*RepoContext
  
619
		Commit     Commit
  
620
		FileDiffs  []FileDiff
  
621
		MaxChanges int
  
622
	}{
  
623
		RepoContext: ctx,
  
624
		Commit: Commit{
  
625
			Hash:           commit.Hash.String(),
  
626
			AuthorName:     commit.Author.Name,
  
627
			AuthorEmail:    commit.Author.Email,
  
628
			AuthorDate:     commit.Author.When,
  
629
			CommitterName:  commit.Committer.Name,
  
630
			CommitterEmail: commit.Committer.Email,
  
631
			CommitterDate:  commit.Committer.When,
  
632
			Message:        commit.Message,
  
633
			Additions:      adds,
  
634
			Deletions:      dels,
  
635
		},
  
636
		FileDiffs:  fileDiffs,
  
637
		MaxChanges: maxChanges,
  
638
	}
  
639
  
  
640
	err = templates.ExecuteTemplate(w, "commit.html", data)
  
641
	if err != nil {
  
642
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
643
	}
  
644
}
  
645
  
  
646
func patchHandler(w http.ResponseWriter, r *http.Request) {
  
647
	repoName := r.PathValue("name")
  
648
	commitHash := r.PathValue("hash")
  
649
  
  
650
	config, err := loadConfig(ConfigPath)
  
651
	if err != nil {
  
652
		http.Error(w, fmt.Sprintf("Error loading config: %v", err), http.StatusInternalServerError)
  
653
		return
  
654
	}
  
655
  
  
656
	var repo *Repository
  
657
	for _, repoItem := range config.Repositories {
  
658
		if repoItem.Name == repoName {
  
659
			repo = &repoItem
  
660
			break
  
661
		}
  
662
	}
  
663
  
  
664
	if repo == nil {
  
665
		http.NotFound(w, r)
  
666
		return
  
667
	}
  
668
  
  
669
	gitRepo, err := git.PlainOpen(repo.Path)
  
670
	if err != nil {
  
671
		http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError)
  
672
		return
  
673
	}
  
674
  
  
675
	hash := plumbing.NewHash(commitHash)
  
676
	commit, err := gitRepo.CommitObject(hash)
  
677
	if err != nil {
  
678
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
679
		return
  
680
	}
  
681
  
  
682
	w.Header().Set("Content-Type", "text/plain")
  
683
  
  
684
	currentTree, err := commit.Tree()
  
685
	if err != nil {
  
686
		http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
  
687
		return
  
688
	}
  
689
  
  
690
	var parentTree *object.Tree
  
691
	if commit.NumParents() > 0 {
  
692
		parent, _ := commit.Parent(0)
  
693
		parentTree, err = parent.Tree()
  
694
		if err != nil {
  
695
			http.Error(w, fmt.Sprintf("Error getting parent tree: %v", err), http.StatusInternalServerError)
  
696
			return
  
697
		}
  
698
	}
  
699
  
  
700
	patch, err := parentTree.Patch(currentTree)
  
701
	if err != nil {
  
702
		http.Error(w, fmt.Sprintf("Error generating patch: %v", err), http.StatusInternalServerError)
  
703
		return
  
704
	}
  
705
	fmt.Fprint(w, patch.String())
  
706
}
  
707
  
  
708
func readmeHandler(w http.ResponseWriter, r *http.Request) {
  
709
	ctx, err := getRepoContext(w, r)
  
710
	if err != nil {
  
711
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
712
		return
  
713
	}
  
714
  
  
715
	if ctx.ReadmeName == "" {
  
716
		http.NotFound(w, r)
  
717
		return
  
718
	}
  
719
  
  
720
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
721
	if err != nil {
  
722
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
723
		return
  
724
	}
  
725
  
  
726
	file, err := commit.File(ctx.ReadmeName)
  
727
	if err != nil {
  
728
		http.NotFound(w, r)
  
729
		return
  
730
	}
  
731
  
  
732
	content, err := file.Contents()
  
733
	if err != nil {
  
734
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
735
		return
  
736
	}
  
737
  
  
738
	data := struct {
  
739
		*RepoContext
  
740
		Content template.HTML
  
741
	}{
  
742
		RepoContext: ctx,
  
743
		Content:     renderMarkdown(content),
  
744
	}
  
745
  
  
746
	err = templates.ExecuteTemplate(w, "readme.html", data)
  
747
	if err != nil {
  
748
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
749
	}
  
750
}
  
751
  
  
752
func licenseHandler(w http.ResponseWriter, r *http.Request) {
  
753
	ctx, err := getRepoContext(w, r)
  
754
	if err != nil {
  
755
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
756
		return
  
757
	}
  
758
  
  
759
	if ctx.LicenseName == "" {
  
760
		http.NotFound(w, r)
  
761
		return
  
762
	}
  
763
  
  
764
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
765
	if err != nil {
  
766
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
767
		return
  
768
	}
  
769
  
  
770
	file, err := commit.File(ctx.LicenseName)
  
771
	if err != nil {
  
772
		http.NotFound(w, r)
  
773
		return
  
774
	}
  
775
  
  
776
	content, err := file.Contents()
  
777
	if err != nil {
  
778
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
779
		return
  
780
	}
  
781
  
  
782
	data := struct {
  
783
		*RepoContext
  
784
		Content template.HTML
  
785
	}{
  
786
		RepoContext: ctx,
  
787
		Content:     renderMarkdown(content),
  
788
	}
  
789
  
  
790
	err = templates.ExecuteTemplate(w, "license.html", data)
  
791
	if err != nil {
  
792
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
793
	}
  
794
}
  
795
  
  
796
func markersHandler(w http.ResponseWriter, r *http.Request) {
  
797
	ctx, err := getRepoContext(w, r)
  
798
	if err != nil {
  
799
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
800
		return
  
801
	}
  
802
  
  
803
	markers, err := scanMarkers(ctx)
  
804
	if err != nil {
  
805
		http.Error(w, fmt.Sprintf("Error scanning markers: %v", err), http.StatusInternalServerError)
  
806
		return
  
807
	}
  
808
  
  
809
	data := struct {
  
810
		*RepoContext
  
811
		Markers []Marker
  
812
	}{
  
813
		RepoContext: ctx,
  
814
		Markers:     markers,
  
815
	}
  
816
  
  
817
	err = templates.ExecuteTemplate(w, "markers.html", data)
  
818
	if err != nil {
  
819
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
820
	}
  
821
}
  
822
  
  
823
func repoCommitsRSSHandler(w http.ResponseWriter, r *http.Request) {
  
824
	ctx, err := getRepoContext(w, r)
  
825
	if err != nil {
  
826
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
827
		return
  
828
	}
  
829
  
  
830
	cIter, err := ctx.GitRepo.Log(&git.LogOptions{From: ctx.Hash})
  
831
	if err != nil {
  
832
		http.Error(w, fmt.Sprintf("Error getting log: %v", err), http.StatusInternalServerError)
  
833
		return
  
834
	}
  
835
  
  
836
	scheme := "http"
  
837
	if r.TLS != nil {
  
838
		scheme = "https"
  
839
	}
  
840
	baseURL := fmt.Sprintf("%s://%s", scheme, r.Host)
  
841
  
  
842
	rss := RSS{
  
843
		Version: "2.0",
  
844
		Channel: Channel{
  
845
			Title:       fmt.Sprintf("%s - Commits", ctx.Repo.Name),
  
846
			Link:        fmt.Sprintf("%s/r/%s?ref=%s", baseURL, ctx.Repo.Name, ctx.CurrentRef),
  
847
			Description: fmt.Sprintf("Commit history for %s (%s)", ctx.Repo.Name, ctx.CurrentRef),
  
848
		},
  
849
	}
  
850
  
  
851
	count := 0
  
852
	err = cIter.ForEach(func(c *object.Commit) error {
  
853
		if count >= 20 {
  
854
			return fmt.Errorf("limit reached")
  
855
		}
  
856
  
  
857
		hash := c.Hash.String()
  
858
		item := RSSItem{
  
859
			Title:       strings.Split(c.Message, "\n")[0],
  
860
			Link:        fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash),
  
861
			Description: c.Message,
  
862
			PubDate:     c.Author.When.Format(time.RFC1123Z),
  
863
			GUID:        fmt.Sprintf("%s/r/%s/c/%s", baseURL, ctx.Repo.Name, hash),
  
864
		}
  
865
		rss.Channel.Items = append(rss.Channel.Items, item)
  
866
		count++
  
867
		return nil
  
868
	})
  
869
  
  
870
	if err != nil && err.Error() != "limit reached" {
  
871
		http.Error(w, fmt.Sprintf("Error iterating commits: %v", err), http.StatusInternalServerError)
  
872
		return
  
873
	}
  
874
  
  
875
	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
  
876
	fmt.Fprint(w, xml.Header)
  
877
	enc := xml.NewEncoder(w)
  
878
	enc.Indent("", "  ")
  
879
	if err := enc.Encode(rss); err != nil {
  
880
		log.Printf("Error encoding RSS: %v", err)
  
881
	}
  
882
}
  
883
  
  
884
func repoTagsRSSHandler(w http.ResponseWriter, r *http.Request) {
  
885
	name := r.PathValue("name")
  
886
	config := GlobalConfig
  
887
  
  
888
	var repo *Repository
  
889
	for _, repoItem := range config.Repositories {
  
890
		if repoItem.Name == name {
  
891
			repo = &repoItem
  
892
			break
  
893
		}
  
894
	}
  
895
  
  
896
	if repo == nil {
  
897
		http.NotFound(w, r)
  
898
		return
  
899
	}
  
900
  
  
901
	gitRepo, err := git.PlainOpen(repo.Path)
  
902
	if err != nil {
  
903
		http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError)
  
904
		return
  
905
	}
  
906
  
  
907
	tIter, err := gitRepo.Tags()
  
908
	if err != nil {
  
909
		http.Error(w, fmt.Sprintf("Error getting tags: %v", err), http.StatusInternalServerError)
  
910
		return
  
911
	}
  
912
  
  
913
	type tagInfo struct {
  
914
		Name string
  
915
		Date time.Time
  
916
		Hash string
  
917
	}
  
918
	var tags []tagInfo
  
919
  
  
920
	err = tIter.ForEach(func(ref *plumbing.Reference) error {
  
921
		obj, err := gitRepo.TagObject(ref.Hash())
  
922
		if err != nil {
  
923
			// Lightweight tag
  
924
			commit, err := gitRepo.CommitObject(ref.Hash())
  
925
			if err == nil {
  
926
				tags = append(tags, tagInfo{
  
927
					Name: ref.Name().Short(),
  
928
					Date: commit.Author.When,
  
929
					Hash: ref.Hash().String(),
  
930
				})
  
931
			}
  
932
		} else {
  
933
			// Annotated tag
  
934
			if _, err := obj.Commit(); err == nil {
  
935
				tags = append(tags, tagInfo{
  
936
					Name: ref.Name().Short(),
  
937
					Date: obj.Tagger.When,
  
938
					Hash: ref.Hash().String(),
  
939
				})
  
940
			} else {
  
941
				tags = append(tags, tagInfo{
  
942
					Name: ref.Name().Short(),
  
943
					Date: obj.Tagger.When,
  
944
					Hash: obj.Target.String(),
  
945
				})
  
946
			}
  
947
		}
  
948
		return nil
  
949
	})
  
950
  
  
951
	sort.Slice(tags, func(i, j int) bool {
  
952
		return tags[i].Date.After(tags[j].Date)
  
953
	})
  
954
  
  
955
	if len(tags) > 20 {
  
956
		tags = tags[:20]
  
957
	}
  
958
  
  
959
	scheme := "http"
  
960
	if r.TLS != nil {
  
961
		scheme = "https"
  
962
	}
  
963
	baseURL := fmt.Sprintf("%s://%s", scheme, r.Host)
  
964
  
  
965
	rss := RSS{
  
966
		Version: "2.0",
  
967
		Channel: Channel{
  
968
			Title:       fmt.Sprintf("%s - Tags", repo.Name),
  
969
			Link:        fmt.Sprintf("%s/r/%s", baseURL, repo.Name),
  
970
			Description: fmt.Sprintf("Tags for %s", repo.Name),
  
971
		},
  
972
	}
  
973
  
  
974
	for _, t := range tags {
  
975
		item := RSSItem{
  
976
			Title:       t.Name,
  
977
			Link:        fmt.Sprintf("%s/r/%s?ref=%s", baseURL, repo.Name, t.Name),
  
978
			Description: fmt.Sprintf("Tag %s at %s", t.Name, t.Hash),
  
979
			PubDate:     t.Date.Format(time.RFC1123Z),
  
980
			GUID:        fmt.Sprintf("%s/r/%s/tags/%s", baseURL, repo.Name, t.Name),
  
981
		}
  
982
		rss.Channel.Items = append(rss.Channel.Items, item)
  
983
	}
  
984
  
  
985
	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
  
986
	fmt.Fprint(w, xml.Header)
  
987
	enc := xml.NewEncoder(w)
  
988
	enc.Indent("", "  ")
  
989
	if err := enc.Encode(rss); err != nil {
  
990
		log.Printf("Error encoding RSS: %v", err)
  
991
	}
  
992
}
  
diff --git a/hcommit.go b/hcommit.go
  
1
package main
  
2
  
  
3
import (
  
4
	"fmt"
  
5
	"net/http"
  
6
	"strings"
  
7
  
  
8
	"github.com/go-git/go-git/v5"
  
9
	"github.com/go-git/go-git/v5/plumbing"
  
10
	"github.com/go-git/go-git/v5/plumbing/format/diff"
  
11
	"github.com/go-git/go-git/v5/plumbing/object"
  
12
)
  
13
  
  
14
func commitHandler(w http.ResponseWriter, r *http.Request) {
  
15
	ctx, err := getRepoContext(w, r)
  
16
	if err != nil {
  
17
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
18
		return
  
19
	}
  
20
  
  
21
	commitHash := r.PathValue("hash")
  
22
	hash := plumbing.NewHash(commitHash)
  
23
	commit, err := ctx.GitRepo.CommitObject(hash)
  
24
	if err != nil {
  
25
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
26
		return
  
27
	}
  
28
  
  
29
	var fileDiffs []FileDiff
  
30
	maxChanges := 0
  
31
	if commit.NumParents() > 0 {
  
32
		parent, _ := commit.Parent(0)
  
33
		patch, err := parent.Patch(commit)
  
34
		if err == nil {
  
35
			for _, fp := range patch.FilePatches() {
  
36
				from, to := fp.Files()
  
37
				name := ""
  
38
				mode := ""
  
39
				if to != nil {
  
40
					name = to.Path()
  
41
					mode = formatMode(to.Mode())
  
42
				} else if from != nil {
  
43
					name = from.Path()
  
44
					mode = formatMode(from.Mode())
  
45
				}
  
46
  
  
47
				fileAdd, fileDel := 0, 0
  
48
				isBinary := fp.IsBinary()
  
49
				deleted := to == nil
  
50
				var oldSize, newSize int64
  
51
  
  
52
				if isBinary {
  
53
					if from != nil {
  
54
						obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, from.Hash())
  
55
						if blob, ok := obj.(*object.Blob); ok {
  
56
							oldSize = blob.Size
  
57
						}
  
58
					}
  
59
					if to != nil {
  
60
						obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, to.Hash())
  
61
						if blob, ok := obj.(*object.Blob); ok {
  
62
							newSize = blob.Size
  
63
						}
  
64
					}
  
65
				}
  
66
  
  
67
				var diffLines []DiffLine
  
68
				leftNo, rightNo := 1, 1
  
69
  
  
70
				var delLines []string
  
71
				var addLines []string
  
72
  
  
73
				flush := func() {
  
74
					max := len(delLines)
  
75
					if len(addLines) > max {
  
76
						max = len(addLines)
  
77
					}
  
78
					for i := 0; i < max; i++ {
  
79
						line := DiffLine{}
  
80
						if i < len(delLines) && i < len(addLines) {
  
81
							line.LeftNo = fmt.Sprintf("%d", leftNo)
  
82
							line.Left = delLines[i]
  
83
							line.RightNo = fmt.Sprintf("%d", rightNo)
  
84
							line.Right = addLines[i]
  
85
							line.Type = "mod"
  
86
							leftNo++
  
87
							rightNo++
  
88
							fileAdd++
  
89
							fileDel++
  
90
						} else if i < len(delLines) {
  
91
							line.LeftNo = fmt.Sprintf("%d", leftNo)
  
92
							line.Left = delLines[i]
  
93
							line.Type = "del"
  
94
							leftNo++
  
95
							fileDel++
  
96
						} else if i < len(addLines) {
  
97
							line.RightNo = fmt.Sprintf("%d", rightNo)
  
98
							line.Right = addLines[i]
  
99
							line.Type = "add"
  
100
							rightNo++
  
101
							fileAdd++
  
102
						}
  
103
						diffLines = append(diffLines, line)
  
104
					}
  
105
					delLines = nil
  
106
					addLines = nil
  
107
				}
  
108
  
  
109
				for _, chunk := range fp.Chunks() {
  
110
					lines := strings.Split(strings.TrimSuffix(chunk.Content(), "\n"), "\n")
  
111
					switch chunk.Type() {
  
112
					case diff.Equal:
  
113
						flush()
  
114
						for _, line := range lines {
  
115
							diffLines = append(diffLines, DiffLine{
  
116
								LeftNo:  fmt.Sprintf("%d", leftNo),
  
117
								Left:    line,
  
118
								RightNo: fmt.Sprintf("%d", rightNo),
  
119
								Right:   line,
  
120
								Type:    "eq",
  
121
							})
  
122
							leftNo++
  
123
							rightNo++
  
124
						}
  
125
					case diff.Delete:
  
126
						delLines = append(delLines, lines...)
  
127
					case diff.Add:
  
128
						addLines = append(addLines, lines...)
  
129
					}
  
130
				}
  
131
				flush()
  
132
  
  
133
				if fileAdd+fileDel > maxChanges {
  
134
					maxChanges = fileAdd + fileDel
  
135
				}
  
136
  
  
137
				visible := make([]bool, len(diffLines))
  
138
				for i, line := range diffLines {
  
139
					if line.Type == "add" || line.Type == "del" || line.Type == "mod" {
  
140
						for j := i - 3; j <= i+3; j++ {
  
141
							if j >= 0 && j < len(diffLines) {
  
142
								visible[j] = true
  
143
							}
  
144
						}
  
145
					}
  
146
				}
  
147
  
  
148
				var filteredLines []DiffLine
  
149
				lastWasGap := false
  
150
				for i, isVisible := range visible {
  
151
					if isVisible {
  
152
						filteredLines = append(filteredLines, diffLines[i])
  
153
						lastWasGap = false
  
154
					} else {
  
155
						if !lastWasGap {
  
156
							filteredLines = append(filteredLines, DiffLine{Type: "gap"})
  
157
							lastWasGap = true
  
158
						}
  
159
					}
  
160
				}
  
161
  
  
162
				fileDiffs = append(fileDiffs, FileDiff{
  
163
					Name:     name,
  
164
					Lines:    filteredLines,
  
165
					Addition: fileAdd,
  
166
					Deletion: fileDel,
  
167
					IsBinary: isBinary,
  
168
					Mode:     mode,
  
169
					OldSize:  oldSize,
  
170
					NewSize:  newSize,
  
171
					Deleted:  deleted,
  
172
				})
  
173
			}
  
174
		}
  
175
	}
  
176
  
  
177
	stats, _ := commit.Stats()
  
178
	adds, dels := 0, 0
  
179
	for _, s := range stats {
  
180
		adds += s.Addition
  
181
		dels += s.Deletion
  
182
	}
  
183
  
  
184
	data := struct {
  
185
		*RepoContext
  
186
		Commit     Commit
  
187
		FileDiffs  []FileDiff
  
188
		MaxChanges int
  
189
	}{
  
190
		RepoContext: ctx,
  
191
		Commit: Commit{
  
192
			Hash:           commit.Hash.String(),
  
193
			AuthorName:     commit.Author.Name,
  
194
			AuthorEmail:    commit.Author.Email,
  
195
			AuthorDate:     commit.Author.When,
  
196
			CommitterName:  commit.Committer.Name,
  
197
			CommitterEmail: commit.Committer.Email,
  
198
			CommitterDate:  commit.Committer.When,
  
199
			Message:        commit.Message,
  
200
			Additions:      adds,
  
201
			Deletions:      dels,
  
202
		},
  
203
		FileDiffs:  fileDiffs,
  
204
		MaxChanges: maxChanges,
  
205
	}
  
206
  
  
207
	err = templates.ExecuteTemplate(w, "commit.html", data)
  
208
	if err != nil {
  
209
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
210
	}
  
211
}
  
212
  
  
213
func patchHandler(w http.ResponseWriter, r *http.Request) {
  
214
	repoName := r.PathValue("name")
  
215
	commitHash := r.PathValue("hash")
  
216
  
  
217
	config, err := loadConfig(ConfigPath)
  
218
	if err != nil {
  
219
		http.Error(w, fmt.Sprintf("Error loading config: %v", err), http.StatusInternalServerError)
  
220
		return
  
221
	}
  
222
  
  
223
	var repo *Repository
  
224
	for _, repoItem := range config.Repositories {
  
225
		if repoItem.Name == repoName {
  
226
			repo = &repoItem
  
227
			break
  
228
		}
  
229
	}
  
230
  
  
231
	if repo == nil {
  
232
		http.NotFound(w, r)
  
233
		return
  
234
	}
  
235
  
  
236
	gitRepo, err := git.PlainOpen(repo.Path)
  
237
	if err != nil {
  
238
		http.Error(w, fmt.Sprintf("Error opening repository: %v", err), http.StatusInternalServerError)
  
239
		return
  
240
	}
  
241
  
  
242
	hash := plumbing.NewHash(commitHash)
  
243
	commit, err := gitRepo.CommitObject(hash)
  
244
	if err != nil {
  
245
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
246
		return
  
247
	}
  
248
  
  
249
	w.Header().Set("Content-Type", "text/plain")
  
250
  
  
251
	currentTree, err := commit.Tree()
  
252
	if err != nil {
  
253
		http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
  
254
		return
  
255
	}
  
256
  
  
257
	var parentTree *object.Tree
  
258
	if commit.NumParents() > 0 {
  
259
		parent, _ := commit.Parent(0)
  
260
		parentTree, err = parent.Tree()
  
261
		if err != nil {
  
262
			http.Error(w, fmt.Sprintf("Error getting parent tree: %v", err), http.StatusInternalServerError)
  
263
			return
  
264
		}
  
265
	}
  
266
  
  
267
	patch, err := parentTree.Patch(currentTree)
  
268
	if err != nil {
  
269
		http.Error(w, fmt.Sprintf("Error generating patch: %v", err), http.StatusInternalServerError)
  
270
		return
  
271
	}
  
272
	fmt.Fprint(w, patch.String())
  
273
}
diff --git a/hhome.go b/hhome.go
  
1
package main
  
2
  
  
3
import (
  
4
	"net/http"
  
5
)
  
6
  
  
7
func homeHandler(w http.ResponseWriter, r *http.Request) {
  
8
	config := GlobalConfig
  
9
  
  
10
	groupsMap := make(map[string][]Repository)
  
11
	var groupOrder []string
  
12
  
  
13
	for _, repo := range config.Repositories {
  
14
		if _, ok := groupsMap[repo.Group]; !ok {
  
15
			groupOrder = append(groupOrder, repo.Group)
  
16
		}
  
17
		groupsMap[repo.Group] = append(groupsMap[repo.Group], repo)
  
18
	}
  
19
  
  
20
	var grouped []GroupedRepositories
  
21
	for _, groupName := range groupOrder {
  
22
		grouped = append(grouped, GroupedRepositories{
  
23
			Name:         groupName,
  
24
			Repositories: groupsMap[groupName],
  
25
		})
  
26
	}
  
27
  
  
28
	err := templates.ExecuteTemplate(w, "repositories.html", struct {
  
29
		Groups []GroupedRepositories
  
30
		Repo   *Repository
  
31
	}{
  
32
		Groups: grouped,
  
33
		Repo:   nil,
  
34
	})
  
35
  
  
36
	if err != nil {
  
37
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
38
	}
  
39
}
diff --git a/hinfo.go b/hinfo.go
  
1
package main
  
2
  
  
3
import (
  
4
	"fmt"
  
5
	"html/template"
  
6
	"net/http"
  
7
)
  
8
  
  
9
func readmeHandler(w http.ResponseWriter, r *http.Request) {
  
10
	ctx, err := getRepoContext(w, r)
  
11
	if err != nil {
  
12
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
13
		return
  
14
	}
  
15
  
  
16
	if ctx.ReadmeName == "" {
  
17
		http.NotFound(w, r)
  
18
		return
  
19
	}
  
20
  
  
21
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
22
	if err != nil {
  
23
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
24
		return
  
25
	}
  
26
  
  
27
	file, err := commit.File(ctx.ReadmeName)
  
28
	if err != nil {
  
29
		http.NotFound(w, r)
  
30
		return
  
31
	}
  
32
  
  
33
	content, err := file.Contents()
  
34
	if err != nil {
  
35
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
36
		return
  
37
	}
  
38
  
  
39
	data := struct {
  
40
		*RepoContext
  
41
		Content template.HTML
  
42
	}{
  
43
		RepoContext: ctx,
  
44
		Content:     renderMarkdown(content),
  
45
	}
  
46
  
  
47
	err = templates.ExecuteTemplate(w, "readme.html", data)
  
48
	if err != nil {
  
49
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
50
	}
  
51
}
  
52
  
  
53
func licenseHandler(w http.ResponseWriter, r *http.Request) {
  
54
	ctx, err := getRepoContext(w, r)
  
55
	if err != nil {
  
56
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
57
		return
  
58
	}
  
59
  
  
60
	if ctx.LicenseName == "" {
  
61
		http.NotFound(w, r)
  
62
		return
  
63
	}
  
64
  
  
65
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
66
	if err != nil {
  
67
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
68
		return
  
69
	}
  
70
  
  
71
	file, err := commit.File(ctx.LicenseName)
  
72
	if err != nil {
  
73
		http.NotFound(w, r)
  
74
		return
  
75
	}
  
76
  
  
77
	content, err := file.Contents()
  
78
	if err != nil {
  
79
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
80
		return
  
81
	}
  
82
  
  
83
	data := struct {
  
84
		*RepoContext
  
85
		Content template.HTML
  
86
	}{
  
87
		RepoContext: ctx,
  
88
		Content:     renderMarkdown(content),
  
89
	}
  
90
  
  
91
	err = templates.ExecuteTemplate(w, "license.html", data)
  
92
	if err != nil {
  
93
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
94
	}
  
95
}
  
96
  
  
97
func markersHandler(w http.ResponseWriter, r *http.Request) {
  
98
	ctx, err := getRepoContext(w, r)
  
99
	if err != nil {
  
100
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
101
		return
  
102
	}
  
103
  
  
104
	markers, err := scanMarkers(ctx)
  
105
	if err != nil {
  
106
		http.Error(w, fmt.Sprintf("Error scanning markers: %v", err), http.StatusInternalServerError)
  
107
		return
  
108
	}
  
109
  
  
110
	data := struct {
  
111
		*RepoContext
  
112
		Markers []Marker
  
113
	}{
  
114
		RepoContext: ctx,
  
115
		Markers:     markers,
  
116
	}
  
117
  
  
118
	err = templates.ExecuteTemplate(w, "markers.html", data)
  
119
	if err != nil {
  
120
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
121
	}
  
122
}
diff --git a/hrepo.go b/hrepo.go
  
1
package main
  
2
  
  
3
import (
  
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
  
  
19
func 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
  
  
184
func 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
  
  
245
func 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
}
diff --git a/htree.go b/htree.go
  
1
package main
  
2
  
  
3
import (
  
4
	"archive/tar"
  
5
	"compress/gzip"
  
6
	"fmt"
  
7
	"html/template"
  
8
	"io"
  
9
	"log"
  
10
	"net/http"
  
11
	"path"
  
12
	"sort"
  
13
	"strings"
  
14
  
  
15
	"github.com/go-git/go-git/v5/plumbing"
  
16
	"github.com/go-git/go-git/v5/plumbing/object"
  
17
)
  
18
  
  
19
func treeHandler(w http.ResponseWriter, r *http.Request) {
  
20
	ctx, err := getRepoContext(w, r)
  
21
	if err != nil {
  
22
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
23
		return
  
24
	}
  
25
  
  
26
	pathValue := r.PathValue("path")
  
27
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
28
	if err != nil {
  
29
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
30
		return
  
31
	}
  
32
  
  
33
	tree, err := commit.Tree()
  
34
	if err != nil {
  
35
		http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
  
36
		return
  
37
	}
  
38
  
  
39
	if pathValue != "" {
  
40
		tree, err = tree.Tree(pathValue)
  
41
		if err != nil {
  
42
			http.NotFound(w, r)
  
43
			return
  
44
		}
  
45
	}
  
46
  
  
47
	var entries []TreeEntry
  
48
	for _, entry := range tree.Entries {
  
49
		fullPath := entry.Name
  
50
		if pathValue != "" {
  
51
			fullPath = pathValue + "/" + entry.Name
  
52
		}
  
53
  
  
54
		isDir := entry.Mode.IsFile() == false
  
55
  
  
56
		var size int64
  
57
		if !isDir {
  
58
			obj, _ := ctx.GitRepo.Object(plumbing.AnyObject, entry.Hash)
  
59
			if blob, ok := obj.(*object.Blob); ok {
  
60
				size = blob.Size
  
61
			}
  
62
		}
  
63
  
  
64
		entries = append(entries, TreeEntry{
  
65
			Name:  entry.Name,
  
66
			Path:  fullPath,
  
67
			IsDir: isDir,
  
68
			Size:  size,
  
69
			Mode:  entry.Mode.String(),
  
70
		})
  
71
	}
  
72
  
  
73
	sort.Slice(entries, func(i, j int) bool {
  
74
		if entries[i].IsDir != entries[j].IsDir {
  
75
			return entries[i].IsDir
  
76
		}
  
77
		return entries[i].Name < entries[j].Name
  
78
	})
  
79
  
  
80
	data := struct {
  
81
		*RepoContext
  
82
		Entries []TreeEntry
  
83
		Path    string
  
84
		View    string
  
85
	}{
  
86
		RepoContext: ctx,
  
87
		Entries:     entries,
  
88
		Path:        pathValue,
  
89
		View:        "tree",
  
90
	}
  
91
  
  
92
	err = templates.ExecuteTemplate(w, "tree.html", data)
  
93
	if err != nil {
  
94
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
95
	}
  
96
}
  
97
  
  
98
func blobHandler(w http.ResponseWriter, r *http.Request) {
  
99
	ctx, err := getRepoContext(w, r)
  
100
	if err != nil {
  
101
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
102
		return
  
103
	}
  
104
  
  
105
	pathValue := r.PathValue("path")
  
106
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
107
	if err != nil {
  
108
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
109
		return
  
110
	}
  
111
  
  
112
	file, err := commit.File(pathValue)
  
113
	if err != nil {
  
114
		http.NotFound(w, r)
  
115
		return
  
116
	}
  
117
  
  
118
	content, err := file.Contents()
  
119
	if err != nil {
  
120
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
121
		return
  
122
	}
  
123
  
  
124
	data := struct {
  
125
		*RepoContext
  
126
		Path    string
  
127
		Content template.HTML
  
128
	}{
  
129
		RepoContext: ctx,
  
130
		Path:        pathValue,
  
131
		Content:     highlight(pathValue, content),
  
132
	}
  
133
  
  
134
	err = templates.ExecuteTemplate(w, "blob.html", data)
  
135
	if err != nil {
  
136
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
137
	}
  
138
}
  
139
  
  
140
func rawHandler(w http.ResponseWriter, r *http.Request) {
  
141
	ctx, err := getRepoContext(w, r)
  
142
	if err != nil {
  
143
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
144
		return
  
145
	}
  
146
  
  
147
	pathValue := r.PathValue("path")
  
148
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
149
	if err != nil {
  
150
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
151
		return
  
152
	}
  
153
  
  
154
	file, err := commit.File(pathValue)
  
155
	if err != nil {
  
156
		http.NotFound(w, r)
  
157
		return
  
158
	}
  
159
  
  
160
	reader, err := file.Reader()
  
161
	if err != nil {
  
162
		http.Error(w, fmt.Sprintf("Error reading file: %v", err), http.StatusInternalServerError)
  
163
		return
  
164
	}
  
165
	defer reader.Close()
  
166
  
  
167
	w.Header().Set("Content-Type", "application/octet-stream")
  
168
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", pathValue))
  
169
	io.Copy(w, reader)
  
170
}
  
171
  
  
172
func archiveHandler(w http.ResponseWriter, r *http.Request) {
  
173
	ctx, err := getRepoContext(w, r)
  
174
	if err != nil {
  
175
		http.Error(w, err.Error(), http.StatusInternalServerError)
  
176
		return
  
177
	}
  
178
  
  
179
	pathValue := r.PathValue("path")
  
180
	commit, err := ctx.GitRepo.CommitObject(ctx.Hash)
  
181
	if err != nil {
  
182
		http.Error(w, fmt.Sprintf("Error getting commit: %v", err), http.StatusInternalServerError)
  
183
		return
  
184
	}
  
185
  
  
186
	tree, err := commit.Tree()
  
187
	if err != nil {
  
188
		http.Error(w, fmt.Sprintf("Error getting tree: %v", err), http.StatusInternalServerError)
  
189
		return
  
190
	}
  
191
  
  
192
	if pathValue != "" {
  
193
		tree, err = tree.Tree(pathValue)
  
194
		if err != nil {
  
195
			http.NotFound(w, r)
  
196
			return
  
197
		}
  
198
	}
  
199
  
  
200
	filename := ctx.Repo.Name
  
201
	if pathValue != "" {
  
202
		filename = path.Base(pathValue)
  
203
	}
  
204
	filename = fmt.Sprintf("%s-%s.tar.gz", filename, ctx.CurrentRef)
  
205
  
  
206
	w.Header().Set("Content-Type", "application/gzip")
  
207
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
  
208
  
  
209
	gw := gzip.NewWriter(w)
  
210
	defer gw.Close()
  
211
  
  
212
	tw := tar.NewWriter(gw)
  
213
	defer tw.Close()
  
214
  
  
215
	err = tree.Files().ForEach(func(f *object.File) error {
  
216
		hdr := &tar.Header{
  
217
			Name: strings.TrimPrefix(strings.TrimPrefix(f.Name, pathValue), "/"),
  
218
			Mode: int64(f.Mode),
  
219
			Size: f.Size,
  
220
		}
  
221
  
  
222
		if err := tw.WriteHeader(hdr); err != nil {
  
223
			return err
  
224
		}
  
225
  
  
226
		reader, err := f.Reader()
  
227
		if err != nil {
  
228
			return err
  
229
		}
  
230
		defer reader.Close()
  
231
  
  
232
		_, err = io.Copy(tw, reader)
  
233
		return err
  
234
	})
  
235
  
  
236
	if err != nil {
  
237
		log.Printf("Error creating archive: %v", err)
  
238
	}
  
239
}
diff --git a/languages.go b/languages.go
...
13
)
13
)
14
  
14
  
15
var languageColors = map[string]string{
15
var languageColors = map[string]string{
16
	"1C Enterprise":     "#814CCC",
16
	"1C Enterprise":                "#814CCC",
17
	"ActionScript":      "#882B0F",
17
	"ActionScript":                 "#882B0F",
18
	"Ada":               "#02f88c",
18
	"Ada":                          "#02f88c",
19
	"Agda":              "#315665",
19
	"Agda":                         "#315665",
20
	"AGS Script":        "#B9D9FF",
20
	"AGS Script":                   "#B9D9FF",
21
	"Alloy":             "#64C800",
21
	"Alloy":                        "#64C800",
22
	"AMPL":              "#E6EFBB",
22
	"AMPL":                         "#E6EFBB",
23
	"Ant Build System":  "#A9157E",
23
	"Ant Build System":             "#A9157E",
24
	"ANTLR":             "#9DC3FF",
24
	"ANTLR":                        "#9DC3FF",
25
	"ApacheConf":        "#d12127",
25
	"ApacheConf":                   "#d12127",
26
	"Apex":              "#1797c0",
26
	"Apex":                         "#1797c0",
27
	"API Blueprint":     "#2ACCA8",
27
	"API Blueprint":                "#2ACCA8",
28
	"APL":               "#5A8164",
28
	"APL":                          "#5A8164",
29
	"AppleScript":       "#101F1F",
29
	"AppleScript":                  "#101F1F",
30
	"Arc":               "#aa2afe",
30
	"Arc":                          "#aa2afe",
31
	"Arduino":           "#bd7911",
31
	"Arduino":                      "#bd7911",
32
	"ASP":               "#6a40fd",
32
	"ASP":                          "#6a40fd",
33
	"AspectJ":           "#a957b0",
33
	"AspectJ":                      "#a957b0",
34
	"Assembly":          "#6E4C13",
34
	"Assembly":                     "#6E4C13",
35
	"ATS":               "#1ac620",
35
	"ATS":                          "#1ac620",
36
	"Augeas":            "#9CC134",
36
	"Augeas":                       "#9CC134",
37
	"AutoHotkey":        "#6594b9",
37
	"AutoHotkey":                   "#6594b9",
38
	"AutoIt":            "#1C3552",
38
	"AutoIt":                       "#1C3552",
39
	"Awk":               "#c30e9b",
39
	"Awk":                          "#c30e9b",
40
	"Ballerina":         "#FF5000",
40
	"Ballerina":                    "#FF5000",
41
	"Batchfile":         "#C1F12E",
41
	"Batchfile":                    "#C1F12E",
42
	"Befunge":            "#2F2530",
42
	"Befunge":                      "#2F2530",
43
	"Bicep":             "#519aba",
43
	"Bicep":                        "#519aba",
44
	"Bison":             "#6A463F",
44
	"Bison":                        "#6A463F",
45
	"BitBake":           "#00bce4",
45
	"BitBake":                      "#00bce4",
46
	"Blade":             "#f7523f",
46
	"Blade":                        "#f7523f",
47
	"BlitzBasic":        "#00FFAE",
47
	"BlitzBasic":                   "#00FFAE",
48
	"BlitzMax":          "#cd6400",
48
	"BlitzMax":                     "#cd6400",
49
	"Bluespec":          "#12223c",
49
	"Bluespec":                     "#12223c",
50
	"Boo":               "#d4bec1",
50
	"Boo":                          "#d4bec1",
51
	"Brainfuck":         "#2F2530",
51
	"Brainfuck":                    "#2F2530",
52
	"Brightscript":      "#662D91",
52
	"Brightscript":                 "#662D91",
53
	"C":                 "#555555",
53
	"C":                            "#555555",
54
	"C#":                "#178600",
54
	"C#":                           "#178600",
55
	"C++":               "#f34b7d",
55
	"C++":                          "#f34b7d",
56
	"C3":                "#2563eb",
56
	"C3":                           "#2563eb",
57
	"Caddyfile":         "#22b638",
57
	"Caddyfile":                    "#22b638",
58
	"Cairo":             "#ff4a48",
58
	"Cairo":                        "#ff4a48",
59
	"Ceylon":            "#dfa535",
59
	"Ceylon":                       "#dfa535",
60
	"Chapel":            "#8dc63f",
60
	"Chapel":                       "#8dc63f",
61
	"ChucK":             "#3f8000",
61
	"ChucK":                        "#3f8000",
62
	"Cirru":             "#ccccff",
62
	"Cirru":                        "#ccccff",
63
	"Clarion":           "#db901e",
63
	"Clarion":                      "#db901e",
64
	"Clean":             "#3F85AF",
64
	"Clean":                        "#3F85AF",
65
	"Click":             "#E4E6F3",
65
	"Click":                        "#E4E6F3",
66
	"CLIPS":             "#00A300",
66
	"CLIPS":                        "#00A300",
67
	"Clojure":           "#db5855",
67
	"Clojure":                      "#db5855",
68
	"CMake":             "#DA3434",
68
	"CMake":                        "#DA3434",
69
	"COBOL":             "#1d2021",
69
	"COBOL":                        "#1d2021",
70
	"CodeQL":            "#140f46",
70
	"CodeQL":                       "#140f46",
71
	"CoffeeScript":      "#244776",
71
	"CoffeeScript":                 "#244776",
72
	"ColdFusion":        "#ed2cd6",
72
	"ColdFusion":                   "#ed2cd6",
73
	"Common Lisp":       "#3fb68b",
73
	"Common Lisp":                  "#3fb68b",
74
	"Component Pascal":  "#B0CE4E",
74
	"Component Pascal":             "#B0CE4E",
75
	"Crystal":           "#000100",
75
	"Crystal":                      "#000100",
76
	"CSON":              "#244776",
76
	"CSON":                         "#244776",
77
	"Csound":            "#1a1a1a",
77
	"Csound":                       "#1a1a1a",
78
	"CSS":               "#563d7c",
78
	"CSS":                          "#563d7c",
79
	"Cuda":              "#3A4E3A",
79
	"Cuda":                         "#3A4E3A",
80
	"Curry":             "#531242",
80
	"Curry":                        "#531242",
81
	"Cycript":           "#000000",
81
	"Cycript":                      "#000000",
82
	"Cython":            "#fedf5b",
82
	"Cython":                       "#fedf5b",
83
	"D":                 "#ba595e",
83
	"D":                            "#ba595e",
84
	"Dart":              "#00B4AB",
84
	"Dart":                         "#00B4AB",
85
	"DataWeave":         "#003a52",
85
	"DataWeave":                    "#003a52",
86
	"Dhall":             "#dfafff",
86
	"Dhall":                        "#dfafff",
87
	"Dockerfile":        "#384d54",
87
	"Dockerfile":                   "#384d54",
88
	"Dogescript":        "#cca760",
88
	"Dogescript":                   "#cca760",
89
	"DTrace":            "#000000",
89
	"DTrace":                       "#000000",
90
	"Dylan":             "#6c616e",
90
	"Dylan":                        "#6c616e",
91
	"E":                 "#ccce35",
91
	"E":                            "#ccce35",
92
	"eC":                "#913960",
92
	"eC":                           "#913960",
93
	"ECL":               "#8a1267",
93
	"ECL":                          "#8a1267",
94
	"Eiffel":            "#4d6977",
94
	"Eiffel":                       "#4d6977",
95
	"EJS":               "#a91e50",
95
	"EJS":                          "#a91e50",
96
	"Elixir":            "#6e4a7e",
96
	"Elixir":                       "#6e4a7e",
97
	"Elm":               "#60B5CC",
97
	"Elm":                          "#60B5CC",
98
	"Emacs Lisp":        "#c065db",
98
	"Emacs Lisp":                   "#c065db",
99
	"EmberScript":       "#FFF4F3",
99
	"EmberScript":                  "#FFF4F3",
100
	"EQ":                "#a78649",
100
	"EQ":                           "#a78649",
101
	"Erlang":            "#B83998",
101
	"Erlang":                       "#B83998",
102
	"F#":                "#b845fc",
102
	"F#":                           "#b845fc",
103
	"F*":                "#572e30",
103
	"F*":                           "#572e30",
104
	"Factor":            "#636746",
104
	"Factor":                       "#636746",
105
	"Fancy":             "#7b9db4",
105
	"Fancy":                        "#7b9db4",
106
	"Fantom":            "#14253c",
106
	"Fantom":                       "#14253c",
107
	"Faust":             "#c37240",
107
	"Faust":                        "#c37240",
108
	"Fennel":            "#fff3d7",
108
	"Fennel":                       "#fff3d7",
109
	"fish":              "#4aae47",
109
	"fish":                         "#4aae47",
110
	"FLUX":              "#88ccff",
110
	"FLUX":                         "#88ccff",
111
	"Forth":             "#341708",
111
	"Forth":                        "#341708",
112
	"Fortran":           "#4d41b1",
112
	"Fortran":                      "#4d41b1",
113
	"FreeBASIC":         "#141AC9",
113
	"FreeBASIC":                    "#141AC9",
114
	"Frege":             "#00cafe",
114
	"Frege":                        "#00cafe",
115
	"Futhark":           "#5f021f",
115
	"Futhark":                      "#5f021f",
116
	"G-code":            "#D08CF2",
116
	"G-code":                       "#D08CF2",
117
	"Game Maker Language": "#71b417",
117
	"Game Maker Language":          "#71b417",
118
	"GAML":              "#FFC766",
118
	"GAML":                         "#FFC766",
119
	"GAMS":              "#f49a22",
119
	"GAMS":                         "#f49a22",
120
	"GAP":               "#0000cc",
120
	"GAP":                          "#0000cc",
121
	"GDScript":          "#355570",
121
	"GDScript":                     "#355570",
122
	"Genie":             "#fb855d",
122
	"Genie":                        "#fb855d",
123
	"Genshi":            "#951531",
123
	"Genshi":                       "#951531",
124
	"Gentoo Ebuild":     "#9400ff",
124
	"Gentoo Ebuild":                "#9400ff",
125
	"Gherkin":           "#5B2063",
125
	"Gherkin":                      "#5B2063",
126
	"Gleam":             "#ffaff3",
126
	"Gleam":                        "#ffaff3",
127
	"GLSL":              "#5686a5",
127
	"GLSL":                         "#5686a5",
128
	"Glyph":             "#c1ac7f",
128
	"Glyph":                        "#c1ac7f",
129
	"Gnuplot":           "#f0a9f0",
129
	"Gnuplot":                      "#f0a9f0",
130
	"Go":                "#00ADD8",
130
	"Go":                           "#00ADD8",
131
	"Golo":              "#88562A",
131
	"Golo":                         "#88562A",
132
	"Gosu":              "#82937f",
132
	"Gosu":                         "#82937f",
133
	"Grace":             "#615f8b",
133
	"Grace":                        "#615f8b",
134
	"Gradle":            "#02303a",
134
	"Gradle":                       "#02303a",
135
	"GraphQL":           "#e10098",
135
	"GraphQL":                      "#e10098",
136
	"Groovy":            "#4298b8",
136
	"Groovy":                       "#4298b8",
137
	"Hack":              "#878787",
137
	"Hack":                         "#878787",
138
	"Haml":              "#ece2a9",
138
	"Haml":                         "#ece2a9",
139
	"Handlebars":        "#f7931e",
139
	"Handlebars":                   "#f7931e",
140
	"Harbour":           "#0e60e3",
140
	"Harbour":                      "#0e60e3",
141
	"Haskell":           "#5e5086",
141
	"Haskell":                      "#5e5086",
142
	"Haxe":              "#df7900",
142
	"Haxe":                         "#df7900",
143
	"HCL":               "#844FBA",
143
	"HCL":                          "#844FBA",
144
	"HiveQL":            "#dce200",
144
	"HiveQL":                       "#dce200",
145
	"HolyC":             "#ffefaf",
145
	"HolyC":                        "#ffefaf",
146
	"HTML":              "#e34c26",
146
	"HTML":                         "#e34c26",
147
	"Hy":                "#7790B2",
147
	"Hy":                           "#7790B2",
148
	"IDL":               "#a3522f",
148
	"IDL":                          "#a3522f",
149
	"Idris":             "#b30000",
149
	"Idris":                        "#b30000",
150
	"Ignore List":       "#000000",
150
	"Ignore List":                  "#000000",
151
	"IGOR Pro":          "#0000cc",
151
	"IGOR Pro":                     "#0000cc",
152
	"Imba":              "#16cec6",
152
	"Imba":                         "#16cec6",
153
	"Inform 7":          "#3d9970",
153
	"Inform 7":                     "#3d9970",
154
	"INI":               "#d1dbe0",
154
	"INI":                          "#d1dbe0",
155
	"Inno Setup":        "#264b99",
155
	"Inno Setup":                   "#264b99",
156
	"Io":                "#a9188d",
156
	"Io":                           "#a9188d",
157
	"Ioke":              "#078193",
157
	"Ioke":                         "#078193",
158
	"Isabelle":          "#FEFE00",
158
	"Isabelle":                     "#FEFE00",
159
	"J":                 "#9EEDFF",
159
	"J":                            "#9EEDFF",
160
	"Janet":             "#0886a5",
160
	"Janet":                        "#0886a5",
161
	"Java":              "#b07219",
161
	"Java":                         "#b07219",
162
	"JavaScript":        "#f1e05a",
162
	"JavaScript":                   "#f1e05a",
163
	"Jinja":             "#a52a22",
163
	"Jinja":                        "#a52a22",
164
	"Jison":             "#56b3cb",
164
	"Jison":                        "#56b3cb",
165
	"Jolie":             "#843179",
165
	"Jolie":                        "#843179",
166
	"JSON":              "#292929",
166
	"JSON":                         "#292929",
167
	"Jsonnet":           "#0064bd",
167
	"Jsonnet":                      "#0064bd",
168
	"Julia":             "#a270ba",
168
	"Julia":                        "#a270ba",
169
	"Jupyter Notebook":  "#DA5B0B",
169
	"Jupyter Notebook":             "#DA5B0B",
170
	"Just":              "#384d54",
170
	"Just":                         "#384d54",
171
	"Kaitai Struct":     "#773b37",
171
	"Kaitai Struct":                "#773b37",
172
	"KCL":               "#7ABABF",
172
	"KCL":                          "#7ABABF",
173
	"Kotlin":            "#A97BFF",
173
	"Kotlin":                       "#A97BFF",
174
	"KRL":               "#28430A",
174
	"KRL":                          "#28430A",
175
	"LabVIEW":           "#fede06",
175
	"LabVIEW":                      "#fede06",
176
	"Lasso":             "#999999",
176
	"Lasso":                        "#999999",
177
	"Latte":             "#f2a542",
177
	"Latte":                        "#f2a542",
178
	"Lean":              "#3d6117",
178
	"Lean":                         "#3d6117",
179
	"Less":              "#1d365d",
179
	"Less":                         "#1d365d",
180
	"Lex":               "#DBCA00",
180
	"Lex":                          "#DBCA00",
181
	"LigoLANG":          "#0e74ff",
181
	"LigoLANG":                     "#0e74ff",
182
	"LilyPond":          "#9ccc7c",
182
	"LilyPond":                     "#9ccc7c",
183
	"Liquid":            "#67b8de",
183
	"Liquid":                       "#67b8de",
184
	"LiveScript":        "#499886",
184
	"LiveScript":                   "#499886",
185
	"LLVM":              "#185619",
185
	"LLVM":                         "#185619",
186
	"Logtalk":           "#295b9a",
186
	"Logtalk":                      "#295b9a",
187
	"LOLCODE":           "#cc9900",
187
	"LOLCODE":                      "#cc9900",
188
	"LookML":            "#652B81",
188
	"LookML":                       "#652B81",
189
	"LSL":               "#3d9970",
189
	"LSL":                          "#3d9970",
190
	"Lua":               "#000080",
190
	"Lua":                          "#000080",
191
	"Luau":              "#00A2FF",
191
	"Luau":                         "#00A2FF",
192
	"M4":                "#000000",
192
	"M4":                           "#000000",
193
	"Macaulay2":         "#d8ffff",
193
	"Macaulay2":                    "#d8ffff",
194
	"Makefile":          "#427819",
194
	"Makefile":                     "#427819",
195
	"Markdown":          "#083fa1",
195
	"Markdown":                     "#083fa1",
196
	"Marko":             "#42bff2",
196
	"Marko":                        "#42bff2",
197
	"Mask":              "#f97732",
197
	"Mask":                         "#f97732",
198
	"MATLAB":            "#e16737",
198
	"MATLAB":                       "#e16737",
199
	"Max":               "#c4a79c",
199
	"Max":                          "#c4a79c",
200
	"MAXScript":         "#00a6a6",
200
	"MAXScript":                    "#00a6a6",
201
	"MDX":               "#fcb32c",
201
	"MDX":                          "#fcb32c",
202
	"Mercury":           "#ff2b2b",
202
	"Mercury":                      "#ff2b2b",
203
	"Meson":             "#007800",
203
	"Meson":                        "#007800",
204
	"Metal":             "#8f14e9",
204
	"Metal":                        "#8f14e9",
205
	"MiniYAML":          "#ff1111",
205
	"MiniYAML":                     "#ff1111",
206
	"Mint":              "#02b046",
206
	"Mint":                         "#02b046",
207
	"Mirah":             "#c7a938",
207
	"Mirah":                        "#c7a938",
208
	"Modelica":          "#de1d31",
208
	"Modelica":                     "#de1d31",
209
	"Modula-2":          "#10253f",
209
	"Modula-2":                     "#10253f",
210
	"Mojo":              "#ff4c1f",
210
	"Mojo":                         "#ff4c1f",
211
	"MoonScript":        "#ff4585",
211
	"MoonScript":                   "#ff4585",
212
	"Move":              "#4a137a",
212
	"Move":                         "#4a137a",
213
	"MQL4":              "#62A8D6",
213
	"MQL4":                         "#62A8D6",
214
	"MQL5":              "#4A76B8",
214
	"MQL5":                         "#4A76B8",
215
	"MTML":              "#b7e1f4",
215
	"MTML":                         "#b7e1f4",
216
	"Mustache":          "#724b3b",
216
	"Mustache":                     "#724b3b",
217
	"Nemerle":           "#3d3c6e",
217
	"Nemerle":                      "#3d3c6e",
218
	"nesC":              "#94B0C7",
218
	"nesC":                         "#94B0C7",
219
	"NetLinx":           "#0aa0ff",
219
	"NetLinx":                      "#0aa0ff",
220
	"NetLogo":           "#ff6375",
220
	"NetLogo":                      "#ff6375",
221
	"NewLisp":           "#87AED7",
221
	"NewLisp":                      "#87AED7",
222
	"Nextflow":          "#3ac486",
222
	"Nextflow":                     "#3ac486",
223
	"Nginx":             "#009639",
223
	"Nginx":                        "#009639",
224
	"Nim":               "#ffc200",
224
	"Nim":                          "#ffc200",
225
	"Nit":               "#009917",
225
	"Nit":                          "#009917",
226
	"Nix":               "#7e7eff",
226
	"Nix":                          "#7e7eff",
227
	"Nushell":           "#4E9906",
227
	"Nushell":                      "#4E9906",
228
	"NWScript":          "#111522",
228
	"NWScript":                     "#111522",
229
	"Objective-C":       "#438eff",
229
	"Objective-C":                  "#438eff",
230
	"Objective-C++":     "#6866fb",
230
	"Objective-C++":                "#6866fb",
231
	"Objective-J":       "#ff0c5a",
231
	"Objective-J":                  "#ff0c5a",
232
	"OCaml":             "#ef7a08",
232
	"OCaml":                        "#ef7a08",
233
	"Odin":              "#60AFFE",
233
	"Odin":                         "#60AFFE",
234
	"Omgrofl":           "#cabbff",
234
	"Omgrofl":                      "#cabbff",
235
	"ooc":               "#b0b77e",
235
	"ooc":                          "#b0b77e",
236
	"Opal":              "#f7ede0",
236
	"Opal":                         "#f7ede0",
237
	"Open Policy Agent": "#7d9199",
237
	"Open Policy Agent":            "#7d9199",
238
	"OpenCL":            "#ed2e2d",
238
	"OpenCL":                       "#ed2e2d",
239
	"OpenEdge ABL":      "#5ce600",
239
	"OpenEdge ABL":                 "#5ce600",
240
	"OpenQASM":          "#AA70FF",
240
	"OpenQASM":                     "#AA70FF",
241
	"OpenSCAD":          "#e5cd45",
241
	"OpenSCAD":                     "#e5cd45",
242
	"Org":               "#77aa99",
242
	"Org":                          "#77aa99",
243
	"Ox":                "#000000",
243
	"Ox":                           "#000000",
244
	"Oxygene":           "#cdd0e3",
244
	"Oxygene":                      "#cdd0e3",
245
	"Oz":                "#fab738",
245
	"Oz":                           "#fab738",
246
	"P4":                "#7055b5",
246
	"P4":                           "#7055b5",
247
	"Papyrus":           "#660000",
247
	"Papyrus":                      "#660000",
248
	"Parrot":            "#f3ca0a",
248
	"Parrot":                       "#f3ca0a",
249
	"Pascal":            "#E3F171",
249
	"Pascal":                       "#E3F171",
250
	"Pawn":              "#dbb284",
250
	"Pawn":                         "#dbb284",
251
	"Pep8":              "#C76F5B",
251
	"Pep8":                         "#C76F5B",
252
	"Perl":              "#0298c3",
252
	"Perl":                         "#0298c3",
253
	"PHP":               "#4f5d95",
253
	"PHP":                          "#4f5d95",
254
	"PicoLisp":          "#6067af",
254
	"PicoLisp":                     "#6067af",
255
	"PigLatin":          "#fce7de",
255
	"PigLatin":                     "#fce7de",
256
	"Pike":              "#005390",
256
	"Pike":                         "#005390",
257
	"PLpgSQL":           "#336790",
257
	"PLpgSQL":                      "#336790",
258
	"PLSQL":             "#dad8d8",
258
	"PLSQL":                        "#dad8d8",
259
	"PogoScript":        "#d80073",
259
	"PogoScript":                   "#d80073",
260
	"Polar":             "#316880",
260
	"Polar":                        "#316880",
261
	"Pony":              "#000000",
261
	"Pony":                         "#000000",
262
	"PostScript":        "#da291c",
262
	"PostScript":                   "#da291c",
263
	"PowerShell":        "#012456",
263
	"PowerShell":                   "#012456",
264
	"Prisma":            "#0c344b",
264
	"Prisma":                       "#0c344b",
265
	"Processing":        "#0096D8",
265
	"Processing":                   "#0096D8",
266
	"Prolog":            "#74283c",
266
	"Prolog":                       "#74283c",
267
	"Promela":           "#de3900",
267
	"Promela":                      "#de3900",
268
	"Protocol Buffer":   "#000000",
268
	"Protocol Buffer":              "#000000",
269
	"Pug":               "#a86454",
269
	"Pug":                          "#a86454",
270
	"Puppet":            "#302B6D",
270
	"Puppet":                       "#302B6D",
271
	"PureBasic":         "#5a6986",
271
	"PureBasic":                    "#5a6986",
272
	"PureScript":        "#1D222D",
272
	"PureScript":                   "#1D222D",
273
	"Python":            "#3572A5",
273
	"Python":                       "#3572A5",
274
	"QMake":             "#000000",
274
	"QMake":                        "#000000",
275
	"QML":               "#44a51c",
275
	"QML":                          "#44a51c",
276
	"Qt Script":         "#00b0ff",
276
	"Qt Script":                    "#00b0ff",
277
	"Quake":             "#882303",
277
	"Quake":                        "#882303",
278
	"R":                 "#198CE7",
278
	"R":                            "#198CE7",
279
	"Racket":            "#3c5caa",
279
	"Racket":                       "#3c5caa",
280
	"Ragel":             "#9d5200",
280
	"Ragel":                        "#9d5200",
281
	"Raku":              "#0000fb",
281
	"Raku":                         "#0000fb",
282
	"RAML":              "#77d9fb",
282
	"RAML":                         "#77d9fb",
283
	"Razor":             "#512be4",
283
	"Razor":                        "#512be4",
284
	"Rebol":             "#358a5b",
284
	"Rebol":                        "#358a5b",
285
	"Red":               "#ee0000",
285
	"Red":                          "#ee0000",
286
	"Redcode":           "#000000",
286
	"Redcode":                      "#000000",
287
	"Ren'Py":            "#ff7f7f",
287
	"Ren'Py":                       "#ff7f7f",
288
	"RenderScript":      "#000000",
288
	"RenderScript":                 "#000000",
289
	"Rescript":          "#ed4e4e",
289
	"Rescript":                     "#ed4e4e",
290
	"REXX":              "#d90e09",
290
	"REXX":                         "#d90e09",
291
	"Ring":              "#2D54CB",
291
	"Ring":                         "#2D54CB",
292
	"Riot":              "#A71E22",
292
	"Riot":                         "#A71E22",
293
	"RMarkdown":         "#198ce7",
293
	"RMarkdown":                    "#198ce7",
294
	"RobotFramework":    "#00c0b5",
294
	"RobotFramework":               "#00c0b5",
295
	"Roff":              "#ecdebe",
295
	"Roff":                         "#ecdebe",
296
	"Rouge":             "#cc0000",
296
	"Rouge":                        "#cc0000",
297
	"Ruby":              "#701516",
297
	"Ruby":                         "#701516",
298
	"RUNOFF":            "#660000",
298
	"RUNOFF":                       "#660000",
299
	"Rust":              "#dea584",
299
	"Rust":                         "#dea584",
300
	"Sage":              "#000000",
300
	"Sage":                         "#000000",
301
	"SaltStack":         "#646464",
301
	"SaltStack":                    "#646464",
302
	"SAS":               "#B34936",
302
	"SAS":                          "#B34936",
303
	"Sass":              "#a53b70",
303
	"Sass":                         "#a53b70",
304
	"Scala":             "#c22d40",
304
	"Scala":                        "#c22d40",
305
	"Scaml":             "#bd181a",
305
	"Scaml":                        "#bd181a",
306
	"Scheme":            "#1e4aec",
306
	"Scheme":                       "#1e4aec",
307
	"Scilab":            "#ca0f21",
307
	"Scilab":                       "#ca0f21",
308
	"SCSS":              "#c6538c",
308
	"SCSS":                         "#c6538c",
309
	"sed":               "#64b970",
309
	"sed":                          "#64b970",
310
	"Self":              "#0579aa",
310
	"Self":                         "#0579aa",
311
	"ShaderLab":         "#222c37",
311
	"ShaderLab":                    "#222c37",
312
	"Shell":             "#89e051",
312
	"Shell":                        "#89e051",
313
	"Shen":              "#120F14",
313
	"Shen":                         "#120F14",
314
	"Sieve":             "#000000",
314
	"Sieve":                        "#000000",
315
	"Slash":             "#007eff",
315
	"Slash":                        "#007eff",
316
	"Slice":             "#003fa2",
316
	"Slice":                        "#003fa2",
317
	"Slim":              "#2b2b2b",
317
	"Slim":                         "#2b2b2b",
318
	"Smali":             "#000000",
318
	"Smali":                        "#000000",
319
	"Smalltalk":         "#596706",
319
	"Smalltalk":                    "#596706",
320
	"Smarty":            "#f0c040",
320
	"Smarty":                       "#f0c040",
321
	"Smithy":            "#c44536",
321
	"Smithy":                       "#c44536",
322
	"SmPL":              "#c92223",
322
	"SmPL":                         "#c92223",
323
	"Solidity":          "#AA6746",
323
	"Solidity":                     "#AA6746",
324
	"SourcePawn":        "#f69e1d",
324
	"SourcePawn":                   "#f69e1d",
325
	"SPARQL":            "#0C4597",
325
	"SPARQL":                       "#0C4597",
326
	"SQF":               "#3F3F3F",
326
	"SQF":                          "#3F3F3F",
327
	"SQL":               "#e38c00",
327
	"SQL":                          "#e38c00",
328
	"SQLPL":             "#e38c00",
328
	"SQLPL":                        "#e38c00",
329
	"Squirrel":          "#800000",
329
	"Squirrel":                     "#800000",
330
	"SRecode Template":  "#348a34",
330
	"SRecode Template":             "#348a34",
331
	"Stan":              "#b2011d",
331
	"Stan":                         "#b2011d",
332
	"Standard ML":       "#dc566d",
332
	"Standard ML":                  "#dc566d",
333
	"Starlark":          "#76d275",
333
	"Starlark":                     "#76d275",
334
	"Stata":             "#1a5f91",
334
	"Stata":                        "#1a5f91",
335
	"STL":               "#373b3e",
335
	"STL":                          "#373b3e",
336
	"Stylus":            "#ff6347",
336
	"Stylus":                       "#ff6347",
337
	"SuperCollider":     "#46390b",
337
	"SuperCollider":                "#46390b",
338
	"Svelte":            "#ff3e00",
338
	"Svelte":                       "#ff3e00",
339
	"SVG":               "#ff9900",
339
	"SVG":                          "#ff9900",
340
	"Swift":             "#F05138",
340
	"Swift":                        "#F05138",
341
	"SWIG":              "#000000",
341
	"SWIG":                         "#000000",
342
	"SystemVerilog":     "#DAE1C2",
342
	"SystemVerilog":                "#DAE1C2",
343
	"Tcl":               "#e4cc98",
343
	"Tcl":                          "#e4cc98",
344
	"Tcsh":              "#000000",
344
	"Tcsh":                         "#000000",
345
	"Terra":             "#000000",
345
	"Terra":                        "#000000",
346
	"TeX":               "#3D6117",
346
	"TeX":                          "#3D6117",
347
	"Thrift":            "#D88E35",
347
	"Thrift":                       "#D88E35",
348
	"TI Program":        "#A0AAAD",
348
	"TI Program":                   "#A0AAAD",
349
	"TLA":               "#4b0082",
349
	"TLA":                          "#4b0082",
350
	"TOML":              "#9c4221",
350
	"TOML":                         "#9c4221",
351
	"TSQL":              "#e38c00",
351
	"TSQL":                         "#e38c00",
352
	"TSX":               "#3178c6",
352
	"TSX":                          "#3178c6",
353
	"Turing":            "#cf142b",
353
	"Turing":                       "#cf142b",
354
	"Turtle":            "#EEFF11",
354
	"Turtle":                       "#EEFF11",
355
	"Twig":              "#c1d026",
355
	"Twig":                         "#c1d026",
356
	"TXL":               "#0178b8",
356
	"TXL":                          "#0178b8",
357
	"TypeScript":        "#3178c6",
357
	"TypeScript":                   "#3178c6",
358
	"Typst":             "#239dad",
358
	"Typst":                        "#239dad",
359
	"Unified Parallel C": "#4e3617",
359
	"Unified Parallel C":           "#4e3617",
360
	"Unity3D Asset":     "#222c37",
360
	"Unity3D Asset":                "#222c37",
361
	"Uno":               "#9933cc",
361
	"Uno":                          "#9933cc",
362
	"UnrealScript":      "#a54c4d",
362
	"UnrealScript":                 "#a54c4d",
363
	"UrWeb":             "#ccc",
363
	"UrWeb":                        "#ccc",
364
	"V":                 "#4f87c4",
364
	"V":                            "#4f87c4",
365
	"Vala":              "#fbe5cd",
365
	"Vala":                         "#fbe5cd",
366
	"Valve Data Format": "#f26025",
366
	"Valve Data Format":            "#f26025",
367
	"VBA":               "#867db1",
367
	"VBA":                          "#867db1",
368
	"VBScript":          "#15dcdc",
368
	"VBScript":                     "#15dcdc",
369
	"VCL":               "#148AA8",
369
	"VCL":                          "#148AA8",
370
	"Verilog":           "#b2b7f8",
370
	"Verilog":                      "#b2b7f8",
371
	"VHDL":              "#adb2cb",
371
	"VHDL":                         "#adb2cb",
372
	"Vim Help File":     "#199f4b",
372
	"Vim Help File":                "#199f4b",
373
	"Vim Script":        "#199f4b",
373
	"Vim Script":                   "#199f4b",
374
	"Visual Basic .NET": "#9400ff",
374
	"Visual Basic .NET":            "#9400ff",
375
	"Volt":              "#1F1F1F",
375
	"Volt":                         "#1F1F1F",
376
	"Vue":               "#41b883",
376
	"Vue":                          "#41b883",
377
	"Vyper":             "#2980b9",
377
	"Vyper":                        "#2980b9",
378
	"WDL":               "#42f1f4",
378
	"WDL":                          "#42f1f4",
379
	"WebAssembly":       "#04133b",
379
	"WebAssembly":                  "#04133b",
380
	"WebIDL":            "#000000",
380
	"WebIDL":                       "#000000",
381
	"Whiley":            "#d5c397",
381
	"Whiley":                       "#d5c397",
382
	"Wikitext":          "#fc5757",
382
	"Wikitext":                     "#fc5757",
383
	"Windows Registry Entries": "#52a5df",
383
	"Windows Registry Entries":     "#52a5df",
384
	"Witcher Script":    "#ff0000",
384
	"Witcher Script":               "#ff0000",
385
	"Wollok":            "#a23738",
385
	"Wollok":                       "#a23738",
386
	"World of Warcraft Addon Data": "#f7e43a",
386
	"World of Warcraft Addon Data": "#f7e43a",
387
	"Wren":              "#383838",
387
	"Wren":                         "#383838",
388
	"X10":               "#4B6BEF",
388
	"X10":                          "#4B6BEF",
389
	"xBase":             "#403a40",
389
	"xBase":                        "#403a40",
390
	"XC":                "#99FF33",
390
	"XC":                           "#99FF33",
391
	"XML":               "#0060ac",
391
	"XML":                          "#0060ac",
392
	"XML Property List": "#0060ac",
392
	"XML Property List":            "#0060ac",
393
	"Xojo":              "#81bd41",
393
	"Xojo":                         "#81bd41",
394
	"Xonsh":             "#285880",
394
	"Xonsh":                        "#285880",
395
	"XOTL":              "#000000",
395
	"XOTL":                         "#000000",
396
	"XQuery":            "#5232e7",
396
	"XQuery":                       "#5232e7",
397
	"XSLT":              "#EB8E35",
397
	"XSLT":                         "#EB8E35",
398
	"Xtend":             "#24255d",
398
	"Xtend":                        "#24255d",
399
	"Yacc":              "#4B6C4B",
399
	"Yacc":                         "#4B6C4B",
400
	"YAML":              "#cb171e",
400
	"YAML":                         "#cb171e",
401
	"YANG":              "#000000",
401
	"YANG":                         "#000000",
402
	"YARA":              "#220000",
402
	"YARA":                         "#220000",
403
	"YASnippet":         "#32AB90",
403
	"YASnippet":                    "#32AB90",
404
	"ZAP":               "#0d6616",
404
	"ZAP":                          "#0d6616",
405
	"Zeek":              "#000000",
405
	"Zeek":                         "#000000",
406
	"ZenScript":         "#00BCD1",
406
	"ZenScript":                    "#00BCD1",
407
	"Zephir":            "#118f9e",
407
	"Zephir":                       "#118f9e",
408
	"Zig":               "#ec915c",
408
	"Zig":                          "#ec915c",
409
	"ZIL":               "#dc75e5",
409
	"ZIL":                          "#dc75e5",
410
	"Zimpl":             "#d67711",
410
	"Zimpl":                        "#d67711",
411
	"Tree-sitter Query": "#9440ff",
411
	"Tree-sitter Query":            "#9440ff",
412
}
412
}
413
  
413
  
414
func getLanguageStats(repoName string, commitHash string, tree *object.Tree) ([]LanguageStat, error) {
414
func getLanguageStats(repoName string, commitHash string, tree *object.Tree) ([]LanguageStat, error) {
...
432
	resultsChan := make(chan result, numWorkers*2)
432
	resultsChan := make(chan result, numWorkers*2)
433
	var wg sync.WaitGroup
433
	var wg sync.WaitGroup
434
  
434
  
435
	// Workers
  
436
	for i := 0; i < numWorkers; i++ {
435
	for i := 0; i < numWorkers; i++ {
437
		wg.Add(1)
436
		wg.Add(1)
438
		go func() {
437
		go func() {
...
475
		}()
474
		}()
476
	}
475
	}
477
  
476
  
478
	// Collector
  
479
	languages := make(map[string]int64)
477
	languages := make(map[string]int64)
480
	var totalSize int64
478
	var totalSize int64
481
	done := make(chan struct{})
479
	done := make(chan struct{})
...
487
		close(done)
485
		close(done)
488
	}()
486
	}()
489
  
487
  
490
	// Producer
  
491
	err := tree.Files().ForEach(func(f *object.File) error {
488
	err := tree.Files().ForEach(func(f *object.File) error {
492
		filesChan <- f
489
		filesChan <- f
493
		return nil
490
		return nil
...
525
		currentOffset += stats[i].Percentage
522
		currentOffset += stats[i].Percentage
526
	}
523
	}
527
  
524
  
528
	// Store in cache
  
529
	langCache.Store(key, stats)
525
	langCache.Store(key, stats)
530
	NotifySave()
526
	NotifySave()
531
  
527
  
...
538
	if color, ok := languageColors[lang]; ok {
534
	if color, ok := languageColors[lang]; ok {
539
		return color
535
		return color
540
	}
536
	}
541
	return "#8b8b8b" // Default gray
537
	return "#8b8b8b"
542
}
538
}
diff --git a/mdalerts.go b/mdalerts.go
...
65
  
65
  
66
func (a *alertTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
66
func (a *alertTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
67
	source := reader.Source()
67
	source := reader.Source()
68
	
68
  
69
	type matchInfo struct {
69
	type matchInfo struct {
70
		container ast.Node
70
		container ast.Node
71
		para      *ast.Paragraph
71
		para      *ast.Paragraph
...
99
			return ast.WalkContinue, nil
99
			return ast.WalkContinue, nil
100
		}
100
		}
101
  
101
  
102
		// Check paragraph text directly using p.Text(source)
  
103
		pText := p.Text(source)
102
		pText := p.Text(source)
104
		raw := string(pText)
103
		raw := string(pText)
105
		trimmed := strings.TrimLeft(raw, " \t\n\r")
104
		trimmed := strings.TrimLeft(raw, " \t\n\r")
...
diff --git a/models.go b/models.go
1
package main
1
package main
2
  
2
  
3
import (
3
import (
4
	"time"
  
5
	"encoding/xml"
4
	"encoding/xml"
  
5
	"time"
6
  
6
  
7
	"github.com/go-git/go-git/v5"
7
	"github.com/go-git/go-git/v5"
8
	"github.com/go-git/go-git/v5/plumbing"
8
	"github.com/go-git/go-git/v5/plumbing"
...
diff --git a/static/style.css b/static/style.css
...
254
  
254
  
255
	.markdown-alert {
255
	.markdown-alert {
256
		padding: 0.75rem 1rem;
256
		padding: 0.75rem 1rem;
257
		margin-bottom: 1rem;
  
258
		color: inherit;
257
		color: inherit;
259
		border: 1px solid;
258
		border: 1px solid;
260
		border-left: 0.25em solid;
259
		border-left: 0.25em solid;
  
260
  
  
261
		margin-block-start: 1em;
  
262
		margin-block-end: 1em;
261
  
263
  
262
		.markdown-alert-title {
264
		.markdown-alert-title {
263
			display: flex;
265
			display: flex;
...