2019-04-20 05:47:00 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
2019-06-26 21:15:26 +03:00
package git
2019-04-20 05:47:00 +03:00
import (
"bufio"
2019-11-30 17:40:22 +03:00
"context"
2019-04-20 05:47:00 +03:00
"fmt"
"io"
"os"
"os/exec"
"regexp"
"code.gitea.io/gitea/modules/process"
)
// BlamePart represents block of blame - continuous lines with one sha
type BlamePart struct {
Sha string
Lines [ ] string
}
// BlameReader returns part of file blame one by one
type BlameReader struct {
cmd * exec . Cmd
pid int64
output io . ReadCloser
2020-11-10 05:14:02 +03:00
reader * bufio . Reader
2019-04-20 05:47:00 +03:00
lastSha * string
2019-11-30 17:40:22 +03:00
cancel context . CancelFunc
2019-04-20 05:47:00 +03:00
}
var shaLineRegex = regexp . MustCompile ( "^([a-z0-9]{40})" )
2021-07-08 14:38:13 +03:00
// NextPart returns next part of blame (sequential code lines with the same commit)
2019-04-20 05:47:00 +03:00
func ( r * BlameReader ) NextPart ( ) ( * BlamePart , error ) {
var blamePart * BlamePart
2020-11-10 05:14:02 +03:00
reader := r . reader
2019-04-20 05:47:00 +03:00
if r . lastSha != nil {
2019-06-12 22:41:28 +03:00
blamePart = & BlamePart { * r . lastSha , make ( [ ] string , 0 ) }
2019-04-20 05:47:00 +03:00
}
2020-11-10 05:14:02 +03:00
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
}
2019-04-20 05:47:00 +03:00
if len ( line ) == 0 {
2020-11-10 05:14:02 +03:00
// isPrefix will be false
2019-04-20 05:47:00 +03:00
continue
}
2020-11-10 05:14:02 +03:00
lines := shaLineRegex . FindSubmatch ( line )
2019-04-20 05:47:00 +03:00
if lines != nil {
2020-11-10 05:14:02 +03:00
sha1 := string ( lines [ 1 ] )
2019-04-20 05:47:00 +03:00
if blamePart == nil {
2019-06-12 22:41:28 +03:00
blamePart = & BlamePart { sha1 , make ( [ ] string , 0 ) }
2019-04-20 05:47:00 +03:00
}
if blamePart . Sha != sha1 {
r . lastSha = & sha1
2020-11-10 05:14:02 +03:00
// need to munch to end of line...
for isPrefix {
_ , isPrefix , err = reader . ReadLine ( )
if err != nil && err != io . EOF {
return blamePart , err
}
}
2019-04-20 05:47:00 +03:00
return blamePart , nil
}
} else if line [ 0 ] == '\t' {
code := line [ 1 : ]
2020-11-10 05:14:02 +03:00
blamePart . Lines = append ( blamePart . Lines , string ( code ) )
}
// need to munch to end of line...
for isPrefix {
_ , isPrefix , err = reader . ReadLine ( )
if err != nil && err != io . EOF {
return blamePart , err
}
2019-04-20 05:47:00 +03:00
}
}
r . lastSha = nil
return blamePart , nil
}
// Close BlameReader - don't run NextPart after invoking that
func ( r * BlameReader ) Close ( ) error {
2019-11-30 17:40:22 +03:00
defer process . GetManager ( ) . Remove ( r . pid )
2020-07-01 16:01:17 +03:00
r . cancel ( )
_ = r . output . Close ( )
2019-04-20 05:47:00 +03:00
if err := r . cmd . Wait ( ) ; err != nil {
return fmt . Errorf ( "Wait: %v" , err )
}
return nil
}
// CreateBlameReader creates reader for given repository, commit and file
2020-07-01 16:01:17 +03:00
func CreateBlameReader ( ctx context . Context , repoPath , commitID , file string ) ( * BlameReader , error ) {
2019-11-13 10:01:19 +03:00
gitRepo , err := OpenRepository ( repoPath )
2019-04-20 05:47:00 +03:00
if err != nil {
return nil , err
}
2019-11-13 10:01:19 +03:00
gitRepo . Close ( )
2019-04-20 05:47:00 +03:00
2020-07-01 16:01:17 +03:00
return createBlameReader ( ctx , repoPath , GitExecutable , "blame" , commitID , "--porcelain" , "--" , file )
2019-04-20 05:47:00 +03:00
}
2020-07-01 16:01:17 +03:00
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 := context . WithCancel ( ctx )
2019-11-30 17:40:22 +03:00
cmd := exec . CommandContext ( ctx , command [ 0 ] , command [ 1 : ] ... )
2019-04-20 05:47:00 +03:00
cmd . Dir = dir
cmd . Stderr = os . Stderr
stdout , err := cmd . StdoutPipe ( )
if err != nil {
2019-11-30 17:40:22 +03:00
defer cancel ( )
2019-04-20 05:47:00 +03:00
return nil , fmt . Errorf ( "StdoutPipe: %v" , err )
}
if err = cmd . Start ( ) ; err != nil {
2019-11-30 17:40:22 +03:00
defer cancel ( )
2019-04-20 05:47:00 +03:00
return nil , fmt . Errorf ( "Start: %v" , err )
}
2019-11-30 17:40:22 +03:00
pid := process . GetManager ( ) . Add ( fmt . Sprintf ( "GetBlame [repo_path: %s]" , dir ) , cancel )
2019-04-20 05:47:00 +03:00
2020-11-10 05:14:02 +03:00
reader := bufio . NewReader ( stdout )
2019-04-20 05:47:00 +03:00
return & BlameReader {
cmd ,
pid ,
stdout ,
2020-11-10 05:14:02 +03:00
reader ,
2019-04-20 05:47:00 +03:00
nil ,
2019-11-30 17:40:22 +03:00
cancel ,
2019-04-20 05:47:00 +03:00
} , nil
}