github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/git/repo_attribute.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 "bytes" 10 "context" 11 "fmt" 12 "io" 13 "os" 14 "strconv" 15 "strings" 16 17 "github.com/gitbundle/modules/log" 18 ) 19 20 // CheckAttributeOpts represents the possible options to CheckAttribute 21 type CheckAttributeOpts struct { 22 CachedOnly bool 23 AllAttributes bool 24 Attributes []string 25 Filenames []string 26 IndexFile string 27 WorkTree string 28 } 29 30 // CheckAttribute return the Blame object of file 31 func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) { 32 env := []string{} 33 34 if len(opts.IndexFile) > 0 { 35 env = append(env, "GIT_INDEX_FILE="+opts.IndexFile) 36 } 37 if len(opts.WorkTree) > 0 { 38 env = append(env, "GIT_WORK_TREE="+opts.WorkTree) 39 } 40 41 if len(env) > 0 { 42 env = append(os.Environ(), env...) 43 } 44 45 stdOut := new(bytes.Buffer) 46 stdErr := new(bytes.Buffer) 47 48 cmdArgs := []string{"check-attr", "-z"} 49 50 if opts.AllAttributes { 51 cmdArgs = append(cmdArgs, "-a") 52 } else { 53 for _, attribute := range opts.Attributes { 54 if attribute != "" { 55 cmdArgs = append(cmdArgs, attribute) 56 } 57 } 58 } 59 60 if opts.CachedOnly { 61 cmdArgs = append(cmdArgs, "--cached") 62 } 63 64 cmdArgs = append(cmdArgs, "--") 65 66 for _, arg := range opts.Filenames { 67 if arg != "" { 68 cmdArgs = append(cmdArgs, arg) 69 } 70 } 71 72 cmd := NewCommand(repo.Ctx, cmdArgs...) 73 74 if err := cmd.Run(&RunOpts{ 75 Env: env, 76 Dir: repo.Path, 77 Stdout: stdOut, 78 Stderr: stdErr, 79 }); err != nil { 80 return nil, fmt.Errorf("failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String()) 81 } 82 83 // FIXME: This is incorrect on versions < 1.8.5 84 fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) 85 86 if len(fields)%3 != 1 { 87 return nil, fmt.Errorf("wrong number of fields in return from check-attr") 88 } 89 90 name2attribute2info := make(map[string]map[string]string) 91 92 for i := 0; i < (len(fields) / 3); i++ { 93 filename := string(fields[3*i]) 94 attribute := string(fields[3*i+1]) 95 info := string(fields[3*i+2]) 96 attribute2info := name2attribute2info[filename] 97 if attribute2info == nil { 98 attribute2info = make(map[string]string) 99 } 100 attribute2info[attribute] = info 101 name2attribute2info[filename] = attribute2info 102 } 103 104 return name2attribute2info, nil 105 } 106 107 // CheckAttributeReader provides a reader for check-attribute content that can be long running 108 type CheckAttributeReader struct { 109 // params 110 Attributes []string 111 Repo *Repository 112 IndexFile string 113 WorkTree string 114 115 stdinReader io.ReadCloser 116 stdinWriter *os.File 117 stdOut attributeWriter 118 cmd *Command 119 env []string 120 ctx context.Context 121 cancel context.CancelFunc 122 } 123 124 // Init initializes the CheckAttributeReader 125 func (c *CheckAttributeReader) Init(ctx context.Context) error { 126 cmdArgs := []string{"check-attr", "--stdin", "-z"} 127 128 if len(c.IndexFile) > 0 { 129 cmdArgs = append(cmdArgs, "--cached") 130 c.env = append(c.env, "GIT_INDEX_FILE="+c.IndexFile) 131 } 132 133 if len(c.WorkTree) > 0 { 134 c.env = append(c.env, "GIT_WORK_TREE="+c.WorkTree) 135 } 136 137 c.env = append(c.env, "GIT_FLUSH=1") 138 139 if len(c.Attributes) == 0 { 140 lw := new(nulSeparatedAttributeWriter) 141 lw.attributes = make(chan attributeTriple) 142 lw.closed = make(chan struct{}) 143 144 c.stdOut = lw 145 c.stdOut.Close() 146 return fmt.Errorf("no provided Attributes to check") 147 } 148 149 cmdArgs = append(cmdArgs, c.Attributes...) 150 cmdArgs = append(cmdArgs, "--") 151 152 c.ctx, c.cancel = context.WithCancel(ctx) 153 c.cmd = NewCommand(c.ctx, cmdArgs...) 154 155 var err error 156 157 c.stdinReader, c.stdinWriter, err = os.Pipe() 158 if err != nil { 159 c.cancel() 160 return err 161 } 162 163 lw := new(nulSeparatedAttributeWriter) 164 lw.attributes = make(chan attributeTriple, 5) 165 lw.closed = make(chan struct{}) 166 c.stdOut = lw 167 return nil 168 } 169 170 // Run run cmd 171 func (c *CheckAttributeReader) Run() error { 172 defer func() { 173 _ = c.stdinReader.Close() 174 _ = c.stdOut.Close() 175 }() 176 stdErr := new(bytes.Buffer) 177 err := c.cmd.Run(&RunOpts{ 178 Env: c.env, 179 Dir: c.Repo.Path, 180 Stdin: c.stdinReader, 181 Stdout: c.stdOut, 182 Stderr: stdErr, 183 }) 184 if err != nil && // If there is an error we need to return but: 185 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) 186 err.Error() != "signal: killed" { // 2. We should not pass up errors due to the program being killed 187 return fmt.Errorf("failed to run attr-check. Error: %w\nStderr: %s", err, stdErr.String()) 188 } 189 return nil 190 } 191 192 // CheckPath check attr for given path 193 func (c *CheckAttributeReader) CheckPath(path string) (rs map[string]string, err error) { 194 defer func() { 195 if err != nil { 196 log.Error("CheckPath returns error: %v", err) 197 } 198 }() 199 200 select { 201 case <-c.ctx.Done(): 202 return nil, c.ctx.Err() 203 default: 204 } 205 206 if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil { 207 defer c.Close() 208 return nil, err 209 } 210 211 rs = make(map[string]string) 212 for range c.Attributes { 213 select { 214 case attr, ok := <-c.stdOut.ReadAttribute(): 215 if !ok { 216 return nil, c.ctx.Err() 217 } 218 rs[attr.Attribute] = attr.Value 219 case <-c.ctx.Done(): 220 return nil, c.ctx.Err() 221 } 222 } 223 return rs, nil 224 } 225 226 // Close close pip after use 227 func (c *CheckAttributeReader) Close() error { 228 c.cancel() 229 err := c.stdinWriter.Close() 230 return err 231 } 232 233 type attributeWriter interface { 234 io.WriteCloser 235 ReadAttribute() <-chan attributeTriple 236 } 237 238 type attributeTriple struct { 239 Filename string 240 Attribute string 241 Value string 242 } 243 244 type nulSeparatedAttributeWriter struct { 245 tmp []byte 246 attributes chan attributeTriple 247 closed chan struct{} 248 working attributeTriple 249 pos int 250 } 251 252 func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) { 253 l, read := len(p), 0 254 255 nulIdx := bytes.IndexByte(p, '\x00') 256 for nulIdx >= 0 { 257 wr.tmp = append(wr.tmp, p[:nulIdx]...) 258 switch wr.pos { 259 case 0: 260 wr.working = attributeTriple{ 261 Filename: string(wr.tmp), 262 } 263 case 1: 264 wr.working.Attribute = string(wr.tmp) 265 case 2: 266 wr.working.Value = string(wr.tmp) 267 } 268 wr.tmp = wr.tmp[:0] 269 wr.pos++ 270 if wr.pos > 2 { 271 wr.attributes <- wr.working 272 wr.pos = 0 273 } 274 read += nulIdx + 1 275 if l > read { 276 p = p[nulIdx+1:] 277 nulIdx = bytes.IndexByte(p, '\x00') 278 } else { 279 return l, nil 280 } 281 } 282 wr.tmp = append(wr.tmp, p...) 283 return len(p), nil 284 } 285 286 func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { 287 return wr.attributes 288 } 289 290 func (wr *nulSeparatedAttributeWriter) Close() error { 291 select { 292 case <-wr.closed: 293 return nil 294 default: 295 } 296 close(wr.attributes) 297 close(wr.closed) 298 return nil 299 } 300 301 type lineSeparatedAttributeWriter struct { 302 tmp []byte 303 attributes chan attributeTriple 304 closed chan struct{} 305 } 306 307 func (wr *lineSeparatedAttributeWriter) Write(p []byte) (n int, err error) { 308 l := len(p) 309 310 nlIdx := bytes.IndexByte(p, '\n') 311 for nlIdx >= 0 { 312 wr.tmp = append(wr.tmp, p[:nlIdx]...) 313 314 if len(wr.tmp) == 0 { 315 // This should not happen 316 if len(p) > nlIdx+1 { 317 wr.tmp = wr.tmp[:0] 318 p = p[nlIdx+1:] 319 nlIdx = bytes.IndexByte(p, '\n') 320 continue 321 } else { 322 return l, nil 323 } 324 } 325 326 working := attributeTriple{} 327 if wr.tmp[0] == '"' { 328 sb := new(strings.Builder) 329 remaining := string(wr.tmp[1:]) 330 for len(remaining) > 0 { 331 rn, _, tail, err := strconv.UnquoteChar(remaining, '"') 332 if err != nil { 333 if len(remaining) > 2 && remaining[0] == '"' && remaining[1] == ':' && remaining[2] == ' ' { 334 working.Filename = sb.String() 335 wr.tmp = []byte(remaining[3:]) 336 break 337 } 338 return l, fmt.Errorf("unexpected tail %s", string(remaining)) 339 } 340 _, _ = sb.WriteRune(rn) 341 remaining = tail 342 } 343 } else { 344 idx := bytes.IndexByte(wr.tmp, ':') 345 if idx < 0 { 346 return l, fmt.Errorf("unexpected input %s", string(wr.tmp)) 347 } 348 working.Filename = string(wr.tmp[:idx]) 349 if len(wr.tmp) < idx+2 { 350 return l, fmt.Errorf("unexpected input %s", string(wr.tmp)) 351 } 352 wr.tmp = wr.tmp[idx+2:] 353 } 354 355 idx := bytes.IndexByte(wr.tmp, ':') 356 if idx < 0 { 357 return l, fmt.Errorf("unexpected input %s", string(wr.tmp)) 358 } 359 360 working.Attribute = string(wr.tmp[:idx]) 361 if len(wr.tmp) < idx+2 { 362 return l, fmt.Errorf("unexpected input %s", string(wr.tmp)) 363 } 364 365 working.Value = string(wr.tmp[idx+2:]) 366 367 wr.attributes <- working 368 wr.tmp = wr.tmp[:0] 369 if len(p) > nlIdx+1 { 370 p = p[nlIdx+1:] 371 nlIdx = bytes.IndexByte(p, '\n') 372 continue 373 } else { 374 return l, nil 375 } 376 } 377 378 wr.tmp = append(wr.tmp, p...) 379 return l, nil 380 } 381 382 func (wr *lineSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple { 383 return wr.attributes 384 } 385 386 func (wr *lineSeparatedAttributeWriter) Close() error { 387 select { 388 case <-wr.closed: 389 return nil 390 default: 391 } 392 close(wr.attributes) 393 close(wr.closed) 394 return nil 395 } 396 397 // Create a check attribute reader for the current repository and provided commit ID 398 func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeReader, context.CancelFunc) { 399 indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID) 400 if err != nil { 401 return nil, func() {} 402 } 403 404 checker := &CheckAttributeReader{ 405 Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, 406 Repo: repo, 407 IndexFile: indexFilename, 408 WorkTree: worktree, 409 } 410 ctx, cancel := context.WithCancel(repo.Ctx) 411 if err := checker.Init(ctx); err != nil { 412 log.Error("Unable to open checker for %s. Error: %v", commitID, err) 413 } else { 414 go func() { 415 err := checker.Run() 416 if err != nil && err != ctx.Err() { 417 log.Error("Unable to open checker for %s. Error: %v", commitID, err) 418 } 419 cancel() 420 }() 421 } 422 deferable := func() { 423 _ = checker.Close() 424 cancel() 425 deleteTemporaryFile() 426 } 427 428 return checker, deferable 429 }