2019-04-20 04:47:00 +02:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2019-04-20 04:47:00 +02:00
2019-06-27 02:15:26 +08:00
package git
2019-04-20 04:47:00 +02:00
import (
"bufio"
2023-08-13 10:11:20 +08:00
"bytes"
2019-11-30 08:40:22 -06:00
"context"
2019-04-20 04:47:00 +02:00
"fmt"
"io"
"os"
2023-08-13 10:11:20 +08:00
"code.gitea.io/gitea/modules/log"
2023-09-16 19:42:34 +02:00
"code.gitea.io/gitea/modules/util"
2019-04-20 04:47:00 +02:00
)
// BlamePart represents block of blame - continuous lines with one sha
type BlamePart struct {
2023-12-01 02:26:52 +01:00
Sha string
Lines [ ] string
PreviousSha string
PreviousPath string
2019-04-20 04:47:00 +02:00
}
// BlameReader returns part of file blame one by one
type BlameReader struct {
2023-02-09 04:51:02 +01:00
output io . WriteCloser
reader io . ReadCloser
bufferedReader * bufio . Reader
done chan error
lastSha * string
2023-09-16 19:42:34 +02:00
ignoreRevsFile * string
2023-12-13 21:02:00 +00:00
objectFormat ObjectFormat
2023-09-16 19:42:34 +02:00
}
func ( r * BlameReader ) UsesIgnoreRevs ( ) bool {
return r . ignoreRevsFile != nil
2019-04-20 04:47:00 +02:00
}
2021-07-08 07:38:13 -04:00
// NextPart returns next part of blame (sequential code lines with the same commit)
2019-04-20 04:47:00 +02:00
func ( r * BlameReader ) NextPart ( ) ( * BlamePart , error ) {
var blamePart * BlamePart
if r . lastSha != nil {
2023-12-01 02:26:52 +01:00
blamePart = & BlamePart {
Sha : * r . lastSha ,
Lines : make ( [ ] string , 0 ) ,
}
2019-04-20 04:47:00 +02:00
}
2023-12-13 21:02:00 +00:00
const previousHeader = "previous "
2023-12-01 02:26:52 +01:00
var lineBytes [ ] byte
2020-11-10 02:14:02 +00:00
var isPrefix bool
var err error
for err != io . EOF {
2023-12-01 02:26:52 +01:00
lineBytes , isPrefix , err = r . bufferedReader . ReadLine ( )
2020-11-10 02:14:02 +00:00
if err != nil && err != io . EOF {
return blamePart , err
}
2019-04-20 04:47:00 +02:00
2023-12-01 02:26:52 +01:00
if len ( lineBytes ) == 0 {
2020-11-10 02:14:02 +00:00
// isPrefix will be false
2019-04-20 04:47:00 +02:00
continue
}
2023-12-13 21:02:00 +00:00
var objectID string
objectFormatLength := r . objectFormat . FullLength ( )
2019-04-20 04:47:00 +02:00
2023-12-13 21:02:00 +00:00
if len ( lineBytes ) > objectFormatLength && lineBytes [ objectFormatLength ] == ' ' && r . objectFormat . IsValid ( string ( lineBytes [ 0 : objectFormatLength ] ) ) {
objectID = string ( lineBytes [ 0 : objectFormatLength ] )
}
if len ( objectID ) > 0 {
2019-04-20 04:47:00 +02:00
if blamePart == nil {
2023-12-01 02:26:52 +01:00
blamePart = & BlamePart {
2023-12-13 21:02:00 +00:00
Sha : objectID ,
2023-12-01 02:26:52 +01:00
Lines : make ( [ ] string , 0 ) ,
}
2019-04-20 04:47:00 +02:00
}
2023-12-13 21:02:00 +00:00
if blamePart . Sha != objectID {
r . lastSha = & objectID
2020-11-10 02:14:02 +00:00
// need to munch to end of line...
for isPrefix {
2023-02-09 04:51:02 +01:00
_ , isPrefix , err = r . bufferedReader . ReadLine ( )
2020-11-10 02:14:02 +00:00
if err != nil && err != io . EOF {
return blamePart , err
}
}
2019-04-20 04:47:00 +02:00
return blamePart , nil
}
2023-12-13 21:02:00 +00:00
} else if lineBytes [ 0 ] == '\t' {
blamePart . Lines = append ( blamePart . Lines , string ( lineBytes [ 1 : ] ) )
} else if bytes . HasPrefix ( lineBytes , [ ] byte ( previousHeader ) ) {
offset := len ( previousHeader ) // already includes a space
blamePart . PreviousSha = string ( lineBytes [ offset : offset + objectFormatLength ] )
offset += objectFormatLength + 1 // +1 for space
blamePart . PreviousPath = string ( lineBytes [ offset : ] )
2020-11-10 02:14:02 +00:00
}
// need to munch to end of line...
for isPrefix {
2023-02-09 04:51:02 +01:00
_ , isPrefix , err = r . bufferedReader . ReadLine ( )
2020-11-10 02:14:02 +00:00
if err != nil && err != io . EOF {
return blamePart , err
}
2019-04-20 04:47:00 +02:00
}
}
r . lastSha = nil
return blamePart , nil
}
// Close BlameReader - don't run NextPart after invoking that
func ( r * BlameReader ) Close ( ) error {
2024-02-25 14:05:23 +01:00
if r . bufferedReader == nil {
return nil
}
2023-01-03 16:17:13 +08:00
err := <- r . done
2023-02-09 04:51:02 +01:00
r . bufferedReader = nil
2023-01-03 16:17:13 +08:00
_ = r . reader . Close ( )
2020-07-01 14:01:17 +01:00
_ = r . output . Close ( )
2023-09-16 19:42:34 +02:00
if r . ignoreRevsFile != nil {
_ = util . Remove ( * r . ignoreRevsFile )
}
2023-01-03 16:17:13 +08:00
return err
2019-04-20 04:47:00 +02:00
}
// CreateBlameReader creates reader for given repository, commit and file
2023-12-13 21:02:00 +00:00
func CreateBlameReader ( ctx context . Context , objectFormat ObjectFormat , repoPath string , commit * Commit , file string , bypassBlameIgnore bool ) ( * BlameReader , error ) {
2023-09-16 19:42:34 +02:00
var ignoreRevsFile * string
2024-05-07 00:34:16 +08:00
if DefaultFeatures ( ) . CheckVersionAtLeast ( "2.23" ) && ! bypassBlameIgnore {
2023-09-16 19:42:34 +02:00
ignoreRevsFile = tryCreateBlameIgnoreRevsFile ( commit )
}
cmd := NewCommandContextNoGlobals ( ctx , "blame" , "--porcelain" )
if ignoreRevsFile != nil {
// Possible improvement: use --ignore-revs-file /dev/stdin on unix
// There is no equivalent on Windows. May be implemented if Gitea uses an external git backend.
cmd . AddOptionValues ( "--ignore-revs-file" , * ignoreRevsFile )
}
cmd . AddDynamicArguments ( commit . ID . String ( ) ) .
2023-01-03 16:17:13 +08:00
AddDashesAndList ( file ) .
SetDescription ( fmt . Sprintf ( "GetBlame [repo_path: %s]" , repoPath ) )
reader , stdout , err := os . Pipe ( )
2019-04-20 04:47:00 +02:00
if err != nil {
2023-09-16 19:42:34 +02:00
if ignoreRevsFile != nil {
_ = util . Remove ( * ignoreRevsFile )
}
2023-01-03 16:17:13 +08:00
return nil , err
2019-04-20 04:47:00 +02:00
}
2023-01-03 16:17:13 +08:00
done := make ( chan error , 1 )
2019-04-20 04:47:00 +02:00
2023-09-16 19:42:34 +02:00
go func ( ) {
2023-08-13 10:11:20 +08:00
stderr := bytes . Buffer { }
// TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close"
err := cmd . Run ( & RunOpts {
2023-01-03 16:17:13 +08:00
UseContextTimeout : true ,
2023-09-16 19:42:34 +02:00
Dir : repoPath ,
2023-01-03 16:17:13 +08:00
Stdout : stdout ,
2023-08-13 10:11:20 +08:00
Stderr : & stderr ,
} )
2023-01-03 16:17:13 +08:00
done <- err
2023-08-13 10:11:20 +08:00
_ = stdout . Close ( )
if err != nil {
log . Error ( "Error running git blame (dir: %v): %v, stderr: %v" , repoPath , err , stderr . String ( ) )
}
2023-09-16 19:42:34 +02:00
} ( )
2019-04-20 04:47:00 +02:00
2023-02-09 04:51:02 +01:00
bufferedReader := bufio . NewReader ( reader )
2019-04-20 04:47:00 +02:00
return & BlameReader {
2023-02-09 04:51:02 +01:00
output : stdout ,
reader : reader ,
bufferedReader : bufferedReader ,
done : done ,
2023-09-16 19:42:34 +02:00
ignoreRevsFile : ignoreRevsFile ,
2023-12-13 21:02:00 +00:00
objectFormat : objectFormat ,
2019-04-20 04:47:00 +02:00
} , nil
}
2023-09-16 19:42:34 +02:00
func tryCreateBlameIgnoreRevsFile ( commit * Commit ) * string {
entry , err := commit . GetTreeEntryByPath ( ".git-blame-ignore-revs" )
if err != nil {
return nil
}
r , err := entry . Blob ( ) . DataAsync ( )
if err != nil {
return nil
}
defer r . Close ( )
f , err := os . CreateTemp ( "" , "gitea_git-blame-ignore-revs" )
if err != nil {
return nil
}
_ , err = io . Copy ( f , r )
_ = f . Close ( )
if err != nil {
_ = util . Remove ( f . Name ( ) )
return nil
}
return util . ToPointer ( f . Name ( ) )
}