265 lines
6.0 KiB
Go
265 lines
6.0 KiB
Go
package gitnaturalapi
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
type MissingCapability struct {
|
|
URL string
|
|
Capability string
|
|
}
|
|
|
|
func (e *MissingCapability) Error() string {
|
|
return fmt.Sprintf("server at %s is missing required capability %s", e.URL, e.Capability)
|
|
}
|
|
|
|
func prepareRequest(url string, commitOrRef string, needFilter bool) (resolvedRef string, capabilities []string, err error) {
|
|
var info *InfoRefsUploadPackResponse
|
|
if strings.HasPrefix(commitOrRef, "refs/") {
|
|
info, err = GetInfoRefs(url)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
resolved, ok := info.Refs[commitOrRef]
|
|
if !ok {
|
|
return "", nil, fmt.Errorf("ref %s not found", commitOrRef)
|
|
}
|
|
commitOrRef = resolved
|
|
}
|
|
|
|
caps, err := GetCapabilities(url, info)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
for _, c := range DefaultCapabilities {
|
|
if slices.Contains(caps, c) {
|
|
capabilities = append(capabilities, c)
|
|
}
|
|
}
|
|
for _, c := range NecessaryCapabilities {
|
|
if slices.Contains(caps, c) {
|
|
capabilities = append(capabilities, c)
|
|
} else {
|
|
return "", nil, &MissingCapability{URL: url, Capability: c}
|
|
}
|
|
}
|
|
for _, c := range RequiredCapabilities {
|
|
if !slices.Contains(caps, c) {
|
|
return "", nil, &MissingCapability{URL: url, Capability: c}
|
|
}
|
|
}
|
|
if needFilter {
|
|
if slices.Contains(caps, "filter") {
|
|
capabilities = append(capabilities, "filter")
|
|
} else {
|
|
return "", nil, &MissingCapability{URL: url, Capability: "filter"}
|
|
}
|
|
}
|
|
|
|
return commitOrRef, capabilities, nil
|
|
}
|
|
|
|
func GetObject(url string, blobHash string) (*ParsedObject, error) {
|
|
ref, caps, err := prepareRequest(url, blobHash, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deepen := 1
|
|
want, err := CreateWantRequest(ref, caps, &deepen, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := FetchPackfile(url, want)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result.Objects[blobHash], nil
|
|
}
|
|
|
|
func GetDirectoryTreeAt(url string, commitOrRef string, nestLimit *int) (*Tree, error) {
|
|
ref, caps, err := prepareRequest(url, commitOrRef, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
want, err := CreateWantRequest(ref, caps, nestLimit, "blob:none")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := FetchPackfile(url, want)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commit := result.Objects[ref]
|
|
if commit == nil {
|
|
return nil, fmt.Errorf("commit %s not found in packfile", ref)
|
|
}
|
|
|
|
treeHash := string(commit.Data[5:45])
|
|
rootTree := result.Objects[treeHash]
|
|
if rootTree == nil {
|
|
return nil, fmt.Errorf("root tree %s not found in packfile", treeHash)
|
|
}
|
|
|
|
return LoadTree(rootTree, result.Objects, nestLimit), nil
|
|
}
|
|
|
|
func ShallowCloneRepositoryAt(url string, commitOrRef string) (*Commit, *Tree, error) {
|
|
ref, caps, err := prepareRequest(url, commitOrRef, false)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
deepen := 1
|
|
want, err := CreateWantRequest(ref, caps, &deepen, "")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
result, err := FetchPackfile(url, want)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
commitObj := result.Objects[ref]
|
|
if commitObj == nil {
|
|
return nil, nil, fmt.Errorf("commit %s not found in packfile", ref)
|
|
}
|
|
|
|
treeHash := string(commitObj.Data[5:45])
|
|
rootTree := result.Objects[treeHash]
|
|
if rootTree == nil {
|
|
return nil, nil, fmt.Errorf("root tree %s not found in packfile", treeHash)
|
|
}
|
|
|
|
commit, err := ParseCommit(commitObj.Data, commitObj.Hash)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
tree := LoadTree(rootTree, result.Objects, nil)
|
|
return commit, tree, nil
|
|
}
|
|
|
|
func FetchCommitsOnly(url string, commitOrRef string, maxCommits *int) ([]*Commit, error) {
|
|
ref, caps, err := prepareRequest(url, commitOrRef, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
want, err := CreateWantRequest(ref, caps, maxCommits, "tree:0")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := FetchPackfile(url, want)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commitMap := make(map[string]*Commit, len(result.Objects))
|
|
for hash, obj := range result.Objects {
|
|
commit, err := ParseCommit(obj.Data, hash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commitMap[hash] = commit
|
|
}
|
|
|
|
// sort topologically starting from the requested ref
|
|
sorted := make([]*Commit, 0, len(commitMap))
|
|
visited := make(map[string]bool, len(commitMap))
|
|
var visit func(hash string)
|
|
visit = func(hash string) {
|
|
if visited[hash] {
|
|
return
|
|
}
|
|
c, ok := commitMap[hash]
|
|
if !ok {
|
|
return
|
|
}
|
|
visited[hash] = true
|
|
sorted = append(sorted, c)
|
|
for _, parent := range c.Parents {
|
|
visit(parent)
|
|
}
|
|
}
|
|
visit(ref)
|
|
|
|
for _, c := range commitMap {
|
|
if !visited[c.Hash] {
|
|
sorted = append(sorted, c)
|
|
}
|
|
}
|
|
|
|
return sorted, nil
|
|
}
|
|
|
|
func GetSingleCommit(url string, commitOrRef string) (*Commit, error) {
|
|
maxCommits := 1
|
|
commits, err := FetchCommitsOnly(url, commitOrRef, &maxCommits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(commits) == 0 {
|
|
return nil, fmt.Errorf("no commit found for reference: %s", commitOrRef)
|
|
}
|
|
return commits[0], nil
|
|
}
|
|
|
|
func GetObjectByPath(url string, commitOrRef string, path string) (*TreeEntry, error) {
|
|
normalizedPath := strings.ReplaceAll(path, "\\", "/")
|
|
normalizedPath = strings.TrimLeft(normalizedPath, "/")
|
|
normalizedPath = strings.TrimRight(normalizedPath, "/")
|
|
|
|
var pathSegments []string
|
|
if normalizedPath != "" {
|
|
pathSegments = strings.Split(normalizedPath, "/")
|
|
}
|
|
requiredDepth := len(pathSegments)
|
|
|
|
tree, err := GetDirectoryTreeAt(url, commitOrRef, &requiredDepth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currentLevel := tree
|
|
nextSegment:
|
|
for i, segment := range pathSegments {
|
|
isLastSegment := i == len(pathSegments)-1
|
|
|
|
for _, dir := range currentLevel.Directories {
|
|
if dir.Name == segment {
|
|
if isLastSegment {
|
|
return &TreeEntry{Path: segment, Mode: "40000", IsDir: true, Hash: dir.Hash}, nil
|
|
}
|
|
if dir.Content != nil {
|
|
currentLevel = dir.Content
|
|
continue nextSegment
|
|
}
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
if isLastSegment {
|
|
for _, file := range currentLevel.Files {
|
|
if file.Name == segment {
|
|
return &TreeEntry{Path: segment, Mode: "100644", IsDir: false, Hash: file.Hash}, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|