diff --git a/main.go b/main.go index a2244f3..73f1af6 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,8 @@ import ( "sync" "time" + "modify/utils" + "github.com/bmatcuk/doublestar/v4" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" @@ -25,49 +27,12 @@ type GlobalStats struct { FailedFiles int } -var stats GlobalStats = GlobalStats{} - var ( - gitFlag = flag.Bool("git", false, "Use git to manage files") - resetFlag = flag.Bool("reset", false, "Reset files to their original state") - logLevel = flag.String("loglevel", "INFO", "Set log level: ERROR, WARNING, INFO, DEBUG, TRACE") - cookfile = flag.String("cook", "**/cook.yml", "Path to cook config files, can be globbed") - parallelfiles = flag.Int("P", 100, "Number of files to process in parallel") - repo *git.Repository - worktree *git.Worktree + repo *git.Repository + worktree *git.Worktree + stats GlobalStats = GlobalStats{} ) -type ModifyCommand struct { - Pattern string `yaml:"pattern"` - LuaExpr string `yaml:"lua"` - Files []string `yaml:"files"` - Git bool `yaml:"git"` - Reset bool `yaml:"reset"` - LogLevel string `yaml:"loglevel"` -} -type ReplaceCommand struct { - From int - To int - With string -} -type CookFile []ModifyCommand - -func (c *ModifyCommand) Validate() error { - if c.Pattern == "" { - return fmt.Errorf("pattern is required") - } - if c.LuaExpr == "" { - return fmt.Errorf("lua expression is required") - } - if len(c.Files) == 0 { - return fmt.Errorf("at least one file is required") - } - if c.LogLevel == "" { - c.LogLevel = "INFO" - } - return nil -} - func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options] <...files_or_globs>\n", os.Args[0]) @@ -92,13 +57,13 @@ func main() { flag.Parse() args := flag.Args() - level := logger.ParseLevel(*logLevel) + level := logger.ParseLevel(*utils.LogLevel) logger.Init(level) logger.Info("Initializing with log level: %s", level.String()) // The plan is: // Load all commands - commands, err := LoadCommands(args) + commands, err := utils.LoadCommands(args) if err != nil { logger.Error("Failed to load commands: %v", err) flag.Usage() @@ -106,7 +71,7 @@ func main() { } // Then aggregate all the globs and deduplicate them - globs := AggregateGlobs(commands) + globs := utils.AggregateGlobs(commands) // Resolve all the files for all the globs logger.Info("Found %d unique file patterns", len(globs)) @@ -121,7 +86,7 @@ func main() { // For each file check every glob of every command // Maybe memoize this part // That way we know what commands affect what files - associations, err := AssociateFilesWithCommands(files, commands) + associations, err := utils.AssociateFilesWithCommands(files, commands) if err != nil { logger.Error("Failed to associate files with commands: %v", err) return @@ -139,7 +104,7 @@ func main() { fileDataStr := string(fileData) // Aggregate all the modifications and execute them - modifications := []ReplaceCommand{} + modifications := []utils.ReplaceCommand{} for _, command := range commands { logger.Info("Processing file %q with command %q", file, command.Pattern) // TODO: Run processor and return modifications @@ -151,7 +116,8 @@ func main() { }) logger.Trace("Applying %d replacement commands in reverse order", len(modifications)) - fileDataStr = ExecuteModifications(modifications, fileDataStr) + fileDataStr, count := utils.ExecuteModifications(modifications, fileDataStr) + logger.Info("Executed %d modifications for file %q", count, file) err = os.WriteFile(file, []byte(fileDataStr), 0644) if err != nil { @@ -257,132 +223,6 @@ func main() { } } -func ExecuteModifications(modifications []ReplaceCommand, fileData string) string { - var err error - for _, modification := range modifications { - fileData, err = modification.Execute(fileData) - if err != nil { - logger.Error("Failed to execute modification: %v", err) - continue - } - } - return fileData -} - -func (m *ReplaceCommand) Execute(fileDataStr string) (string, error) { - err := m.Validate(len(fileDataStr)) - if err != nil { - return "", fmt.Errorf("failed to validate modification: %v", err) - } - - logger.Trace("Replace pos %d-%d with %q", m.From, m.To, m.With) - return fileDataStr[:m.From] + m.With + fileDataStr[m.To:], nil -} - -func (m *ReplaceCommand) Validate(maxsize int) error { - if m.To < m.From { - return fmt.Errorf("command to is less than from: %v", m) - } - if m.From > maxsize || m.To > maxsize { - return fmt.Errorf("command from or to is greater than replacement length: %v", m) - } - return nil -} - -func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string][]ModifyCommand, error) { - associationCount := 0 - fileCommands := make(map[string][]ModifyCommand) - for _, file := range files { - for _, command := range commands { - for _, glob := range command.Files { - // TODO: Maybe memoize this function call - matches, err := doublestar.Match(glob, file) - if err != nil { - logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err) - continue - } - if matches { - logger.Debug("Found match for file %q and command %q", file, command.Pattern) - fileCommands[file] = append(fileCommands[file], command) - associationCount++ - } - } - } - logger.Debug("Found %d commands for file %q", len(fileCommands[file]), file) - if len(fileCommands[file]) == 0 { - logger.Info("No commands found for file %q", file) - } - } - logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands)) - return fileCommands, nil -} - -func AggregateGlobs(commands []ModifyCommand) map[string]struct{} { - logger.Info("Aggregating globs for %d commands", len(commands)) - globs := make(map[string]struct{}) - for _, command := range commands { - for _, glob := range command.Files { - globs[glob] = struct{}{} - } - } - logger.Info("Found %d unique globs", len(globs)) - return globs -} - -func LoadCommands(args []string) ([]ModifyCommand, error) { - commands := []ModifyCommand{} - - logger.Info("Loading commands from cook files: %s", *cookfile) - newcommands, err := LoadCommandsFromCookFiles(*cookfile) - if err != nil { - return nil, fmt.Errorf("failed to load commands from cook files: %w", err) - } - logger.Info("Successfully loaded %d commands from cook files", len(newcommands)) - commands = append(commands, newcommands...) - logger.Info("Now total commands: %d", len(commands)) - - logger.Info("Loading commands from arguments: %v", args) - newcommands, err = LoadCommandFromArgs(args) - if err != nil { - return nil, fmt.Errorf("failed to load commands from args: %w", err) - } - logger.Info("Successfully loaded %d commands from args", len(newcommands)) - commands = append(commands, newcommands...) - logger.Info("Now total commands: %d", len(commands)) - - return commands, nil -} - -func LoadCommandFromArgs(args []string) ([]ModifyCommand, error) { - // Cannot reset without git, right? - if *resetFlag { - *gitFlag = true - } - - if len(args) < 3 { - return nil, fmt.Errorf("at least %d arguments are required", 3) - } - - command := ModifyCommand{ - Pattern: args[0], - LuaExpr: args[1], - Files: args[2:], - Git: *gitFlag, - Reset: *resetFlag, - LogLevel: *logLevel, - } - - if err := command.Validate(); err != nil { - return nil, fmt.Errorf("invalid command: %w", err) - } - - return []ModifyCommand{command}, nil -} - -func LoadCommandsFromCookFiles(s string) ([]ModifyCommand, error) { - return nil, nil -} - func setupGit() error { cwd, err := os.Getwd() if err != nil { diff --git a/utils/flags.go b/utils/flags.go new file mode 100644 index 0000000..5790ca4 --- /dev/null +++ b/utils/flags.go @@ -0,0 +1,13 @@ +package utils + +import ( + "flag" +) + +var ( + GitFlag = flag.Bool("git", false, "Use git to manage files") + ResetFlag = flag.Bool("reset", false, "Reset files to their original state") + LogLevel = flag.String("loglevel", "INFO", "Set log level: ERROR, WARNING, INFO, DEBUG, TRACE") + Cookfile = flag.String("cook", "**/cook.yml", "Path to cook config files, can be globbed") + ParallelFiles = flag.Int("P", 100, "Number of files to process in parallel") +) diff --git a/utils/modifycommand.go b/utils/modifycommand.go new file mode 100644 index 0000000..3afd74a --- /dev/null +++ b/utils/modifycommand.go @@ -0,0 +1,128 @@ +package utils + +import ( + "fmt" + "modify/logger" + + "github.com/bmatcuk/doublestar/v4" +) + +type ModifyCommand struct { + Pattern string `yaml:"pattern"` + LuaExpr string `yaml:"lua"` + Files []string `yaml:"files"` + Git bool `yaml:"git"` + Reset bool `yaml:"reset"` + LogLevel string `yaml:"loglevel"` +} +type CookFile []ModifyCommand + +func (c *ModifyCommand) Validate() error { + if c.Pattern == "" { + return fmt.Errorf("pattern is required") + } + if c.LuaExpr == "" { + return fmt.Errorf("lua expression is required") + } + if len(c.Files) == 0 { + return fmt.Errorf("at least one file is required") + } + if c.LogLevel == "" { + c.LogLevel = "INFO" + } + return nil +} + +func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string][]ModifyCommand, error) { + associationCount := 0 + fileCommands := make(map[string][]ModifyCommand) + for _, file := range files { + for _, command := range commands { + for _, glob := range command.Files { + // TODO: Maybe memoize this function call + matches, err := doublestar.Match(glob, file) + if err != nil { + logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err) + continue + } + if matches { + logger.Debug("Found match for file %q and command %q", file, command.Pattern) + fileCommands[file] = append(fileCommands[file], command) + associationCount++ + } + } + } + logger.Debug("Found %d commands for file %q", len(fileCommands[file]), file) + if len(fileCommands[file]) == 0 { + logger.Info("No commands found for file %q", file) + } + } + logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands)) + return fileCommands, nil +} + +func AggregateGlobs(commands []ModifyCommand) map[string]struct{} { + logger.Info("Aggregating globs for %d commands", len(commands)) + globs := make(map[string]struct{}) + for _, command := range commands { + for _, glob := range command.Files { + globs[glob] = struct{}{} + } + } + logger.Info("Found %d unique globs", len(globs)) + return globs +} + +func LoadCommands(args []string) ([]ModifyCommand, error) { + commands := []ModifyCommand{} + + logger.Info("Loading commands from cook files: %s", *Cookfile) + newcommands, err := LoadCommandsFromCookFiles(*Cookfile) + if err != nil { + return nil, fmt.Errorf("failed to load commands from cook files: %w", err) + } + logger.Info("Successfully loaded %d commands from cook files", len(newcommands)) + commands = append(commands, newcommands...) + logger.Info("Now total commands: %d", len(commands)) + + logger.Info("Loading commands from arguments: %v", args) + newcommands, err = LoadCommandFromArgs(args) + if err != nil { + return nil, fmt.Errorf("failed to load commands from args: %w", err) + } + logger.Info("Successfully loaded %d commands from args", len(newcommands)) + commands = append(commands, newcommands...) + logger.Info("Now total commands: %d", len(commands)) + + return commands, nil +} + +func LoadCommandFromArgs(args []string) ([]ModifyCommand, error) { + // Cannot reset without git, right? + if *ResetFlag { + *GitFlag = true + } + + if len(args) < 3 { + return nil, fmt.Errorf("at least %d arguments are required", 3) + } + + command := ModifyCommand{ + Pattern: args[0], + LuaExpr: args[1], + Files: args[2:], + Git: *GitFlag, + Reset: *ResetFlag, + LogLevel: *LogLevel, + } + + if err := command.Validate(); err != nil { + return nil, fmt.Errorf("invalid command: %w", err) + } + + return []ModifyCommand{command}, nil +} + +func LoadCommandsFromCookFiles(s string) ([]ModifyCommand, error) { + return nil, nil +} diff --git a/utils/replacecommand.go b/utils/replacecommand.go new file mode 100644 index 0000000..6cabd31 --- /dev/null +++ b/utils/replacecommand.go @@ -0,0 +1,47 @@ +package utils + +import ( + "fmt" + "modify/logger" +) + +type ReplaceCommand struct { + From int + To int + With string +} + +func ExecuteModifications(modifications []ReplaceCommand, fileData string) (string, int) { + var err error + executed := 0 + for _, modification := range modifications { + fileData, err = modification.Execute(fileData) + if err != nil { + logger.Error("Failed to execute modification: %v", err) + continue + } + executed++ + } + logger.Info("Executed %d modifications", executed) + return fileData, executed +} + +func (m *ReplaceCommand) Execute(fileDataStr string) (string, error) { + err := m.Validate(len(fileDataStr)) + if err != nil { + return "", fmt.Errorf("failed to validate modification: %v", err) + } + + logger.Trace("Replace pos %d-%d with %q", m.From, m.To, m.With) + return fileDataStr[:m.From] + m.With + fileDataStr[m.To:], nil +} + +func (m *ReplaceCommand) Validate(maxsize int) error { + if m.To < m.From { + return fmt.Errorf("command to is less than from: %v", m) + } + if m.From > maxsize || m.To > maxsize { + return fmt.Errorf("command from or to is greater than replacement length: %v", m) + } + return nil +}