Resolve absolute destinations inside the candidate checkout

This commit is contained in:
2026-10-07 20:29:16 +00:00
parent 9d2ca66a79
commit 1dfa774164
4 changed files with 117 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"path"
"path/filepath"
"strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
@@ -97,16 +98,25 @@ type Destination struct {
Path string
}
// ResolveDestination resolves a repository-relative destination from the
// source file's directory using only the captured Git tree.
// 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.
func (tree *CandidateTree) ResolveDestination(ctx context.Context, sourcePath, destination string) (Destination, error) {
if err := ctx.Err(); err != nil {
return Destination{}, err
}
if strings.HasPrefix(destination, "/") {
return Destination{Kind: DestinationOutsideCheckout, Path: path.Clean(destination)}, nil
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, path.Dir(sourcePath), strings.Split(destination, "/"), make(map[string]bool))
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) {
@@ -146,11 +156,19 @@ func (tree *CandidateTree) resolveDestination(ctx context.Context, directory str
return Destination{}, fmt.Errorf("read committed symlink %q: %w", partial, err)
}
target := string(linkText)
if strings.HasPrefix(target, "/") {
return Destination{Kind: DestinationOutsideCheckout, Path: unresolvedPath(target, remaining)}, nil
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, directory, strings.Split(target, "/"), active)
destination, err := tree.resolveDestination(ctx, targetDirectory, strings.Split(target, "/"), active)
delete(active, partial)
if err != nil || len(remaining) == 0 {
return destination, err
@@ -174,6 +192,20 @@ func (tree *CandidateTree) resolveDestination(ctx context.Context, directory str
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 {