mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-22 09:01:25 +00:00
before: gitrepo vs git packages after: git package fully handle all git operations by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide path details. benefits: 1. remove all unnecessary wrappers, developers no need to struggle with "which package should be used" 2. simplify code, RepositoryFacade can (will) be used everywhere, all "path" details are (will be) hidden
210 lines
4.7 KiB
Go
210 lines
4.7 KiB
Go
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package attribute
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/git/gitcmd"
|
|
"gitea.dev/modules/log"
|
|
)
|
|
|
|
// BatchChecker provides a reader for check-attribute content that can be long running
|
|
type BatchChecker struct {
|
|
attributesNum int
|
|
repo *git.Repository
|
|
stdinWriter io.WriteCloser
|
|
stdOut *nulSeparatedAttributeWriter
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
cmd *gitcmd.Command
|
|
}
|
|
|
|
// NewBatchChecker creates a check attribute reader for the current repository and provided commit ID
|
|
// If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo
|
|
func NewBatchChecker(ctx context.Context, repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer func() {
|
|
if returnedErr != nil {
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
cmd, envs, cleanup, err := checkAttrCommand(ctx, repo, treeish, nil, attributes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if returnedErr != nil {
|
|
cleanup()
|
|
}
|
|
}()
|
|
|
|
cmd.AddArguments("--stdin")
|
|
|
|
checker = &BatchChecker{
|
|
attributesNum: len(attributes),
|
|
repo: repo,
|
|
ctx: ctx,
|
|
cmd: cmd,
|
|
cancel: func() {
|
|
cancel()
|
|
cleanup()
|
|
},
|
|
}
|
|
|
|
stdinWriter, stdinWriterClose := cmd.MakeStdinPipe()
|
|
checker.stdinWriter = stdinWriter
|
|
|
|
lw := new(nulSeparatedAttributeWriter)
|
|
lw.attributes = make(chan attributeTriple, len(attributes))
|
|
lw.closed = make(chan struct{})
|
|
checker.stdOut = lw
|
|
|
|
cmd.WithEnv(envs).
|
|
WithRepo(repo).
|
|
WithStdoutCopy(lw)
|
|
|
|
go func() {
|
|
defer stdinWriterClose()
|
|
defer checker.cancel()
|
|
defer lw.Close()
|
|
|
|
err := cmd.RunWithStderr(ctx)
|
|
if err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
|
log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
|
|
}
|
|
}()
|
|
|
|
return checker, nil
|
|
}
|
|
|
|
// CheckPath check attr for given path
|
|
func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
|
|
defer func() {
|
|
if err != nil && err != c.ctx.Err() {
|
|
log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.repo.Path), err)
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-c.ctx.Done():
|
|
return nil, c.ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
|
|
defer c.Close()
|
|
return nil, err
|
|
}
|
|
|
|
reportTimeout := func() error {
|
|
stdOutClosed := false
|
|
select {
|
|
case <-c.stdOut.closed:
|
|
stdOutClosed = true
|
|
default:
|
|
}
|
|
debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.repo.Path))
|
|
debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
|
|
if c.cmd != nil {
|
|
debugMsg += fmt.Sprintf(", process state: %q", c.cmd.ProcessState())
|
|
}
|
|
_ = c.Close()
|
|
return fmt.Errorf("CheckPath timeout: %s", debugMsg)
|
|
}
|
|
|
|
rs = NewAttributes()
|
|
for i := 0; i < c.attributesNum; i++ {
|
|
select {
|
|
case <-time.After(5 * time.Second):
|
|
// there is no "hang" problem now. This code is just used to catch other potential problems.
|
|
return nil, reportTimeout()
|
|
case attr, ok := <-c.stdOut.ReadAttribute():
|
|
if !ok {
|
|
return nil, c.ctx.Err()
|
|
}
|
|
rs.m[attr.Attribute] = Attribute(attr.Value)
|
|
case <-c.ctx.Done():
|
|
return nil, c.ctx.Err()
|
|
}
|
|
}
|
|
return rs, nil
|
|
}
|
|
|
|
func (c *BatchChecker) Close() error {
|
|
c.cancel()
|
|
err := c.stdinWriter.Close()
|
|
return err
|
|
}
|
|
|
|
type attributeTriple struct {
|
|
Filename string
|
|
Attribute string
|
|
Value string
|
|
}
|
|
|
|
type nulSeparatedAttributeWriter struct {
|
|
tmp []byte
|
|
attributes chan attributeTriple
|
|
closed chan struct{}
|
|
working attributeTriple
|
|
pos int
|
|
}
|
|
|
|
func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
|
|
l, read := len(p), 0
|
|
|
|
nulIdx := bytes.IndexByte(p, '\x00')
|
|
for nulIdx >= 0 {
|
|
wr.tmp = append(wr.tmp, p[:nulIdx]...)
|
|
switch wr.pos {
|
|
case 0:
|
|
wr.working = attributeTriple{
|
|
Filename: string(wr.tmp),
|
|
}
|
|
case 1:
|
|
wr.working.Attribute = string(wr.tmp)
|
|
case 2:
|
|
wr.working.Value = string(wr.tmp)
|
|
}
|
|
wr.tmp = wr.tmp[:0]
|
|
wr.pos++
|
|
if wr.pos > 2 {
|
|
wr.attributes <- wr.working
|
|
wr.pos = 0
|
|
}
|
|
read += nulIdx + 1
|
|
if l > read {
|
|
p = p[nulIdx+1:]
|
|
nulIdx = bytes.IndexByte(p, '\x00')
|
|
} else {
|
|
return l, nil
|
|
}
|
|
}
|
|
wr.tmp = append(wr.tmp, p...)
|
|
return l, nil
|
|
}
|
|
|
|
func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
|
|
return wr.attributes
|
|
}
|
|
|
|
func (wr *nulSeparatedAttributeWriter) Close() error {
|
|
select {
|
|
case <-wr.closed:
|
|
return nil
|
|
default:
|
|
}
|
|
close(wr.attributes)
|
|
close(wr.closed)
|
|
return nil
|
|
}
|