Files
forge/modules/git/tree_entry.go
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

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())
})
}