97 lines
2.2 KiB
Go
97 lines
2.2 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
writer.go
|
|
|
|
In-place updater for requirements.txt that preserves comments and original casing
|
|
|
|
Matches package names in PEP 503 normalized form while keeping the original
|
|
capitalization in the file. Writes atomically via temp file and rename.
|
|
|
|
Key exports:
|
|
UpdateFile - replaces version specifiers for the given packages in a requirements.txt
|
|
|
|
Connects to:
|
|
update.go - called when the target file ends in .txt
|
|
pypi/client.go - imports NormalizeName for case-insensitive name matching
|
|
*/
|
|
|
|
package requirements
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/CarterPerez-dev/angela/internal/pypi"
|
|
)
|
|
|
|
// UpdateFile replaces version specifiers for the given packages in a
|
|
// requirements.txt file, preserving all comments and formatting
|
|
func UpdateFile(path string, updates map[string]string) error {
|
|
data, err := os.ReadFile(path) //nolint:gosec
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
|
|
content := data
|
|
for pkg, spec := range updates {
|
|
pattern := buildPattern(pkg)
|
|
found := false
|
|
content = pattern.ReplaceAllFunc(
|
|
content,
|
|
func(match []byte) []byte {
|
|
found = true
|
|
return replaceSpec(pattern, match, spec)
|
|
},
|
|
)
|
|
if !found {
|
|
return fmt.Errorf(
|
|
"dependency %q not found in %s", pkg, path,
|
|
)
|
|
}
|
|
}
|
|
|
|
path = filepath.Clean(path)
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, content, 0o600); err != nil {
|
|
return fmt.Errorf("write temp: %w", err)
|
|
}
|
|
if err := os.Rename(tmp, path); err != nil {
|
|
_ = os.Remove(tmp) //nolint:errcheck
|
|
return fmt.Errorf("rename: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildPattern(name string) *regexp.Regexp {
|
|
normalized := pypi.NormalizeName(name)
|
|
parts := strings.Split(normalized, "-")
|
|
for i, p := range parts {
|
|
parts[i] = regexp.QuoteMeta(p)
|
|
}
|
|
namePattern := strings.Join(parts, `[-_.]?`)
|
|
|
|
return regexp.MustCompile(
|
|
`(?im)^(` + namePattern + `)` +
|
|
`(\[[^\]]*\])?` +
|
|
`(\s*[><=!~][^\n;#]*)`,
|
|
)
|
|
}
|
|
|
|
func replaceSpec(
|
|
re *regexp.Regexp, match []byte, newSpec string,
|
|
) []byte {
|
|
groups := re.FindSubmatch(match)
|
|
if len(groups) < 4 {
|
|
return match
|
|
}
|
|
|
|
var b []byte
|
|
b = append(b, groups[1]...)
|
|
b = append(b, groups[2]...)
|
|
b = append(b, []byte(newSpec)...)
|
|
return b
|
|
}
|