3 Commits

Author SHA1 Message Date
cb53596c46 Log errors in red in summary 2025-11-20 15:41:36 +01:00
88ef93e9af Fix up the filesystem summary logger 2025-11-20 15:37:58 +01:00
610ba20276 Add version flag 2025-11-20 15:32:59 +01:00
5 changed files with 119 additions and 30 deletions

View File

@@ -15,6 +15,7 @@ type FileSystem interface {
MkdirAll(path string, perm os.FileMode) error MkdirAll(path string, perm os.FileMode) error
Symlink(source, target string) error Symlink(source, target string) error
Link(source, target string) error Link(source, target string) error
RecordLinkAttempt(kind string, source, target string, err error, dryRun bool)
SummaryLines() []string SummaryLines() []string
IsDryRun() bool IsDryRun() bool
} }
@@ -47,12 +48,15 @@ func (r operationRecord) summaryLine() (string, bool) {
status := "OK" status := "OK"
if r.err != nil { if r.err != nil {
status = fmt.Sprintf("FAIL: %v", r.err) status = fmt.Sprintf("%sFAIL%s: %v", BRed, Reset, r.err)
} else if r.dryRun { } else if r.dryRun {
status = "DRY-RUN" status = "DRY-RUN"
} }
return fmt.Sprintf("%s %s -> %s (%s)", kindLabel, r.source, r.target, status), true source := FormatSourcePath(r.source)
target := FormatTargetPath(r.target)
return fmt.Sprintf("%s %s → %s (%s)", kindLabel, source, target, status), true
} }
type baseFileSystem struct { type baseFileSystem struct {
@@ -119,16 +123,20 @@ func (fs *realFileSystem) MkdirAll(path string, perm os.FileMode) error {
func (fs *realFileSystem) Symlink(source, target string) error { func (fs *realFileSystem) Symlink(source, target string) error {
err := os.Symlink(source, target) err := os.Symlink(source, target)
fs.addOperation(opSymlink, source, target, err, false) fs.RecordLinkAttempt(opSymlink, source, target, err, false)
return err return err
} }
func (fs *realFileSystem) Link(source, target string) error { func (fs *realFileSystem) Link(source, target string) error {
err := os.Link(source, target) err := os.Link(source, target)
fs.addOperation(opHardlink, source, target, err, false) fs.RecordLinkAttempt(opHardlink, source, target, err, false)
return err return err
} }
func (fs *realFileSystem) RecordLinkAttempt(kind string, source, target string, err error, dryRun bool) {
fs.addOperation(kind, source, target, err, dryRun)
}
func (fs *realFileSystem) SummaryLines() []string { func (fs *realFileSystem) SummaryLines() []string {
return summarizeOperations(fs.snapshot()) return summarizeOperations(fs.snapshot())
} }
@@ -162,15 +170,19 @@ func (fs *dryRunFileSystem) MkdirAll(path string, perm os.FileMode) error {
} }
func (fs *dryRunFileSystem) Symlink(source, target string) error { func (fs *dryRunFileSystem) Symlink(source, target string) error {
fs.addOperation(opSymlink, source, target, nil, true) fs.RecordLinkAttempt(opSymlink, source, target, nil, true)
return nil return nil
} }
func (fs *dryRunFileSystem) Link(source, target string) error { func (fs *dryRunFileSystem) Link(source, target string) error {
fs.addOperation(opHardlink, source, target, nil, true) fs.RecordLinkAttempt(opHardlink, source, target, nil, true)
return nil return nil
} }
func (fs *dryRunFileSystem) RecordLinkAttempt(kind string, source, target string, err error, dryRun bool) {
fs.addOperation(kind, source, target, err, dryRun)
}
func (fs *dryRunFileSystem) SummaryLines() []string { func (fs *dryRunFileSystem) SummaryLines() []string {
return summarizeOperations(fs.snapshot()) return summarizeOperations(fs.snapshot())
} }

19
filesystem_test.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSummaryLineMarksFailuresInRed(t *testing.T) {
fs := NewRealFileSystem()
fs.RecordLinkAttempt(opSymlink, "/tmp/source", "/tmp/target", fmt.Errorf("boom"), false)
lines := fs.SummaryLines()
assert.Equal(t, 1, len(lines))
assert.Contains(t, lines[0], BRed+"FAIL"+Reset)
assert.Contains(t, lines[0], "boom")
}

View File

@@ -177,19 +177,28 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
return return
} }
linkKind := opSymlink
if instruction.Hard {
linkKind = opHardlink
}
reportErr := func(err error) {
filesystem.RecordLinkAttempt(linkKind, instruction.Source, instruction.Target, err, filesystem.IsDryRun())
status <- err
}
if !FileExists(instruction.Source) { if !FileExists(instruction.Source) {
status <- fmt.Errorf("instruction source %s does not exist", FormatSourcePath(instruction.Source)) reportErr(fmt.Errorf("instruction source %s does not exist", FormatSourcePath(instruction.Source)))
return return
} }
if instruction.Files { if instruction.Files {
info, err := os.Stat(instruction.Source) info, err := os.Stat(instruction.Source)
if err != nil { if err != nil {
status <- fmt.Errorf("could not stat source %s; err: %v", FormatSourcePath(instruction.Source), err) reportErr(fmt.Errorf("could not stat source %s; err: %v", FormatSourcePath(instruction.Source), err))
return return
} }
if info.IsDir() { if info.IsDir() {
status <- fmt.Errorf("source %s is a directory but files=true", FormatSourcePath(instruction.Source)) reportErr(fmt.Errorf("source %s is a directory but files=true", FormatSourcePath(instruction.Source)))
return return
} }
} }
@@ -207,31 +216,31 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
if FileExists(instruction.Target) { if FileExists(instruction.Target) {
if instruction.Files { if instruction.Files {
if info, err := os.Stat(instruction.Target); err == nil && info.IsDir() { if info, err := os.Stat(instruction.Target); err == nil && info.IsDir() {
status <- fmt.Errorf("target %s is a directory but files=true", FormatTargetPath(instruction.Target)) reportErr(fmt.Errorf("target %s is a directory but files=true", FormatTargetPath(instruction.Target)))
return return
} }
} }
if instruction.Force { if instruction.Force {
isSymlink, err := IsSymlink(instruction.Target) isSymlink, err := IsSymlink(instruction.Target)
if err != nil { if err != nil {
status <- fmt.Errorf("could not determine whether %s is a sym link or not, stopping; err: %v", reportErr(fmt.Errorf("could not determine whether %s is a sym link or not, stopping; err: %v",
FormatTargetPath(instruction.Target), err) FormatTargetPath(instruction.Target), err))
return return
} }
if instruction.Hard { if instruction.Hard {
info, err := os.Stat(instruction.Target) info, err := os.Stat(instruction.Target)
if err != nil { if err != nil {
status <- fmt.Errorf("could not stat %s, stopping; err: %v", reportErr(fmt.Errorf("could not stat %s, stopping; err: %v",
FormatTargetPath(instruction.Target), err) FormatTargetPath(instruction.Target), err))
return return
} }
if info.Mode().IsRegular() && info.Name() == filepath.Base(instruction.Source) { if info.Mode().IsRegular() && info.Name() == filepath.Base(instruction.Source) {
LogTarget("Overwriting existing file %s", instruction.Target) LogTarget("Overwriting existing file %s", instruction.Target)
err := filesystem.Remove(instruction.Target) err := filesystem.Remove(instruction.Target)
if err != nil { if err != nil {
status <- fmt.Errorf("could not remove existing file %s; err: %v", reportErr(fmt.Errorf("could not remove existing file %s; err: %v",
FormatTargetPath(instruction.Target), err) FormatTargetPath(instruction.Target), err))
return return
} }
} }
@@ -241,27 +250,27 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
LogTarget("Removing symlink at %s", instruction.Target) LogTarget("Removing symlink at %s", instruction.Target)
err = filesystem.Remove(instruction.Target) err = filesystem.Remove(instruction.Target)
if err != nil { if err != nil {
status <- fmt.Errorf("failed deleting %s due to %v", reportErr(fmt.Errorf("failed deleting %s due to %v",
FormatTargetPath(instruction.Target), err) FormatTargetPath(instruction.Target), err))
return return
} }
} else { } else {
if !instruction.Delete { if !instruction.Delete {
status <- fmt.Errorf("refusing to delte actual (non symlink) file %s", reportErr(fmt.Errorf("refusing to delte actual (non symlink) file %s",
FormatTargetPath(instruction.Target)) FormatTargetPath(instruction.Target)))
return return
} }
LogImportant("Deleting (!!!) %s", instruction.Target) LogImportant("Deleting (!!!) %s", instruction.Target)
err = filesystem.RemoveAll(instruction.Target) err = filesystem.RemoveAll(instruction.Target)
if err != nil { if err != nil {
status <- fmt.Errorf("failed deleting %s due to %v", reportErr(fmt.Errorf("failed deleting %s due to %v",
FormatTargetPath(instruction.Target), err) FormatTargetPath(instruction.Target), err))
return return
} }
} }
} else { } else {
status <- fmt.Errorf("target %s exists - handle manually or set the 'forced' flag (3rd field)", reportErr(fmt.Errorf("target %s exists - handle manually or set the 'forced' flag (3rd field)",
FormatTargetPath(instruction.Target)) FormatTargetPath(instruction.Target)))
return return
} }
} }
@@ -270,8 +279,8 @@ func (instruction *LinkInstruction) RunAsync(status chan (error)) {
if _, err := os.Stat(targetDir); os.IsNotExist(err) { if _, err := os.Stat(targetDir); os.IsNotExist(err) {
err = filesystem.MkdirAll(targetDir, 0755) err = filesystem.MkdirAll(targetDir, 0755)
if err != nil { if err != nil {
status <- fmt.Errorf("failed creating directory %s due to %v", reportErr(fmt.Errorf("failed creating directory %s due to %v",
FormatTargetPath(targetDir), err) FormatTargetPath(targetDir), err))
return return
} }
} }

View File

@@ -420,8 +420,8 @@ func TestLinkInstruction_RunAsync(t *testing.T) {
summary := filesystem.SummaryLines() summary := filesystem.SummaryLines()
assert.Equal(t, 1, len(summary)) assert.Equal(t, 1, len(summary))
assert.Contains(t, summary[0], "DRY-RUN") assert.Contains(t, summary[0], "DRY-RUN")
assert.Contains(t, summary[0], srcFile) assert.Contains(t, summary[0], FormatSourcePath(srcFile))
assert.Contains(t, summary[0], target) assert.Contains(t, summary[0], FormatTargetPath(target))
}) })
} }

