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
- 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>
274 lines
7.1 KiB
Go
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,
|
|
})
|
|
}
|