code.gitea.io/gitea@v1.19.3/modules/git/repo_attribute.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package git 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "io" 11 "os" 12 13 "code.gitea.io/gitea/modules/log" 14 ) 15 16 // CheckAttributeOpts represents the possible options to CheckAttribute 17 type CheckAttributeOpts struct { 18 CachedOnly bool 19 AllAttributes bool 20 Attributes []string 21 Filenames []string 22 IndexFile string 23 WorkTree string 24 } 25 26 // CheckAttribute return the Blame object of file 27 func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { 28 env := []string{} 29 30 if len(opts.IndexFile) > 0 { 31 env = append(env, "GIT_INDEX_FILE="+opts.IndexFile) 32 } 33 if len(opts.WorkTree) > 0 { 34 env = append(env, "GIT_WORK_TREE="+opts.WorkTree) 35 } 36 37 if len(env) > 0 { 38 env = append(os.Environ(), env...) 39 } 40 41 stdOut := new(bytes.Buffer) 42 stdErr := new(bytes.Buffer) 43 44 cmd := NewCommand(repo.Ctx, "check-attr", "-z") 45 46 if opts.AllAttributes { 47 cmd.AddArguments("-a") 48 } else { 49 for _, attribute := range opts.Attributes { 50 if attribute != "" { 51 cmd.AddDynamicArguments(attribute) 52 } 53 } 54 } 55 56 if opts.CachedOnly { 57 cmd.AddArguments("--cached") 58 } 59 60 cmd.AddDashesAndList(opts.Filenames...) 61 62 if err := cmd.Run(&RunOpts{ 63 Env: env, 64 Dir: repo.Path, 65 Stdout: stdOut, 66 Stderr: stdErr, 67 }); err != nil { 68 return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) 69 } 70 71 // FIXME: This is incorrect on versions < 1.8.5 72 fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) 73 74 if len(fields)%3 != 1 { 75 return nil, fmt.Errorf("wrong number of fields in return from check-attr") 76 } 77 78 name2attribute2info := make(map[string]map[string]string) 79 80 for i := 0; i < (len(fields) / 3); i++ { 81 filename := string(fields[3*i]) 82 attribute := string(fields[3*i+1]) 83 info := string(fields[3*i+2]) 84 attribute2info := name2attribute2info[filename] 85 if attribute2info == nil { 86 attribute2info = make(map[string]string) 87 } 88 attribute2info[attribute] = info 89 name2attribute2info[filename] = attribute2info 90 } 91 92 return name2attribute2info, nil 93 } 94 95 // CheckAttributeReader provides a reader for check-attribute content that can be long running 96 type CheckAttributeReader struct { 97 // params 98 Attributes []string 99 Repo *Repository 100 IndexFile string 101 WorkTree string 102 103 stdinReader io.ReadCloser 104 stdinWriter *os.File 105 stdOut attributeWriter 106 cmd *Command 107 env []string 108 ctx context.Context 109 cancel context.CancelFunc 110 } 111 112 // Init initializes the CheckAttributeReader 113 func (c *CheckAttributeReader) Init(ctx context.Context) error { 114 if len(c.Attributes) == 0 { 115 lw := new(nulSeparatedAttributeWriter) 116 lw.attributes = make(chan attributeTriple) 117 lw.closed = make(chan struct{}) 118 119 c.stdOut = lw 120 c.stdOut.Close() 121 return fmt.Errorf("no provided Attributes to check") 122 } 123 124 c.ctx, c.cancel = context.WithCancel(ctx) 125 c.cmd = NewCommand(c.ctx, "check-attr", "--stdin", "-z") 126 127 if len(c.IndexFile) > 0 { 128 c.cmd.AddArguments("--cached") 129 c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile) 130 } 131 132 if len(c.WorkTree) > 0 { 133 c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree) 134 } 135 136 c.env = append(c.env, "GIT_FLUSH=1") 137 138 c.cmd.AddDynamicArguments(c.Attributes...) 139 140 var err error 141 142 c.stdinReader, c.stdinWriter, err = os.Pipe() 143 if err != nil { 144 c.cancel() 145 return err 146 } 147 148 lw := new(nulSeparatedAttributeWriter) 149 lw.attributes = make(chan attributeTriple, 5) 150 lw.closed = make(chan struct{}) 151 c.stdOut = lw 152 return nil 153 } 154 155 // Run run cmd 156 func (c *CheckAttributeReader) Run() error { 157 defer func() { 158 _ = c.stdinReader.Close() 159 _ = c.stdOut.Close() 160 }() 161 stdErr := new(bytes.Buffer) 162 err := c.cmd.Run(&RunOpts{ 163 Env: c.env, 164 Dir: c.Repo.Path, 165 Stdin: c.stdinReader, 166 Stdout: c.stdOut, 167 Stderr: stdErr, 168 }) 169 if err != nil && // If there is an error we need to return but: 170 c.ctx.Err() != err && // 1. Ignore the context error if the context is cancelled or exceeds the deadline (RunWithContext could return c.ctx.Err() which is Canceled or DeadlineExceeded) 171 err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed 172 return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) 173 } 174 return nil 175 } 176 177 // CheckPath check attr for given path 178 func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) { 179 defer func() { 180 if err != nil && err != c.ctx.Err() { 181 log.Error("Unexpected error when checking path %s in %s. Error: %v", path, c.Repo.Path, err) 182 } 183 }() 184 185 select { 186 case <-c.ctx.Done(): 187 return nil, c.ctx.Err() 188 default: 189 } 190 191 if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { 192 defer c.Close() 193 return nil, err 194 } 195 196 rs = make(map[string]string) 197 for range c.Attributes { 198 select { 199 case attr, ok := <-c.stdOut.ReadAttribute(): 200 if !ok { 201 return nil, c.ctx.Err() 202 } 203 rs[attr.Attribute] = attr.Value 204 case <-c.ctx.Done(): 205 return nil, c.ctx.Err() 206 } 207 } 208 return rs, nil 209 } 210 211 // Close close pip after use 212 func (c *CheckAttributeReader) Close() error { 213 c.cancel() 214 err := c.stdinWriter.Close() 215 return err 216 } 217 218 type attributeWriter interface { 219 io.WriteCloser 220 ReadAttribute() <-chan attributeTriple 221 } 222 223 type attributeTriple struct { 224 Filename string 225 Attribute string 226 Value string 227 } 228 229 type nulSeparatedAttributeWriter struct { 230 tmp []byte 231 attributes chan attributeTriple 232 closed chan struct{} 233 working attributeTriple 234 pos int 235 } 236 237 func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { 238 l, read := len(p), 0 239 240 nulIdx := bytes.IndexByte(p, '\x00') 241 for nulIdx >= 0 { 242 wr.tmp = append(wr.tmp, p[:nulIdx]...) 243 switch wr.pos { 244 case 0: 245 wr.working = attributeTriple{ 246 Filename: string(wr.tmp), 247 } 248 case 1: 249 wr.working.Attribute = string(wr.tmp) 250 case 2: 251 wr.working.Value = string(wr.tmp) 252 } 253 wr.tmp = wr.tmp[:0] 254 wr.pos++ 255 if wr.pos > 2 { 256 wr.attributes <- wr.working 257 wr.pos = 0 258 } 259 read += nulIdx + 1 260 if l > read { 261 p = p[nulIdx+1:] 262 nulIdx = bytes.IndexByte(p, '\x00') 263 } else { 264 return l, nil 265 } 266 } 267 wr.tmp = append(wr.tmp, p...) 268 return len(p), nil 269 } 270 271 func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { 272 return wr.attributes 273 } 274 275 func (wr *nulSeparatedAttributeWriter) Close() error { 276 select { 277 case <-wr.closed: 278 return nil 279 default: 280 } 281 close(wr.attributes) 282 close(wr.closed) 283 return nil 284 } 285 286 // Create a check attribute reader for the current repository and provided commit ID 287 func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { 288 indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) 289 if err != nil { 290 return nil, func() {} 291 } 292 293 checker := &CheckAttributeReader{ 294 Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, 295 Repo: repo, 296 IndexFile: indexFilename, 297 WorkTree: worktree, 298 } 299 ctx, cancel := context.WithCancel(repo.Ctx) 300 if err := checker.Init(ctx); err != nil { 301 log.Error("Unable to open checker for %s. Error: %v", commitID, err) 302 } else { 303 go func() { 304 err := checker.Run() 305 if err != nil && err != ctx.Err() { 306 log.Error("Unable to open checker for %s. Error: %v", commitID, err) 307 } 308 cancel() 309 }() 310 } 311 deferable := func() { 312 _ = checker.Close() 313 cancel() 314 deleteTemporaryFile() 315 } 316 317 return checker, deferable 318 }