|
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/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 |
} |