112 lines
2.9 KiB
Go
112 lines
2.9 KiB
Go
package replace
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// PlannedOperation represents a rename that will be executed during apply.
|
|
type PlannedOperation struct {
|
|
Result Result
|
|
TargetRelative string
|
|
TargetAbsolute string
|
|
}
|
|
|
|
// Preview computes replacements and writes a human-readable summary to out.
|
|
func Preview(ctx context.Context, req *ReplaceRequest, parseResult ParseArgsResult, out io.Writer) (Summary, []PlannedOperation, error) {
|
|
summary := NewSummary()
|
|
for _, dup := range parseResult.Duplicates {
|
|
summary.AddDuplicate(dup)
|
|
}
|
|
|
|
planned := make([]PlannedOperation, 0)
|
|
plannedTargets := make(map[string]string) // target rel -> source rel to detect duplicates
|
|
|
|
err := TraverseCandidates(ctx, req, func(candidate Candidate) error {
|
|
res := ApplyPatterns(candidate, parseResult.Patterns, parseResult.Replacement)
|
|
summary.RecordCandidate(res)
|
|
|
|
if !res.Changed {
|
|
return nil
|
|
}
|
|
|
|
dir := filepath.Dir(candidate.RelativePath)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
|
|
targetRelative := res.ProposedName
|
|
if dir != "" {
|
|
targetRelative = filepath.ToSlash(filepath.Join(dir, res.ProposedName))
|
|
} else {
|
|
targetRelative = filepath.ToSlash(res.ProposedName)
|
|
}
|
|
|
|
if targetRelative == candidate.RelativePath {
|
|
return nil
|
|
}
|
|
|
|
if existing, ok := plannedTargets[targetRelative]; ok && existing != candidate.RelativePath {
|
|
summary.AddConflict(ConflictDetail{
|
|
OriginalPath: candidate.RelativePath,
|
|
ProposedPath: targetRelative,
|
|
Reason: "duplicate target generated in preview",
|
|
})
|
|
return nil
|
|
}
|
|
|
|
targetAbsolute := filepath.Join(req.WorkingDir, filepath.FromSlash(targetRelative))
|
|
if info, err := os.Stat(targetAbsolute); err == nil {
|
|
if candidate.OriginalPath != targetAbsolute {
|
|
// Case-only renames are allowed on case-insensitive filesystems. Compare file identity.
|
|
if origInfo, origErr := os.Stat(candidate.OriginalPath); origErr == nil {
|
|
if os.SameFile(info, origInfo) {
|
|
// Same file—case-only update permitted.
|
|
goto recordOperation
|
|
}
|
|
}
|
|
|
|
reason := "target already exists"
|
|
if info.IsDir() {
|
|
reason = "target directory already exists"
|
|
}
|
|
summary.AddConflict(ConflictDetail{
|
|
OriginalPath: candidate.RelativePath,
|
|
ProposedPath: targetRelative,
|
|
Reason: reason,
|
|
})
|
|
return nil
|
|
}
|
|
}
|
|
|
|
plannedTargets[targetRelative] = candidate.RelativePath
|
|
|
|
if out != nil {
|
|
fmt.Fprintf(out, "%s -> %s\n", candidate.RelativePath, targetRelative)
|
|
}
|
|
|
|
recordOperation:
|
|
planned = append(planned, PlannedOperation{
|
|
Result: res,
|
|
TargetRelative: targetRelative,
|
|
TargetAbsolute: targetAbsolute,
|
|
})
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return Summary{}, nil, err
|
|
}
|
|
|
|
if summary.ReplacementWasEmpty(parseResult.Replacement) {
|
|
if out != nil {
|
|
fmt.Fprintln(out, "Warning: replacement string is empty; matched patterns will be removed.")
|
|
}
|
|
}
|
|
|
|
return summary, planned, nil
|
|
}
|