Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Add file history and blame views

67a308badf4a88298639271a1201ff45cc3737a6 · Keenan Nemetz · 15d ago · parent f204e7ee

Add file history and blame views

A blob's toolbar now links to the commits that touched its path and to per-line attribution. File history uses git log for the path, following renames for blobs; blame groups consecutive lines by commit and shows the author and age in a left column. Both are read-only and reuse the existing commit-link and escaping rules.

core/git/git.go +69

@@ -641,6 +641,75 @@ func (r *Repo) Grep(rev, pattern string, maxBytes int) ([]byte, bool, error) {
641 641 return out, truncated, nil
642 642 }
643 643
644 // LogPath lists commits that touched one path. follow asks git to follow a
645 // rename; it is only valid for a single file, so directory history uses false.
646 func (r *Repo) LogPath(rev, path string, n int, follow bool) ([]Commit, error) {
647 args := []string{"log", "--format=" + commitFormat, "-n", strconv.Itoa(n)}
648 if follow {
649 args = append(args, "--follow")
650 }
651 args = append(args, rev, "--", path)
652 out, errs, err := r.run(nil, args...)
653 if err != nil {
654 return nil, fmt.Errorf("log %q -- %q: %w: %s", rev, path, err, errs)
655 }
656 return parseCommits(out), nil
657 }
658
659 // BlameLine is one line of `git blame` output with the commit that last touched
660 // it. Line is the line number in the blamed revision.
661 type BlameLine struct {
662 SHA string
663 Author string
664 When int64
665 Line int
666 Text string
667 }
668
669 // Blame reads line attribution for one file, bounded by maxBytes. A cut result
670 // is reported rather than silently shown as the whole file.
671 func (r *Repo) Blame(rev, path string, maxBytes int) ([]BlameLine, bool, error) {
672 if maxBytes < 1 {
673 maxBytes = 1
674 }
675 out, _, err := r.runHead(maxBytes+1,
676 "blame", "--line-porcelain", rev, "--", path)
677 if err != nil {
678 return nil, false, fmt.Errorf("blame %q -- %q: %w", rev, path, err)
679 }
680 truncated := len(out) > maxBytes
681 if truncated {
682 out = out[:maxBytes]
683 }
684 var lines []BlameLine
685 var cur BlameLine
686 have := false
687 for _, line := range strings.Split(string(out), "\n") {
688 switch {
689 case strings.HasPrefix(line, "\t"):
690 if have {
691 cur.Text = line[1:]
692 lines = append(lines, cur)
693 have = false
694 }
695 case strings.HasPrefix(line, "author "):
696 cur.Author = strings.TrimPrefix(line, "author ")
697 case strings.HasPrefix(line, "author-time "):
698 cur.When, _ = strconv.ParseInt(strings.TrimPrefix(line, "author-time "), 10, 64)
699 default:
700 fields := strings.Fields(line)
701 if len(fields) >= 3 {
702 sha := strings.TrimPrefix(fields[0], "^")
703 if n, err := strconv.Atoi(fields[2]); err == nil && len(sha) == 40 {
704 cur = BlameLine{SHA: sha, Line: n}
705 have = true
706 }
707 }
708 }
709 }
710 return lines, truncated, nil
711 }
712
644 713 func (r *Repo) CountCommits(rev string) (int, error) {
645 714 out, _, err := r.run(nil, "rev-list", "--count", rev)
646 715 if err != nil {