chore(go): wrap core filesystem errors with context

This commit is contained in:
Micheal Wilkinson
2026-03-21 20:52:13 +00:00
parent bc0a6747b8
commit c793925828
6 changed files with 135 additions and 56 deletions

View File

@@ -99,7 +99,7 @@ func (a *App) Clone(uri string, destination string) error {
func (a *App) List() error {
if err := os.MkdirAll(a.ReposDir, 0o750); err != nil {
return err
return fmt.Errorf("ensure repos directory: %w", err)
}
var castles []string
@@ -114,13 +114,13 @@ func (a *App) List() error {
castleRoot := filepath.Dir(path)
rel, err := filepath.Rel(a.ReposDir, castleRoot)
if err != nil {
return err
return fmt.Errorf("resolve castle path %q: %w", castleRoot, err)
}
castles = append(castles, rel)
return filepath.SkipDir
})
if err != nil {
return err
return fmt.Errorf("scan repos directory: %w", err)
}
sort.Strings(castles)
@@ -132,7 +132,7 @@ func (a *App) List() error {
}
_, writeErr := fmt.Fprintf(a.Stdout, "%s %s\n", castle, strings.TrimSpace(remote))
if writeErr != nil {
return writeErr
return fmt.Errorf("write castle listing: %w", writeErr)
}
}
@@ -234,13 +234,13 @@ func (a *App) Destroy(castle string) error {
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("castle %q not found", castle)
}
return err
return fmt.Errorf("stat castle %q: %w", castle, err)
}
if !a.Force {
confirmed, confirmErr := a.confirmDestroy(castle)
if confirmErr != nil {
return confirmErr
return fmt.Errorf("confirm destroy for %q: %w", castle, confirmErr)
}
if !confirmed {
return nil
@@ -252,7 +252,7 @@ func (a *App) Destroy(castle string) error {
castleHome := filepath.Join(castleRoot, "home")
if info, statErr := os.Stat(castleHome); statErr == nil && info.IsDir() {
if unlinkErr := a.UnlinkCastle(castle); unlinkErr != nil {
return unlinkErr
return fmt.Errorf("unlink castle %q before destroy: %w", castle, unlinkErr)
}
}
}
@@ -267,12 +267,12 @@ func (a *App) confirmDestroy(castle string) (bool, error) {
}
if _, err := fmt.Fprintf(a.Stdout, "Destroy castle %q? [y/N]: ", castle); err != nil {
return false, err
return false, fmt.Errorf("write destroy prompt: %w", err)
}
line, err := bufio.NewReader(reader).ReadString('\n')
if err != nil && !errors.Is(err, io.EOF) {
return false, err
return false, fmt.Errorf("read destroy confirmation: %w", err)
}
return isAffirmativeResponse(line), nil
@@ -392,15 +392,15 @@ func (a *App) Generate(castlePath string) error {
absCastle, err := filepath.Abs(trimmed)
if err != nil {
return err
return fmt.Errorf("resolve castle path %q: %w", trimmed, err)
}
if err := os.MkdirAll(absCastle, 0o750); err != nil {
return err
return fmt.Errorf("create castle path %q: %w", absCastle, err)
}
if err := a.runGit(absCastle, "init"); err != nil {
return err
return fmt.Errorf("initialize git repository %q: %w", absCastle, err)
}
githubUser := ""
@@ -412,11 +412,15 @@ func (a *App) Generate(castlePath string) error {
repoName := filepath.Base(absCastle)
url := fmt.Sprintf("git@github.com:%s/%s.git", githubUser, repoName)
if err := a.runGit(absCastle, "remote", "add", "origin", url); err != nil {
return err
return fmt.Errorf("add origin remote for %q: %w", absCastle, err)
}
}
return os.MkdirAll(filepath.Join(absCastle, "home"), 0o750)
if err := os.MkdirAll(filepath.Join(absCastle, "home"), 0o750); err != nil {
return fmt.Errorf("create home directory for %q: %w", absCastle, err)
}
return nil
}
func (a *App) Link(castle string) error {
@@ -435,11 +439,11 @@ func (a *App) LinkCastle(castle string) error {
subdirs, err := readSubdirs(filepath.Join(a.ReposDir, castle, ".homesick_subdir"))
if err != nil {
return err
return fmt.Errorf("read subdirs for castle %q: %w", castle, err)
}
if err := a.linkEach(castleHome, castleHome, subdirs); err != nil {
return err
return fmt.Errorf("link castle %q: %w", castle, err)
}
for _, subdir := range subdirs {
@@ -448,11 +452,11 @@ func (a *App) LinkCastle(castle string) error {
if errors.Is(err, os.ErrNotExist) {
continue
}
return err
return fmt.Errorf("stat subdir %q for castle %q: %w", base, castle, err)
}
if err := a.linkEach(castleHome, base, subdirs); err != nil {
return err
return fmt.Errorf("link subdir %q for castle %q: %w", subdir, castle, err)
}
}
@@ -475,11 +479,11 @@ func (a *App) UnlinkCastle(castle string) error {
subdirs, err := readSubdirs(filepath.Join(a.ReposDir, castle, ".homesick_subdir"))
if err != nil {
return err
return fmt.Errorf("read subdirs for castle %q: %w", castle, err)
}
if err := a.unlinkEach(castleHome, castleHome, subdirs); err != nil {
return err
return fmt.Errorf("unlink castle %q: %w", castle, err)
}
for _, subdir := range subdirs {
@@ -488,11 +492,11 @@ func (a *App) UnlinkCastle(castle string) error {
if errors.Is(err, os.ErrNotExist) {
continue
}
return err
return fmt.Errorf("stat subdir %q for castle %q: %w", base, castle, err)
}
if err := a.unlinkEach(castleHome, base, subdirs); err != nil {
return err
return fmt.Errorf("unlink subdir %q for castle %q: %w", subdir, castle, err)
}
}
@@ -522,15 +526,15 @@ func (a *App) TrackPath(filePath string, castle string) error {
absolutePath, err := filepath.Abs(strings.TrimRight(trimmedFile, string(filepath.Separator)))
if err != nil {
return err
return fmt.Errorf("resolve tracked file %q: %w", trimmedFile, err)
}
if _, err := os.Lstat(absolutePath); err != nil {
return err
return fmt.Errorf("stat tracked file %q: %w", absolutePath, err)
}
relativeDir, err := filepath.Rel(a.HomeDir, filepath.Dir(absolutePath))
if err != nil {
return err
return fmt.Errorf("resolve tracked file directory for %q: %w", absolutePath, err)
}
if relativeDir == ".." || strings.HasPrefix(relativeDir, ".."+string(filepath.Separator)) {
return fmt.Errorf("track requires file under %s", a.HomeDir)
@@ -541,18 +545,18 @@ func (a *App) TrackPath(filePath string, castle string) error {
castleTargetDir = castleHome
}
if err := os.MkdirAll(castleTargetDir, 0o750); err != nil {
return err
return fmt.Errorf("create tracked file directory %q: %w", castleTargetDir, err)
}
trackedPath := filepath.Join(castleTargetDir, filepath.Base(absolutePath))
if _, err := os.Lstat(trackedPath); err == nil {
return fmt.Errorf("%s already exists", trackedPath)
} else if !errors.Is(err, os.ErrNotExist) {
return err
return fmt.Errorf("stat tracked destination %q: %w", trackedPath, err)
}
if err := os.Rename(absolutePath, trackedPath); err != nil {
return err
return fmt.Errorf("move tracked file into castle %q: %w", trackedPath, err)
}
subdirChanged := false
@@ -560,21 +564,21 @@ func (a *App) TrackPath(filePath string, castle string) error {
subdirPath := filepath.Join(castleRoot, ".homesick_subdir")
subdirChanged, err = appendUniqueSubdir(subdirPath, relativeDir)
if err != nil {
return err
return fmt.Errorf("record tracked subdir %q: %w", relativeDir, err)
}
}
if err := a.linkPath(trackedPath, absolutePath); err != nil {
return err
return fmt.Errorf("relink tracked file %q: %w", absolutePath, err)
}
repo, err := git.PlainOpen(castleRoot)
if err != nil {
return err
return fmt.Errorf("open git repository for castle %q: %w", castle, err)
}
worktree, err := repo.Worktree()
if err != nil {
return err
return fmt.Errorf("open worktree for castle %q: %w", castle, err)
}
trackedRelativePath := filepath.Join("home", relativeDir, filepath.Base(absolutePath))
@@ -582,12 +586,12 @@ func (a *App) TrackPath(filePath string, castle string) error {
trackedRelativePath = filepath.Join("home", filepath.Base(absolutePath))
}
if _, err := worktree.Add(filepath.ToSlash(filepath.Clean(trackedRelativePath))); err != nil {
return err
return fmt.Errorf("stage tracked file %q: %w", trackedRelativePath, err)
}
if subdirChanged {
if _, err := worktree.Add(".homesick_subdir"); err != nil {
return err
return fmt.Errorf("stage subdir metadata: %w", err)
}
}
@@ -597,7 +601,7 @@ func (a *App) TrackPath(filePath string, castle string) error {
func appendUniqueSubdir(path string, subdir string) (bool, error) {
existing, err := readSubdirs(path)
if err != nil {
return false, err
return false, fmt.Errorf("load subdir metadata %q: %w", path, err)
}
cleanSubdir := filepath.Clean(subdir)
@@ -609,12 +613,12 @@ func appendUniqueSubdir(path string, subdir string) (bool, error) {
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) // #nosec G304 — internal metadata file
if err != nil {
return false, err
return false, fmt.Errorf("open subdir metadata %q: %w", path, err)
}
defer file.Close()
if _, err := file.WriteString(cleanSubdir + "\n"); err != nil {
return false, err
return false, fmt.Errorf("write subdir metadata %q: %w", path, err)
}
return true, nil
@@ -623,7 +627,7 @@ func appendUniqueSubdir(path string, subdir string) (bool, error) {
func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) error {
entries, err := os.ReadDir(baseDir)
if err != nil {
return err
return fmt.Errorf("read castle directory %q: %w", baseDir, err)
}
for _, entry := range entries {
@@ -635,7 +639,7 @@ func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) erro
source := filepath.Join(baseDir, name)
ignore, err := matchesIgnoredDir(castleHome, source, subdirs)
if err != nil {
return err
return fmt.Errorf("check ignored directory %q: %w", source, err)
}
if ignore {
continue
@@ -643,7 +647,7 @@ func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) erro
relDir, err := filepath.Rel(castleHome, baseDir)
if err != nil {
return err
return fmt.Errorf("resolve castle relative directory %q: %w", baseDir, err)
}
destination := filepath.Join(a.HomeDir, relDir, name)
@@ -652,7 +656,7 @@ func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) erro
}
if err := a.linkPath(source, destination); err != nil {
return err
return fmt.Errorf("link %q to %q: %w", source, destination, err)
}
}
@@ -662,7 +666,7 @@ func (a *App) linkEach(castleHome string, baseDir string, subdirs []string) erro
func (a *App) unlinkEach(castleHome string, baseDir string, subdirs []string) error {
entries, err := os.ReadDir(baseDir)
if err != nil {
return err
return fmt.Errorf("read castle directory %q: %w", baseDir, err)
}
for _, entry := range entries {
@@ -674,7 +678,7 @@ func (a *App) unlinkEach(castleHome string, baseDir string, subdirs []string) er
source := filepath.Join(baseDir, name)
ignore, err := matchesIgnoredDir(castleHome, source, subdirs)
if err != nil {
return err
return fmt.Errorf("check ignored directory %q: %w", source, err)
}
if ignore {
continue
@@ -682,7 +686,7 @@ func (a *App) unlinkEach(castleHome string, baseDir string, subdirs []string) er
relDir, err := filepath.Rel(castleHome, baseDir)
if err != nil {
return err
return fmt.Errorf("resolve castle relative directory %q: %w", baseDir, err)
}
destination := filepath.Join(a.HomeDir, relDir, name)
@@ -691,7 +695,7 @@ func (a *App) unlinkEach(castleHome string, baseDir string, subdirs []string) er
}
if err := unlinkPath(destination); err != nil {
return err
return fmt.Errorf("unlink %q: %w", destination, err)
}
}
@@ -717,11 +721,11 @@ func unlinkPath(destination string) error {
func (a *App) linkPath(source string, destination string) error {
absSource, err := filepath.Abs(source)
if err != nil {
return err
return fmt.Errorf("resolve link source %q: %w", source, err)
}
if err := os.MkdirAll(filepath.Dir(destination), 0o750); err != nil {
return err
return fmt.Errorf("create destination parent %q: %w", filepath.Dir(destination), err)
}
info, err := os.Lstat(destination)
@@ -738,14 +742,14 @@ func (a *App) linkPath(source string, destination string) error {
}
if rmErr := os.RemoveAll(destination); rmErr != nil {
return rmErr
return fmt.Errorf("remove existing destination %q: %w", destination, rmErr)
}
} else if !errors.Is(err, os.ErrNotExist) {
return err
return fmt.Errorf("stat destination %q: %w", destination, err)
}
if err := os.Symlink(absSource, destination); err != nil {
return err
return fmt.Errorf("create symlink %q -> %q: %w", destination, absSource, err)
}
return nil
@@ -757,7 +761,7 @@ func readSubdirs(path string) ([]string, error) {
if errors.Is(err, os.ErrNotExist) {
return []string{}, nil
}
return nil, err
return nil, fmt.Errorf("read subdirs %q: %w", path, err)
}
lines := strings.Split(string(data), "\n")
@@ -776,7 +780,7 @@ func readSubdirs(path string) ([]string, error) {
func matchesIgnoredDir(castleHome string, candidate string, subdirs []string) (bool, error) {
absCandidate, err := filepath.Abs(candidate)
if err != nil {
return false, err
return false, fmt.Errorf("resolve candidate path %q: %w", candidate, err)
}
ignoreSet := map[string]struct{}{}
@@ -853,7 +857,7 @@ func (a *App) Rc(castle string, force bool) error {
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("castle %q not found", castle)
}
return err
return fmt.Errorf("stat castle %q: %w", castle, err)
}
homesickD := filepath.Join(castleRoot, ".homesick.d")
@@ -890,12 +894,12 @@ func (a *App) Rc(castle string, force bool) error {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
return fmt.Errorf("stat rc hooks directory %q: %w", homesickD, err)
}
entries, err := os.ReadDir(homesickD)
if err != nil {
return err
return fmt.Errorf("read rc hooks %q: %w", homesickD, err)
}
// ReadDir returns entries in sorted order already.
@@ -905,7 +909,7 @@ func (a *App) Rc(castle string, force bool) error {
}
info, infoErr := entry.Info()
if infoErr != nil {
return infoErr
return fmt.Errorf("read rc hook metadata %q: %w", entry.Name(), infoErr)
}
if info.Mode()&0o111 == 0 {
// Not executable — skip.