mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
171 lines
4.4 KiB
Go
171 lines
4.4 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package git
|
|
|
|
import (
|
|
"context"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/util"
|
|
)
|
|
|
|
// TreeEntry the leaf in the git tree
|
|
type TreeEntry struct {
|
|
ID ObjectID
|
|
|
|
name string
|
|
ptree *Tree
|
|
|
|
entryMode EntryMode
|
|
|
|
size int64
|
|
sized bool
|
|
}
|
|
|
|
// Name returns the name of the entry (base name)
|
|
func (te *TreeEntry) Name() string {
|
|
return te.name
|
|
}
|
|
|
|
// Mode returns the mode of the entry
|
|
func (te *TreeEntry) Mode() EntryMode {
|
|
return te.entryMode
|
|
}
|
|
|
|
// IsSubModule if the entry is a submodule
|
|
func (te *TreeEntry) IsSubModule() bool {
|
|
return te.entryMode.IsSubModule()
|
|
}
|
|
|
|
// IsDir if the entry is a sub dir
|
|
func (te *TreeEntry) IsDir() bool {
|
|
return te.entryMode.IsDir()
|
|
}
|
|
|
|
// IsLink if the entry is a symlink
|
|
func (te *TreeEntry) IsLink() bool {
|
|
return te.entryMode.IsLink()
|
|
}
|
|
|
|
// IsRegular if the entry is a regular file
|
|
func (te *TreeEntry) IsRegular() bool {
|
|
return te.entryMode.IsRegular()
|
|
}
|
|
|
|
// IsExecutable if the entry is an executable file (not necessarily binary)
|
|
func (te *TreeEntry) IsExecutable() bool {
|
|
return te.entryMode.IsExecutable()
|
|
}
|
|
|
|
// Type returns the type of the entry (commit, tree, blob)
|
|
func (te *TreeEntry) Type() string {
|
|
switch te.Mode() {
|
|
case EntryModeCommit:
|
|
return "commit"
|
|
case EntryModeTree:
|
|
return "tree"
|
|
default:
|
|
return "blob"
|
|
}
|
|
}
|
|
|
|
type EntryFollowResult struct {
|
|
SymlinkContent string
|
|
TargetFullPath string
|
|
TargetEntry *TreeEntry
|
|
}
|
|
|
|
func EntryFollowLink(ctx context.Context, gitRepo *Repository, commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) {
|
|
if !te.IsLink() {
|
|
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath)
|
|
}
|
|
|
|
// git's filename max length is 4096, hopefully a link won't be longer than multiple of that
|
|
const maxSymlinkSize = 20 * 4096
|
|
if te.Blob(gitRepo).Size() > maxSymlinkSize {
|
|
return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath)
|
|
}
|
|
|
|
link, err := te.Blob(gitRepo).GetBlobContent(maxSymlinkSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.HasPrefix(link, "/") {
|
|
// It's said that absolute path will be stored as is in Git
|
|
return &EntryFollowResult{SymlinkContent: link}, util.ErrorWrap(util.ErrUnprocessableContent, "%q is an absolute symlink", fullPath)
|
|
}
|
|
|
|
targetFullPath := path.Join(path.Dir(fullPath), link)
|
|
targetEntry, err := commit.GetTreeEntryByPath(ctx, gitRepo, targetFullPath)
|
|
if err != nil {
|
|
return &EntryFollowResult{SymlinkContent: link}, err
|
|
}
|
|
return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil
|
|
}
|
|
|
|
func EntryFollowLinks(ctx context.Context, gitRepo *Repository, commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) {
|
|
limit := util.OptionalArg(optLimit, 10)
|
|
treeEntry, fullPath := firstTreeEntry, firstFullPath
|
|
for range limit {
|
|
res, err = EntryFollowLink(ctx, gitRepo, commit, fullPath, treeEntry)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
treeEntry, fullPath = res.TargetEntry, res.TargetFullPath
|
|
if !treeEntry.IsLink() {
|
|
break
|
|
}
|
|
}
|
|
if treeEntry.IsLink() {
|
|
return res, util.ErrorWrap(util.ErrUnprocessableContent, "%q has too many links", firstFullPath)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (te *TreeEntry) Tree(gitRepo *Repository) *Tree {
|
|
t, err := gitRepo.getTree(te.ID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return t
|
|
}
|
|
|
|
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
|
|
func (te *TreeEntry) GetSubJumpablePathName(ctx context.Context, gitRepo *Repository) string {
|
|
if te.IsSubModule() || !te.IsDir() {
|
|
return ""
|
|
}
|
|
tree, err := te.ptree.SubTree(ctx, gitRepo, te.Name())
|
|
if err != nil {
|
|
return te.Name()
|
|
}
|
|
entries, _ := tree.ListEntries(ctx, gitRepo)
|
|
if len(entries) == 1 && entries[0].IsDir() {
|
|
name := entries[0].GetSubJumpablePathName(ctx, gitRepo)
|
|
if name != "" {
|
|
return te.Name() + "/" + name
|
|
}
|
|
}
|
|
return te.Name()
|
|
}
|
|
|
|
// Entries a list of entry
|
|
type Entries []*TreeEntry
|
|
|
|
// CustomSort customizable string comparing sort entry list
|
|
func (tes Entries) CustomSort(cmp func(s1, s2 string) int) {
|
|
slices.SortFunc(tes, func(a, b *TreeEntry) int {
|
|
s1Dir, s2Dir := a.IsDir() || a.IsSubModule(), b.IsDir() || b.IsSubModule()
|
|
if s1Dir != s2Dir {
|
|
if s1Dir {
|
|
return -1
|
|
}
|
|
return 1
|
|
}
|
|
return cmp(a.Name(), b.Name())
|
|
})
|
|
}
|