mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
210 lines
4.7 KiB
Go
210 lines
4.7 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package attribute
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/git/gitcmd"
|
|
"github.com/hanzoai/git/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(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) {
|
|
ctx, cancel := context.WithCancel(repo.Ctx)
|
|
defer func() {
|
|
if returnedErr != nil {
|
|
cancel()
|
|
}
|
|
}()
|
|
|
|
cmd, envs, cleanup, err := checkAttrCommand(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).
|
|
WithDir(repo.Path).
|
|
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
|
|
}
|