Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
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]>
2026-09-10 17:50:35 -07:00

156 lines
5.1 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package files
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/modules/git"
"github.com/hanzoai/git/modules/setting"
api "github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/modules/util"
"github.com/hanzoai/git/routers/api/v1/utils"
)
func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) {
var size int64
for _, treePath := range treePaths {
// ok if fails, then will be nil
fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{
TreePath: treePath,
IncludeSingleFileContent: true,
IncludeCommitMetadata: true,
})
if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" {
// if content isn't empty (e.g., due to the single blob being too large), add file size to response size
size += int64(len(*fileContents.Content))
}
if size > setting.API.DefaultMaxResponseSize {
break // stop if max response size would be exceeded
}
files = append(files, fileContents)
if len(files) == setting.API.DefaultPagingNum {
break // stop if paging num reached
}
}
return files
}
func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) {
files := GetContentsListFromTreePaths(ctx, repo, gitRepo, refCommit, treeNames)
fileCommitResponse, _ := GetFileCommitResponse(repo, gitRepo, refCommit.Commit) // ok if fails, then will be nil
verification := GetPayloadCommitVerification(ctx, refCommit.Commit)
filesResponse := &api.FilesResponse{
Files: files,
Commit: fileCommitResponse,
Verification: verification,
}
return filesResponse, nil
}
// constructs a FileResponse with the file at the index from FilesResponse
func GetFileResponseFromFilesResponse(filesResponse *api.FilesResponse, index int) *api.FileResponse {
content := &api.ContentsResponse{}
if len(filesResponse.Files) > index {
content = filesResponse.Files[index]
}
fileResponse := &api.FileResponse{
Content: content,
Commit: filesResponse.Commit,
Verification: filesResponse.Verification,
}
return fileResponse
}
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
func GetFileCommitResponse(repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
if repo == nil {
return nil, errors.New("repo cannot be nil")
}
if commit == nil {
return nil, errors.New("commit cannot be nil")
}
commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()))
commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + url.PathEscape(commit.TreeID.String()))
parents := make([]*api.CommitMeta, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
if parent, err := commit.Parent(gitRepo, i); err == nil && parent != nil {
parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + url.PathEscape(parent.ID.String()))
parents[i] = &api.CommitMeta{
SHA: parent.ID.String(),
URL: parentCommitURL.String(),
}
}
}
commitHTMLURL, _ := url.Parse(repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()))
fileCommit := &api.FileCommitResponse{
CommitMeta: api.CommitMeta{
SHA: commit.ID.String(),
URL: commitURL.String(),
},
HTMLURL: commitHTMLURL.String(),
Author: &api.CommitUser{
Identity: api.Identity{
Name: commit.Author.Name,
Email: commit.Author.Email,
},
Date: commit.Author.When.UTC().Format(time.RFC3339),
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: commit.Committer.Name,
Email: commit.Committer.Email,
},
Date: commit.Committer.When.UTC().Format(time.RFC3339),
},
Message: commit.MessageUTF8(),
Tree: &api.CommitMeta{
URL: commitTreeURL.String(),
SHA: commit.TreeID.String(),
},
Parents: parents,
}
return fileCommit, nil
}
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
type ErrFilenameInvalid struct {
Path string
}
// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
func IsErrFilenameInvalid(err error) bool {
_, ok := err.(ErrFilenameInvalid)
return ok
}
func (err ErrFilenameInvalid) Error() string {
return fmt.Sprintf("path contains a malformed path component [path: %s]", err.Path)
}
func (err ErrFilenameInvalid) Unwrap() error {
return util.ErrInvalidArgument
}
// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part)
func CleanGitTreePath(name string) string {
name = util.PathJoinRel(name)
// Git disallows any filenames to have a .git directory in them.
for part := range strings.SplitSeq(name, "/") {
if strings.EqualFold(part, ".git") {
return ""
}
}
if name == "." {
name = ""
}
return name
}