新增:获取仓库blame内容接口

This commit is contained in:
yystopf 2023-01-03 11:41:57 +08:00
parent 64352810b1
commit 484a4ce01a
4 changed files with 280 additions and 0 deletions

181
modules/git/blame.go Normal file
View File

@ -0,0 +1,181 @@
package git
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
gitea_git "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/process"
hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
)
type BlameReader struct {
cmd *exec.Cmd
output io.ReadCloser
reader *bufio.Reader
lastSha *string
cancel context.CancelFunc // Cancels the context that this reader runs in
finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table
}
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
func GetBlameCommit(repo *gitea_git.Repository, sha string) *hat_api.BlameCommit {
commit, err := repo.GetCommit(sha)
if err != nil {
return &hat_api.BlameCommit{}
}
var apiParents []string
for i := 0; i < commit.ParentCount(); i++ {
sha, _ := commit.ParentID(i)
apiParents = append(apiParents, sha.String())
}
return &hat_api.BlameCommit{
ID: sha,
Author: commit.Author,
Commiter: commit.Committer,
CommitMessage: commit.CommitMessage,
Parents: apiParents,
AuthoredTime: commit.Author.When,
CommittedTime: commit.Committer.When,
CreatedTime: commit.Committer.When,
}
}
func (r *BlameReader) NextApiPart(repo *gitea_git.Repository) (*hat_api.BlamePart, error) {
var blamePart *hat_api.BlamePart
reader := r.reader
effectLine := 0
if r.lastSha != nil {
blamePart = &hat_api.BlamePart{
Sha: *r.lastSha,
Commit: GetBlameCommit(repo, *r.lastSha),
PreviousNumber: 0,
CurrentNumber: 0,
EffectLine: effectLine,
Lines: make([]string, 0),
}
}
var line []byte
var isPrefix bool
var err error
for err != io.EOF {
line, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
if len(line) == 0 {
continue
}
lines := shaLineRegex.FindSubmatch(line)
if lines != nil {
lineString := string(line)
sha1 := string(lines[1])
lineString = strings.Replace(lineString, sha1, "", 1)
lineArray := strings.Split(lineString, " ")
previousNumber, _ := strconv.Atoi(lineArray[1])
if blamePart == nil {
blamePart = &hat_api.BlamePart{
Sha: sha1,
Commit: GetBlameCommit(repo, sha1),
PreviousNumber: previousNumber,
CurrentNumber: 0,
EffectLine: effectLine,
Lines: make([]string, 0),
}
}
if blamePart.Sha != sha1 {
r.lastSha = &sha1
// need to munch to end of line...
for isPrefix {
_, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
return blamePart, nil
}
} else if line[0] == '\t' {
code := line[1:]
effectLine += 1
blamePart.Lines = append(blamePart.Lines, string(code))
}
blamePart.EffectLine = effectLine
for isPrefix {
_, isPrefix, err = reader.ReadLine()
if err != nil && err != io.EOF {
return blamePart, err
}
}
}
r.lastSha = nil
return blamePart, nil
}
func (r *BlameReader) Close() error {
defer r.finished() // Only remove the process from the process table when the underlying command is closed
r.cancel() // However, first cancel our own context early
_ = r.output.Close()
if err := r.cmd.Wait(); err != nil {
return fmt.Errorf("Wait: %w", err)
}
return nil
}
func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) {
return createBlameReader(ctx, repoPath, gitea_git.GitExecutable, "blame", commitID, "--porcelain", "--", file)
}
func createBlameReader(ctx context.Context, dir string, command ...string) (*BlameReader, error) {
// Here we use the provided context - this should be tied to the request performing the blame so that it does not hang around.
ctx, cancel, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("GetBlame [repo_path: %s]", dir))
cmd := exec.CommandContext(ctx, command[0], command[1:]...)
cmd.Dir = dir
cmd.Stderr = os.Stderr
process.SetSysProcAttribute(cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
defer finished()
return nil, fmt.Errorf("StdoutPipe: %w", err)
}
if err = cmd.Start(); err != nil {
defer finished()
_ = stdout.Close()
return nil, fmt.Errorf("Start: %w", err)
}
reader := bufio.NewReader(stdout)
return &BlameReader{
cmd: cmd,
output: stdout,
reader: reader,
cancel: cancel,
finished: finished,
}, nil
}

27
modules/structs/blame.go Normal file
View File

@ -0,0 +1,27 @@
package structs
import (
"time"
"code.gitea.io/gitea/modules/git"
)
type BlameCommit struct {
ID string `json:"id"`
Author *git.Signature `json:"author"`
Commiter *git.Signature `json:"commiter"`
CommitMessage string `json:"commit_message"`
Parents []string `json:"parents"`
AuthoredTime time.Time `json:"authored_time"`
CommittedTime time.Time `json:"committed_time"`
CreatedTime time.Time `json:"created_time"`
}
type BlamePart struct {
Sha string `json:"-"`
Commit *BlameCommit `json:"commit"`
PreviousNumber int `json:"previous_number"`
CurrentNumber int `json:"current_number"`
EffectLine int `json:"effect_line"`
Lines []string `json:"lines"`
}

View File

@ -134,6 +134,7 @@ func Routers(ctx gocontext.Context) *web.Route {
m.Get("/{sha}", repo.GetSingleCommit)
})
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
m.Get("/blame", context.RepoRef(), repo.GetRepoRefBlame)
}, repoAssignment())
})
m.Group("/users", func() {

71
routers/hat/repo/blame.go Normal file
View File

@ -0,0 +1,71 @@
package repo
import (
"net/http"
"code.gitea.io/gitea/modules/context"
gitea_git "code.gitea.io/gitea/modules/git"
hat_git "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/git"
hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
)
func GetRepoRefBlame(ctx *context.APIContext) {
if ctx.Repo.Repository.IsEmpty {
ctx.NotFound()
return
}
var commit *gitea_git.Commit
if sha := ctx.FormString("sha"); len(sha) > 0 {
var err error
commit, err = ctx.Repo.GitRepo.GetCommit(sha)
if err != nil {
if gitea_git.IsErrNotExist(err) {
ctx.NotFound()
} else {
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
}
return
}
}
filepath := ctx.FormString("filepath")
entry, err := commit.GetTreeEntryByPath(filepath)
if err != nil {
ctx.NotFoundOrServerError("commit.GetTreeEntryByPath", gitea_git.IsErrNotExist, err)
return
}
blob := entry.Blob()
numLines, err := blob.GetBlobLineCount()
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBlobLineCount", err)
return
}
blameReader, err := hat_git.CreateBlameReader(ctx, ctx.Repo.Repository.RepoPath(), commit.ID.String(), filepath)
if err != nil {
ctx.Error(http.StatusInternalServerError, "CreateBlameReader", err)
return
}
defer blameReader.Close()
blameParts := make([]hat_api.BlamePart, 0)
currentNumber := 1
for {
blamePart, err := blameReader.NextApiPart(ctx.Repo.GitRepo)
if err != nil {
ctx.Error(http.StatusInternalServerError, "NextApiPart", err)
return
}
if blamePart == nil {
break
}
blamePart.CurrentNumber = currentNumber
blameParts = append(blameParts, *blamePart)
currentNumber += blamePart.EffectLine
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"num_lines": numLines,
"blame_parts": blameParts,
})
}