add nip34/git-natural-api, using the same approach as https://jsr.io/@fiatjaf/git-natural-api.
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user