split main.go into modules, add --version flag

This commit is contained in:
Thomas G. Lopes
2026-03-04 20:21:06 +00:00
parent 0c40072aa8
commit cad40d0af9
6 changed files with 951 additions and 900 deletions
+402
View File
@@ -0,0 +1,402 @@
package main
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
func applyPackage(filesDir, targetRoot string, cfg *packageConfig) error {
if err := ensureDir(targetRoot); err != nil {
return err
}
return filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if path == filesDir {
return nil
}
rel, err := filepath.Rel(filesDir, path)
if err != nil {
return err
}
if shouldIgnorePath(rel, cfg) {
if entry.IsDir() {
return filepath.SkipDir
}
return nil
}
targetPath := filepath.Join(targetRoot, rel)
if entry.IsDir() {
return ensureDir(targetPath)
}
srcAbs, err := filepath.Abs(path)
if err != nil {
return err
}
return linkFile(srcAbs, targetPath)
})
}
func linkFile(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
if info, err := os.Lstat(dst); err == nil {
if info.Mode()&os.ModeSymlink != 0 {
current, err := os.Readlink(dst)
if err != nil {
return err
}
if current == src {
return nil
}
return fmt.Errorf("conflict at %s (points to %s)", dst, current)
}
return fmt.Errorf("conflict at %s (exists and is not a symlink)", dst)
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
return os.Symlink(src, dst)
}
func ensureDir(path string) error {
info, err := os.Lstat(path)
if err == nil {
if info.IsDir() {
return nil
}
return fmt.Errorf("%s exists and is not a directory", path)
}
if !errors.Is(err, os.ErrNotExist) {
return err
}
return os.MkdirAll(path, 0o755)
}
func findStaleLinks(filesDir, targetRoot string) ([]string, error) {
filesAbs, err := filepath.Abs(filesDir)
if err != nil {
return nil, err
}
var stale []string
walkErr := filepath.WalkDir(targetRoot, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink == 0 {
return nil
}
src, err := os.Readlink(path)
if err != nil {
return err
}
if !filepath.IsAbs(src) {
src = filepath.Join(filepath.Dir(path), src)
}
src = filepath.Clean(src)
if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs {
return nil
}
if _, err := os.Stat(src); errors.Is(err, os.ErrNotExist) {
stale = append(stale, path)
}
return nil
})
if walkErr != nil {
return nil, walkErr
}
return stale, nil
}
func removeLinks(paths []string, dryRun bool) error {
for _, path := range paths {
if dryRun {
fmt.Printf("dry-run: remove %s\n", path)
continue
}
if err := os.Remove(path); err != nil {
return err
}
fmt.Printf("removed %s\n", path)
}
return nil
}
func handleStaleLinks(stales []string) error {
if len(stales) == 0 {
return nil
}
repo, err := repoPath()
if err != nil {
return err
}
reader := newReader()
for _, path := range stales {
fmt.Printf("stale: %s\n", path)
canUnlink, err := staleHasRepoFile(path, repo)
if err != nil {
return err
}
prompt := "action [p=prune, u=unlink, i=ignore]: "
if !canUnlink {
prompt = "action [p=prune, i=ignore]: "
}
fmt.Print(prompt)
choice, err := reader.ReadString('\n')
if err != nil {
return err
}
choice = strings.TrimSpace(strings.ToLower(choice))
if choice == "" || choice == "i" {
continue
}
if choice == "p" {
if err := os.Remove(path); err != nil {
return err
}
fmt.Printf("removed %s\n", path)
continue
}
if choice == "u" && canUnlink {
if err := unlinkStale(path, repo); err != nil {
return err
}
continue
}
fmt.Println("invalid choice; skipping")
}
return nil
}
func staleHasRepoFile(targetPath, repo string) (bool, error) {
repoPath, err := repoPathForTarget(targetPath, repo)
if err != nil {
return false, err
}
if repoPath == "" {
return false, nil
}
if _, err := os.Stat(repoPath); errors.Is(err, os.ErrNotExist) {
return false, nil
} else if err != nil {
return false, err
}
return true, nil
}
func unlinkStale(targetPath, repo string) error {
repoPath, err := repoPathForTarget(targetPath, repo)
if err != nil {
return err
}
if repoPath == "" {
return nil
}
if err := os.Remove(targetPath); err != nil {
return err
}
if err := copyFile(repoPath, targetPath); err != nil {
return err
}
if err := os.Remove(repoPath); err != nil {
return err
}
fmt.Printf("unlinked %s (removed %s)\n", targetPath, repoPath)
return nil
}
func restorePackage(filesDir, targetRoot string, dryRun bool) error {
filesAbs, err := filepath.Abs(filesDir)
if err != nil {
return err
}
return filepath.WalkDir(filesDir, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if path == filesDir {
return nil
}
if entry.IsDir() {
return nil
}
rel, err := filepath.Rel(filesDir, path)
if err != nil {
return err
}
targetPath := filepath.Join(targetRoot, rel)
return restoreOne(path, targetPath, filesAbs, dryRun)
})
}
func restorePath(filesDir, targetRoot, relPath string, dryRun bool) error {
filesAbs, err := filepath.Abs(filesDir)
if err != nil {
return err
}
relPath = filepath.Clean(relPath)
if strings.HasPrefix(relPath, "..") || filepath.IsAbs(relPath) {
return fmt.Errorf("invalid relative path %q", relPath)
}
sourcePath := filepath.Join(filesDir, relPath)
info, err := os.Lstat(sourcePath)
if err != nil {
return err
}
if info.IsDir() {
return filepath.WalkDir(sourcePath, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
rel, err := filepath.Rel(filesDir, path)
if err != nil {
return err
}
targetPath := filepath.Join(targetRoot, rel)
return restoreOne(path, targetPath, filesAbs, dryRun)
})
}
rel, err := filepath.Rel(filesDir, sourcePath)
if err != nil {
return err
}
return restoreOne(sourcePath, filepath.Join(targetRoot, rel), filesAbs, dryRun)
}
func restoreOne(sourcePath, targetPath, filesAbs string, dryRun bool) error {
info, err := os.Lstat(targetPath)
if errors.Is(err, os.ErrNotExist) {
return nil
}
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink == 0 {
return nil
}
src, err := os.Readlink(targetPath)
if err != nil {
return err
}
if !filepath.IsAbs(src) {
src = filepath.Join(filepath.Dir(targetPath), src)
}
src = filepath.Clean(src)
if !strings.HasPrefix(src, filesAbs+string(os.PathSeparator)) && src != filesAbs {
return nil
}
if dryRun {
fmt.Printf("dry-run: restore %s\n", targetPath)
return nil
}
if err := os.Remove(targetPath); err != nil {
return err
}
fmt.Printf("restored %s\n", targetPath)
return copyFile(sourcePath, targetPath)
}
func copyFile(src, dst string) error {
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
info, err := srcFile.Stat()
if err != nil {
return err
}
dstFile, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
return nil
}
func moveDirContents(srcDir, destDir string) error {
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(srcDir, entry.Name())
destPath := filepath.Join(destDir, entry.Name())
if _, err := os.Stat(destPath); err == nil {
return fmt.Errorf("destination already exists: %s", destPath)
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
if err := os.Rename(srcPath, destPath); err != nil {
return err
}
}
return nil
}