gitea/routers/api/v2/batch.go
logikonline dfc94f6408
Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Build Docker Image (push) Blocked by required conditions
Build and Release / Create Release (push) Blocked by required conditions
Build and Release / Lint and Test (push) Has been cancelled
fix: resolve all golangci-lint errors in v2 API
- Replace encoding/json with modules/json (depguard)
- Add error handling for json.Unmarshal and WriteItem calls (errcheck)
- Use slices.Contains instead of manual loops (modernize)
- Use any instead of interface{} (modernize)
- Use min/max built-in functions (modernize)
- Use strings.FieldsSeq and strings.SplitSeq (modernize)
- Use range over int for loops (modernize)
- Use http.MethodOptions constant (usestdlibvars)
- Use tagged switch statements (staticcheck)
- Use += and /= operators (gocritic)
- Fix gofumpt formatting issues
- Remove unused streamLargeFile function
- Remove unused primaryLang parameter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 15:49:52 -05:00

274 lines
7.1 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"net/http"
repo_model "code.gitea.io/gitea/models/repo"
apierrors "code.gitea.io/gitea/modules/errors"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/services/context"
)
// BatchFileRequest represents a request to get multiple files
type BatchFileRequest struct {
Owner string `json:"owner" binding:"Required"`
Repo string `json:"repo" binding:"Required"`
Ref string `json:"ref"`
Paths []string `json:"paths" binding:"Required"`
Format string `json:"format"` // "content" or "metadata"
}
// BatchFileResult represents the result for a single file in batch
type BatchFileResult struct {
Path string `json:"path"`
Content string `json:"content,omitempty"`
Encoding string `json:"encoding,omitempty"`
SHA string `json:"sha,omitempty"`
Size int64 `json:"size,omitempty"`
Type string `json:"type,omitempty"`
Error string `json:"error,omitempty"`
}
// BatchFileResponse represents the response for batch file retrieval
type BatchFileResponse struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Ref string `json:"ref"`
Results []BatchFileResult `json:"results"`
}
// BatchGetFiles retrieves multiple files in a single request
// This is optimized for AI tools that need to fetch multiple files at once
func BatchGetFiles(ctx *context.APIContext) {
var req BatchFileRequest
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "body",
"error": err.Error(),
})
return
}
// Validate path count (limit to prevent abuse)
if len(req.Paths) > 100 {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "paths",
"message": "maximum 100 paths per request",
"count": len(req.Paths),
})
return
}
if len(req.Paths) == 0 {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "paths",
"message": "at least one path is required",
})
return
}
// Get repository
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, req.Owner, req.Repo)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorWithCode(apierrors.RepoNotFound, map[string]any{
"owner": req.Owner,
"repo": req.Repo,
})
return
}
ctx.APIErrorWithCode(apierrors.InternalError, map[string]any{
"error": err.Error(),
})
return
}
// Check access (basic check - user must be signed in or repo is public)
if repo.IsPrivate && !ctx.IsSigned {
ctx.APIErrorWithCode(apierrors.PermAccessDenied)
return
}
// Open git repo
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
if err != nil {
ctx.APIErrorWithCode(apierrors.InternalError, map[string]any{
"error": "failed to open repository",
})
return
}
defer gitRepo.Close()
// Determine ref
ref := req.Ref
if ref == "" {
ref = repo.DefaultBranch
}
// Get commit for ref
commit, err := gitRepo.GetCommit(ref)
if err != nil {
// Try as branch
commit, err = gitRepo.GetBranchCommit(ref)
if err != nil {
// Try as tag
commit, err = gitRepo.GetTagCommit(ref)
if err != nil {
ctx.APIErrorWithCode(apierrors.RefNotFound, map[string]any{
"ref": ref,
})
return
}
}
}
// Fetch each file
results := make([]BatchFileResult, 0, len(req.Paths))
metadataOnly := req.Format == "metadata"
for _, path := range req.Paths {
result := BatchFileResult{Path: path}
entry, err := commit.GetTreeEntryByPath(path)
if err != nil {
result.Error = "file not found"
results = append(results, result)
continue
}
result.SHA = entry.ID.String()
result.Size = entry.Size()
result.Type = entry.Mode().String()
if !metadataOnly && !entry.IsDir() {
// Get file content (limit size to prevent memory issues)
if entry.Size() > 10*1024*1024 { // 10MB limit
result.Error = "file too large (>10MB)"
} else {
blob := entry.Blob()
reader, err := blob.DataAsync()
if err != nil {
result.Error = "failed to read file content"
} else {
defer reader.Close()
content := make([]byte, entry.Size())
_, err = reader.Read(content)
if err != nil && err.Error() != "EOF" {
result.Error = "failed to read file content"
} else {
result.Content = string(content)
result.Encoding = "utf-8"
}
}
}
}
results = append(results, result)
}
ctx.JSON(http.StatusOK, BatchFileResponse{
Owner: req.Owner,
Repo: req.Repo,
Ref: ref,
Results: results,
})
}
// BatchRepoRequest represents a request to get info about multiple repos
type BatchRepoRequest struct {
Repos []struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
} `json:"repos" binding:"Required"`
Fields []string `json:"fields"` // Which fields to include
}
// BatchRepoResult represents the result for a single repo in batch
type BatchRepoResult struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
FullName string `json:"full_name,omitempty"`
Description string `json:"description,omitempty"`
Private bool `json:"private,omitempty"`
Fork bool `json:"fork,omitempty"`
Stars int `json:"stars,omitempty"`
Forks int `json:"forks,omitempty"`
Language string `json:"language,omitempty"`
Error string `json:"error,omitempty"`
}
// BatchGetRepos retrieves information about multiple repositories
func BatchGetRepos(ctx *context.APIContext) {
var req BatchRepoRequest
if err := json.NewDecoder(ctx.Req.Body).Decode(&req); err != nil {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "body",
"error": err.Error(),
})
return
}
// Validate repo count
if len(req.Repos) > 50 {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "repos",
"message": "maximum 50 repositories per request",
"count": len(req.Repos),
})
return
}
if len(req.Repos) == 0 {
ctx.APIErrorWithCode(apierrors.ValidationFailed, map[string]any{
"field": "repos",
"message": "at least one repository is required",
})
return
}
results := make([]BatchRepoResult, 0, len(req.Repos))
for _, repoRef := range req.Repos {
result := BatchRepoResult{
Owner: repoRef.Owner,
Repo: repoRef.Repo,
}
repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, repoRef.Owner, repoRef.Repo)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
result.Error = "repository not found"
} else {
result.Error = "failed to fetch repository"
}
results = append(results, result)
continue
}
// Check access
if repo.IsPrivate && !ctx.IsSigned {
result.Error = "access denied"
results = append(results, result)
continue
}
result.FullName = repo.FullName()
result.Description = repo.Description
result.Private = repo.IsPrivate
result.Fork = repo.IsFork
result.Stars = repo.NumStars
result.Forks = repo.NumForks
result.Language = repo.PrimaryLanguage.Language
results = append(results, result)
}
ctx.JSON(http.StatusOK, map[string]any{
"results": results,
})
}