Compare commits
	
		
			18 Commits
		
	
	
		
			v5.0.0
			...
			052c670627
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 052c670627 | |||
| 67fd215d0e | |||
| 9ecbbff6fa | |||
| 774ac0f0ca | |||
| b785d24a08 | |||
| 22f991e72e | |||
| 5518b27663 | |||
| 0b899dea2c | |||
| 3424fea8ad | |||
| ddc1d83d58 | |||
| 4b0a85411d | |||
| 46e871b626 | |||
| 258dcc88e7 | |||
| 75bf449bed | |||
| 58586395fb | |||
| c5a68af5e6 | |||
| b4c0284734 | |||
| c5d1dad8de | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
| *.exe | *.exe | ||||||
| .qodo | .qodo | ||||||
|  | *.sqlite | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										41
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -18,6 +18,19 @@ | |||||||
| 				"*.yml", | 				"*.yml", | ||||||
| 			] | 			] | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "Launch Package (Payday 2)", | ||||||
|  | 			"type": "go", | ||||||
|  | 			"request": "launch", | ||||||
|  | 			"mode": "auto", | ||||||
|  | 			"program": "${workspaceFolder}", | ||||||
|  | 			"cwd": "C:/Users/Administrator/Seafile/Games-Payday2", | ||||||
|  | 			"args": [ | ||||||
|  | 				"-loglevel", | ||||||
|  | 				"trace", | ||||||
|  | 				"*.yml", | ||||||
|  | 			] | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"name": "Launch Package (Barotrauma cookfile)", | 			"name": "Launch Package (Barotrauma cookfile)", | ||||||
| 			"type": "go", | 			"type": "go", | ||||||
| @@ -32,6 +45,28 @@ | |||||||
| 				"cookassistant.yml", | 				"cookassistant.yml", | ||||||
| 			] | 			] | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "Launch Package (Quasimorph cookfile)", | ||||||
|  | 			"type": "go", | ||||||
|  | 			"request": "launch", | ||||||
|  | 			"mode": "auto", | ||||||
|  | 			"program": "${workspaceFolder}", | ||||||
|  | 			"cwd": "C:/Users/Administrator/Seafile/Games-Quasimorph", | ||||||
|  | 			"args": [ | ||||||
|  | 				"cook.yml", | ||||||
|  | 			] | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"name": "Launch Package (Rimworld cookfile)", | ||||||
|  | 			"type": "go", | ||||||
|  | 			"request": "launch", | ||||||
|  | 			"mode": "auto", | ||||||
|  | 			"program": "${workspaceFolder}", | ||||||
|  | 			"cwd": "C:/Users/Administrator/Seafile/Games-Rimworld/294100", | ||||||
|  | 			"args": [ | ||||||
|  | 				"cookVehicles.yml", | ||||||
|  | 			] | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"name": "Launch Package (Workspace)", | 			"name": "Launch Package (Workspace)", | ||||||
| 			"type": "go", | 			"type": "go", | ||||||
| @@ -39,11 +74,7 @@ | |||||||
| 			"mode": "auto", | 			"mode": "auto", | ||||||
| 			"program": "${workspaceFolder}", | 			"program": "${workspaceFolder}", | ||||||
| 			"args": [ | 			"args": [ | ||||||
| 				"-loglevel", | 				"tester.yml", | ||||||
| 				"trace", |  | ||||||
| 				"(?-s)LightComponent!anyrange=\"(!num)\"", |  | ||||||
| 				"*4", |  | ||||||
| 				"**/Outpost*.xml" |  | ||||||
| 			] | 			] | ||||||
| 		} | 		} | ||||||
| 	] | 	] | ||||||
|   | |||||||
| @@ -1,8 +1,9 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"modify/logger" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func main() { | func main() { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"modify/utils" | 	"cook/utils" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"testing" | 	"testing" | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,8 +1,9 @@ | |||||||
| module modify | module cook | ||||||
|  |  | ||||||
| go 1.24.1 | go 1.24.2 | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
|  | 	git.site.quack-lab.dev/dave/cylogger v1.2.2 | ||||||
| 	github.com/bmatcuk/doublestar/v4 v4.8.1 | 	github.com/bmatcuk/doublestar/v4 v4.8.1 | ||||||
| 	github.com/stretchr/testify v1.10.0 | 	github.com/stretchr/testify v1.10.0 | ||||||
| 	github.com/yuin/gopher-lua v1.1.1 | 	github.com/yuin/gopher-lua v1.1.1 | ||||||
| @@ -20,7 +21,10 @@ require ( | |||||||
| 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect | ||||||
| 	github.com/go-git/go-billy/v5 v5.6.2 // indirect | 	github.com/go-git/go-billy/v5 v5.6.2 // indirect | ||||||
| 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect | 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect | ||||||
|  | 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||||
|  | 	github.com/jinzhu/now v1.1.5 // indirect | ||||||
| 	github.com/kevinburke/ssh_config v1.2.0 // indirect | 	github.com/kevinburke/ssh_config v1.2.0 // indirect | ||||||
|  | 	github.com/mattn/go-sqlite3 v1.14.22 // indirect | ||||||
| 	github.com/pjbgf/sha1cd v0.3.2 // indirect | 	github.com/pjbgf/sha1cd v0.3.2 // indirect | ||||||
| 	github.com/pmezard/go-difflib v1.0.0 // indirect | 	github.com/pmezard/go-difflib v1.0.0 // indirect | ||||||
| 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect | 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect | ||||||
| @@ -28,11 +32,15 @@ require ( | |||||||
| 	github.com/xanzy/ssh-agent v0.3.3 // indirect | 	github.com/xanzy/ssh-agent v0.3.3 // indirect | ||||||
| 	golang.org/x/crypto v0.35.0 // indirect | 	golang.org/x/crypto v0.35.0 // indirect | ||||||
| 	golang.org/x/sys v0.30.0 // indirect | 	golang.org/x/sys v0.30.0 // indirect | ||||||
|  | 	golang.org/x/text v0.22.0 // indirect | ||||||
| 	gopkg.in/warnings.v0 v0.1.2 // indirect | 	gopkg.in/warnings.v0 v0.1.2 // indirect | ||||||
|  | 	gorm.io/gorm v1.30.0 // indirect | ||||||
| ) | ) | ||||||
|  |  | ||||||
| require ( | require ( | ||||||
|  | 	git.site.quack-lab.dev/dave/cyutils v1.0.0 | ||||||
| 	github.com/go-git/go-git/v5 v5.14.0 | 	github.com/go-git/go-git/v5 v5.14.0 | ||||||
| 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect | 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect | ||||||
| 	golang.org/x/net v0.35.0 // indirect | 	golang.org/x/net v0.35.0 // indirect | ||||||
|  | 	gorm.io/driver/sqlite v1.6.0 | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,5 +1,9 @@ | |||||||
| dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= | ||||||
| dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= | ||||||
|  | git.site.quack-lab.dev/dave/cylogger v1.2.2 h1:4xUXASEBlG9NiGxh7f57xHh9imW4unHzakIEpQoKC5E= | ||||||
|  | git.site.quack-lab.dev/dave/cylogger v1.2.2/go.mod h1:VS9MI4Y/cwjCBZgel7dSfCQlwtAgHmfvixOoBgBhtKg= | ||||||
|  | git.site.quack-lab.dev/dave/cyutils v1.0.0 h1:yp/jkM2M7UZ+UIQuy+vPI7yDvTUdpbEdFL8h0lzUTvA= | ||||||
|  | git.site.quack-lab.dev/dave/cyutils v1.0.0/go.mod h1:luGNFimplFhkpRLebhkVTNjG2wYfPAs+pu+UIMhBYbE= | ||||||
| github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= | ||||||
| github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= | ||||||
| github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= | ||||||
| @@ -38,6 +42,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= | |||||||
| github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= | ||||||
| github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= | ||||||
| github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= | ||||||
|  | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||||
|  | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||||
|  | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||||
|  | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||||
| github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= | ||||||
| github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= | ||||||
| github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||||||
| @@ -47,6 +55,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | |||||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||||
| github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||||||
| github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= | ||||||
|  | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= | ||||||
|  | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | ||||||
| github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= | ||||||
| github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= | ||||||
| github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= | ||||||
| @@ -104,3 +114,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | |||||||
| gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
|  | gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= | ||||||
|  | gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= | ||||||
|  | gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= | ||||||
|  | gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= | ||||||
|   | |||||||
							
								
								
									
										465
									
								
								logger/logger.go
									
									
									
									
									
								
							
							
						
						
									
										465
									
								
								logger/logger.go
									
									
									
									
									
								
							| @@ -1,465 +0,0 @@ | |||||||
| package logger |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"sync" |  | ||||||
| 	"time" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // LogLevel defines the severity of log messages |  | ||||||
| type LogLevel int |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	// LevelError is for critical errors that should always be displayed |  | ||||||
| 	LevelError LogLevel = iota |  | ||||||
| 	// LevelWarning is for important warnings |  | ||||||
| 	LevelWarning |  | ||||||
| 	// LevelInfo is for informational messages |  | ||||||
| 	LevelInfo |  | ||||||
| 	// LevelDebug is for detailed debugging information |  | ||||||
| 	LevelDebug |  | ||||||
| 	// LevelTrace is for very detailed tracing information |  | ||||||
| 	LevelTrace |  | ||||||
| 	// LevelLua is specifically for output from Lua scripts |  | ||||||
| 	LevelLua |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var levelNames = map[LogLevel]string{ |  | ||||||
| 	LevelError:   "ERROR", |  | ||||||
| 	LevelWarning: "WARNING", |  | ||||||
| 	LevelInfo:    "INFO", |  | ||||||
| 	LevelDebug:   "DEBUG", |  | ||||||
| 	LevelTrace:   "TRACE", |  | ||||||
| 	LevelLua:     "LUA", |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var levelColors = map[LogLevel]string{ |  | ||||||
| 	LevelError:   "\033[1;31m", // Bold Red |  | ||||||
| 	LevelWarning: "\033[1;33m", // Bold Yellow |  | ||||||
| 	LevelInfo:    "\033[1;32m", // Bold Green |  | ||||||
| 	LevelDebug:   "\033[1;36m", // Bold Cyan |  | ||||||
| 	LevelTrace:   "\033[1;35m", // Bold Magenta |  | ||||||
| 	LevelLua:     "\033[1;34m", // Bold Blue |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ResetColor is the ANSI code to reset text color |  | ||||||
| const ResetColor = "\033[0m" |  | ||||||
|  |  | ||||||
| // Logger is our custom logger with level support |  | ||||||
| type Logger struct { |  | ||||||
| 	mu            sync.Mutex |  | ||||||
| 	out           io.Writer |  | ||||||
| 	currentLevel  LogLevel |  | ||||||
| 	prefix        string |  | ||||||
| 	flag          int |  | ||||||
| 	useColors     bool |  | ||||||
| 	callerOffset  int |  | ||||||
| 	defaultFields map[string]interface{} |  | ||||||
| 	showGoroutine bool |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	// DefaultLogger is the global logger instance |  | ||||||
| 	DefaultLogger *Logger |  | ||||||
| 	// defaultLogLevel is the default log level if not specified |  | ||||||
| 	defaultLogLevel = LevelInfo |  | ||||||
| 	// Global mutex for DefaultLogger initialization |  | ||||||
| 	initMutex sync.Mutex |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // ParseLevel converts a string log level to LogLevel |  | ||||||
| func ParseLevel(levelStr string) LogLevel { |  | ||||||
| 	switch strings.ToUpper(levelStr) { |  | ||||||
| 	case "ERROR": |  | ||||||
| 		return LevelError |  | ||||||
| 	case "WARNING", "WARN": |  | ||||||
| 		return LevelWarning |  | ||||||
| 	case "INFO": |  | ||||||
| 		return LevelInfo |  | ||||||
| 	case "DEBUG": |  | ||||||
| 		return LevelDebug |  | ||||||
| 	case "TRACE": |  | ||||||
| 		return LevelTrace |  | ||||||
| 	case "LUA": |  | ||||||
| 		return LevelLua |  | ||||||
| 	default: |  | ||||||
| 		return defaultLogLevel |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // String returns the string representation of the log level |  | ||||||
| func (l LogLevel) String() string { |  | ||||||
| 	if name, ok := levelNames[l]; ok { |  | ||||||
| 		return name |  | ||||||
| 	} |  | ||||||
| 	return fmt.Sprintf("Level(%d)", l) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // New creates a new Logger instance |  | ||||||
| func New(out io.Writer, prefix string, flag int) *Logger { |  | ||||||
| 	return &Logger{ |  | ||||||
| 		out:           out, |  | ||||||
| 		currentLevel:  defaultLogLevel, |  | ||||||
| 		prefix:        prefix, |  | ||||||
| 		flag:          flag, |  | ||||||
| 		useColors:     true, |  | ||||||
| 		callerOffset:  0, |  | ||||||
| 		defaultFields: make(map[string]interface{}), |  | ||||||
| 		showGoroutine: true, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Init initializes the DefaultLogger |  | ||||||
| func Init(level LogLevel) { |  | ||||||
| 	initMutex.Lock() |  | ||||||
| 	defer initMutex.Unlock() |  | ||||||
|  |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		DefaultLogger = New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.SetLevel(level) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetLevel sets the current log level |  | ||||||
| func (l *Logger) SetLevel(level LogLevel) { |  | ||||||
| 	l.mu.Lock() |  | ||||||
| 	defer l.mu.Unlock() |  | ||||||
| 	l.currentLevel = level |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetLevel returns the current log level |  | ||||||
| func (l *Logger) GetLevel() LogLevel { |  | ||||||
| 	l.mu.Lock() |  | ||||||
| 	defer l.mu.Unlock() |  | ||||||
| 	return l.currentLevel |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetCallerOffset sets the caller offset for correct file and line reporting |  | ||||||
| func (l *Logger) SetCallerOffset(offset int) { |  | ||||||
| 	l.mu.Lock() |  | ||||||
| 	defer l.mu.Unlock() |  | ||||||
| 	l.callerOffset = offset |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetShowGoroutine sets whether to include goroutine ID in log messages |  | ||||||
| func (l *Logger) SetShowGoroutine(show bool) { |  | ||||||
| 	l.mu.Lock() |  | ||||||
| 	defer l.mu.Unlock() |  | ||||||
| 	l.showGoroutine = show |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ShowGoroutine returns whether goroutine ID is included in log messages |  | ||||||
| func (l *Logger) ShowGoroutine() bool { |  | ||||||
| 	l.mu.Lock() |  | ||||||
| 	defer l.mu.Unlock() |  | ||||||
| 	return l.showGoroutine |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // WithField adds a field to the logger's context |  | ||||||
| func (l *Logger) WithField(key string, value interface{}) *Logger { |  | ||||||
| 	newLogger := &Logger{ |  | ||||||
| 		out:           l.out, |  | ||||||
| 		currentLevel:  l.currentLevel, |  | ||||||
| 		prefix:        l.prefix, |  | ||||||
| 		flag:          l.flag, |  | ||||||
| 		useColors:     l.useColors, |  | ||||||
| 		callerOffset:  l.callerOffset, |  | ||||||
| 		defaultFields: make(map[string]interface{}), |  | ||||||
| 		showGoroutine: l.showGoroutine, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Copy existing fields |  | ||||||
| 	for k, v := range l.defaultFields { |  | ||||||
| 		newLogger.defaultFields[k] = v |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Add new field |  | ||||||
| 	newLogger.defaultFields[key] = value |  | ||||||
| 	return newLogger |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // WithFields adds multiple fields to the logger's context |  | ||||||
| func (l *Logger) WithFields(fields map[string]interface{}) *Logger { |  | ||||||
| 	newLogger := &Logger{ |  | ||||||
| 		out:           l.out, |  | ||||||
| 		currentLevel:  l.currentLevel, |  | ||||||
| 		prefix:        l.prefix, |  | ||||||
| 		flag:          l.flag, |  | ||||||
| 		useColors:     l.useColors, |  | ||||||
| 		callerOffset:  l.callerOffset, |  | ||||||
| 		defaultFields: make(map[string]interface{}), |  | ||||||
| 		showGoroutine: l.showGoroutine, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Copy existing fields |  | ||||||
| 	for k, v := range l.defaultFields { |  | ||||||
| 		newLogger.defaultFields[k] = v |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Add new fields |  | ||||||
| 	for k, v := range fields { |  | ||||||
| 		newLogger.defaultFields[k] = v |  | ||||||
| 	} |  | ||||||
| 	return newLogger |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetGoroutineID extracts the goroutine ID from the runtime stack |  | ||||||
| func GetGoroutineID() string { |  | ||||||
| 	buf := make([]byte, 64) |  | ||||||
| 	n := runtime.Stack(buf, false) |  | ||||||
| 	// Format of first line is "goroutine N [state]:" |  | ||||||
| 	// We only need the N part |  | ||||||
| 	buf = buf[:n] |  | ||||||
| 	idField := bytes.Fields(bytes.Split(buf, []byte{':'})[0])[1] |  | ||||||
| 	return string(idField) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // formatMessage formats a log message with level, time, file, and line information |  | ||||||
| func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string { |  | ||||||
| 	var msg string |  | ||||||
| 	if len(args) > 0 { |  | ||||||
| 		msg = fmt.Sprintf(format, args...) |  | ||||||
| 	} else { |  | ||||||
| 		msg = format |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Format default fields if any |  | ||||||
| 	var fields string |  | ||||||
| 	if len(l.defaultFields) > 0 { |  | ||||||
| 		var pairs []string |  | ||||||
| 		for k, v := range l.defaultFields { |  | ||||||
| 			pairs = append(pairs, fmt.Sprintf("%s=%v", k, v)) |  | ||||||
| 		} |  | ||||||
| 		fields = " " + strings.Join(pairs, " ") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var levelColor, resetColor string |  | ||||||
| 	if l.useColors { |  | ||||||
| 		levelColor = levelColors[level] |  | ||||||
| 		resetColor = ResetColor |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var caller string |  | ||||||
| 	if l.flag&log.Lshortfile != 0 || l.flag&log.Llongfile != 0 { |  | ||||||
| 		// Find the actual caller by scanning up the stack |  | ||||||
| 		// until we find a function outside the logger package |  | ||||||
| 		var file string |  | ||||||
| 		var line int |  | ||||||
| 		var ok bool |  | ||||||
|  |  | ||||||
| 		// Start at a reasonable depth and scan up to 10 frames |  | ||||||
| 		for depth := 4; depth < 15; depth++ { |  | ||||||
| 			_, file, line, ok = runtime.Caller(depth) |  | ||||||
| 			if !ok { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// If the caller is not in the logger package, we found our caller |  | ||||||
| 			if !strings.Contains(file, "logger/logger.go") { |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if !ok { |  | ||||||
| 			file = "???" |  | ||||||
| 			line = 0 |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if l.flag&log.Lshortfile != 0 { |  | ||||||
| 			file = filepath.Base(file) |  | ||||||
| 		} |  | ||||||
| 		caller = fmt.Sprintf("%-25s ", file+":"+strconv.Itoa(line)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Format the timestamp with fixed width |  | ||||||
| 	var timeStr string |  | ||||||
| 	if l.flag&(log.Ldate|log.Ltime|log.Lmicroseconds) != 0 { |  | ||||||
| 		t := time.Now() |  | ||||||
| 		if l.flag&log.Ldate != 0 { |  | ||||||
| 			timeStr += fmt.Sprintf("%04d/%02d/%02d ", t.Year(), t.Month(), t.Day()) |  | ||||||
| 		} |  | ||||||
| 		if l.flag&(log.Ltime|log.Lmicroseconds) != 0 { |  | ||||||
| 			timeStr += fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second()) |  | ||||||
| 			if l.flag&log.Lmicroseconds != 0 { |  | ||||||
| 				timeStr += fmt.Sprintf(".%06d", t.Nanosecond()/1000) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		timeStr = fmt.Sprintf("%-15s ", timeStr) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Add goroutine ID if enabled, with fixed width |  | ||||||
| 	var goroutineStr string |  | ||||||
| 	if l.showGoroutine { |  | ||||||
| 		goroutineID := GetGoroutineID() |  | ||||||
| 		goroutineStr = fmt.Sprintf("[g:%-4s] ", goroutineID) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Create a colored level indicator with both brackets colored |  | ||||||
| 	levelStr := fmt.Sprintf("%s[%s]%s", levelColor, levelNames[level], levelColor) |  | ||||||
| 	// Add a space after the level and before the reset color |  | ||||||
| 	levelColumn := fmt.Sprintf("%s %s", levelStr, resetColor) |  | ||||||
|  |  | ||||||
| 	return fmt.Sprintf("%s%s%s%s%s%s%s\n", |  | ||||||
| 		l.prefix, timeStr, caller, goroutineStr, levelColumn, msg, fields) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // log logs a message at the specified level |  | ||||||
| func (l *Logger) log(level LogLevel, format string, args ...interface{}) { |  | ||||||
| 	// Always show LUA level logs regardless of the current log level |  | ||||||
| 	if level != LevelLua && level > l.currentLevel { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	l.mu.Lock() |  | ||||||
| 	defer l.mu.Unlock() |  | ||||||
|  |  | ||||||
| 	msg := l.formatMessage(level, format, args...) |  | ||||||
| 	fmt.Fprint(l.out, msg) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Error logs an error message |  | ||||||
| func (l *Logger) Error(format string, args ...interface{}) { |  | ||||||
| 	l.log(LevelError, format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Warning logs a warning message |  | ||||||
| func (l *Logger) Warning(format string, args ...interface{}) { |  | ||||||
| 	l.log(LevelWarning, format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Info logs an informational message |  | ||||||
| func (l *Logger) Info(format string, args ...interface{}) { |  | ||||||
| 	l.log(LevelInfo, format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Debug logs a debug message |  | ||||||
| func (l *Logger) Debug(format string, args ...interface{}) { |  | ||||||
| 	l.log(LevelDebug, format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Trace logs a trace message |  | ||||||
| func (l *Logger) Trace(format string, args ...interface{}) { |  | ||||||
| 	l.log(LevelTrace, format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Lua logs a Lua message |  | ||||||
| func (l *Logger) Lua(format string, args ...interface{}) { |  | ||||||
| 	l.log(LevelLua, format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Global log functions that use DefaultLogger |  | ||||||
|  |  | ||||||
| // Error logs an error message using the default logger |  | ||||||
| func Error(format string, args ...interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.Error(format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Warning logs a warning message using the default logger |  | ||||||
| func Warning(format string, args ...interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.Warning(format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Info logs an informational message using the default logger |  | ||||||
| func Info(format string, args ...interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.Info(format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Debug logs a debug message using the default logger |  | ||||||
| func Debug(format string, args ...interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.Debug(format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Trace logs a trace message using the default logger |  | ||||||
| func Trace(format string, args ...interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.Trace(format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Lua logs a Lua message using the default logger |  | ||||||
| func Lua(format string, args ...interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.Lua(format, args...) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // LogPanic logs a panic error and its stack trace |  | ||||||
| func LogPanic(r interface{}) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	stack := make([]byte, 4096) |  | ||||||
| 	n := runtime.Stack(stack, false) |  | ||||||
| 	DefaultLogger.Error("PANIC: %v\n%s", r, stack[:n]) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetLevel sets the log level for the default logger |  | ||||||
| func SetLevel(level LogLevel) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(level) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.SetLevel(level) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetLevel gets the log level for the default logger |  | ||||||
| func GetLevel() LogLevel { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	return DefaultLogger.GetLevel() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // WithField returns a new logger with the field added to the default logger's context |  | ||||||
| func WithField(key string, value interface{}) *Logger { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	return DefaultLogger.WithField(key, value) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // WithFields returns a new logger with the fields added to the default logger's context |  | ||||||
| func WithFields(fields map[string]interface{}) *Logger { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	return DefaultLogger.WithFields(fields) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SetShowGoroutine enables or disables goroutine ID display in the default logger |  | ||||||
| func SetShowGoroutine(show bool) { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	DefaultLogger.SetShowGoroutine(show) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ShowGoroutine returns whether goroutine ID is included in default logger's messages |  | ||||||
| func ShowGoroutine() bool { |  | ||||||
| 	if DefaultLogger == nil { |  | ||||||
| 		Init(defaultLogLevel) |  | ||||||
| 	} |  | ||||||
| 	return DefaultLogger.ShowGoroutine() |  | ||||||
| } |  | ||||||
| @@ -1,49 +0,0 @@ | |||||||
| package logger |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"runtime/debug" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // PanicHandler handles a panic and logs it |  | ||||||
| func PanicHandler() { |  | ||||||
| 	if r := recover(); r != nil { |  | ||||||
| 		goroutineID := GetGoroutineID() |  | ||||||
| 		stackTrace := debug.Stack() |  | ||||||
| 		Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SafeGo launches a goroutine with panic recovery |  | ||||||
| // Usage: logger.SafeGo(func() { ... your code ... }) |  | ||||||
| func SafeGo(f func()) { |  | ||||||
| 	go func() { |  | ||||||
| 		defer PanicHandler() |  | ||||||
| 		f() |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SafeGoWithArgs launches a goroutine with panic recovery and passes arguments |  | ||||||
| // Usage: logger.SafeGoWithArgs(func(arg1, arg2 interface{}) { ... }, "value1", 42) |  | ||||||
| func SafeGoWithArgs(f func(...interface{}), args ...interface{}) { |  | ||||||
| 	go func() { |  | ||||||
| 		defer PanicHandler() |  | ||||||
| 		f(args...) |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SafeExec executes a function with panic recovery |  | ||||||
| // Useful for code that should not panic |  | ||||||
| func SafeExec(f func()) (err error) { |  | ||||||
| 	defer func() { |  | ||||||
| 		if r := recover(); r != nil { |  | ||||||
| 			goroutineID := GetGoroutineID() |  | ||||||
| 			stackTrace := debug.Stack() |  | ||||||
| 			Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace) |  | ||||||
| 			err = fmt.Errorf("panic recovered: %v", r) |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	f() |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
							
								
								
									
										123
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								main.go
									
									
									
									
									
								
							| @@ -8,12 +8,12 @@ import ( | |||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"modify/processor" | 	"cook/processor" | ||||||
| 	"modify/utils" | 	"cook/utils" | ||||||
|  |  | ||||||
| 	"github.com/go-git/go-git/v5" | 	"gopkg.in/yaml.v3" | ||||||
|  |  | ||||||
| 	"modify/logger" | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type GlobalStats struct { | type GlobalStats struct { | ||||||
| @@ -25,8 +25,6 @@ type GlobalStats struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	repo     *git.Repository |  | ||||||
| 	worktree *git.Worktree |  | ||||||
| 	stats GlobalStats = GlobalStats{ | 	stats GlobalStats = GlobalStats{ | ||||||
| 		ModificationsPerCommand: sync.Map{}, | 		ModificationsPerCommand: sync.Map{}, | ||||||
| 	} | 	} | ||||||
| @@ -36,8 +34,6 @@ func main() { | |||||||
| 	flag.Usage = func() { | 	flag.Usage = func() { | ||||||
| 		fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0]) | 		fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0]) | ||||||
| 		fmt.Fprintf(os.Stderr, "\nOptions:\n") | 		fmt.Fprintf(os.Stderr, "\nOptions:\n") | ||||||
| 		fmt.Fprintf(os.Stderr, "  -git\n") |  | ||||||
| 		fmt.Fprintf(os.Stderr, "        Use git to manage files\n") |  | ||||||
| 		fmt.Fprintf(os.Stderr, "  -reset\n") | 		fmt.Fprintf(os.Stderr, "  -reset\n") | ||||||
| 		fmt.Fprintf(os.Stderr, "        Reset files to their original state\n") | 		fmt.Fprintf(os.Stderr, "        Reset files to their original state\n") | ||||||
| 		fmt.Fprintf(os.Stderr, "  -loglevel string\n") | 		fmt.Fprintf(os.Stderr, "  -loglevel string\n") | ||||||
| @@ -57,15 +53,30 @@ func main() { | |||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 	args := flag.Args() | 	args := flag.Args() | ||||||
|  |  | ||||||
| 	level := logger.ParseLevel(*utils.LogLevel) | 	logger.InitFlag() | ||||||
| 	logger.Init(level) | 	logger.Info("Initializing with log level: %s", logger.GetLevel().String()) | ||||||
| 	logger.Info("Initializing with log level: %s", level.String()) |  | ||||||
|  | 	db, err := utils.GetDB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("Failed to get database: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	workdone, err := HandleSpecialArgs(args, err, db) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("Failed to handle special args: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	if workdone { | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// The plan is: | 	// The plan is: | ||||||
| 	// Load all commands | 	// Load all commands | ||||||
| 	commands, err := utils.LoadCommands(args) | 	commands, err := utils.LoadCommands(args) | ||||||
| 	if err != nil { | 	if err != nil || len(commands) == 0 { | ||||||
| 		logger.Error("Failed to load commands: %v", err) | 		logger.Error("Failed to load commands: %v", err) | ||||||
|  | 		CreateExampleConfig() | ||||||
| 		flag.Usage() | 		flag.Usage() | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @@ -85,7 +96,6 @@ func main() { | |||||||
| 		logger.Trace("Regex: %s", command.Regex) | 		logger.Trace("Regex: %s", command.Regex) | ||||||
| 		logger.Trace("Files: %v", command.Files) | 		logger.Trace("Files: %v", command.Files) | ||||||
| 		logger.Trace("Lua: %s", command.Lua) | 		logger.Trace("Lua: %s", command.Lua) | ||||||
| 		logger.Trace("Git: %t", command.Git) |  | ||||||
| 		logger.Trace("Reset: %t", command.Reset) | 		logger.Trace("Reset: %t", command.Reset) | ||||||
| 		logger.Trace("Isolate: %t", command.Isolate) | 		logger.Trace("Isolate: %t", command.Isolate) | ||||||
| 		logger.Trace("LogLevel: %s", command.LogLevel) | 		logger.Trace("LogLevel: %s", command.LogLevel) | ||||||
| @@ -110,6 +120,12 @@ func main() { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	err = utils.ResetWhereNecessary(associations, db) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("Failed to reset files where necessary: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Then for each file run all commands associated with the file | 	// Then for each file run all commands associated with the file | ||||||
| 	workers := make(chan struct{}, *utils.ParallelFiles) | 	workers := make(chan struct{}, *utils.ParallelFiles) | ||||||
| 	wg := sync.WaitGroup{} | 	wg := sync.WaitGroup{} | ||||||
| @@ -142,19 +158,16 @@ func main() { | |||||||
| 		logger.Debug("Created logger for command %q with log level %s", cmdName, cmdLogLevel.String()) | 		logger.Debug("Created logger for command %q with log level %s", cmdName, cmdLogLevel.String()) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// This aggregation is great but what if one modification replaces the whole entire file? |  | ||||||
| 	// Shit...... |  | ||||||
| 	// TODO: Add "Isolate" field to modifications which makes them run alone |  | ||||||
| 	for file, association := range associations { | 	for file, association := range associations { | ||||||
| 		workers <- struct{}{} | 		workers <- struct{}{} | ||||||
| 		wg.Add(1) | 		wg.Add(1) | ||||||
| 		logger.SafeGoWithArgs(func(args ...interface{}) { | 		logger.SafeGoWithArgs(func(args ...interface{}) { | ||||||
| 			defer func() { <-workers }() | 			defer func() { <-workers }() | ||||||
| 			defer wg.Done() | 			defer wg.Done() | ||||||
|  |  | ||||||
| 			// Track per-file processing time | 			// Track per-file processing time | ||||||
| 			fileStartTime := time.Now() | 			fileStartTime := time.Now() | ||||||
|  |  | ||||||
|  | 			logger.Debug("Reading file %q", file) | ||||||
| 			fileData, err := os.ReadFile(file) | 			fileData, err := os.ReadFile(file) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.Error("Failed to read file %q: %v", file, err) | 				logger.Error("Failed to read file %q: %v", file, err) | ||||||
| @@ -162,18 +175,28 @@ func main() { | |||||||
| 			} | 			} | ||||||
| 			fileDataStr := string(fileData) | 			fileDataStr := string(fileData) | ||||||
|  |  | ||||||
|  | 			logger.Debug("Saving file %q to database", file) | ||||||
|  | 			err = db.SaveFile(file, fileData) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.Error("Failed to save file %q to database: %v", file, err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			logger.Debug("Running isolate commands for file %q", file) | ||||||
| 			fileDataStr, err = RunIsolateCommands(association, file, fileDataStr, &fileMutex) | 			fileDataStr, err = RunIsolateCommands(association, file, fileDataStr, &fileMutex) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.Error("Failed to run isolate commands for file %q: %v", file, err) | 				logger.Error("Failed to run isolate commands for file %q: %v", file, err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			logger.Debug("Running other commands for file %q", file) | ||||||
| 			fileDataStr, err = RunOtherCommands(file, fileDataStr, association, &fileMutex, commandLoggers) | 			fileDataStr, err = RunOtherCommands(file, fileDataStr, association, &fileMutex, commandLoggers) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.Error("Failed to run other commands for file %q: %v", file, err) | 				logger.Error("Failed to run other commands for file %q: %v", file, err) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			logger.Debug("Writing file %q", file) | ||||||
| 			err = os.WriteFile(file, []byte(fileDataStr), 0644) | 			err = os.WriteFile(file, []byte(fileDataStr), 0644) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.Error("Failed to write file %q: %v", file, err) | 				logger.Error("Failed to write file %q: %v", file, err) | ||||||
| @@ -254,12 +277,76 @@ func main() { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func HandleSpecialArgs(args []string, err error, db utils.DB) (bool, error) { | ||||||
|  | 	switch args[0] { | ||||||
|  | 	case "reset": | ||||||
|  | 		err = utils.ResetAllFiles(db) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.Error("Failed to reset all files: %v", err) | ||||||
|  | 			return true, err | ||||||
|  | 		} | ||||||
|  | 		logger.Info("All files reset") | ||||||
|  | 		return true, nil | ||||||
|  | 	case "dump": | ||||||
|  | 		err = db.RemoveAllFiles() | ||||||
|  | 		if err != nil { | ||||||
|  | 			logger.Error("Failed to remove all files from database: %v", err) | ||||||
|  | 			return true, err | ||||||
|  | 		} | ||||||
|  | 		logger.Info("All files removed from database") | ||||||
|  | 		return true, nil | ||||||
|  | 	} | ||||||
|  | 	return false, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func CreateExampleConfig() { | ||||||
|  | 	commands := []utils.ModifyCommand{ | ||||||
|  | 		{ | ||||||
|  | 			Name:     "DoubleNumericValues", | ||||||
|  | 			Regex:    "<value>(\\d+)</value>", | ||||||
|  | 			Lua:      "v1 * 2", | ||||||
|  | 			Files:    []string{"data/*.xml"}, | ||||||
|  | 			LogLevel: "INFO", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:     "UpdatePrices", | ||||||
|  | 			Regex:    "price=\"(\\d+)\"", | ||||||
|  | 			Lua:      "if num(v1) < 100 then return v1 * 1.5 else return v1 end", | ||||||
|  | 			Files:    []string{"items/*.xml", "shop/*.xml"}, | ||||||
|  | 			LogLevel: "DEBUG", | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:     "IsolatedTagUpdate", | ||||||
|  | 			Regex:    "<tag>(.*?)</tag>", | ||||||
|  | 			Lua:      "string.upper(s1)", | ||||||
|  | 			Files:    []string{"config.xml"}, | ||||||
|  | 			Isolate:  true, | ||||||
|  | 			NoDedup:  true, | ||||||
|  | 			LogLevel: "TRACE", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := yaml.Marshal(commands) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("Failed to marshal example config: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err = os.WriteFile("example_cook.yml", data, 0644) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("Failed to write example_cook.yml: %v", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.Info("Wrote example_cook.yml") | ||||||
|  | } | ||||||
|  |  | ||||||
| func RunOtherCommands(file string, fileDataStr string, association utils.FileCommandAssociation, fileMutex *sync.Mutex, commandLoggers map[string]*logger.Logger) (string, error) { | func RunOtherCommands(file string, fileDataStr string, association utils.FileCommandAssociation, fileMutex *sync.Mutex, commandLoggers map[string]*logger.Logger) (string, error) { | ||||||
| 	// Aggregate all the modifications and execute them | 	// Aggregate all the modifications and execute them | ||||||
| 	modifications := []utils.ReplaceCommand{} | 	modifications := []utils.ReplaceCommand{} | ||||||
| 	for _, command := range association.Commands { | 	for _, command := range association.Commands { | ||||||
| 		// Use command-specific logger if available, otherwise fall back to default logger | 		// Use command-specific logger if available, otherwise fall back to default logger | ||||||
| 		cmdLogger := logger.DefaultLogger | 		cmdLogger := logger.Default | ||||||
| 		if cmdLog, ok := commandLoggers[command.Name]; ok { | 		if cmdLog, ok := commandLoggers[command.Name]; ok { | ||||||
| 			cmdLogger = cmdLog | 			cmdLogger = cmdLog | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -2,11 +2,12 @@ package processor | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"net/http" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| 	lua "github.com/yuin/gopher-lua" | 	lua "github.com/yuin/gopher-lua" | ||||||
|  |  | ||||||
| 	"modify/logger" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Maybe we make this an interface again for the shits and giggles | // Maybe we make this an interface again for the shits and giggles | ||||||
| @@ -178,6 +179,7 @@ function ceil(x) return math.ceil(x) end | |||||||
| function upper(s) return string.upper(s) end | function upper(s) return string.upper(s) end | ||||||
| function lower(s) return string.lower(s) end | function lower(s) return string.lower(s) end | ||||||
| function format(s, ...) return string.format(s, ...) end | function format(s, ...) return string.format(s, ...) end | ||||||
|  | function trim(s) return string.gsub(s, "^%s*(.-)%s*$", "%1") end | ||||||
|  |  | ||||||
| -- String split helper | -- String split helper | ||||||
| function strsplit(inputstr, sep) | function strsplit(inputstr, sep) | ||||||
| @@ -249,6 +251,7 @@ modified = false | |||||||
|  |  | ||||||
| 	logger.Debug("Setting up Lua print function to Go") | 	logger.Debug("Setting up Lua print function to Go") | ||||||
| 	L.SetGlobal("print", L.NewFunction(printToGo)) | 	L.SetGlobal("print", L.NewFunction(printToGo)) | ||||||
|  | 	L.SetGlobal("fetch", L.NewFunction(fetch)) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -324,3 +327,90 @@ func printToGo(L *lua.LState) int { | |||||||
| 	logger.Lua("%s", message) | 	logger.Lua("%s", message) | ||||||
| 	return 0 | 	return 0 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func fetch(L *lua.LState) int { | ||||||
|  | 	// Get URL from first argument | ||||||
|  | 	url := L.ToString(1) | ||||||
|  | 	if url == "" { | ||||||
|  | 		L.Push(lua.LNil) | ||||||
|  | 		L.Push(lua.LString("URL is required")) | ||||||
|  | 		return 2 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Get options from second argument if provided | ||||||
|  | 	var method string = "GET" | ||||||
|  | 	var headers map[string]string = make(map[string]string) | ||||||
|  | 	var body string = "" | ||||||
|  |  | ||||||
|  | 	if L.GetTop() > 1 { | ||||||
|  | 		options := L.ToTable(2) | ||||||
|  | 		if options != nil { | ||||||
|  | 			// Get method | ||||||
|  | 			if methodVal := options.RawGetString("method"); methodVal != lua.LNil { | ||||||
|  | 				method = methodVal.String() | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get headers | ||||||
|  | 			if headersVal := options.RawGetString("headers"); headersVal != lua.LNil { | ||||||
|  | 				if headersTable, ok := headersVal.(*lua.LTable); ok { | ||||||
|  | 					headersTable.ForEach(func(key lua.LValue, value lua.LValue) { | ||||||
|  | 						headers[key.String()] = value.String() | ||||||
|  | 					}) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Get body | ||||||
|  | 			if bodyVal := options.RawGetString("body"); bodyVal != lua.LNil { | ||||||
|  | 				body = bodyVal.String() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create HTTP request | ||||||
|  | 	req, err := http.NewRequest(method, url, strings.NewReader(body)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		L.Push(lua.LNil) | ||||||
|  | 		L.Push(lua.LString(fmt.Sprintf("Error creating request: %v", err))) | ||||||
|  | 		return 2 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Set headers | ||||||
|  | 	for key, value := range headers { | ||||||
|  | 		req.Header.Set(key, value) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Make request | ||||||
|  | 	client := &http.Client{} | ||||||
|  | 	resp, err := client.Do(req) | ||||||
|  | 	if err != nil { | ||||||
|  | 		L.Push(lua.LNil) | ||||||
|  | 		L.Push(lua.LString(fmt.Sprintf("Error making request: %v", err))) | ||||||
|  | 		return 2 | ||||||
|  | 	} | ||||||
|  | 	defer resp.Body.Close() | ||||||
|  |  | ||||||
|  | 	// Read response body | ||||||
|  | 	bodyBytes, err := io.ReadAll(resp.Body) | ||||||
|  | 	if err != nil { | ||||||
|  | 		L.Push(lua.LNil) | ||||||
|  | 		L.Push(lua.LString(fmt.Sprintf("Error reading response: %v", err))) | ||||||
|  | 		return 2 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Create response table | ||||||
|  | 	responseTable := L.NewTable() | ||||||
|  | 	responseTable.RawSetString("status", lua.LNumber(resp.StatusCode)) | ||||||
|  | 	responseTable.RawSetString("statusText", lua.LString(resp.Status)) | ||||||
|  | 	responseTable.RawSetString("ok", lua.LBool(resp.StatusCode >= 200 && resp.StatusCode < 300)) | ||||||
|  | 	responseTable.RawSetString("body", lua.LString(string(bodyBytes))) | ||||||
|  |  | ||||||
|  | 	// Set headers in response | ||||||
|  | 	headersTable := L.NewTable() | ||||||
|  | 	for key, values := range resp.Header { | ||||||
|  | 		headersTable.RawSetString(key, lua.LString(values[0])) | ||||||
|  | 	} | ||||||
|  | 	responseTable.RawSetString("headers", headersTable) | ||||||
|  |  | ||||||
|  | 	L.Push(responseTable) | ||||||
|  | 	return 1 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,16 +1,15 @@ | |||||||
| package processor | package processor | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"cook/utils" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| 	lua "github.com/yuin/gopher-lua" | 	lua "github.com/yuin/gopher-lua" | ||||||
|  |  | ||||||
| 	"modify/logger" |  | ||||||
| 	"modify/utils" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type CaptureGroup struct { | type CaptureGroup struct { | ||||||
| @@ -33,6 +32,11 @@ func ProcessRegex(content string, command utils.ModifyCommand, filename string) | |||||||
| 	// We don't HAVE to do this multiple times for a pattern | 	// We don't HAVE to do this multiple times for a pattern | ||||||
| 	// But it's quick enough for us to not care | 	// But it's quick enough for us to not care | ||||||
| 	pattern := resolveRegexPlaceholders(command.Regex) | 	pattern := resolveRegexPlaceholders(command.Regex) | ||||||
|  | 	// I'm not too happy about having to trim regex, we could have meaningful whitespace or newlines | ||||||
|  | 	// But it's a compromise that allows us to use | in yaml | ||||||
|  | 	// Otherwise we would have to escape every god damn pair of quotation marks | ||||||
|  | 	// And a bunch of other shit | ||||||
|  | 	pattern = strings.TrimSpace(pattern) | ||||||
| 	logger.Debug("Compiling regex pattern: %s", pattern) | 	logger.Debug("Compiling regex pattern: %s", pattern) | ||||||
|  |  | ||||||
| 	patternCompileStart := time.Now() | 	patternCompileStart := time.Now() | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ package processor | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"cook/utils" | ||||||
| 	"io" | 	"io" | ||||||
| 	"modify/utils" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ package processor | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"io" | 	"io" | ||||||
| 	"modify/logger" |  | ||||||
| 	"os" | 	"os" | ||||||
|  |  | ||||||
|  | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| @@ -20,7 +21,7 @@ func init() { | |||||||
| 		if disableTestLogs { | 		if disableTestLogs { | ||||||
| 			// Create a new logger that writes to nowhere | 			// Create a new logger that writes to nowhere | ||||||
| 			silentLogger := logger.New(io.Discard, "", 0) | 			silentLogger := logger.New(io.Discard, "", 0) | ||||||
| 			logger.DefaultLogger = silentLogger | 			logger.Default = silentLogger | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| package regression | package regression | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"modify/processor" | 	"cook/processor" | ||||||
| 	"modify/utils" | 	"cook/utils" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"testing" | 	"testing" | ||||||
|   | |||||||
							
								
								
									
										120
									
								
								utils/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								utils/db.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"git.site.quack-lab.dev/dave/cylogger" | ||||||
|  | 	"gorm.io/driver/sqlite" | ||||||
|  | 	"gorm.io/gorm" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type DB interface { | ||||||
|  | 	DB() *gorm.DB | ||||||
|  | 	Raw(sql string, args ...any) *gorm.DB | ||||||
|  | 	SaveFile(filePath string, fileData []byte) error | ||||||
|  | 	GetFile(filePath string) ([]byte, error) | ||||||
|  | 	GetAllFiles() ([]FileSnapshot, error) | ||||||
|  | 	RemoveAllFiles() error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type FileSnapshot struct { | ||||||
|  | 	Date     time.Time `gorm:"primaryKey"` | ||||||
|  | 	FilePath string    `gorm:"primaryKey"` | ||||||
|  | 	FileData []byte    `gorm:"type:blob"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type DBWrapper struct { | ||||||
|  | 	db *gorm.DB | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var db *DBWrapper | ||||||
|  |  | ||||||
|  | func GetDB() (DB, error) { | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	dbFile := filepath.Join("data.sqlite") | ||||||
|  | 	db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{ | ||||||
|  | 		// SkipDefaultTransaction: true, | ||||||
|  | 		PrepareStmt: true, | ||||||
|  | 		// Logger:      gormlogger.Default.LogMode(gormlogger.Silent), | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	db.AutoMigrate(&FileSnapshot{}) | ||||||
|  |  | ||||||
|  | 	return &DBWrapper{db: db}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Just a wrapper | ||||||
|  | func (db *DBWrapper) Raw(sql string, args ...any) *gorm.DB { | ||||||
|  | 	return db.db.Raw(sql, args...) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (db *DBWrapper) DB() *gorm.DB { | ||||||
|  | 	return db.db | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (db *DBWrapper) FileExists(filePath string) (bool, error) { | ||||||
|  | 	var count int64 | ||||||
|  | 	err := db.db.Model(&FileSnapshot{}).Where("file_path = ?", filePath).Count(&count).Error | ||||||
|  | 	return count > 0, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (db *DBWrapper) SaveFile(filePath string, fileData []byte) error { | ||||||
|  | 	log := cylogger.Default.WithPrefix(fmt.Sprintf("SaveFile: %q", filePath)) | ||||||
|  | 	exists, err := db.FileExists(filePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Error checking if file exists: %v", err) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	log.Debug("File exists: %t", exists) | ||||||
|  | 	// Nothing to do, file already exists | ||||||
|  | 	if exists { | ||||||
|  | 		log.Debug("File already exists, skipping save") | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Saving file to database") | ||||||
|  | 	return db.db.Create(&FileSnapshot{ | ||||||
|  | 		Date:     time.Now(), | ||||||
|  | 		FilePath: filePath, | ||||||
|  | 		FileData: fileData, | ||||||
|  | 	}).Error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (db *DBWrapper) GetFile(filePath string) ([]byte, error) { | ||||||
|  | 	log := cylogger.Default.WithPrefix(fmt.Sprintf("GetFile: %q", filePath)) | ||||||
|  | 	log.Debug("Getting file from database") | ||||||
|  | 	var fileSnapshot FileSnapshot | ||||||
|  | 	err := db.db.Model(&FileSnapshot{}).Where("file_path = ?", filePath).First(&fileSnapshot).Error | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	log.Debug("File found in database") | ||||||
|  | 	return fileSnapshot.FileData, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (db *DBWrapper) GetAllFiles() ([]FileSnapshot, error) { | ||||||
|  | 	log := cylogger.Default.WithPrefix("GetAllFiles") | ||||||
|  | 	log.Debug("Getting all files from database") | ||||||
|  | 	var fileSnapshots []FileSnapshot | ||||||
|  | 	err := db.db.Model(&FileSnapshot{}).Find(&fileSnapshots).Error | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Found %d files in database", len(fileSnapshots)) | ||||||
|  | 	return fileSnapshots, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (db *DBWrapper) RemoveAllFiles() error { | ||||||
|  | 	log := cylogger.Default.WithPrefix("RemoveAllFiles") | ||||||
|  | 	log.Debug("Removing all files from database") | ||||||
|  | 	err := db.db.Exec("DELETE FROM file_snapshots").Error | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	log.Debug("All files removed from database") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										95
									
								
								utils/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								utils/file.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"git.site.quack-lab.dev/dave/cylogger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func CleanPath(path string) string { | ||||||
|  | 	log := cylogger.Default.WithPrefix(fmt.Sprintf("CleanPath: %q", path)) | ||||||
|  | 	log.Trace("Start") | ||||||
|  | 	path = filepath.Clean(path) | ||||||
|  | 	path = strings.ReplaceAll(path, "\\", "/") | ||||||
|  | 	log.Trace("Done: %q", path) | ||||||
|  | 	return path | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ToAbs(path string) string { | ||||||
|  | 	log := cylogger.Default.WithPrefix(fmt.Sprintf("ToAbs: %q", path)) | ||||||
|  | 	log.Trace("Start") | ||||||
|  | 	if filepath.IsAbs(path) { | ||||||
|  | 		log.Trace("Path is already absolute: %q", path) | ||||||
|  | 		return CleanPath(path) | ||||||
|  | 	} | ||||||
|  | 	cwd, err := os.Getwd() | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("Error getting cwd: %v", err) | ||||||
|  | 		return CleanPath(path) | ||||||
|  | 	} | ||||||
|  | 	log.Trace("Cwd: %q", cwd) | ||||||
|  | 	return CleanPath(filepath.Join(cwd, path)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResetWhereNecessary(associations map[string]FileCommandAssociation, db DB) error { | ||||||
|  | 	log := cylogger.Default.WithPrefix("ResetWhereNecessary") | ||||||
|  | 	log.Debug("Start") | ||||||
|  | 	dirtyFiles := make(map[string]struct{}) | ||||||
|  | 	for _, association := range associations { | ||||||
|  | 		for _, command := range association.Commands { | ||||||
|  | 			log.Debug("Checking command %q for file %q", command.Name, association.File) | ||||||
|  | 			if command.Reset { | ||||||
|  | 				log.Debug("Command %q requires reset for file %q", command.Name, association.File) | ||||||
|  | 				dirtyFiles[association.File] = struct{}{} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		for _, command := range association.IsolateCommands { | ||||||
|  | 			log.Debug("Checking isolate command %q for file %q", command.Name, association.File) | ||||||
|  | 			if command.Reset { | ||||||
|  | 				log.Debug("Isolate command %q requires reset for file %q", command.Name, association.File) | ||||||
|  | 				dirtyFiles[association.File] = struct{}{} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Dirty files: %v", dirtyFiles) | ||||||
|  | 	for file := range dirtyFiles { | ||||||
|  | 		log.Debug("Resetting file %q", file) | ||||||
|  | 		fileData, err := db.GetFile(file) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Warning("Failed to get file %q: %v", file, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		log.Debug("Writing file %q to disk", file) | ||||||
|  | 		err = os.WriteFile(file, fileData, 0644) | ||||||
|  | 		if err != nil { | ||||||
|  | 			log.Warning("Failed to write file %q: %v", file, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		log.Debug("File %q written to disk", file) | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Done") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ResetAllFiles(db DB) error { | ||||||
|  | 	log := cylogger.Default.WithPrefix("ResetAllFiles") | ||||||
|  | 	log.Debug("Start") | ||||||
|  | 	fileSnapshots, err := db.GetAllFiles() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Found %d files in database", len(fileSnapshots)) | ||||||
|  | 	for _, fileSnapshot := range fileSnapshots { | ||||||
|  | 		log.Debug("Resetting file %q", fileSnapshot.FilePath) | ||||||
|  | 		err = os.WriteFile(fileSnapshot.FilePath, fileSnapshot.FileData, 0644) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		log.Debug("File %q written to disk", fileSnapshot.FilePath) | ||||||
|  | 	} | ||||||
|  | 	log.Debug("Done") | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @@ -5,12 +5,6 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	// Deprecated |  | ||||||
| 	GitFlag       = flag.Bool("git", false, "Use git to manage files") |  | ||||||
| 	// Deprecated |  | ||||||
| 	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") | 	ParallelFiles = flag.Int("P", 100, "Number of files to process in parallel") | ||||||
| 	Filter        = flag.String("filter", "", "Filter commands before running them") | 	Filter        = flag.String("f", "", "Filter commands before running them") | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										97
									
								
								utils/git.go
									
									
									
									
									
								
							
							
						
						
									
										97
									
								
								utils/git.go
									
									
									
									
									
								
							| @@ -1,97 +0,0 @@ | |||||||
| package utils |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"modify/logger" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/go-git/go-git/v5/plumbing/object" |  | ||||||
| 	"github.com/go-git/go-git/v5" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	Repo     *git.Repository |  | ||||||
| 	Worktree *git.Worktree |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func SetupGit() error { |  | ||||||
| 	cwd, err := os.Getwd() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to get current working directory: %w", err) |  | ||||||
| 	} |  | ||||||
| 	logger.Debug("Current working directory obtained: %s", cwd) |  | ||||||
|  |  | ||||||
| 	logger.Debug("Attempting to open git repository at %s", cwd) |  | ||||||
| 	Repo, err = git.PlainOpen(cwd) |  | ||||||
| 	if err != nil { |  | ||||||
| 		logger.Debug("No existing git repository found at %s, attempting to initialize a new git repository.", cwd) |  | ||||||
| 		Repo, err = git.PlainInit(cwd, false) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("failed to initialize a new git repository at %s: %w", cwd, err) |  | ||||||
| 		} |  | ||||||
| 		logger.Info("Successfully initialized a new git repository at %s", cwd) |  | ||||||
| 	} else { |  | ||||||
| 		logger.Info("Successfully opened existing git repository at %s", cwd) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	logger.Debug("Attempting to obtain worktree for repository at %s", cwd) |  | ||||||
| 	Worktree, err = Repo.Worktree() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to obtain worktree for repository at %s: %w", cwd, err) |  | ||||||
| 	} |  | ||||||
| 	logger.Debug("Successfully obtained worktree for repository at %s", cwd) |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func CleanupGitFiles(files []string) error { |  | ||||||
| 	for _, file := range files { |  | ||||||
| 		logger.Debug("Checking git status for file: %s", file) |  | ||||||
| 		status, err := Worktree.Status() |  | ||||||
| 		if err != nil { |  | ||||||
| 			logger.Error("Error getting worktree status: %v", err) |  | ||||||
| 			fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err) |  | ||||||
| 			return fmt.Errorf("error getting worktree status: %w", err) |  | ||||||
| 		} |  | ||||||
| 		if status.IsUntracked(file) { |  | ||||||
| 			logger.Info("Detected untracked file: %s. Adding to git index.", file) |  | ||||||
| 			_, err = Worktree.Add(file) |  | ||||||
| 			if err != nil { |  | ||||||
| 				logger.Error("Error adding file to git: %v", err) |  | ||||||
| 				fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err) |  | ||||||
| 				return fmt.Errorf("error adding file to git: %w", err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			filename := filepath.Base(file) |  | ||||||
| 			logger.Info("File %s added successfully. Committing with message: 'Track %s'", filename, filename) |  | ||||||
| 			_, err = Worktree.Commit("Track "+filename, &git.CommitOptions{ |  | ||||||
| 				Author: &object.Signature{ |  | ||||||
| 					Name:  "Big Chef", |  | ||||||
| 					Email: "bigchef@bigchef.com", |  | ||||||
| 					When:  time.Now(), |  | ||||||
| 				}, |  | ||||||
| 			}) |  | ||||||
| 			if err != nil { |  | ||||||
| 				logger.Error("Error committing file: %v", err) |  | ||||||
| 				fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err) |  | ||||||
| 				return fmt.Errorf("error committing file: %w", err) |  | ||||||
| 			} |  | ||||||
| 			logger.Info("Successfully committed file: %s", filename) |  | ||||||
| 		} else { |  | ||||||
| 			logger.Info("File %s is already tracked. Restoring it to the working tree.", file) |  | ||||||
| 			err := Worktree.Restore(&git.RestoreOptions{ |  | ||||||
| 				Files:    []string{file}, |  | ||||||
| 				Staged:   true, |  | ||||||
| 				Worktree: true, |  | ||||||
| 			}) |  | ||||||
| 			if err != nil { |  | ||||||
| 				logger.Error("Error restoring file: %v", err) |  | ||||||
| 				fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err) |  | ||||||
| 				return fmt.Errorf("error restoring file: %w", err) |  | ||||||
| 			} |  | ||||||
| 			logger.Info("File %s restored successfully", file) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @@ -2,10 +2,11 @@ package utils | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"modify/logger" |  | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| 	"github.com/bmatcuk/doublestar/v4" | 	"github.com/bmatcuk/doublestar/v4" | ||||||
| 	"gopkg.in/yaml.v3" | 	"gopkg.in/yaml.v3" | ||||||
| ) | ) | ||||||
| @@ -15,11 +16,11 @@ type ModifyCommand struct { | |||||||
| 	Regex    string   `yaml:"regex"` | 	Regex    string   `yaml:"regex"` | ||||||
| 	Lua      string   `yaml:"lua"` | 	Lua      string   `yaml:"lua"` | ||||||
| 	Files    []string `yaml:"files"` | 	Files    []string `yaml:"files"` | ||||||
| 	Git      bool     `yaml:"git"` |  | ||||||
| 	Reset    bool     `yaml:"reset"` | 	Reset    bool     `yaml:"reset"` | ||||||
| 	LogLevel string   `yaml:"loglevel"` | 	LogLevel string   `yaml:"loglevel"` | ||||||
| 	Isolate  bool     `yaml:"isolate"` | 	Isolate  bool     `yaml:"isolate"` | ||||||
| 	NoDedup  bool     `yaml:"nodedup"` | 	NoDedup  bool     `yaml:"nodedup"` | ||||||
|  | 	Disabled bool     `yaml:"disable"` | ||||||
| } | } | ||||||
| type CookFile []ModifyCommand | type CookFile []ModifyCommand | ||||||
|  |  | ||||||
| @@ -56,6 +57,24 @@ func Matches(path string, glob string) (bool, error) { | |||||||
| 	return matches, nil | 	return matches, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func SplitPattern(pattern string) (string, string) { | ||||||
|  | 	static, pattern := doublestar.SplitPattern(pattern) | ||||||
|  |  | ||||||
|  | 	cwd, err := os.Getwd() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", "" | ||||||
|  | 	} | ||||||
|  | 	if static == "" { | ||||||
|  | 		static = cwd | ||||||
|  | 	} | ||||||
|  | 	if !filepath.IsAbs(static) { | ||||||
|  | 		static = filepath.Join(cwd, static) | ||||||
|  | 		static = filepath.Clean(static) | ||||||
|  | 	} | ||||||
|  | 	static = strings.ReplaceAll(static, "\\", "/") | ||||||
|  | 	return static, pattern | ||||||
|  | } | ||||||
|  |  | ||||||
| type FileCommandAssociation struct { | type FileCommandAssociation struct { | ||||||
| 	File            string | 	File            string | ||||||
| 	IsolateCommands []ModifyCommand | 	IsolateCommands []ModifyCommand | ||||||
| @@ -67,6 +86,7 @@ func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[s | |||||||
| 	fileCommands := make(map[string]FileCommandAssociation) | 	fileCommands := make(map[string]FileCommandAssociation) | ||||||
|  |  | ||||||
| 	for _, file := range files { | 	for _, file := range files { | ||||||
|  | 		file = strings.ReplaceAll(file, "\\", "/") | ||||||
| 		fileCommands[file] = FileCommandAssociation{ | 		fileCommands[file] = FileCommandAssociation{ | ||||||
| 			File:            file, | 			File:            file, | ||||||
| 			IsolateCommands: []ModifyCommand{}, | 			IsolateCommands: []ModifyCommand{}, | ||||||
| @@ -74,7 +94,9 @@ func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[s | |||||||
| 		} | 		} | ||||||
| 		for _, command := range commands { | 		for _, command := range commands { | ||||||
| 			for _, glob := range command.Files { | 			for _, glob := range command.Files { | ||||||
| 				matches, err := Matches(file, glob) | 				static, pattern := SplitPattern(glob) | ||||||
|  | 				patternFile := strings.Replace(file, static+`/`, "", 1) | ||||||
|  | 				matches, err := Matches(patternFile, pattern) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err) | 					logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err) | ||||||
| 					continue | 					continue | ||||||
| @@ -131,9 +153,11 @@ func ExpandGLobs(patterns map[string]struct{}) ([]string, error) { | |||||||
| 	logger.Debug("Expanding patterns from directory: %s", cwd) | 	logger.Debug("Expanding patterns from directory: %s", cwd) | ||||||
| 	for pattern := range patterns { | 	for pattern := range patterns { | ||||||
| 		logger.Trace("Processing pattern: %s", pattern) | 		logger.Trace("Processing pattern: %s", pattern) | ||||||
| 		matches, _ := doublestar.Glob(os.DirFS(cwd), pattern) | 		static, pattern := SplitPattern(pattern) | ||||||
|  | 		matches, _ := doublestar.Glob(os.DirFS(static), pattern) | ||||||
| 		logger.Debug("Found %d matches for pattern %s", len(matches), pattern) | 		logger.Debug("Found %d matches for pattern %s", len(matches), pattern) | ||||||
| 		for _, m := range matches { | 		for _, m := range matches { | ||||||
|  | 			m = filepath.Join(static, m) | ||||||
| 			info, err := os.Stat(m) | 			info, err := os.Stat(m) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.Warning("Error getting file info for %s: %v", m, err) | 				logger.Warning("Error getting file info for %s: %v", m, err) | ||||||
| @@ -155,69 +179,41 @@ func ExpandGLobs(patterns map[string]struct{}) ([]string, error) { | |||||||
| func LoadCommands(args []string) ([]ModifyCommand, error) { | func LoadCommands(args []string) ([]ModifyCommand, error) { | ||||||
| 	commands := []ModifyCommand{} | 	commands := []ModifyCommand{} | ||||||
|  |  | ||||||
| 	logger.Info("Loading commands from cook files: %s", *Cookfile) | 	logger.Info("Loading commands from cook files: %s", args) | ||||||
| 	newcommands, err := LoadCommandsFromCookFiles(*Cookfile) | 	for _, arg := range args { | ||||||
|  | 		newcommands, err := LoadCommandsFromCookFiles(arg) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("failed to load commands from cook files: %w", err) | 			return nil, fmt.Errorf("failed to load commands from cook files: %w", err) | ||||||
| 		} | 		} | ||||||
| 		logger.Info("Successfully loaded %d commands from cook files", len(newcommands)) | 		logger.Info("Successfully loaded %d commands from cook files", len(newcommands)) | ||||||
| 	commands = append(commands, newcommands...) | 		for _, cmd := range newcommands { | ||||||
| 	logger.Info("Now total commands: %d", len(commands)) | 			if cmd.Disabled { | ||||||
|  | 				logger.Info("Skipping disabled command: %s", cmd.Name) | ||||||
| 	logger.Info("Loading commands from arguments: %v", args) | 				continue | ||||||
| 	newcommands, err = LoadCommandFromArgs(args) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if len(commands) == 0 { |  | ||||||
| 			return nil, fmt.Errorf("failed to load commands from args: %w", err) |  | ||||||
| 			} | 			} | ||||||
| 		logger.Warning("Failed to load commands from args: %v", err) | 			commands = append(commands, cmd) | ||||||
| 		} | 		} | ||||||
| 	logger.Info("Successfully loaded %d commands from args", len(newcommands)) |  | ||||||
| 	commands = append(commands, newcommands...) |  | ||||||
| 		logger.Info("Now total commands: %d", len(commands)) | 		logger.Info("Now total commands: %d", len(commands)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.Info("Loaded %d commands from all cook file", len(commands)) | ||||||
| 	return commands, nil | 	return commands, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func LoadCommandFromArgs(args []string) ([]ModifyCommand, error) { | func LoadCommandsFromCookFiles(pattern string) ([]ModifyCommand, error) { | ||||||
| 	// Cannot reset without git, right? | 	static, pattern := SplitPattern(pattern) | ||||||
| 	if *ResetFlag { |  | ||||||
| 		*GitFlag = true |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(args) < 3 { |  | ||||||
| 		return nil, fmt.Errorf("at least %d arguments are required", 3) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	command := ModifyCommand{ |  | ||||||
| 		Regex:    args[0], |  | ||||||
| 		Lua:      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) { |  | ||||||
| 	cwd, err := os.Getwd() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to get current working directory: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	commands := []ModifyCommand{} | 	commands := []ModifyCommand{} | ||||||
| 	cookFiles, err := doublestar.Glob(os.DirFS(cwd), *Cookfile) | 	cookFiles, err := doublestar.Glob(os.DirFS(static), pattern) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("failed to glob cook files: %w", err) | 		return nil, fmt.Errorf("failed to glob cook files: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, cookFile := range cookFiles { | 	for _, cookFile := range cookFiles { | ||||||
|  | 		cookFile = filepath.Join(static, cookFile) | ||||||
|  | 		cookFile = filepath.Clean(cookFile) | ||||||
|  | 		cookFile = strings.ReplaceAll(cookFile, "\\", "/") | ||||||
|  | 		logger.Info("Loading commands from cook file: %s", cookFile) | ||||||
|  |  | ||||||
| 		cookFileData, err := os.ReadFile(cookFile) | 		cookFileData, err := os.ReadFile(cookFile) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("failed to read cook file: %w", err) | 			return nil, fmt.Errorf("failed to read cook file: %w", err) | ||||||
|   | |||||||
| @@ -269,128 +269,6 @@ func TestAggregateGlobs(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestLoadCommandFromArgs(t *testing.T) { |  | ||||||
| 	// Save original flags |  | ||||||
| 	origGitFlag := *GitFlag |  | ||||||
| 	origResetFlag := *ResetFlag |  | ||||||
| 	origLogLevel := *LogLevel |  | ||||||
|  |  | ||||||
| 	// Restore original flags after test |  | ||||||
| 	defer func() { |  | ||||||
| 		*GitFlag = origGitFlag |  | ||||||
| 		*ResetFlag = origResetFlag |  | ||||||
| 		*LogLevel = origLogLevel |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	// Test cases |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name        string |  | ||||||
| 		args        []string |  | ||||||
| 		gitFlag     bool |  | ||||||
| 		resetFlag   bool |  | ||||||
| 		logLevel    string |  | ||||||
| 		shouldError bool |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name:        "Valid command", |  | ||||||
| 			args:        []string{"pattern", "expr", "file1", "file2"}, |  | ||||||
| 			gitFlag:     false, |  | ||||||
| 			resetFlag:   false, |  | ||||||
| 			logLevel:    "INFO", |  | ||||||
| 			shouldError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "Not enough args", |  | ||||||
| 			args:        []string{"pattern", "expr"}, |  | ||||||
| 			gitFlag:     false, |  | ||||||
| 			resetFlag:   false, |  | ||||||
| 			logLevel:    "INFO", |  | ||||||
| 			shouldError: true, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "With git flag", |  | ||||||
| 			args:        []string{"pattern", "expr", "file1"}, |  | ||||||
| 			gitFlag:     true, |  | ||||||
| 			resetFlag:   false, |  | ||||||
| 			logLevel:    "INFO", |  | ||||||
| 			shouldError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "With reset flag (forces git flag)", |  | ||||||
| 			args:        []string{"pattern", "expr", "file1"}, |  | ||||||
| 			gitFlag:     false, |  | ||||||
| 			resetFlag:   true, |  | ||||||
| 			logLevel:    "INFO", |  | ||||||
| 			shouldError: false, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name:        "With custom log level", |  | ||||||
| 			args:        []string{"pattern", "expr", "file1"}, |  | ||||||
| 			gitFlag:     false, |  | ||||||
| 			resetFlag:   false, |  | ||||||
| 			logLevel:    "DEBUG", |  | ||||||
| 			shouldError: false, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, tc := range tests { |  | ||||||
| 		t.Run(tc.name, func(t *testing.T) { |  | ||||||
| 			// Set flags for this test case |  | ||||||
| 			*GitFlag = tc.gitFlag |  | ||||||
| 			*ResetFlag = tc.resetFlag |  | ||||||
| 			*LogLevel = tc.logLevel |  | ||||||
|  |  | ||||||
| 			commands, err := LoadCommandFromArgs(tc.args) |  | ||||||
|  |  | ||||||
| 			if tc.shouldError { |  | ||||||
| 				if err == nil { |  | ||||||
| 					t.Errorf("Expected an error but got none") |  | ||||||
| 				} |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if err != nil { |  | ||||||
| 				t.Errorf("Unexpected error: %v", err) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if len(commands) != 1 { |  | ||||||
| 				t.Errorf("Expected 1 command, got %d", len(commands)) |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			cmd := commands[0] |  | ||||||
|  |  | ||||||
| 			// Check command properties |  | ||||||
| 			if cmd.Regex != tc.args[0] { |  | ||||||
| 				t.Errorf("Expected pattern %q, got %q", tc.args[0], cmd.Regex) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if cmd.Lua != tc.args[1] { |  | ||||||
| 				t.Errorf("Expected LuaExpr %q, got %q", tc.args[1], cmd.Lua) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if len(cmd.Files) != len(tc.args)-2 { |  | ||||||
| 				t.Errorf("Expected %d files, got %d", len(tc.args)-2, len(cmd.Files)) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			// When reset is true, git should be true regardless of what was set |  | ||||||
| 			expectedGit := tc.gitFlag || tc.resetFlag |  | ||||||
| 			if cmd.Git != expectedGit { |  | ||||||
| 				t.Errorf("Expected Git flag %v, got %v", expectedGit, cmd.Git) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if cmd.Reset != tc.resetFlag { |  | ||||||
| 				t.Errorf("Expected Reset flag %v, got %v", tc.resetFlag, cmd.Reset) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if cmd.LogLevel != tc.logLevel { |  | ||||||
| 				t.Errorf("Expected LogLevel %q, got %q", tc.logLevel, cmd.LogLevel) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Successfully unmarshal valid YAML data into ModifyCommand slice | // Successfully unmarshal valid YAML data into ModifyCommand slice | ||||||
| func TestLoadCommandsFromCookFileSuccess(t *testing.T) { | func TestLoadCommandsFromCookFileSuccess(t *testing.T) { | ||||||
| 	// Arrange | 	// Arrange | ||||||
| @@ -556,155 +434,6 @@ func TestLoadCommandsFromCookFileLegitExample(t *testing.T) { | |||||||
| 	assert.Equal(t, "crewlayabout", commands[0].Name) | 	assert.Equal(t, "crewlayabout", commands[0].Name) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Valid command with minimum 3 arguments returns a ModifyCommand slice with correct values |  | ||||||
| func TestLoadCommandFromArgsWithValidArguments(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	oldGitFlag := GitFlag |  | ||||||
| 	oldResetFlag := ResetFlag |  | ||||||
| 	oldLogLevel := LogLevel |  | ||||||
|  |  | ||||||
| 	gitValue := true |  | ||||||
| 	resetValue := false |  | ||||||
| 	logLevelValue := "info" |  | ||||||
|  |  | ||||||
| 	GitFlag = &gitValue |  | ||||||
| 	ResetFlag = &resetValue |  | ||||||
| 	LogLevel = &logLevelValue |  | ||||||
|  |  | ||||||
| 	defer func() { |  | ||||||
| 		GitFlag = oldGitFlag |  | ||||||
| 		ResetFlag = oldResetFlag |  | ||||||
| 		LogLevel = oldLogLevel |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	args := []string{"*.go", "return x", "file1.go", "file2.go"} |  | ||||||
|  |  | ||||||
| 	// Execute |  | ||||||
| 	commands, err := LoadCommandFromArgs(args) |  | ||||||
|  |  | ||||||
| 	// Assert |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.Len(t, commands, 1) |  | ||||||
| 	assert.Equal(t, "*.go", commands[0].Regex) |  | ||||||
| 	assert.Equal(t, "return x", commands[0].Lua) |  | ||||||
| 	assert.Equal(t, []string{"file1.go", "file2.go"}, commands[0].Files) |  | ||||||
| 	assert.Equal(t, true, commands[0].Git) |  | ||||||
| 	assert.Equal(t, false, commands[0].Reset) |  | ||||||
| 	assert.Equal(t, "info", commands[0].LogLevel) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Less than 3 arguments returns an error with appropriate message |  | ||||||
| func TestLoadCommandFromArgsWithInsufficientArguments(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	oldGitFlag := GitFlag |  | ||||||
| 	oldResetFlag := ResetFlag |  | ||||||
| 	oldLogLevel := LogLevel |  | ||||||
|  |  | ||||||
| 	gitValue := false |  | ||||||
| 	resetValue := false |  | ||||||
| 	logLevelValue := "info" |  | ||||||
|  |  | ||||||
| 	GitFlag = &gitValue |  | ||||||
| 	ResetFlag = &resetValue |  | ||||||
| 	LogLevel = &logLevelValue |  | ||||||
|  |  | ||||||
| 	defer func() { |  | ||||||
| 		GitFlag = oldGitFlag |  | ||||||
| 		ResetFlag = oldResetFlag |  | ||||||
| 		LogLevel = oldLogLevel |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	testCases := []struct { |  | ||||||
| 		name string |  | ||||||
| 		args []string |  | ||||||
| 	}{ |  | ||||||
| 		{"empty args", []string{}}, |  | ||||||
| 		{"one arg", []string{"*.go"}}, |  | ||||||
| 		{"two args", []string{"*.go", "return x"}}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, tc := range testCases { |  | ||||||
| 		t.Run(tc.name, func(t *testing.T) { |  | ||||||
| 			// Execute |  | ||||||
| 			commands, err := LoadCommandFromArgs(tc.args) |  | ||||||
|  |  | ||||||
| 			// Assert |  | ||||||
| 			assert.Error(t, err) |  | ||||||
| 			assert.Nil(t, commands) |  | ||||||
| 			assert.Contains(t, err.Error(), "at least 3 arguments are required") |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Pattern, Lua, and Files fields are correctly populated from args |  | ||||||
| func TestLoadCommandFromArgsPopulatesFieldsCorrectly(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	oldGitFlag := GitFlag |  | ||||||
| 	oldResetFlag := ResetFlag |  | ||||||
| 	oldLogLevel := LogLevel |  | ||||||
|  |  | ||||||
| 	gitValue := false |  | ||||||
| 	resetValue := false |  | ||||||
| 	logLevelValue := "debug" |  | ||||||
|  |  | ||||||
| 	GitFlag = &gitValue |  | ||||||
| 	ResetFlag = &resetValue |  | ||||||
| 	LogLevel = &logLevelValue |  | ||||||
|  |  | ||||||
| 	defer func() { |  | ||||||
| 		GitFlag = oldGitFlag |  | ||||||
| 		ResetFlag = oldResetFlag |  | ||||||
| 		LogLevel = oldLogLevel |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	args := []string{"*.txt", "print('Hello')", "file1.txt", "file2.txt"} |  | ||||||
|  |  | ||||||
| 	// Execute |  | ||||||
| 	commands, err := LoadCommandFromArgs(args) |  | ||||||
|  |  | ||||||
| 	// Assert |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.Len(t, commands, 1) |  | ||||||
| 	assert.Equal(t, "*.txt", commands[0].Regex) |  | ||||||
| 	assert.Equal(t, "print('Hello')", commands[0].Lua) |  | ||||||
| 	assert.Equal(t, []string{"file1.txt", "file2.txt"}, commands[0].Files) |  | ||||||
| 	assert.Equal(t, false, commands[0].Git) |  | ||||||
| 	assert.Equal(t, false, commands[0].Reset) |  | ||||||
| 	assert.Equal(t, "debug", commands[0].LogLevel) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Git flag is set to true when ResetFlag is true |  | ||||||
| func TestLoadCommandFromArgsSetsGitFlagWhenResetFlagIsTrue(t *testing.T) { |  | ||||||
| 	// Setup |  | ||||||
| 	oldGitFlag := GitFlag |  | ||||||
| 	oldResetFlag := ResetFlag |  | ||||||
| 	oldLogLevel := LogLevel |  | ||||||
|  |  | ||||||
| 	gitValue := false |  | ||||||
| 	resetValue := true |  | ||||||
| 	logLevelValue := "info" |  | ||||||
|  |  | ||||||
| 	GitFlag = &gitValue |  | ||||||
| 	ResetFlag = &resetValue |  | ||||||
| 	LogLevel = &logLevelValue |  | ||||||
|  |  | ||||||
| 	defer func() { |  | ||||||
| 		GitFlag = oldGitFlag |  | ||||||
| 		ResetFlag = oldResetFlag |  | ||||||
| 		LogLevel = oldLogLevel |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	args := []string{"*.go", "return x", "file1.go", "file2.go"} |  | ||||||
|  |  | ||||||
| 	// Execute |  | ||||||
| 	commands, err := LoadCommandFromArgs(args) |  | ||||||
|  |  | ||||||
| 	// Assert |  | ||||||
| 	assert.NoError(t, err) |  | ||||||
| 	assert.Len(t, commands, 1) |  | ||||||
| 	assert.Equal(t, true, commands[0].Git) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // TODO: Figure out how to mock shit | // TODO: Figure out how to mock shit | ||||||
| // Can't be asked doing that right now... | // Can't be asked doing that right now... | ||||||
| // Successfully loads commands from multiple YAML files in the current directory | // Successfully loads commands from multiple YAML files in the current directory | ||||||
|   | |||||||
| @@ -2,8 +2,9 @@ package utils | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"modify/logger" |  | ||||||
| 	"sort" | 	"sort" | ||||||
|  |  | ||||||
|  | 	logger "git.site.quack-lab.dev/dave/cylogger" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type ReplaceCommand struct { | type ReplaceCommand struct { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user