Resolve committed module identity from Git objects
This commit is contained in:
@@ -310,137 +310,32 @@ func (tree *CandidateTree) Entry(path string) (git.TreeEntry, bool) {
|
||||
}
|
||||
|
||||
// DestinationKind classifies a committed local destination.
|
||||
type DestinationKind string
|
||||
type DestinationKind = git.DestinationKind
|
||||
|
||||
const (
|
||||
DestinationEntry DestinationKind = "entry"
|
||||
DestinationDirectory DestinationKind = "directory"
|
||||
DestinationMissing DestinationKind = "missing"
|
||||
DestinationOutsideCheckout DestinationKind = "outside-checkout"
|
||||
DestinationSubmodule DestinationKind = "submodule"
|
||||
DestinationSymlinkCycle DestinationKind = "symlink-cycle"
|
||||
DestinationEntry = git.DestinationEntry
|
||||
DestinationDirectory = git.DestinationDirectory
|
||||
DestinationMissing = git.DestinationMissing
|
||||
DestinationOutsideCheckout = git.DestinationOutsideCheckout
|
||||
DestinationSubmodule = git.DestinationSubmodule
|
||||
DestinationSymlinkCycle = git.DestinationSymlinkCycle
|
||||
)
|
||||
|
||||
// Destination records the resolved identity and its classification.
|
||||
type Destination struct {
|
||||
Kind DestinationKind
|
||||
Path string
|
||||
}
|
||||
type Destination = git.Destination
|
||||
|
||||
// ResolveDestination resolves local destinations using the captured Git tree.
|
||||
// Relative destinations start at the source directory; absolute destinations
|
||||
// within the repository directory map back to committed paths.
|
||||
// ResolveDestination uses shared committed reference traversal with the tree's
|
||||
// indexed metadata and scoped symlink reader.
|
||||
func (tree *CandidateTree) ResolveDestination(ctx context.Context, sourcePath, destination string) (Destination, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Destination{}, err
|
||||
resolver := git.PathResolver{
|
||||
RepositoryDir: tree.repositoryDir,
|
||||
Lookup: func(_ context.Context, name string) (git.TreeEntry, bool, error) {
|
||||
if entry, exists := tree.Entry(name); exists {
|
||||
return entry, true, nil
|
||||
}
|
||||
_, exists := tree.directories[name]
|
||||
return git.TreeEntry{Path: name, Type: "tree"}, exists, nil
|
||||
},
|
||||
ReadLink: tree.readSymlinkTarget,
|
||||
}
|
||||
directory := path.Dir(sourcePath)
|
||||
if path.IsAbs(destination) {
|
||||
relative, inside, err := tree.relativeAbsoluteDestination(destination)
|
||||
if err != nil {
|
||||
return Destination{}, err
|
||||
}
|
||||
if !inside {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: destination}, nil
|
||||
}
|
||||
directory, destination = ".", relative
|
||||
}
|
||||
return tree.resolveDestination(ctx, directory, strings.Split(destination, "/"), make(map[string]bool))
|
||||
}
|
||||
|
||||
func (tree *CandidateTree) resolveDestination(ctx context.Context, directory string, components []string, active map[string]bool) (Destination, error) {
|
||||
for index, component := range components {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Destination{}, err
|
||||
}
|
||||
remaining := components[index+1:]
|
||||
switch component {
|
||||
case "", ".":
|
||||
continue
|
||||
case "..":
|
||||
if directory == "." {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: unresolvedPath("..", remaining)}, nil
|
||||
}
|
||||
directory = path.Dir(directory)
|
||||
continue
|
||||
}
|
||||
partial := path.Join(directory, component)
|
||||
entry, exists := tree.Entry(partial)
|
||||
if !exists {
|
||||
if _, isDirectory := tree.directories[partial]; !isDirectory {
|
||||
return Destination{Kind: DestinationMissing, Path: unresolvedPath(partial, remaining)}, nil
|
||||
}
|
||||
directory = partial
|
||||
continue
|
||||
}
|
||||
if entry.Mode == "160000" {
|
||||
return Destination{Kind: DestinationSubmodule, Path: unresolvedPath(partial, remaining)}, nil
|
||||
}
|
||||
if entry.Mode == "120000" {
|
||||
if active[partial] {
|
||||
return Destination{Kind: DestinationSymlinkCycle, Path: partial}, nil
|
||||
}
|
||||
entry, _ := tree.Entry(partial)
|
||||
linkText, err := tree.readSymlinkTarget(ctx, entry)
|
||||
if err != nil {
|
||||
return Destination{}, fmt.Errorf("read committed symlink %q: %w", partial, err)
|
||||
}
|
||||
target := linkText
|
||||
targetDirectory := directory
|
||||
if path.IsAbs(target) {
|
||||
relative, inside, err := tree.relativeAbsoluteDestination(target)
|
||||
if err != nil {
|
||||
return Destination{}, err
|
||||
}
|
||||
if !inside {
|
||||
return Destination{Kind: DestinationOutsideCheckout, Path: unresolvedPath(target, remaining)}, nil
|
||||
}
|
||||
targetDirectory, target = ".", relative
|
||||
}
|
||||
active[partial] = true
|
||||
destination, err := tree.resolveDestination(ctx, targetDirectory, strings.Split(target, "/"), active)
|
||||
delete(active, partial)
|
||||
if err != nil || len(remaining) == 0 {
|
||||
return destination, err
|
||||
}
|
||||
switch destination.Kind {
|
||||
case DestinationDirectory:
|
||||
return tree.resolveDestination(ctx, destination.Path, remaining, active)
|
||||
case DestinationEntry:
|
||||
destination.Kind = DestinationMissing
|
||||
case DestinationSymlinkCycle:
|
||||
return destination, nil
|
||||
}
|
||||
destination.Path = unresolvedPath(destination.Path, remaining)
|
||||
return destination, nil
|
||||
}
|
||||
if len(remaining) != 0 {
|
||||
return Destination{Kind: DestinationMissing, Path: unresolvedPath(partial, remaining)}, nil
|
||||
}
|
||||
return Destination{Kind: DestinationEntry, Path: partial}, nil
|
||||
}
|
||||
return Destination{Kind: DestinationDirectory, Path: directory}, nil
|
||||
}
|
||||
|
||||
func (tree *CandidateTree) relativeAbsoluteDestination(destination string) (string, bool, error) {
|
||||
root, err := filepath.Abs(tree.repositoryDir)
|
||||
if err != nil {
|
||||
return "", false, fmt.Errorf("resolve candidate repository directory: %w", err)
|
||||
}
|
||||
root = filepath.ToSlash(root)
|
||||
if destination == root {
|
||||
return ".", true, nil
|
||||
}
|
||||
// Preserve the suffix for component traversal, including symlinks before '..'.
|
||||
relative, inside := strings.CutPrefix(destination, strings.TrimSuffix(root, "/")+"/")
|
||||
return relative, inside, nil
|
||||
}
|
||||
|
||||
// unresolvedPath preserves components that could not be traversed. Cleaning
|
||||
// them would hide missing entries, non-directories, or checkout boundaries.
|
||||
func unresolvedPath(base string, remaining []string) string {
|
||||
if len(remaining) == 0 {
|
||||
return base
|
||||
}
|
||||
return base + "/" + strings.Join(remaining, "/")
|
||||
return resolver.ResolveDestination(ctx, sourcePath, destination)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user