53
main.go
View File

@@ -3,10 +3,12 @@ package main
import ( import (
"bufio" "bufio"
"flag" "flag"
"fmt"
"io" "io"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"runtime/debug"
"sync/atomic" "sync/atomic"
utils "git.site.quack-lab.dev/dave/cyutils" utils "git.site.quack-lab.dev/dave/cyutils"
@@ -20,18 +22,27 @@ const ImportantColor = BRed
const DefaultColor = Reset const DefaultColor = Reset
const PathColor = Green const PathColor = Green
var programName = os.Args[0] var (
var undo = false programName = os.Args[0]
undo = false
version = "dev"
)
func main() { func main() {
recurse := flag.String("r", "", "recurse into directories") recurse := flag.String("r", "", "recurse into directories")
file := flag.String("f", "", "file to read instructions from") file := flag.String("f", "", "file to read instructions from")
debug := flag.Bool("d", false, "debug") debug := flag.Bool("d", false, "debug")
undoF := flag.Bool("u", false, "undo") undoF := flag.Bool("u", false, "undo")
versionFlag := flag.Bool("v", false, "print version and exit")
dryRun := flag.Bool("n", false, "dry run (no filesystem changes)") dryRun := flag.Bool("n", false, "dry run (no filesystem changes)")
flag.Parse() flag.Parse()
undo = *undoF undo = *undoF
if *versionFlag {
fmt.Println(getVersionString())
return
}
setupLogging(*debug) setupLogging(*debug)
if *dryRun { if *dryRun {
@@ -248,6 +259,44 @@ func ReadFromFilesRecursively(input string, output chan *LinkInstruction, status
} }
} }
func getVersionString() string {
if version != "" && version != "dev" {
return version
}
if info, ok := debug.ReadBuildInfo(); ok {
if info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}
var revision, modified, vcsTime string
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
revision = setting.Value
case "vcs.modified":
modified = setting.Value
case "vcs.time":
vcsTime = setting.Value
}
}
if revision != "" {
if len(revision) > 7 {
revision = revision[:7]
}
if modified == "true" {
revision += "-dirty"
}
if vcsTime != "" {
revision += " (" + vcsTime + ")"
}
return revision
}
}
return "dev"
}
func ReadFromFile(input string, output chan *LinkInstruction, status chan error, doclose bool) { func ReadFromFile(input string, output chan *LinkInstruction, status chan error, doclose bool) {
if doclose { if doclose {
defer close(output) defer close(output)