gitea forge bundle: name the parent the forge used, match protection rules as the forge does, bound a commit's size and a search's time (issue 539)
The forge's contents and diffpatch endpoints take a branch, never a commit to
build on, so the parent is read from the commit the forge made, and a branch
that moved under the commit is said. Blobs are read at the commit read first,
not at the branch name. Protection rules match as Gitea 1.27 matches them
([...], {a,b}, escapes, a plain name in any case), in the Go bundle and in
guard.ts alike. One commit carries at most 4 MiB per file or patch and 16 MiB
together; code search stops at its deadline and says so. The HTTP client the
two Go bundles copied is one package, internal/forgehttp.
This commit is contained in:
@@ -32,6 +32,7 @@ type Found struct {
|
||||
Files int `json:"files_searched"`
|
||||
Skipped []string `json:"skipped,omitempty"`
|
||||
Truncated bool `json:"truncated"`
|
||||
Stopped bool `json:"stopped_at_deadline,omitempty"`
|
||||
Said []string `json:"said"`
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ func searchCode(ctx context.Context, f *Forge, a SearchAsk) (*Found, error) {
|
||||
ref := a.Ref
|
||||
if ref == "" {
|
||||
var r Repo
|
||||
if err := f.getJSON(ctx, repo, &r); err != nil {
|
||||
if err := f.GetJSON(ctx, repo, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.Empty || r.DefaultBranch == "" {
|
||||
@@ -87,7 +88,7 @@ func searchCode(ctx context.Context, f *Forge, a SearchAsk) (*Found, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
body, err := f.do(ctx, "GET", repo+"/archive/"+seg+".tar.gz", nil, maxArchive)
|
||||
body, err := f.Do(ctx, "GET", repo+"/archive/"+seg+".tar.gz", nil, maxArchive)
|
||||
if err != nil {
|
||||
if errors.Is(err, errNotFound) {
|
||||
return nil, refuse("%s/%s has no ref %s", a.Owner, a.Repo, ref)
|
||||
@@ -95,11 +96,15 @@ func searchCode(ctx context.Context, f *Forge, a SearchAsk) (*Found, error) {
|
||||
return nil, err
|
||||
}
|
||||
out := &Found{Repository: a.Owner + "/" + a.Repo, Ref: ref, Query: a.Query, Matches: []Match{}}
|
||||
if err := searchArchive(body, prefix, re, a.Max, out); err != nil {
|
||||
if err := searchArchive(ctx, body, prefix, re, a.Max, out); err != nil {
|
||||
return nil, fmt.Errorf("the archive of %s at %s could not be read: %w", out.Repository, ref, err)
|
||||
}
|
||||
said := fmt.Sprintf("%d matches in %d files searched", len(out.Matches), out.Files)
|
||||
if out.Truncated {
|
||||
switch {
|
||||
case out.Stopped:
|
||||
said = fmt.Sprintf("the search stopped at its deadline of %s after %d files, with %d matches: the rest of the "+
|
||||
"archive was not searched. Narrow with path", deadline, out.Files, len(out.Matches))
|
||||
case out.Truncated:
|
||||
said = fmt.Sprintf("the first %d matches, and more were found: narrow with path or the query", len(out.Matches))
|
||||
}
|
||||
out.Said = append(out.Said, said)
|
||||
@@ -110,18 +115,32 @@ func searchCode(ctx context.Context, f *Forge, a SearchAsk) (*Found, error) {
|
||||
}
|
||||
|
||||
// searchArchive reads a gzipped tar the forge made — every path under one top directory — and collects the
|
||||
// lines that match, from text files under the prefix.
|
||||
func searchArchive(gz []byte, prefix string, re *regexp.Regexp, max int, out *Found) error {
|
||||
// lines that match, from text files under the prefix. It stops at the call's deadline (novox/hq issue 539):
|
||||
// what it found by then is kept, and out.Stopped says the rest was not searched.
|
||||
func searchArchive(ctx context.Context, gz []byte, prefix string, re *regexp.Regexp, max int, out *Found) error {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(gz))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tr := tar.NewReader(zr)
|
||||
tr := tar.NewReader(ctxReader{ctx, zr})
|
||||
stopped := func(err error) bool {
|
||||
if ctx.Err() != nil && (err == nil || errors.Is(err, ctx.Err())) {
|
||||
out.Stopped = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
for {
|
||||
if stopped(nil) {
|
||||
return nil
|
||||
}
|
||||
h, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if stopped(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -140,6 +159,9 @@ func searchArchive(gz []byte, prefix string, re *regexp.Regexp, max int, out *Fo
|
||||
continue
|
||||
}
|
||||
content, err := io.ReadAll(io.LimitReader(tr, maxFileSearched+1))
|
||||
if stopped(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -169,3 +191,16 @@ func searchArchive(gz []byte, prefix string, re *regexp.Regexp, max int, out *Fo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ctxReader is a reader that ends when its context does: decompressing is the long part of a search.
|
||||
type ctxReader struct {
|
||||
ctx context.Context
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (c ctxReader) Read(p []byte) (int, error) {
|
||||
if err := c.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return c.r.Read(p)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user