Resolve candidate file destinations
This commit is contained in:
@@ -3,6 +3,8 @@ package validation
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
)
|
||||
@@ -13,6 +15,7 @@ type CandidateTree struct {
|
||||
repositoryDir string
|
||||
entries []git.TreeEntry
|
||||
byPath map[string]int
|
||||
directories map[string]struct{}
|
||||
blobs map[string][]byte
|
||||
}
|
||||
|
||||
@@ -27,10 +30,15 @@ func ReadCandidateTree(ctx context.Context, repositoryDir, commit string) (*Cand
|
||||
repositoryDir: repositoryDir,
|
||||
entries: entries,
|
||||
byPath: make(map[string]int, len(entries)),
|
||||
directories: make(map[string]struct{}),
|
||||
blobs: make(map[string][]byte),
|
||||
}
|
||||
tree.directories["."] = struct{}{}
|
||||
for index, entry := range entries {
|
||||
tree.byPath[entry.Path] = index
|
||||
for directory := path.Dir(entry.Path); directory != "."; directory = path.Dir(directory) {
|
||||
tree.directories[directory] = struct{}{}
|
||||
}
|
||||
}
|
||||
return tree, nil
|
||||
}
|
||||
@@ -70,3 +78,89 @@ func (tree *CandidateTree) Entry(path string) (git.TreeEntry, bool) {
|
||||
}
|
||||
return tree.entries[index], true
|
||||
}
|
||||
|
||||
// DestinationKind classifies a committed local destination.
|
||||
type DestinationKind string
|
||||
|
||||
const (
|
||||
DestinationEntry DestinationKind = "entry"
|
||||
DestinationDirectory DestinationKind = "directory"
|
||||
DestinationMissing DestinationKind = "missing"
|
||||
DestinationOutsideCheckout DestinationKind = "outside-checkout"
|
||||
DestinationSubmodule DestinationKind = "submodule"
|
||||
DestinationSymlinkCycle DestinationKind = "symlink-cycle"
|
||||
)
|
||||
|
||||
// Destination records the resolved identity and its classification.
|
||||
type Destination struct {
|
||||
Kind DestinationKind
|
||||
Path string
|
||||
}
|
||||
|
||||
// ResolveDestination resolves a repository-relative destination from the
|
||||
// source file's directory using only the captured Git tree.
|
||||
func (tree *CandidateTree) ResolveDestination(ctx context.Context, sourcePath, destination string) (Destination, error) {
|
||||
if strings.HasPrefix(destination, "/") {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: path.Clean(destination)}, nil
|
||||
}
|
||||
resolved := path.Clean(path.Join(path.Dir(sourcePath), destination))
|
||||
if outsideCheckout(resolved) {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: resolved}, nil
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for {
|
||||
components := strings.Split(resolved, "/")
|
||||
partial := ""
|
||||
restarted := false
|
||||
for index, component := range components {
|
||||
partial = path.Join(partial, component)
|
||||
remaining := strings.Join(components[index+1:], "/")
|
||||
entry, exists := tree.Entry(partial)
|
||||
if !exists {
|
||||
if _, directory := tree.directories[partial]; !directory {
|
||||
return Destination{Kind: DestinationMissing, Path: resolved}, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if entry.Mode == "160000" {
|
||||
return Destination{Kind: DestinationSubmodule, Path: path.Join(partial, remaining)}, nil
|
||||
}
|
||||
if entry.Mode == "120000" {
|
||||
state := partial + "\x00" + remaining
|
||||
if _, cycle := seen[state]; cycle {
|
||||
return Destination{Kind: DestinationSymlinkCycle, Path: partial}, nil
|
||||
}
|
||||
seen[state] = struct{}{}
|
||||
linkText, err := tree.ReadFile(ctx, partial)
|
||||
if err != nil {
|
||||
return Destination{}, fmt.Errorf("read committed symlink %q: %w", partial, err)
|
||||
}
|
||||
target := string(linkText)
|
||||
if strings.HasPrefix(target, "/") {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: target}, nil
|
||||
}
|
||||
resolved = path.Clean(path.Join(path.Dir(partial), target, remaining))
|
||||
if outsideCheckout(resolved) {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: resolved}, nil
|
||||
}
|
||||
restarted = true
|
||||
break
|
||||
}
|
||||
if remaining != "" {
|
||||
return Destination{Kind: DestinationMissing, Path: resolved}, nil
|
||||
}
|
||||
return Destination{Kind: DestinationEntry, Path: partial}, nil
|
||||
}
|
||||
if restarted {
|
||||
continue
|
||||
}
|
||||
if _, directory := tree.directories[resolved]; directory {
|
||||
return Destination{Kind: DestinationDirectory, Path: resolved}, nil
|
||||
}
|
||||
return Destination{Kind: DestinationMissing, Path: resolved}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func outsideCheckout(name string) bool {
|
||||
return name == ".." || strings.HasPrefix(name, "../")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user