Migrate to Git CLI
64886f479dbe875e8df76d496e67b9162e8ae41b
This is to support SHA256 repositories.
1 parent
ae82ac81
git.go
+46 -586
| 1 | 1 | package main |
|
| 2 | 2 | ||
| 3 | 3 | import ( |
|
| 4 | - | "fmt" |
|
| 5 | - | "io" |
|
| 6 | 4 | "sort" |
|
| 7 | 5 | "strings" |
|
| 8 | 6 | "time" |
|
| 9 | - | "unicode/utf8" |
|
| 10 | - | ||
| 11 | - | git "github.com/go-git/go-git/v5" |
|
| 12 | - | "github.com/go-git/go-git/v5/plumbing" |
|
| 13 | - | "github.com/go-git/go-git/v5/plumbing/object" |
|
| 14 | 7 | ) |
|
| 15 | 8 | ||
| 16 | 9 | type RepoInfo struct { |
|
| 17 | 10 | Name string |
|
| 18 | 11 | Path string |
|
| 12 | + | GitDir string |
|
| 19 | 13 | Description string |
|
| 20 | 14 | Owner string |
|
| 21 | 15 | LastUpdated time.Time |
|
| 22 | - | Repo *git.Repository |
|
| 16 | + | Git *gitCLIBackend |
|
| 23 | 17 | } |
|
| 24 | 18 | ||
| 25 | 19 | type CommitInfo struct { |
|
| 26 | 20 | Hash string |
|
| 27 | 21 | ShortHash string |
| 58 | 52 | Date time.Time |
|
| 59 | 53 | IsTag bool |
|
| 60 | 54 | } |
|
| 61 | 55 | ||
| 62 | 56 | type DiffFile struct { |
|
| 63 | - | OldName string |
|
| 64 | - | NewName string |
|
| 65 | - | Status string |
|
| 66 | - | Hunks []DiffHunk |
|
| 57 | + | OldName string |
|
| 58 | + | NewName string |
|
| 59 | + | Status string |
|
| 60 | + | Hunks []DiffHunk |
|
| 67 | 61 | IsBinary bool |
|
| 68 | - | Stats DiffStats |
|
| 62 | + | Stats DiffStats |
|
| 69 | 63 | } |
|
| 70 | 64 | ||
| 71 | 65 | type DiffStats struct { |
|
| 72 | 66 | Added int |
|
| 73 | 67 | Deleted int |
| 83 | 77 | Content string |
|
| 84 | 78 | OldNum int |
|
| 85 | 79 | NewNum int |
|
| 86 | 80 | } |
|
| 87 | 81 | ||
| 88 | - | const maxBlobDisplaySize = 1 << 20 // 1MB |
|
| 89 | - | const diffContextLines = 5 |
|
| 90 | - | ||
| 91 | - | func commitToInfo(c *object.Commit) CommitInfo { |
|
| 92 | - | subject, body, _ := strings.Cut(c.Message, "\n") |
|
| 93 | - | body = strings.TrimSpace(body) |
|
| 94 | - | parents := make([]string, len(c.ParentHashes)) |
|
| 95 | - | for i, p := range c.ParentHashes { |
|
| 96 | - | parents[i] = p.String() |
|
| 97 | - | } |
|
| 98 | - | return CommitInfo{ |
|
| 99 | - | Hash: c.Hash.String(), |
|
| 100 | - | ShortHash: c.Hash.String()[:8], |
|
| 101 | - | Subject: subject, |
|
| 102 | - | Body: body, |
|
| 103 | - | Author: c.Author.Name, |
|
| 104 | - | AuthorEmail: c.Author.Email, |
|
| 105 | - | AuthorDate: c.Author.When, |
|
| 106 | - | Parents: parents, |
|
| 107 | - | } |
|
| 108 | - | } |
|
| 109 | - | ||
| 110 | - | func resolveRef(repo *git.Repository, refStr string) (*plumbing.Hash, error) { |
|
| 111 | - | if refStr == "" { |
|
| 112 | - | head, err := repo.Head() |
|
| 113 | - | if err != nil { |
|
| 114 | - | return nil, err |
|
| 115 | - | } |
|
| 116 | - | h := head.Hash() |
|
| 117 | - | return &h, nil |
|
| 118 | - | } |
|
| 119 | - | h, err := repo.ResolveRevision(plumbing.Revision(refStr)) |
|
| 120 | - | if err != nil { |
|
| 121 | - | return nil, err |
|
| 122 | - | } |
|
| 123 | - | return h, nil |
|
| 124 | - | } |
|
| 125 | - | ||
| 126 | - | func resolveRefAndPath(repo *git.Repository, segments []string) (hash *plumbing.Hash, ref string, path string, err error) { |
|
| 127 | - | // Try progressively longer prefixes as ref names |
|
| 128 | - | for i := 1; i <= len(segments); i++ { |
|
| 129 | - | candidate := strings.Join(segments[:i], "/") |
|
| 130 | - | h, err := resolveRef(repo, candidate) |
|
| 131 | - | if err == nil { |
|
| 132 | - | rest := "" |
|
| 133 | - | if i < len(segments) { |
|
| 134 | - | rest = strings.Join(segments[i:], "/") |
|
| 135 | - | } |
|
| 136 | - | return h, candidate, rest, nil |
|
| 137 | - | } |
|
| 138 | - | } |
|
| 139 | - | return nil, "", "", fmt.Errorf("cannot resolve ref from path segments: %s", strings.Join(segments, "/")) |
|
| 140 | - | } |
|
| 141 | - | ||
| 142 | - | func getDefaultBranch(repo *git.Repository) string { |
|
| 143 | - | head, err := repo.Head() |
|
| 144 | - | if err != nil { |
|
| 145 | - | return "main" |
|
| 146 | - | } |
|
| 147 | - | name := head.Name() |
|
| 148 | - | if name.IsBranch() { |
|
| 149 | - | return name.Short() |
|
| 150 | - | } |
|
| 151 | - | return "main" |
|
| 152 | - | } |
|
| 153 | - | ||
| 154 | - | func getLog(repo *git.Repository, hash plumbing.Hash, page, perPage int) ([]CommitInfo, bool, error) { |
|
| 155 | - | iter, err := repo.Log(&git.LogOptions{From: hash}) |
|
| 156 | - | if err != nil { |
|
| 157 | - | return nil, false, err |
|
| 158 | - | } |
|
| 159 | - | defer iter.Close() |
|
| 160 | - | ||
| 161 | - | skip := page * perPage |
|
| 162 | - | for i := 0; i < skip; i++ { |
|
| 163 | - | if _, err := iter.Next(); err != nil { |
|
| 164 | - | return nil, false, nil |
|
| 165 | - | } |
|
| 166 | - | } |
|
| 167 | - | ||
| 168 | - | var commits []CommitInfo |
|
| 169 | - | for i := 0; i < perPage+1; i++ { |
|
| 170 | - | c, err := iter.Next() |
|
| 171 | - | if err == io.EOF { |
|
| 172 | - | break |
|
| 173 | - | } |
|
| 174 | - | if err != nil { |
|
| 175 | - | return nil, false, err |
|
| 176 | - | } |
|
| 177 | - | if i == perPage { |
|
| 178 | - | return commits, true, nil |
|
| 179 | - | } |
|
| 180 | - | commits = append(commits, commitToInfo(c)) |
|
| 181 | - | } |
|
| 182 | - | return commits, false, nil |
|
| 82 | + | type TreeNode struct { |
|
| 83 | + | Name string |
|
| 84 | + | IsDir bool |
|
| 85 | + | Size int64 |
|
| 86 | + | Path string |
|
| 87 | + | Depth int |
|
| 88 | + | IsOpen bool |
|
| 89 | + | IsActive bool |
|
| 183 | 90 | } |
|
| 184 | 91 | ||
| 185 | - | func getTree(repo *git.Repository, hash plumbing.Hash, path string) ([]TreeEntryInfo, error) { |
|
| 186 | - | commit, err := repo.CommitObject(hash) |
|
| 187 | - | if err != nil { |
|
| 188 | - | return nil, err |
|
| 189 | - | } |
|
| 190 | - | tree, err := commit.Tree() |
|
| 191 | - | if err != nil { |
|
| 192 | - | return nil, err |
|
| 193 | - | } |
|
| 194 | - | ||
| 195 | - | if path != "" { |
|
| 196 | - | tree, err = tree.Tree(path) |
|
| 197 | - | if err != nil { |
|
| 198 | - | return nil, err |
|
| 199 | - | } |
|
| 200 | - | } |
|
| 201 | - | ||
| 202 | - | var entries []TreeEntryInfo |
|
| 203 | - | for _, e := range tree.Entries { |
|
| 204 | - | isDir := !e.Mode.IsFile() |
|
| 205 | - | var size int64 |
|
| 206 | - | if !isDir { |
|
| 207 | - | f, err := tree.TreeEntryFile(&e) |
|
| 208 | - | if err == nil { |
|
| 209 | - | size = f.Size |
|
| 210 | - | } |
|
| 211 | - | } |
|
| 212 | - | ||
| 213 | - | mode := fmt.Sprintf("%06o", uint32(e.Mode)) |
|
| 214 | - | ||
| 215 | - | entries = append(entries, TreeEntryInfo{ |
|
| 216 | - | Name: e.Name, |
|
| 217 | - | IsDir: isDir, |
|
| 218 | - | Size: size, |
|
| 219 | - | Mode: mode, |
|
| 220 | - | Hash: e.Hash.String()[:8], |
|
| 221 | - | }) |
|
| 222 | - | } |
|
| 92 | + | const maxBlobDisplaySize = 1 << 20 // 1MB |
|
| 93 | + | const diffContextLines = 5 |
|
| 223 | 94 | ||
| 95 | + | func sortTreeEntries(entries []TreeEntryInfo) { |
|
| 224 | 96 | sort.Slice(entries, func(i, j int) bool { |
|
| 225 | 97 | if entries[i].IsDir != entries[j].IsDir { |
|
| 226 | 98 | return entries[i].IsDir |
|
| 227 | 99 | } |
|
| 228 | 100 | return entries[i].Name < entries[j].Name |
|
| 229 | 101 | }) |
|
| 230 | - | ||
| 231 | - | return entries, nil |
|
| 232 | - | } |
|
| 233 | - | ||
| 234 | - | func getBlob(repo *git.Repository, hash plumbing.Hash, path string) (*BlobInfo, error) { |
|
| 235 | - | commit, err := repo.CommitObject(hash) |
|
| 236 | - | if err != nil { |
|
| 237 | - | return nil, err |
|
| 238 | - | } |
|
| 239 | - | tree, err := commit.Tree() |
|
| 240 | - | if err != nil { |
|
| 241 | - | return nil, err |
|
| 242 | - | } |
|
| 243 | - | f, err := tree.File(path) |
|
| 244 | - | if err != nil { |
|
| 245 | - | return nil, err |
|
| 246 | - | } |
|
| 247 | - | ||
| 248 | - | info := &BlobInfo{ |
|
| 249 | - | Name: f.Name, |
|
| 250 | - | Size: f.Size, |
|
| 251 | - | } |
|
| 252 | - | ||
| 253 | - | isBin, err := f.IsBinary() |
|
| 254 | - | if err != nil { |
|
| 255 | - | return nil, err |
|
| 256 | - | } |
|
| 257 | - | info.IsBinary = isBin |
|
| 258 | - | ||
| 259 | - | if isBin || f.Size > maxBlobDisplaySize { |
|
| 260 | - | info.IsBinary = true |
|
| 261 | - | return info, nil |
|
| 262 | - | } |
|
| 263 | - | ||
| 264 | - | content, err := f.Contents() |
|
| 265 | - | if err != nil { |
|
| 266 | - | return nil, err |
|
| 267 | - | } |
|
| 268 | - | ||
| 269 | - | if !utf8.ValidString(content) { |
|
| 270 | - | info.IsBinary = true |
|
| 271 | - | return info, nil |
|
| 272 | - | } |
|
| 273 | - | ||
| 274 | - | info.Content = content |
|
| 275 | - | info.Lines = strings.Split(content, "\n") |
|
| 276 | - | // Remove trailing empty line from final newline |
|
| 277 | - | if len(info.Lines) > 0 && info.Lines[len(info.Lines)-1] == "" { |
|
| 278 | - | info.Lines = info.Lines[:len(info.Lines)-1] |
|
| 279 | - | } |
|
| 280 | - | ||
| 281 | - | return info, nil |
|
| 282 | - | } |
|
| 283 | - | ||
| 284 | - | func getRawBlob(repo *git.Repository, hash plumbing.Hash, path string) (io.ReadCloser, string, int64, error) { |
|
| 285 | - | commit, err := repo.CommitObject(hash) |
|
| 286 | - | if err != nil { |
|
| 287 | - | return nil, "", 0, err |
|
| 288 | - | } |
|
| 289 | - | tree, err := commit.Tree() |
|
| 290 | - | if err != nil { |
|
| 291 | - | return nil, "", 0, err |
|
| 292 | - | } |
|
| 293 | - | f, err := tree.File(path) |
|
| 294 | - | if err != nil { |
|
| 295 | - | return nil, "", 0, err |
|
| 296 | - | } |
|
| 297 | - | ||
| 298 | - | reader, err := f.Reader() |
|
| 299 | - | if err != nil { |
|
| 300 | - | return nil, "", 0, err |
|
| 301 | - | } |
|
| 302 | - | ||
| 303 | - | // Detect content type from name |
|
| 304 | - | ct := "application/octet-stream" |
|
| 305 | - | if mtype := mimeFromPath(path); mtype != "" { |
|
| 306 | - | ct = mtype |
|
| 307 | - | } |
|
| 308 | - | ||
| 309 | - | return reader, ct, f.Size, nil |
|
| 310 | - | } |
|
| 311 | - | ||
| 312 | - | var mimeTypes = map[string]string{ |
|
| 313 | - | ".txt": "text/plain; charset=utf-8", |
|
| 314 | - | ".md": "text/plain; charset=utf-8", |
|
| 315 | - | ".go": "text/plain; charset=utf-8", |
|
| 316 | - | ".rs": "text/plain; charset=utf-8", |
|
| 317 | - | ".py": "text/plain; charset=utf-8", |
|
| 318 | - | ".js": "text/plain; charset=utf-8", |
|
| 319 | - | ".ts": "text/plain; charset=utf-8", |
|
| 320 | - | ".c": "text/plain; charset=utf-8", |
|
| 321 | - | ".h": "text/plain; charset=utf-8", |
|
| 322 | - | ".css": "text/css; charset=utf-8", |
|
| 323 | - | ".html": "text/html; charset=utf-8", |
|
| 324 | - | ".json": "application/json", |
|
| 325 | - | ".xml": "application/xml", |
|
| 326 | - | ".png": "image/png", |
|
| 327 | - | ".jpg": "image/jpeg", |
|
| 328 | - | ".jpeg": "image/jpeg", |
|
| 329 | - | ".gif": "image/gif", |
|
| 330 | - | ".svg": "image/svg+xml", |
|
| 331 | - | ".pdf": "application/pdf", |
|
| 332 | - | } |
|
| 333 | - | ||
| 334 | - | func mimeFromPath(path string) string { |
|
| 335 | - | // Use a small set of common types; let the browser figure out the rest |
|
| 336 | - | idx := strings.LastIndex(path, ".") |
|
| 337 | - | if idx < 0 { |
|
| 338 | - | return "" |
|
| 339 | - | } |
|
| 340 | - | ext := strings.ToLower(path[idx:]) |
|
| 341 | - | if t, ok := mimeTypes[ext]; ok { |
|
| 342 | - | return t |
|
| 343 | - | } |
|
| 344 | - | return "" |
|
| 345 | - | } |
|
| 346 | - | ||
| 347 | - | func getCommit(repo *git.Repository, hash plumbing.Hash) (*CommitInfo, error) { |
|
| 348 | - | c, err := repo.CommitObject(hash) |
|
| 349 | - | if err != nil { |
|
| 350 | - | return nil, err |
|
| 351 | - | } |
|
| 352 | - | info := commitToInfo(c) |
|
| 353 | - | return &info, nil |
|
| 354 | - | } |
|
| 355 | - | ||
| 356 | - | func getDiff(repo *git.Repository, hash plumbing.Hash) ([]DiffFile, error) { |
|
| 357 | - | commit, err := repo.CommitObject(hash) |
|
| 358 | - | if err != nil { |
|
| 359 | - | return nil, err |
|
| 360 | - | } |
|
| 361 | - | ||
| 362 | - | var parentTree *object.Tree |
|
| 363 | - | if commit.NumParents() > 0 { |
|
| 364 | - | parent, err := commit.Parent(0) |
|
| 365 | - | if err != nil { |
|
| 366 | - | return nil, err |
|
| 367 | - | } |
|
| 368 | - | parentTree, err = parent.Tree() |
|
| 369 | - | if err != nil { |
|
| 370 | - | return nil, err |
|
| 371 | - | } |
|
| 372 | - | } |
|
| 373 | - | ||
| 374 | - | commitTree, err := commit.Tree() |
|
| 375 | - | if err != nil { |
|
| 376 | - | return nil, err |
|
| 377 | - | } |
|
| 378 | - | ||
| 379 | - | if parentTree == nil { |
|
| 380 | - | // Initial commit: diff against empty tree |
|
| 381 | - | changes, err := object.DiffTree(nil, commitTree) |
|
| 382 | - | if err != nil { |
|
| 383 | - | return nil, err |
|
| 384 | - | } |
|
| 385 | - | return changesToDiffFiles(changes) |
|
| 386 | - | } |
|
| 387 | - | ||
| 388 | - | changes, err := object.DiffTree(parentTree, commitTree) |
|
| 389 | - | if err != nil { |
|
| 390 | - | return nil, err |
|
| 391 | - | } |
|
| 392 | - | return changesToDiffFiles(changes) |
|
| 393 | - | } |
|
| 394 | - | ||
| 395 | - | func changesToDiffFiles(changes object.Changes) ([]DiffFile, error) { |
|
| 396 | - | patch, err := changes.Patch() |
|
| 397 | - | if err != nil { |
|
| 398 | - | return nil, err |
|
| 399 | - | } |
|
| 400 | - | ||
| 401 | - | var files []DiffFile |
|
| 402 | - | for _, fp := range patch.FilePatches() { |
|
| 403 | - | from, to := fp.Files() |
|
| 404 | - | df := DiffFile{ |
|
| 405 | - | IsBinary: fp.IsBinary(), |
|
| 406 | - | } |
|
| 407 | - | ||
| 408 | - | if from != nil { |
|
| 409 | - | df.OldName = from.Path() |
|
| 410 | - | } |
|
| 411 | - | if to != nil { |
|
| 412 | - | df.NewName = to.Path() |
|
| 413 | - | } |
|
| 414 | - | ||
| 415 | - | switch { |
|
| 416 | - | case from == nil && to != nil: |
|
| 417 | - | df.Status = "A" |
|
| 418 | - | case from != nil && to == nil: |
|
| 419 | - | df.Status = "D" |
|
| 420 | - | case from != nil && to != nil && from.Path() != to.Path(): |
|
| 421 | - | df.Status = "R" |
|
| 422 | - | default: |
|
| 423 | - | df.Status = "M" |
|
| 424 | - | } |
|
| 425 | - | ||
| 426 | - | if !fp.IsBinary() { |
|
| 427 | - | oldNum, newNum := 1, 1 |
|
| 428 | - | for _, chunk := range fp.Chunks() { |
|
| 429 | - | lines := strings.Split(chunk.Content(), "\n") |
|
| 430 | - | // Remove trailing empty string from split |
|
| 431 | - | if len(lines) > 0 && lines[len(lines)-1] == "" { |
|
| 432 | - | lines = lines[:len(lines)-1] |
|
| 433 | - | } |
|
| 434 | - | ||
| 435 | - | for _, line := range lines { |
|
| 436 | - | if len(df.Hunks) == 0 { |
|
| 437 | - | df.Hunks = append(df.Hunks, DiffHunk{}) |
|
| 438 | - | } |
|
| 439 | - | last := &df.Hunks[len(df.Hunks)-1] |
|
| 440 | - | ||
| 441 | - | switch chunk.Type() { |
|
| 442 | - | case 0: // Equal |
|
| 443 | - | last.Lines = append(last.Lines, DiffLine{Type: "context", Content: line, OldNum: oldNum, NewNum: newNum}) |
|
| 444 | - | oldNum++ |
|
| 445 | - | newNum++ |
|
| 446 | - | case 1: // Add |
|
| 447 | - | df.Stats.Added++ |
|
| 448 | - | last.Lines = append(last.Lines, DiffLine{Type: "add", Content: line, NewNum: newNum}) |
|
| 449 | - | newNum++ |
|
| 450 | - | case 2: // Delete |
|
| 451 | - | df.Stats.Deleted++ |
|
| 452 | - | last.Lines = append(last.Lines, DiffLine{Type: "del", Content: line, OldNum: oldNum}) |
|
| 453 | - | oldNum++ |
|
| 454 | - | } |
|
| 455 | - | } |
|
| 456 | - | } |
|
| 457 | - | } |
|
| 458 | - | ||
| 459 | - | // Collapse context lines, keeping only diffContextLines around changes. |
|
| 460 | - | if len(df.Hunks) == 1 { |
|
| 461 | - | df.Hunks = collapseContext(df.Hunks[0].Lines) |
|
| 462 | - | } |
|
| 463 | - | ||
| 464 | - | files = append(files, df) |
|
| 465 | - | } |
|
| 466 | - | return files, nil |
|
| 467 | 102 | } |
|
| 468 | 103 | ||
| 469 | 104 | // collapseContext takes a flat list of diff lines and splits them into |
|
| 470 | 105 | // hunks, keeping only diffContextLines of context around add/del lines. |
|
| 471 | 106 | func collapseContext(lines []DiffLine) []DiffHunk { |
| 512 | 147 | } |
|
| 513 | 148 | ||
| 514 | 149 | return hunks |
|
| 515 | 150 | } |
|
| 516 | 151 | ||
| 517 | - | func getBranches(repo *git.Repository) ([]RefInfo, error) { |
|
| 518 | - | iter, err := repo.Branches() |
|
| 519 | - | if err != nil { |
|
| 520 | - | return nil, err |
|
| 521 | - | } |
|
| 522 | - | defer iter.Close() |
|
| 523 | - | ||
| 524 | - | var refs []RefInfo |
|
| 525 | - | err = iter.ForEach(func(ref *plumbing.Reference) error { |
|
| 526 | - | c, err := repo.CommitObject(ref.Hash()) |
|
| 527 | - | if err != nil { |
|
| 528 | - | return nil |
|
| 529 | - | } |
|
| 530 | - | refs = append(refs, RefInfo{ |
|
| 531 | - | Name: ref.Name().Short(), |
|
| 532 | - | Hash: ref.Hash().String(), |
|
| 533 | - | ShortHash: ref.Hash().String()[:8], |
|
| 534 | - | Subject: firstLine(c.Message), |
|
| 535 | - | Author: c.Author.Name, |
|
| 536 | - | Date: c.Author.When, |
|
| 537 | - | }) |
|
| 538 | - | return nil |
|
| 539 | - | }) |
|
| 540 | - | ||
| 541 | - | sort.Slice(refs, func(i, j int) bool { |
|
| 542 | - | return refs[i].Date.After(refs[j].Date) |
|
| 543 | - | }) |
|
| 544 | - | ||
| 545 | - | return refs, err |
|
| 546 | - | } |
|
| 547 | - | ||
| 548 | - | func getTags(repo *git.Repository) ([]RefInfo, error) { |
|
| 549 | - | iter, err := repo.Tags() |
|
| 550 | - | if err != nil { |
|
| 551 | - | return nil, err |
|
| 552 | - | } |
|
| 553 | - | defer iter.Close() |
|
| 554 | - | ||
| 555 | - | var refs []RefInfo |
|
| 556 | - | err = iter.ForEach(func(ref *plumbing.Reference) error { |
|
| 557 | - | hash := ref.Hash() |
|
| 558 | - | ||
| 559 | - | // Try to peel annotated tag |
|
| 560 | - | tagObj, err := repo.TagObject(hash) |
|
| 561 | - | if err == nil { |
|
| 562 | - | commit, err := tagObj.Commit() |
|
| 563 | - | if err == nil { |
|
| 564 | - | refs = append(refs, RefInfo{ |
|
| 565 | - | Name: ref.Name().Short(), |
|
| 566 | - | Hash: commit.Hash.String(), |
|
| 567 | - | ShortHash: commit.Hash.String()[:8], |
|
| 568 | - | Subject: firstLine(commit.Message), |
|
| 569 | - | Author: commit.Author.Name, |
|
| 570 | - | Date: commit.Author.When, |
|
| 571 | - | IsTag: true, |
|
| 572 | - | }) |
|
| 573 | - | return nil |
|
| 574 | - | } |
|
| 575 | - | } |
|
| 576 | - | ||
| 577 | - | // Lightweight tag - try as commit directly |
|
| 578 | - | c, err := repo.CommitObject(hash) |
|
| 579 | - | if err != nil { |
|
| 580 | - | return nil |
|
| 581 | - | } |
|
| 582 | - | refs = append(refs, RefInfo{ |
|
| 583 | - | Name: ref.Name().Short(), |
|
| 584 | - | Hash: hash.String(), |
|
| 585 | - | ShortHash: hash.String()[:8], |
|
| 586 | - | Subject: firstLine(c.Message), |
|
| 587 | - | Author: c.Author.Name, |
|
| 588 | - | Date: c.Author.When, |
|
| 589 | - | IsTag: true, |
|
| 590 | - | }) |
|
| 591 | - | return nil |
|
| 592 | - | }) |
|
| 593 | - | ||
| 594 | - | sort.Slice(refs, func(i, j int) bool { |
|
| 595 | - | return refs[i].Date.After(refs[j].Date) |
|
| 596 | - | }) |
|
| 597 | - | ||
| 598 | - | return refs, err |
|
| 599 | - | } |
|
| 600 | - | ||
| 601 | - | // isTreePath checks if a path in a commit is a directory |
|
| 602 | - | func isTreePath(repo *git.Repository, hash plumbing.Hash, path string) bool { |
|
| 603 | - | commit, err := repo.CommitObject(hash) |
|
| 604 | - | if err != nil { |
|
| 605 | - | return false |
|
| 606 | - | } |
|
| 607 | - | tree, err := commit.Tree() |
|
| 608 | - | if err != nil { |
|
| 609 | - | return false |
|
| 610 | - | } |
|
| 611 | - | if path == "" { |
|
| 612 | - | return true |
|
| 613 | - | } |
|
| 614 | - | _, err = tree.Tree(path) |
|
| 615 | - | return err == nil |
|
| 616 | - | } |
|
| 617 | - | ||
| 618 | - | type TreeNode struct { |
|
| 619 | - | Name string |
|
| 620 | - | IsDir bool |
|
| 621 | - | Size int64 |
|
| 622 | - | Path string |
|
| 623 | - | Depth int |
|
| 624 | - | IsOpen bool |
|
| 625 | - | IsActive bool |
|
| 626 | - | } |
|
| 627 | - | ||
| 628 | - | func buildTreeNodes(repo *git.Repository, hash plumbing.Hash, activePath string) []TreeNode { |
|
| 629 | - | var activeComponents []string |
|
| 630 | - | if activePath != "" { |
|
| 631 | - | activeComponents = strings.Split(activePath, "/") |
|
| 632 | - | } |
|
| 633 | - | return buildTreeLevel(repo, hash, "", 0, activeComponents) |
|
| 634 | - | } |
|
| 635 | - | ||
| 636 | - | func buildTreeLevel(repo *git.Repository, hash plumbing.Hash, basePath string, depth int, activeComponents []string) []TreeNode { |
|
| 637 | - | entries, err := getTree(repo, hash, basePath) |
|
| 638 | - | if err != nil { |
|
| 639 | - | return nil |
|
| 640 | - | } |
|
| 641 | - | ||
| 642 | - | var nodes []TreeNode |
|
| 643 | - | for _, e := range entries { |
|
| 644 | - | fullPath := e.Name |
|
| 645 | - | if basePath != "" { |
|
| 646 | - | fullPath = basePath + "/" + e.Name |
|
| 647 | - | } |
|
| 648 | - | ||
| 649 | - | isOpen := false |
|
| 650 | - | isActive := false |
|
| 651 | - | ||
| 652 | - | if len(activeComponents) > 0 && e.Name == activeComponents[0] { |
|
| 653 | - | if e.IsDir { |
|
| 654 | - | isOpen = true |
|
| 655 | - | if len(activeComponents) == 1 { |
|
| 656 | - | isActive = true |
|
| 657 | - | } |
|
| 658 | - | } else if len(activeComponents) == 1 { |
|
| 659 | - | isActive = true |
|
| 660 | - | } |
|
| 661 | - | } |
|
| 662 | - | ||
| 663 | - | nodes = append(nodes, TreeNode{ |
|
| 664 | - | Name: e.Name, |
|
| 665 | - | IsDir: e.IsDir, |
|
| 666 | - | Size: e.Size, |
|
| 667 | - | Path: fullPath, |
|
| 668 | - | Depth: depth, |
|
| 669 | - | IsOpen: isOpen, |
|
| 670 | - | IsActive: isActive, |
|
| 671 | - | }) |
|
| 672 | - | ||
| 673 | - | if isOpen { |
|
| 674 | - | var childActive []string |
|
| 675 | - | if len(activeComponents) > 1 { |
|
| 676 | - | childActive = activeComponents[1:] |
|
| 677 | - | } |
|
| 678 | - | children := buildTreeLevel(repo, hash, fullPath, depth+1, childActive) |
|
| 679 | - | nodes = append(nodes, children...) |
|
| 680 | - | } |
|
| 681 | - | } |
|
| 682 | - | ||
| 683 | - | return nodes |
|
| 152 | + | var mimeTypes = map[string]string{ |
|
| 153 | + | ".txt": "text/plain; charset=utf-8", |
|
| 154 | + | ".md": "text/plain; charset=utf-8", |
|
| 155 | + | ".go": "text/plain; charset=utf-8", |
|
| 156 | + | ".rs": "text/plain; charset=utf-8", |
|
| 157 | + | ".py": "text/plain; charset=utf-8", |
|
| 158 | + | ".js": "text/plain; charset=utf-8", |
|
| 159 | + | ".ts": "text/plain; charset=utf-8", |
|
| 160 | + | ".c": "text/plain; charset=utf-8", |
|
| 161 | + | ".h": "text/plain; charset=utf-8", |
|
| 162 | + | ".css": "text/css; charset=utf-8", |
|
| 163 | + | ".html": "text/html; charset=utf-8", |
|
| 164 | + | ".json": "application/json", |
|
| 165 | + | ".xml": "application/xml", |
|
| 166 | + | ".png": "image/png", |
|
| 167 | + | ".jpg": "image/jpeg", |
|
| 168 | + | ".jpeg": "image/jpeg", |
|
| 169 | + | ".gif": "image/gif", |
|
| 170 | + | ".svg": "image/svg+xml", |
|
| 171 | + | ".pdf": "application/pdf", |
|
| 684 | 172 | } |
|
| 685 | 173 | ||
| 686 | - | func getReadme(repo *git.Repository, hash plumbing.Hash, dir string) *BlobInfo { |
|
| 687 | - | commit, err := repo.CommitObject(hash) |
|
| 688 | - | if err != nil { |
|
| 689 | - | return nil |
|
| 690 | - | } |
|
| 691 | - | root, err := commit.Tree() |
|
| 692 | - | if err != nil { |
|
| 693 | - | return nil |
|
| 694 | - | } |
|
| 695 | - | tree := root |
|
| 696 | - | if dir != "" { |
|
| 697 | - | tree, err = root.Tree(dir) |
|
| 698 | - | if err != nil { |
|
| 699 | - | return nil |
|
| 700 | - | } |
|
| 701 | - | } |
|
| 702 | - | for _, entry := range tree.Entries { |
|
| 703 | - | lower := strings.ToLower(entry.Name) |
|
| 704 | - | if lower == "readme" || lower == "readme.md" || lower == "readme.txt" { |
|
| 705 | - | path := entry.Name |
|
| 706 | - | if dir != "" { |
|
| 707 | - | path = dir + "/" + entry.Name |
|
| 708 | - | } |
|
| 709 | - | blob, err := getBlob(repo, hash, path) |
|
| 710 | - | if err != nil { |
|
| 711 | - | return nil |
|
| 712 | - | } |
|
| 713 | - | return blob |
|
| 714 | - | } |
|
| 174 | + | func mimeFromPath(path string) string { |
|
| 175 | + | idx := strings.LastIndex(path, ".") |
|
| 176 | + | if idx < 0 { |
|
| 177 | + | return "" |
|
| 715 | 178 | } |
|
| 716 | - | return nil |
|
| 717 | - | } |
|
| 718 | - | ||
| 719 | - | func firstLine(s string) string { |
|
| 720 | - | if i := strings.IndexByte(s, '\n'); i >= 0 { |
|
| 721 | - | return s[:i] |
|
| 179 | + | ext := strings.ToLower(path[idx:]) |
|
| 180 | + | if t, ok := mimeTypes[ext]; ok { |
|
| 181 | + | return t |
|
| 722 | 182 | } |
|
| 723 | - | return s |
|
| 183 | + | return "" |
|
| 724 | 184 | } |
git_cli.go
added
+331 -0
| 1 | + | package main |
|
| 2 | + | ||
| 3 | + | import ( |
|
| 4 | + | "bufio" |
|
| 5 | + | "bytes" |
|
| 6 | + | "fmt" |
|
| 7 | + | "io" |
|
| 8 | + | "os/exec" |
|
| 9 | + | "strconv" |
|
| 10 | + | "strings" |
|
| 11 | + | "time" |
|
| 12 | + | "unicode/utf8" |
|
| 13 | + | ) |
|
| 14 | + | ||
| 15 | + | // gitCLIBackend implements git operations by shelling out to the git CLI. |
|
| 16 | + | // Used for SHA256 repos which go-git cannot handle. |
|
| 17 | + | type gitCLIBackend struct { |
|
| 18 | + | gitDir string |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | func newGitCLIBackend(gitDir string) *gitCLIBackend { |
|
| 22 | + | return &gitCLIBackend{gitDir: gitDir} |
|
| 23 | + | } |
|
| 24 | + | ||
| 25 | + | func (g *gitCLIBackend) cmd(args ...string) (string, error) { |
|
| 26 | + | cmd := exec.Command("git", append([]string{"--git-dir", g.gitDir}, args...)...) |
|
| 27 | + | var stdout, stderr bytes.Buffer |
|
| 28 | + | cmd.Stdout = &stdout |
|
| 29 | + | cmd.Stderr = &stderr |
|
| 30 | + | if err := cmd.Run(); err != nil { |
|
| 31 | + | return "", fmt.Errorf("git %s: %s: %w", strings.Join(args, " "), stderr.String(), err) |
|
| 32 | + | } |
|
| 33 | + | return stdout.String(), nil |
|
| 34 | + | } |
|
| 35 | + | ||
| 36 | + | func (g *gitCLIBackend) pipe(args ...string) (io.ReadCloser, func() error, error) { |
|
| 37 | + | cmd := exec.Command("git", append([]string{"--git-dir", g.gitDir}, args...)...) |
|
| 38 | + | stdout, err := cmd.StdoutPipe() |
|
| 39 | + | if err != nil { |
|
| 40 | + | return nil, nil, err |
|
| 41 | + | } |
|
| 42 | + | if err := cmd.Start(); err != nil { |
|
| 43 | + | return nil, nil, err |
|
| 44 | + | } |
|
| 45 | + | return stdout, cmd.Wait, nil |
|
| 46 | + | } |
|
| 47 | + | ||
| 48 | + | func (g *gitCLIBackend) resolveRef(refStr string) (string, error) { |
|
| 49 | + | if refStr == "" { |
|
| 50 | + | refStr = "HEAD" |
|
| 51 | + | } |
|
| 52 | + | out, err := g.cmd("rev-parse", refStr) |
|
| 53 | + | if err != nil { |
|
| 54 | + | return "", err |
|
| 55 | + | } |
|
| 56 | + | return strings.TrimSpace(out), nil |
|
| 57 | + | } |
|
| 58 | + | ||
| 59 | + | func (g *gitCLIBackend) resolveRefAndPath(segments []string) (hash string, ref string, path string, err error) { |
|
| 60 | + | for i := 1; i <= len(segments); i++ { |
|
| 61 | + | candidate := strings.Join(segments[:i], "/") |
|
| 62 | + | h, err := g.resolveRef(candidate) |
|
| 63 | + | if err == nil { |
|
| 64 | + | rest := "" |
|
| 65 | + | if i < len(segments) { |
|
| 66 | + | rest = strings.Join(segments[i:], "/") |
|
| 67 | + | } |
|
| 68 | + | return h, candidate, rest, nil |
|
| 69 | + | } |
|
| 70 | + | } |
|
| 71 | + | return "", "", "", fmt.Errorf("cannot resolve ref from path segments: %s", strings.Join(segments, "/")) |
|
| 72 | + | } |
|
| 73 | + | ||
| 74 | + | func (g *gitCLIBackend) getDefaultBranch() string { |
|
| 75 | + | out, err := g.cmd("symbolic-ref", "--short", "HEAD") |
|
| 76 | + | if err != nil { |
|
| 77 | + | return "main" |
|
| 78 | + | } |
|
| 79 | + | branch := strings.TrimSpace(out) |
|
| 80 | + | ||
| 81 | + | // Verify the branch actually exists; HEAD may point to a non-existent branch |
|
| 82 | + | // (e.g. HEAD -> master but only a "main" branch exists). |
|
| 83 | + | if _, err := g.resolveRef(branch); err == nil { |
|
| 84 | + | return branch |
|
| 85 | + | } |
|
| 86 | + | ||
| 87 | + | // Fall back to the first available branch. |
|
| 88 | + | branches, err := g.getBranches() |
|
| 89 | + | if err == nil && len(branches) > 0 { |
|
| 90 | + | return branches[0].Name |
|
| 91 | + | } |
|
| 92 | + | return branch |
|
| 93 | + | } |
|
| 94 | + | ||
| 95 | + | func (g *gitCLIBackend) getLog(hash string, page, perPage int) ([]CommitInfo, bool, error) { |
|
| 96 | + | skip := page * perPage |
|
| 97 | + | out, err := g.cmd("log", "--format=%H%n%h%n%an%n%ae%n%aI%n%s%n%b%x00", |
|
| 98 | + | "--skip", strconv.Itoa(skip), "-n", strconv.Itoa(perPage+1), hash) |
|
| 99 | + | if err != nil { |
|
| 100 | + | return nil, false, err |
|
| 101 | + | } |
|
| 102 | + | ||
| 103 | + | entries := strings.Split(out, "\x00") |
|
| 104 | + | var commits []CommitInfo |
|
| 105 | + | for _, entry := range entries { |
|
| 106 | + | entry = strings.TrimSpace(entry) |
|
| 107 | + | if entry == "" { |
|
| 108 | + | continue |
|
| 109 | + | } |
|
| 110 | + | lines := strings.SplitN(entry, "\n", 7) |
|
| 111 | + | if len(lines) < 6 { |
|
| 112 | + | continue |
|
| 113 | + | } |
|
| 114 | + | t, _ := time.Parse(time.RFC3339, strings.TrimSpace(lines[4])) |
|
| 115 | + | body := "" |
|
| 116 | + | if len(lines) > 6 { |
|
| 117 | + | body = strings.TrimSpace(lines[6]) |
|
| 118 | + | } |
|
| 119 | + | commits = append(commits, CommitInfo{ |
|
| 120 | + | Hash: strings.TrimSpace(lines[0]), |
|
| 121 | + | ShortHash: strings.TrimSpace(lines[1]), |
|
| 122 | + | Subject: strings.TrimSpace(lines[5]), |
|
| 123 | + | Body: body, |
|
| 124 | + | Author: strings.TrimSpace(lines[2]), |
|
| 125 | + | AuthorEmail: strings.TrimSpace(lines[3]), |
|
| 126 | + | AuthorDate: t, |
|
| 127 | + | }) |
|
| 128 | + | } |
|
| 129 | + | ||
| 130 | + | if len(commits) > perPage { |
|
| 131 | + | return commits[:perPage], true, nil |
|
| 132 | + | } |
|
| 133 | + | return commits, false, nil |
|
| 134 | + | } |
|
| 135 | + | ||
| 136 | + | func (g *gitCLIBackend) getTree(hash string, path string) ([]TreeEntryInfo, error) { |
|
| 137 | + | treeish := hash |
|
| 138 | + | if path != "" { |
|
| 139 | + | treeish = hash + ":" + path |
|
| 140 | + | } |
|
| 141 | + | out, err := g.cmd("ls-tree", "-l", treeish) |
|
| 142 | + | if err != nil { |
|
| 143 | + | return nil, err |
|
| 144 | + | } |
|
| 145 | + | ||
| 146 | + | var entries []TreeEntryInfo |
|
| 147 | + | scanner := bufio.NewScanner(strings.NewReader(out)) |
|
| 148 | + | for scanner.Scan() { |
|
| 149 | + | line := scanner.Text() |
|
| 150 | + | // format: <mode> SP <type> SP <hash> SP <size>\t<name> |
|
| 151 | + | tabIdx := strings.IndexByte(line, '\t') |
|
| 152 | + | if tabIdx < 0 { |
|
| 153 | + | continue |
|
| 154 | + | } |
|
| 155 | + | name := line[tabIdx+1:] |
|
| 156 | + | meta := strings.Fields(line[:tabIdx]) |
|
| 157 | + | if len(meta) < 4 { |
|
| 158 | + | continue |
|
| 159 | + | } |
|
| 160 | + | mode := meta[0] |
|
| 161 | + | objType := meta[1] |
|
| 162 | + | sizeStr := meta[3] |
|
| 163 | + | ||
| 164 | + | isDir := objType == "tree" |
|
| 165 | + | var size int64 |
|
| 166 | + | if !isDir && sizeStr != "-" { |
|
| 167 | + | size, _ = strconv.ParseInt(sizeStr, 10, 64) |
|
| 168 | + | } |
|
| 169 | + | ||
| 170 | + | objHash := meta[2] |
|
| 171 | + | shortHash := objHash |
|
| 172 | + | if len(shortHash) > 8 { |
|
| 173 | + | shortHash = shortHash[:8] |
|
| 174 | + | } |
|
| 175 | + | ||
| 176 | + | entries = append(entries, TreeEntryInfo{ |
|
| 177 | + | Name: name, |
|
| 178 | + | IsDir: isDir, |
|
| 179 | + | Size: size, |
|
| 180 | + | Mode: mode, |
|
| 181 | + | Hash: shortHash, |
|
| 182 | + | }) |
|
| 183 | + | } |
|
| 184 | + | ||
| 185 | + | sortTreeEntries(entries) |
|
| 186 | + | return entries, nil |
|
| 187 | + | } |
|
| 188 | + | ||
| 189 | + | func (g *gitCLIBackend) getBlob(hash string, path string) (*BlobInfo, error) { |
|
| 190 | + | out, err := g.cmd("cat-file", "-s", hash+":"+path) |
|
| 191 | + | if err != nil { |
|
| 192 | + | return nil, err |
|
| 193 | + | } |
|
| 194 | + | size, _ := strconv.ParseInt(strings.TrimSpace(out), 10, 64) |
|
| 195 | + | ||
| 196 | + | name := path |
|
| 197 | + | if idx := strings.LastIndex(path, "/"); idx >= 0 { |
|
| 198 | + | name = path[idx+1:] |
|
| 199 | + | } |
|
| 200 | + | ||
| 201 | + | info := &BlobInfo{ |
|
| 202 | + | Name: name, |
|
| 203 | + | Size: size, |
|
| 204 | + | } |
|
| 205 | + | ||
| 206 | + | if size > maxBlobDisplaySize { |
|
| 207 | + | info.IsBinary = true |
|
| 208 | + | return info, nil |
|
| 209 | + | } |
|
| 210 | + | ||
| 211 | + | content, err := g.cmd("cat-file", "blob", hash+":"+path) |
|
| 212 | + | if err != nil { |
|
| 213 | + | return nil, err |
|
| 214 | + | } |
|
| 215 | + | ||
| 216 | + | // Check binary |
|
| 217 | + | checkLen := len(content) |
|
| 218 | + | if checkLen > 8000 { |
|
| 219 | + | checkLen = 8000 |
|
| 220 | + | } |
|
| 221 | + | if strings.Contains(content[:checkLen], "\x00") { |
|
| 222 | + | info.IsBinary = true |
|
| 223 | + | return info, nil |
|
| 224 | + | } |
|
| 225 | + | ||
| 226 | + | if !utf8.ValidString(content) { |
|
| 227 | + | info.IsBinary = true |
|
| 228 | + | return info, nil |
|
| 229 | + | } |
|
| 230 | + | ||
| 231 | + | info.Content = content |
|
| 232 | + | info.Lines = strings.Split(content, "\n") |
|
| 233 | + | if len(info.Lines) > 0 && info.Lines[len(info.Lines)-1] == "" { |
|
| 234 | + | info.Lines = info.Lines[:len(info.Lines)-1] |
|
| 235 | + | } |
|
| 236 | + | return info, nil |
|
| 237 | + | } |
|
| 238 | + | ||
| 239 | + | func (g *gitCLIBackend) getRawBlob(hash string, path string) (io.ReadCloser, string, int64, error) { |
|
| 240 | + | out, err := g.cmd("cat-file", "-s", hash+":"+path) |
|
| 241 | + | if err != nil { |
|
| 242 | + | return nil, "", 0, err |
|
| 243 | + | } |
|
| 244 | + | size, _ := strconv.ParseInt(strings.TrimSpace(out), 10, 64) |
|
| 245 | + | ||
| 246 | + | ct := "application/octet-stream" |
|
| 247 | + | if mtype := mimeFromPath(path); mtype != "" { |
|
| 248 | + | ct = mtype |
|
| 249 | + | } |
|
| 250 | + | ||
| 251 | + | reader, wait, err := g.pipe("cat-file", "blob", hash+":"+path) |
|
| 252 | + | if err != nil { |
|
| 253 | + | return nil, "", 0, err |
|
| 254 | + | } |
|
| 255 | + | ||
| 256 | + | return &waitReadCloser{reader: reader, wait: wait}, ct, size, nil |
|
| 257 | + | } |
|
| 258 | + | ||
| 259 | + | type waitReadCloser struct { |
|
| 260 | + | reader io.ReadCloser |
|
| 261 | + | wait func() error |
|
| 262 | + | } |
|
| 263 | + | ||
| 264 | + | func (w *waitReadCloser) Read(p []byte) (int, error) { |
|
| 265 | + | return w.reader.Read(p) |
|
| 266 | + | } |
|
| 267 | + | ||
| 268 | + | func (w *waitReadCloser) Close() error { |
|
| 269 | + | err := w.reader.Close() |
|
| 270 | + | w.wait() |
|
| 271 | + | return err |
|
| 272 | + | } |
|
| 273 | + | ||
| 274 | + | func (g *gitCLIBackend) getCommit(hash string) (*CommitInfo, error) { |
|
| 275 | + | out, err := g.cmd("log", "-1", "--format=%H%n%h%n%an%n%ae%n%aI%n%P%n%s%n%b", hash) |
|
| 276 | + | if err != nil { |
|
| 277 | + | return nil, err |
|
| 278 | + | } |
|
| 279 | + | ||
| 280 | + | lines := strings.SplitN(strings.TrimRight(out, "\n"), "\n", 8) |
|
| 281 | + | if len(lines) < 7 { |
|
| 282 | + | return nil, fmt.Errorf("unexpected git log output") |
|
| 283 | + | } |
|
| 284 | + | ||
| 285 | + | t, _ := time.Parse(time.RFC3339, strings.TrimSpace(lines[4])) |
|
| 286 | + | ||
| 287 | + | var parents []string |
|
| 288 | + | if p := strings.TrimSpace(lines[5]); p != "" { |
|
| 289 | + | parents = strings.Fields(p) |
|
| 290 | + | } |
|
| 291 | + | ||
| 292 | + | body := "" |
|
| 293 | + | if len(lines) > 7 { |
|
| 294 | + | body = strings.TrimSpace(lines[7]) |
|
| 295 | + | } |
|
| 296 | + | ||
| 297 | + | return &CommitInfo{ |
|
| 298 | + | Hash: strings.TrimSpace(lines[0]), |
|
| 299 | + | ShortHash: strings.TrimSpace(lines[1]), |
|
| 300 | + | Subject: strings.TrimSpace(lines[6]), |
|
| 301 | + | Body: body, |
|
| 302 | + | Author: strings.TrimSpace(lines[2]), |
|
| 303 | + | AuthorEmail: strings.TrimSpace(lines[3]), |
|
| 304 | + | AuthorDate: t, |
|
| 305 | + | Parents: parents, |
|
| 306 | + | }, nil |
|
| 307 | + | } |
|
| 308 | + | ||
| 309 | + | func (g *gitCLIBackend) getDiff(hash string) ([]DiffFile, error) { |
|
| 310 | + | var out string |
|
| 311 | + | var err error |
|
| 312 | + | ||
| 313 | + | // Check if this is a root commit (no parents) |
|
| 314 | + | parentOut, perr := g.cmd("rev-parse", hash+"^") |
|
| 315 | + | if perr != nil { |
|
| 316 | + | // Root commit: diff against empty tree |
|
| 317 | + | out, err = g.cmd("diff-tree", "-p", "--no-commit-id", "--root", hash) |
|
| 318 | + | } else { |
|
| 319 | + | parent := strings.TrimSpace(parentOut) |
|
| 320 | + | out, err = g.cmd("diff", parent, hash) |
|
| 321 | + | } |
|
| 322 | + | if err != nil { |
|
| 323 | + | return nil, err |
|
| 324 | + | } |
|
| 325 | + | ||
| 326 | + | return parseDiffOutput(out), nil |
|
| 327 | + | } |
|
| 328 | + | ||
| 329 | + | func parseDiffOutput(output string) []DiffFile { |
|
| 330 | + | var files []DiffFile |
|
| 331 | + | parts := strings.Split(output, " |
+0 -0
go.mod
+0 -24
| 1 | 1 | module source-browser |
|
| 2 | 2 | ||
| 3 | 3 | go 1.25.7 |
|
| 4 | - | ||
| 5 | - | require github.com/go-git/go-git/v5 v5.16.5 |
|
| 6 | - | ||
| 7 | - | require ( |
|
| 8 | - | dario.cat/mergo v1.0.0 // indirect |
|
| 9 | - | github.com/Microsoft/go-winio v0.6.2 // indirect |
|
| 10 | - | github.com/ProtonMail/go-crypto v1.1.6 // indirect |
|
| 11 | - | github.com/cloudflare/circl v1.6.1 // indirect |
|
| 12 | - | github.com/cyphar/filepath-securejoin v0.4.1 // indirect |
|
| 13 | - | github.com/emirpasic/gods v1.18.1 // indirect |
|
| 14 | - | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect |
|
| 15 | - | github.com/go-git/go-billy/v5 v5.6.2 // indirect |
|
| 16 | - | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect |
|
| 17 | - | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect |
|
| 18 | - | github.com/kevinburke/ssh_config v1.2.0 // indirect |
|
| 19 | - | github.com/pjbgf/sha1cd v0.3.2 // indirect |
|
| 20 | - | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect |
|
| 21 | - | github.com/skeema/knownhosts v1.3.1 // indirect |
|
| 22 | - | github.com/xanzy/ssh-agent v0.3.3 // indirect |
|
| 23 | - | golang.org/x/crypto v0.45.0 // indirect |
|
| 24 | - | golang.org/x/net v0.47.0 // indirect |
|
| 25 | - | golang.org/x/sys v0.38.0 // indirect |
|
| 26 | - | gopkg.in/warnings.v0 v0.1.2 // indirect |
|
| 27 | - | ) |
go.sum
+0 -102
| 1 | - | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= |
|
| 2 | - | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= |
|
| 3 | - | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= |
|
| 4 | - | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= |
|
| 5 | - | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= |
|
| 6 | - | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= |
|
| 7 | - | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= |
|
| 8 | - | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= |
|
| 9 | - | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= |
|
| 10 | - | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= |
|
| 11 | - | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= |
|
| 12 | - | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= |
|
| 13 | - | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= |
|
| 14 | - | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= |
|
| 15 | - | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= |
|
| 16 | - | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|
| 17 | - | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|
| 18 | - | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|
| 19 | - | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= |
|
| 20 | - | github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= |
|
| 21 | - | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= |
|
| 22 | - | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= |
|
| 23 | - | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= |
|
| 24 | - | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= |
|
| 25 | - | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= |
|
| 26 | - | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= |
|
| 27 | - | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= |
|
| 28 | - | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= |
|
| 29 | - | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= |
|
| 30 | - | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= |
|
| 31 | - | github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= |
|
| 32 | - | github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= |
|
| 33 | - | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= |
|
| 34 | - | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= |
|
| 35 | - | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= |
|
| 36 | - | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= |
|
| 37 | - | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= |
|
| 38 | - | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= |
|
| 39 | - | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= |
|
| 40 | - | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= |
|
| 41 | - | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= |
|
| 42 | - | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= |
|
| 43 | - | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= |
|
| 44 | - | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= |
|
| 45 | - | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= |
|
| 46 | - | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= |
|
| 47 | - | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= |
|
| 48 | - | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= |
|
| 49 | - | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= |
|
| 50 | - | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= |
|
| 51 | - | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= |
|
| 52 | - | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
|
| 53 | - | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
|
| 54 | - | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|
| 55 | - | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|
| 56 | - | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= |
|
| 57 | - | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= |
|
| 58 | - | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= |
|
| 59 | - | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= |
|
| 60 | - | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= |
|
| 61 | - | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= |
|
| 62 | - | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= |
|
| 63 | - | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|
| 64 | - | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= |
|
| 65 | - | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= |
|
| 66 | - | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= |
|
| 67 | - | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= |
|
| 68 | - | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= |
|
| 69 | - | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= |
|
| 70 | - | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= |
|
| 71 | - | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= |
|
| 72 | - | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= |
|
| 73 | - | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= |
|
| 74 | - | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= |
|
| 75 | - | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= |
|
| 76 | - | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= |
|
| 77 | - | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= |
|
| 78 | - | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|
| 79 | - | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|
| 80 | - | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|
| 81 | - | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
|
| 82 | - | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|
| 83 | - | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|
| 84 | - | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= |
|
| 85 | - | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= |
|
| 86 | - | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
|
| 87 | - | golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= |
|
| 88 | - | golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= |
|
| 89 | - | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
|
| 90 | - | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= |
|
| 91 | - | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= |
|
| 92 | - | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
|
| 93 | - | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|
| 94 | - | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|
| 95 | - | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= |
|
| 96 | - | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= |
|
| 97 | - | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= |
|
| 98 | - | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= |
|
| 99 | - | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|
| 100 | - | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= |
|
| 101 | - | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
|
| 102 | - | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
handler.go
+29 -24
| 4 | 4 | "fmt" |
|
| 5 | 5 | "io" |
|
| 6 | 6 | "net/http" |
|
| 7 | 7 | "strconv" |
|
| 8 | 8 | "strings" |
|
| 9 | - | ||
| 10 | - | "github.com/go-git/go-git/v5/plumbing" |
|
| 11 | 9 | ) |
|
| 12 | 10 | ||
| 13 | 11 | type pageData struct { |
|
| 14 | 12 | SiteTitle string |
|
| 15 | 13 | BaseURL string |
| 149 | 147 | func (s *server) handleSummary(w http.ResponseWriter, r *http.Request, repo *RepoInfo) { |
|
| 150 | 148 | s.renderHome(w, repo, "", nil, "") |
|
| 151 | 149 | } |
|
| 152 | 150 | ||
| 153 | 151 | func (s *server) renderHome(w http.ResponseWriter, repo *RepoInfo, ref string, blob *BlobInfo, activePath string) { |
|
| 152 | + | git := repo.Git |
|
| 153 | + | ||
| 154 | 154 | if ref == "" { |
|
| 155 | - | ref = getDefaultBranch(repo.Repo) |
|
| 155 | + | ref = git.getDefaultBranch() |
|
| 156 | 156 | } |
|
| 157 | 157 | ||
| 158 | - | head, err := resolveRef(repo.Repo, ref) |
|
| 158 | + | hash, err := git.resolveRef(ref) |
|
| 159 | 159 | if err != nil { |
|
| 160 | 160 | pd := s.newPageData(repo, "home", ref) |
|
| 161 | 161 | pd.Data = homeData{ |
|
| 162 | 162 | DefaultRef: ref, |
|
| 163 | 163 | IsEmpty: true, |
|
| 164 | 164 | } |
|
| 165 | 165 | s.tmpl.render(w, "home", pd) |
|
| 166 | 166 | return |
|
| 167 | 167 | } |
|
| 168 | 168 | ||
| 169 | - | tree := buildTreeNodes(repo.Repo, *head, activePath) |
|
| 170 | - | lastCommit, _ := getCommit(repo.Repo, *head) |
|
| 171 | - | branches, _ := getBranches(repo.Repo) |
|
| 169 | + | tree := git.buildTreeNodes(hash, activePath) |
|
| 170 | + | lastCommit, _ := git.getCommit(hash) |
|
| 171 | + | branches, _ := git.getBranches() |
|
| 172 | 172 | ||
| 173 | 173 | // Show README for the active directory (or root if no active path / file selected) |
|
| 174 | 174 | readmeDir := "" |
|
| 175 | 175 | if activePath != "" && blob == nil { |
|
| 176 | 176 | readmeDir = activePath |
|
| 177 | 177 | } |
|
| 178 | - | readme := getReadme(repo.Repo, *head, readmeDir) |
|
| 178 | + | readme := git.getReadme(hash, readmeDir) |
|
| 179 | 179 | ||
| 180 | 180 | pd := s.newPageData(repo, "home", ref) |
|
| 181 | 181 | pd.Data = homeData{ |
|
| 182 | 182 | DefaultRef: ref, |
|
| 183 | 183 | Branches: branches, |
| 209 | 209 | if n, err := strconv.Atoi(p); err == nil && n >= 0 { |
|
| 210 | 210 | page = n |
|
| 211 | 211 | } |
|
| 212 | 212 | } |
|
| 213 | 213 | ||
| 214 | + | git := repo.Git |
|
| 214 | 215 | refStr := rest |
|
| 215 | 216 | if refStr == "" { |
|
| 216 | - | refStr = getDefaultBranch(repo.Repo) |
|
| 217 | + | refStr = git.getDefaultBranch() |
|
| 217 | 218 | } |
|
| 218 | 219 | ||
| 219 | - | hash, err := resolveRef(repo.Repo, refStr) |
|
| 220 | + | hash, err := git.resolveRef(refStr) |
|
| 220 | 221 | if err != nil { |
|
| 221 | 222 | pd := s.newPageData(repo, "log", refStr) |
|
| 222 | 223 | pd.Data = logData{IsEmpty: true, Ref: refStr} |
|
| 223 | 224 | s.tmpl.render(w, "log", pd) |
|
| 224 | 225 | return |
|
| 225 | 226 | } |
|
| 226 | 227 | ||
| 227 | - | commits, hasMore, err := getLog(repo.Repo, *hash, page, 50) |
|
| 228 | + | commits, hasMore, err := git.getLog(hash, page, 50) |
|
| 228 | 229 | if err != nil { |
|
| 229 | 230 | s.renderError(w, r, http.StatusInternalServerError, err.Error()) |
|
| 230 | 231 | return |
|
| 231 | 232 | } |
|
| 232 | 233 | ||
| 233 | - | branches, _ := getBranches(repo.Repo) |
|
| 234 | - | lastCommit, _ := getCommit(repo.Repo, *hash) |
|
| 234 | + | branches, _ := git.getBranches() |
|
| 235 | + | lastCommit, _ := git.getCommit(hash) |
|
| 235 | 236 | ||
| 236 | 237 | pd := s.newPageData(repo, "log", refStr) |
|
| 237 | 238 | pd.Data = logData{ |
|
| 238 | 239 | Branches: branches, |
|
| 239 | 240 | LastCommit: lastCommit, |
| 248 | 249 | } |
|
| 249 | 250 | s.tmpl.render(w, "log", pd) |
|
| 250 | 251 | } |
|
| 251 | 252 | ||
| 252 | 253 | func (s *server) handleTree(w http.ResponseWriter, r *http.Request, repo *RepoInfo, rest string) { |
|
| 254 | + | git := repo.Git |
|
| 255 | + | ||
| 253 | 256 | if rest == "" { |
|
| 254 | - | rest = getDefaultBranch(repo.Repo) |
|
| 257 | + | rest = git.getDefaultBranch() |
|
| 255 | 258 | } |
|
| 256 | 259 | ||
| 257 | 260 | segments := strings.Split(rest, "/") |
|
| 258 | - | hash, ref, path, err := resolveRefAndPath(repo.Repo, segments) |
|
| 261 | + | hash, ref, path, err := git.resolveRefAndPath(segments) |
|
| 259 | 262 | if err != nil { |
|
| 260 | 263 | s.renderError(w, r, http.StatusNotFound, "Ref not found") |
|
| 261 | 264 | return |
|
| 262 | 265 | } |
|
| 263 | 266 | ||
| 264 | 267 | // File: show blob in content view |
|
| 265 | - | if path != "" && !isTreePath(repo.Repo, *hash, path) { |
|
| 266 | - | blob, err := getBlob(repo.Repo, *hash, path) |
|
| 268 | + | if path != "" && !git.isTreePath(hash, path) { |
|
| 269 | + | blob, err := git.getBlob(hash, path) |
|
| 267 | 270 | if err != nil { |
|
| 268 | 271 | s.renderError(w, r, http.StatusNotFound, "File not found") |
|
| 269 | 272 | return |
|
| 270 | 273 | } |
|
| 271 | 274 | s.renderHome(w, repo, ref, blob, path) |
| 280 | 283 | type commitData struct { |
|
| 281 | 284 | Commit *CommitInfo |
|
| 282 | 285 | Files []DiffFile |
|
| 283 | 286 | } |
|
| 284 | 287 | ||
| 285 | - | hash := plumbing.NewHash(rest) |
|
| 286 | - | commit, err := getCommit(repo.Repo, hash) |
|
| 288 | + | git := repo.Git |
|
| 289 | + | commit, err := git.getCommit(rest) |
|
| 287 | 290 | if err != nil { |
|
| 288 | 291 | s.renderError(w, r, http.StatusNotFound, "Commit not found") |
|
| 289 | 292 | return |
|
| 290 | 293 | } |
|
| 291 | 294 | ||
| 292 | - | files, err := getDiff(repo.Repo, hash) |
|
| 295 | + | files, err := git.getDiff(rest) |
|
| 293 | 296 | if err != nil { |
|
| 294 | 297 | s.renderError(w, r, http.StatusInternalServerError, err.Error()) |
|
| 295 | 298 | return |
|
| 296 | 299 | } |
|
| 297 | 300 |
| 308 | 311 | type refsData struct { |
|
| 309 | 312 | Branches []RefInfo |
|
| 310 | 313 | Tags []RefInfo |
|
| 311 | 314 | } |
|
| 312 | 315 | ||
| 313 | - | branches, _ := getBranches(repo.Repo) |
|
| 314 | - | tags, _ := getTags(repo.Repo) |
|
| 316 | + | git := repo.Git |
|
| 317 | + | branches, _ := git.getBranches() |
|
| 318 | + | tags, _ := git.getTags() |
|
| 315 | 319 | ||
| 316 | 320 | pd := s.newPageData(repo, "refs", "") |
|
| 317 | 321 | pd.Data = refsData{Branches: branches, Tags: tags} |
|
| 318 | 322 | s.tmpl.render(w, "refs", pd) |
|
| 319 | 323 | } |
| 322 | 326 | if rest == "" { |
|
| 323 | 327 | s.renderError(w, r, http.StatusNotFound, "No path specified") |
|
| 324 | 328 | return |
|
| 325 | 329 | } |
|
| 326 | 330 | ||
| 331 | + | git := repo.Git |
|
| 327 | 332 | segments := strings.Split(rest, "/") |
|
| 328 | - | hash, _, path, err := resolveRefAndPath(repo.Repo, segments) |
|
| 333 | + | hash, _, path, err := git.resolveRefAndPath(segments) |
|
| 329 | 334 | if err != nil { |
|
| 330 | 335 | s.renderError(w, r, http.StatusNotFound, "Ref not found") |
|
| 331 | 336 | return |
|
| 332 | 337 | } |
|
| 333 | 338 | if path == "" { |
|
| 334 | 339 | s.renderError(w, r, http.StatusNotFound, "No file path") |
|
| 335 | 340 | return |
|
| 336 | 341 | } |
|
| 337 | 342 | ||
| 338 | - | reader, contentType, size, err := getRawBlob(repo.Repo, *hash, path) |
|
| 343 | + | reader, contentType, size, err := git.getRawBlob(hash, path) |
|
| 339 | 344 | if err != nil { |
|
| 340 | 345 | s.renderError(w, r, http.StatusNotFound, "File not found") |
|
| 341 | 346 | return |
|
| 342 | 347 | } |
|
| 343 | 348 | defer reader.Close() |
main.go
+11 -21
| 7 | 7 | "net/http" |
|
| 8 | 8 | "os" |
|
| 9 | 9 | "path/filepath" |
|
| 10 | 10 | "sort" |
|
| 11 | 11 | "strings" |
|
| 12 | - | "time" |
|
| 13 | - | ||
| 14 | - | git "github.com/go-git/go-git/v5" |
|
| 15 | 12 | ) |
|
| 16 | 13 | ||
| 17 | 14 | type server struct { |
|
| 18 | 15 | repos map[string]*RepoInfo |
|
| 19 | 16 | sorted []string |
| 115 | 112 | if !entry.IsDir() || strings.HasPrefix(entry.Name(), ".") { |
|
| 116 | 113 | continue |
|
| 117 | 114 | } |
|
| 118 | 115 | dirPath := filepath.Join(root, entry.Name()) |
|
| 119 | 116 | ||
| 120 | - | var repo *git.Repository |
|
| 121 | 117 | var gitDir string |
|
| 122 | 118 | ||
| 123 | 119 | if isBareRepo(dirPath) { |
|
| 124 | 120 | gitDir = dirPath |
|
| 125 | 121 | } else if nonBare && isWorkTreeRepo(dirPath) { |
| 131 | 127 | // Repos are private by default; skip unless a 'public' file exists. |
|
| 132 | 128 | if _, err := os.Stat(filepath.Join(gitDir, "public")); err != nil { |
|
| 133 | 129 | continue |
|
| 134 | 130 | } |
|
| 135 | 131 | ||
| 136 | - | repo, err = git.PlainOpen(dirPath) |
|
| 137 | - | if err != nil { |
|
| 138 | - | log.Printf("skip %s: %v", entry.Name(), err) |
|
| 139 | - | continue |
|
| 140 | - | } |
|
| 141 | - | ||
| 142 | 132 | name := strings.TrimSuffix(entry.Name(), ".git") |
|
| 143 | 133 | ||
| 144 | 134 | desc := "" |
|
| 145 | 135 | descBytes, err := os.ReadFile(filepath.Join(gitDir, "description")) |
|
| 146 | 136 | if err == nil { |
| 154 | 144 | ownerBytes, err := os.ReadFile(filepath.Join(gitDir, "owner")) |
|
| 155 | 145 | if err == nil { |
|
| 156 | 146 | owner = strings.TrimSpace(string(ownerBytes)) |
|
| 157 | 147 | } |
|
| 158 | 148 | ||
| 159 | - | var lastUpdated time.Time |
|
| 160 | - | head, err := repo.Head() |
|
| 161 | - | if err == nil { |
|
| 162 | - | c, err := repo.CommitObject(head.Hash()) |
|
| 163 | - | if err == nil { |
|
| 164 | - | lastUpdated = c.Author.When |
|
| 165 | - | } |
|
| 166 | - | } |
|
| 149 | + | git := newGitCLIBackend(gitDir) |
|
| 167 | 150 | ||
| 168 | - | repos[name] = &RepoInfo{ |
|
| 151 | + | info := &RepoInfo{ |
|
| 169 | 152 | Name: name, |
|
| 170 | 153 | Path: dirPath, |
|
| 154 | + | GitDir: gitDir, |
|
| 171 | 155 | Description: desc, |
|
| 172 | 156 | Owner: owner, |
|
| 173 | - | LastUpdated: lastUpdated, |
|
| 174 | - | Repo: repo, |
|
| 157 | + | Git: git, |
|
| 175 | 158 | } |
|
| 159 | + | ||
| 160 | + | commit, err := git.getCommit("HEAD") |
|
| 161 | + | if err == nil { |
|
| 162 | + | info.LastUpdated = commit.AuthorDate |
|
| 163 | + | } |
|
| 164 | + | ||
| 165 | + | repos[name] = info |
|
| 176 | 166 | } |
|
| 177 | 167 | ||
| 178 | 168 | return repos, nil |
|
| 179 | 169 | } |
|
| 180 | 170 |
template.go
+1 -1
| 127 | 127 | if total < blocks { |
|
| 128 | 128 | blocks = total |
|
| 129 | 129 | } |
|
| 130 | 130 | addBlocks := 0 |
|
| 131 | 131 | if total > 0 { |
|
| 132 | - | addBlocks = (added * blocks + total - 1) / total |
|
| 132 | + | addBlocks = (added*blocks + total - 1) / total |
|
| 133 | 133 | } |
|
| 134 | 134 | if addBlocks > blocks { |
|
| 135 | 135 | addBlocks = blocks |
|
| 136 | 136 | } |
|
| 137 | 137 | delBlocks := blocks - addBlocks |