github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/blame.go (about)

     1  // Copyright 2023 The GitBundle Inc. All rights reserved.
     2  // Copyright 2017 The Gitea Authors. All rights reserved.
     3  // Use of this source code is governed by a MIT-style
     4  // license that can be found in the LICENSE file.
     5  
     6  package git
     7  
     8  import (
     9  	"bufio"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"os/exec"
    15  	"regexp"
    16  
    17  	"github.com/gitbundle/modules/process"
    18  )
    19  
    20  // BlamePart represents block of blame - continuous lines with one sha
    21  type BlamePart struct {
    22  	Sha   string
    23  	Lines []string
    24  }
    25  
    26  // BlameReader returns part of file blame one by one
    27  type BlameReader struct {
    28  	cmd      *exec.Cmd
    29  	output   io.ReadCloser
    30  	reader   *bufio.Reader
    31  	lastSha  *string
    32  	cancel   context.CancelFunc   // Cancels the context that this reader runs in
    33  	finished process.FinishedFunc // Tells the process manager we're finished and it can remove the associated process from the process table
    34  }
    35  
    36  var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
    37  
    38  // NextPart returns next part of blame (sequential code lines with the same commit)
    39  func (r *BlameReader) NextPart() (*BlamePart, error) {
    40  	var blamePart *BlamePart
    41  
    42  	reader := r.reader
    43  
    44  	if r.lastSha != nil {
    45  		blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
    46  	}
    47  
    48  	var line []byte
    49  	var isPrefix bool
    50  	var err error
    51  
    52  	for err != io.EOF {
    53  		line, isPrefix, err = reader.ReadLine()
    54  		if err != nil && err != io.EOF {
    55  			return blamePart, err
    56  		}
    57  
    58  		if len(line) == 0 {
    59  			// isPrefix will be false
    60  			continue
    61  		}
    62  
    63  		lines := shaLineRegex.FindSubmatch(line)
    64  		if lines != nil {
    65  			sha1 := string(lines[1])
    66  
    67  			if blamePart == nil {
    68  				blamePart = &BlamePart{sha1, make([]string, 0)}
    69  			}
    70  
    71  			if blamePart.Sha != sha1 {
    72  				r.lastSha = &sha1
    73  				// need to munch to end of line...
    74  				for isPrefix {
    75  					_, isPrefix, err = reader.ReadLine()
    76  					if err != nil && err != io.EOF {
    77  						return blamePart, err
    78  					}
    79  				}
    80  				return blamePart, nil
    81  			}
    82  		} else if line[0] == '\t' {
    83  			code := line[1:]
    84  
    85  			blamePart.Lines = append(blamePart.Lines, string(code))
    86  		}
    87  
    88  		// need to munch to end of line...
    89  		for isPrefix {
    90  			_, isPrefix, err = reader.ReadLine()
    91  			if err != nil && err != io.EOF {
    92  				return blamePart, err
    93  			}
    94  		}
    95  	}
    96  
    97  	r.lastSha = nil
    98  
    99  	return blamePart, nil
   100  }
   101  
   102  // Close BlameReader - don't run NextPart after invoking that
   103  func (r *BlameReader) Close() error {
   104  	defer r.finished() // Only remove the process from the process table when the underlying command is closed
   105  	r.cancel()         // However, first cancel our own context early
   106  
   107  	_ = r.output.Close()
   108  
   109  	if err := r.cmd.Wait(); err != nil {
   110  		return fmt.Errorf("Wait: %v", err)
   111  	}
   112  
   113  	return nil
   114  }
   115  
   116  // CreateBlameReader creates reader for given repository, commit and file
   117  func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*BlameReader, error) {
   118  	return createBlameReader(ctx, repoPath, GitExecutable, "blame", commitID, "--porcelain", "--", file)
   119  }
   120  
   121  func createBlameReader(ctx context.Context, dir string, command ...string) (*BlameReader, error) {
   122  	// Here we use the provided context - this should be tied to the request performing the blame so that it does not hang around.
   123  	ctx, cancel, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("GetBlame [repo_path: %s]", dir))
   124  
   125  	cmd := exec.CommandContext(ctx, command[0], command[1:]...)
   126  	cmd.Dir = dir
   127  	cmd.Stderr = os.Stderr
   128  	process.SetSysProcAttribute(cmd)
   129  
   130  	stdout, err := cmd.StdoutPipe()
   131  	if err != nil {
   132  		defer finished()
   133  		return nil, fmt.Errorf("StdoutPipe: %v", err)
   134  	}
   135  
   136  	if err = cmd.Start(); err != nil {
   137  		defer finished()
   138  		_ = stdout.Close()
   139  		return nil, fmt.Errorf("Start: %v", err)
   140  	}
   141  
   142  	reader := bufio.NewReader(stdout)
   143  
   144  	return &BlameReader{
   145  		cmd:      cmd,
   146  		output:   stdout,
   147  		reader:   reader,
   148  		cancel:   cancel,
   149  		finished: finished,
   150  	}, nil
   151  }