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 }