40 Commits

Author SHA1 Message Date
299e6d8bfe Fix glob matching when backslashes are used as separators 2025-07-30 14:41:38 +02:00
388822e90a Create example yml even when no args are provided
ie. invalid usage
2025-07-30 14:38:29 +02:00
91993b4548 Improve error handling in ResetAllFiles function by logging warnings for failed file writes instead of returning errors 2025-07-27 12:47:21 +02:00
bb69558aaa Fix issue where invalid isolate commands would prevent other isolate commands from running 2025-07-21 21:28:12 +02:00
052c670627 Add a simple trim to lua 2025-07-21 21:00:11 +02:00
67fd215d0e Don't stroke out when backup doesn't exist in db 2025-07-20 11:48:55 +02:00
9ecbbff6fa Implement special flags for dump and reset db 2025-07-20 11:47:45 +02:00
774ac0f0ca Implement proper "reset" that reads snapshots from database 2025-07-20 11:43:25 +02:00
b785d24a08 Implement saving snapshots to a database 2025-07-20 11:38:08 +02:00
22f991e72e Clean up shop a bit 2025-07-20 11:20:19 +02:00
5518b27663 Remove deprecated flags and rename filter to f 2025-07-20 11:12:58 +02:00
0b899dea2c Add Disabled flag to ModifyCommand 2025-07-19 11:08:51 +02:00
3424fea8ad Dump a basic "config" to example usage on failed command 2025-07-19 01:15:48 +02:00
ddc1d83d58 Fix file associations
It was fucked because we were removing the static path and then cramming
that into assications
2025-04-22 10:53:25 +02:00
4b0a85411d From cook FILE - F I L E - FILEEEEE
This is the 4th time I make the SAME fix
2025-04-22 10:46:03 +02:00
46e871b626 Fix flag collision with logger 2025-04-22 10:45:11 +02:00
258dcc88e7 Fix reference to utils 2025-04-18 12:48:24 +02:00
75bf449bed Remove logger and replace it with a library 2025-04-18 12:47:47 +02:00
58586395fb Add file util for later 2025-04-13 21:31:19 +02:00
c5a68af5e6 PROPERLY implement doublestar 2025-04-13 21:29:18 +02:00
b4c0284734 Add rimworld cook file 2025-04-09 09:47:53 +02:00
c5d1dad8de Rename project to "cook" and ditch loading fgrom args, now files exclusively 2025-04-09 09:47:53 +02:00
4ff2ee80ee And fix the god damn backslashes fuck windows 2025-04-01 11:29:57 +02:00
633eebfd2a Support ~ in globs 2025-04-01 11:29:02 +02:00
5a31703840 Implement per command logger 2025-03-29 17:29:21 +01:00
162d0c758d Fix some tests 2025-03-29 17:29:21 +01:00
14d64495b6 Add deduplicate flag 2025-03-29 17:29:21 +01:00
fe6e97e832 Don't deduplicate (yet) 2025-03-29 17:23:21 +01:00
35b3d8b099 Reduce some of the reads and writes
It's really not necessary
2025-03-28 23:39:11 +01:00
2e3e958e15 Fix some tests and add some logs 2025-03-28 23:31:44 +01:00
955afc4295 Refactor running commands to separate functions 2025-03-28 16:59:22 +01:00
2c487bc443 Implement "Isolate" commands
Commands that run alone one by one on reading and writing the file
This should be used on commands that will modify a large part of the
file (or generally large parts)
Since that can fuck up the indices of other commands when ran together
2025-03-28 16:56:39 +01:00
b77224176b Add file lua value 2025-03-28 16:47:21 +01:00
a2201053c5 Remove some random ass fmt printf 2025-03-28 13:24:12 +01:00
04cedf5ece Fix the concurrent map writes 2025-03-28 11:35:38 +01:00
ebb07854cc Memoize the match table 2025-03-28 11:31:27 +01:00
8a86ae2f40 Add filter flag 2025-03-28 11:20:44 +01:00
e8f16dda2b Housekeeping 2025-03-28 02:14:27 +01:00
513773f641 Again 2025-03-28 01:26:26 +01:00
22914fe243 Add a lil log 2025-03-28 01:24:23 +01:00
21 changed files with 842 additions and 1187 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
*.exe *.exe
.qodo .qodo
*.sqlite

70
.vscode/launch.json vendored
View File

@@ -12,11 +12,23 @@
"program": "${workspaceFolder}", "program": "${workspaceFolder}",
"cwd": "C:/Users/Administrator/Seafile/Games-Barotrauma", "cwd": "C:/Users/Administrator/Seafile/Games-Barotrauma",
"args": [ "args": [
"loglevel", "-loglevel",
"trace", "trace",
"(?-s)LightComponent!anyrange=\"(!num)\"", "-cook",
"*4", "*.yml",
"**/Outpost*.xml" ]
},
{
"name": "Launch Package (Payday 2)",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"cwd": "C:/Users/Administrator/Seafile/Games-Payday2",
"args": [
"-loglevel",
"trace",
"*.yml",
] ]
}, },
{ {
@@ -33,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",
@@ -40,11 +74,29 @@
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}", "program": "${workspaceFolder}",
"args": [ "args": [
"-loglevel", "tester.yml",
"trace", ]
"(?-s)LightComponent!anyrange=\"(!num)\"", },
"*4", {
"**/Outpost*.xml" "name": "Launch Package (Avorion)",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"cwd": "C:/Users/Administrator/Seafile/Games-Avorion/Avorion",
"args": [
"*.yml",
]
},
{
"name": "Launch Package (Minecraft)",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}",
"cwd": "C:/Users/Administrator/Seafile/Games-Minecraft",
"args": [
"cook_tacz.yml",
] ]
} }
] ]

View File

@@ -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() {

View File

@@ -1,7 +1,7 @@
package main package main
import ( import (
"modify/utils" "cook/utils"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"

42
go.mod
View File

@@ -1,38 +1,32 @@
module modify module cook
go 1.24.1 go 1.23.2
require ( require (
git.site.quack-lab.dev/dave/cylogger v1.3.0
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
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.30.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/hexops/valast v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/mattn/go-sqlite3 v1.14.22 // 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/rogpeppe/go-internal v1.14.1 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect golang.org/x/mod v0.21.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/crypto v0.35.0 // indirect golang.org/x/text v0.22.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/tools v0.26.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
mvdan.cc/gofumpt v0.4.0 // indirect
) )
require ( require gorm.io/driver/sqlite v1.6.0
github.com/go-git/go-git/v5 v5.14.0
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
golang.org/x/net v0.35.0 // indirect
)

115
go.sum
View File

@@ -1,106 +1,59 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= git.site.quack-lab.dev/dave/cylogger v1.3.0 h1:eTWPUD+ThVi8kGIsRcE0XDeoH3yFb5miFEODyKUdWJw=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= git.site.quack-lab.dev/dave/cylogger v1.3.0/go.mod h1:wctgZplMvroA4X6p8f4B/LaCKtiBcT1Pp+L14kcS8jk=
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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/hexops/autogold v0.8.1 h1:wvyd/bAJ+Dy+DcE09BoLk6r4Fa5R5W+O+GUzmR985WM=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/hexops/valast v1.5.0 h1:FBTuvVi0wjTngtXJRZXMbkN/Dn6DgsUsBwch2DUJU8Y=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/hexops/valast v1.5.0/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
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/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/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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
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.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=
mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=

View File

@@ -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()
}

View File

@@ -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
}

299
main.go
View File

@@ -4,15 +4,16 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"sort"
"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 {
@@ -20,23 +21,20 @@ type GlobalStats struct {
TotalModifications int TotalModifications int
ProcessedFiles int ProcessedFiles int
FailedFiles int FailedFiles int
ModificationsPerCommand map[string]int ModificationsPerCommand sync.Map
} }
var ( var (
repo *git.Repository stats GlobalStats = GlobalStats{
worktree *git.Worktree ModificationsPerCommand: sync.Map{},
stats GlobalStats = GlobalStats{
ModificationsPerCommand: make(map[string]int),
} }
) )
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
CreateExampleConfig()
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")
@@ -52,26 +50,62 @@ func main() {
fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n") fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n")
fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n") fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
} }
// TODO: Fix bed shitting when doing *.yml in barotrauma directory
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())
if flag.NArg() == 0 {
flag.Usage()
return
}
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)
flag.Usage() flag.Usage()
return return
} }
if *utils.Filter != "" {
logger.Info("Filtering commands by name: %s", *utils.Filter)
commands = utils.FilterCommands(commands, *utils.Filter)
logger.Info("Filtered %d commands", len(commands))
}
// Then aggregate all the globs and deduplicate them // Then aggregate all the globs and deduplicate them
globs := utils.AggregateGlobs(commands) globs := utils.AggregateGlobs(commands)
logger.Debug("Aggregated %d globs before deduplication", utils.CountGlobsBeforeDedup(commands)) logger.Debug("Aggregated %d globs before deduplication", utils.CountGlobsBeforeDedup(commands))
for _, command := range commands {
logger.Trace("Command: %s", command.Name)
logger.Trace("Regex: %s", command.Regex)
logger.Trace("Files: %v", command.Files)
logger.Trace("Lua: %s", command.Lua)
logger.Trace("Reset: %t", command.Reset)
logger.Trace("Isolate: %t", command.Isolate)
logger.Trace("LogLevel: %s", command.LogLevel)
}
// Resolve all the files for all the globs // Resolve all the files for all the globs
logger.Info("Found %d unique file patterns", len(globs)) logger.Info("Found %d unique file patterns", len(globs))
files, err := utils.ExpandGLobs(globs) files, err := utils.ExpandGLobs(globs)
@@ -91,7 +125,12 @@ func main() {
return return
} }
// TODO: Utilize parallel workers for this 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{}
@@ -100,55 +139,69 @@ func main() {
startTime := time.Now() startTime := time.Now()
var fileMutex sync.Mutex var fileMutex sync.Mutex
for file, commands := range associations { // Create a map to store loggers for each command
commandLoggers := make(map[string]*logger.Logger)
for _, command := range commands {
// Create a named logger for each command
cmdName := command.Name
if cmdName == "" {
// If no name is provided, use a short version of the regex pattern
if len(command.Regex) > 20 {
cmdName = command.Regex[:17] + "..."
} else {
cmdName = command.Regex
}
}
// Parse the log level for this specific command
cmdLogLevel := logger.ParseLevel(command.LogLevel)
// Create a logger with the command name as a field
commandLoggers[command.Name] = logger.WithField("command", cmdName)
commandLoggers[command.Name].SetLevel(cmdLogLevel)
logger.Debug("Created logger for command %q with log level %s", cmdName, cmdLogLevel.String())
}
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)
return return
} }
logger.Trace("Loaded %d bytes of data for file %q", len(fileData), file)
fileDataStr := string(fileData) fileDataStr := string(fileData)
// Aggregate all the modifications and execute them logger.Debug("Saving file %q to database", file)
modifications := []utils.ReplaceCommand{} err = db.SaveFile(file, fileData)
for _, command := range commands { if err != nil {
logger.Info("Processing file %q with command %q", file, command.Regex) logger.Error("Failed to save file %q to database: %v", file, err)
commands, err := processor.ProcessRegex(fileDataStr, command)
if err != nil {
logger.Error("Failed to process file %q with command %q: %v", file, command.Regex, err)
return
}
modifications = append(modifications, commands...)
// It is not guranteed that all the commands will be executed...
// TODO: Make this better
// We'd have to pass the map to executemodifications or something...
stats.ModificationsPerCommand[command.Name] += len(commands)
}
if len(modifications) == 0 {
logger.Info("No modifications found for file %q", file)
return return
} }
// Sort commands in reverse order for safe replacements logger.Debug("Running isolate commands for file %q", file)
fileDataStr, count := utils.ExecuteModifications(modifications, fileDataStr) fileDataStr, err = RunIsolateCommands(association, file, fileDataStr, &fileMutex)
if err != nil {
logger.Error("Failed to run isolate commands for file %q: %v", file, err)
return
}
fileMutex.Lock() logger.Debug("Running other commands for file %q", file)
stats.ProcessedFiles++ fileDataStr, err = RunOtherCommands(file, fileDataStr, association, &fileMutex, commandLoggers)
stats.TotalModifications += count if err != nil {
fileMutex.Unlock() logger.Error("Failed to run other commands for file %q: %v", file, err)
return
logger.Info("Executed %d modifications for file %q", count, file) }
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)
@@ -208,11 +261,161 @@ func main() {
// Print summary // Print summary
if stats.TotalModifications == 0 { if stats.TotalModifications == 0 {
logger.Warning("No modifications were made in any files") logger.Warning("No modifications were made in any files")
fmt.Fprintf(os.Stderr, "No modifications were made in any files\n")
} else { } else {
logger.Info("Operation complete! Modified %d values in %d/%d files", logger.Info("Operation complete! Modified %d values in %d/%d files",
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles) stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
fmt.Printf("Operation complete! Modified %d values in %d/%d files\n", sortedCommands := []string{}
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles) stats.ModificationsPerCommand.Range(func(key, value interface{}) bool {
sortedCommands = append(sortedCommands, key.(string))
return true
})
sort.Strings(sortedCommands)
for _, command := range sortedCommands {
count, _ := stats.ModificationsPerCommand.Load(command)
if count.(int) > 0 {
logger.Info("\tCommand %q made %d modifications", command, count)
} else {
logger.Warning("\tCommand %q made no modifications", command)
}
}
} }
} }
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) {
// Aggregate all the modifications and execute them
modifications := []utils.ReplaceCommand{}
for _, command := range association.Commands {
// Use command-specific logger if available, otherwise fall back to default logger
cmdLogger := logger.Default
if cmdLog, ok := commandLoggers[command.Name]; ok {
cmdLogger = cmdLog
}
cmdLogger.Info("Processing file %q with command %q", file, command.Regex)
newModifications, err := processor.ProcessRegex(fileDataStr, command, file)
if err != nil {
logger.Error("Failed to process file %q with command %q: %v", file, command.Regex, err)
continue
}
modifications = append(modifications, newModifications...)
// It is not guranteed that all the commands will be executed...
// TODO: Make this better
// We'd have to pass the map to executemodifications or something...
count, ok := stats.ModificationsPerCommand.Load(command.Name)
if !ok {
count = 0
}
stats.ModificationsPerCommand.Store(command.Name, count.(int)+len(newModifications))
cmdLogger.Debug("Command %q generated %d modifications", command.Name, len(newModifications))
}
if len(modifications) == 0 {
logger.Warning("No modifications found for file %q", file)
return fileDataStr, nil
}
// Sort commands in reverse order for safe replacements
var count int
fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr)
fileMutex.Lock()
stats.ProcessedFiles++
stats.TotalModifications += count
fileMutex.Unlock()
logger.Info("Executed %d modifications for file %q", count, file)
return fileDataStr, nil
}
func RunIsolateCommands(association utils.FileCommandAssociation, file string, fileDataStr string, fileMutex *sync.Mutex) (string, error) {
for _, isolateCommand := range association.IsolateCommands {
logger.Info("Processing file %q with isolate command %q", file, isolateCommand.Regex)
modifications, err := processor.ProcessRegex(fileDataStr, isolateCommand, file)
if err != nil {
logger.Error("Failed to process file %q with isolate command %q: %v", file, isolateCommand.Regex, err)
continue
}
if len(modifications) == 0 {
logger.Warning("No modifications found for file %q", file)
continue
}
var count int
fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr)
fileMutex.Lock()
stats.ProcessedFiles++
stats.TotalModifications += count
fileMutex.Unlock()
logger.Info("Executed %d isolate modifications for file %q", count, file)
}
return fileDataStr, nil
}

View File

@@ -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
}

View File

@@ -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 {
@@ -21,7 +20,9 @@ type CaptureGroup struct {
} }
// ProcessContent applies regex replacement with Lua processing // ProcessContent applies regex replacement with Lua processing
func ProcessRegex(content string, command utils.ModifyCommand) ([]utils.ReplaceCommand, error) { // The filename here exists ONLY so we can pass it to the lua environment
// It's not used for anything else
func ProcessRegex(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
var commands []utils.ReplaceCommand var commands []utils.ReplaceCommand
logger.Trace("Processing regex: %q", command.Regex) logger.Trace("Processing regex: %q", command.Regex)
@@ -31,6 +32,11 @@ func ProcessRegex(content string, command utils.ModifyCommand) ([]utils.ReplaceC
// 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()
@@ -79,6 +85,7 @@ func ProcessRegex(content string, command utils.ModifyCommand) ([]utils.ReplaceC
logger.Error("Error creating Lua state: %v", err) logger.Error("Error creating Lua state: %v", err)
return commands, fmt.Errorf("error creating Lua state: %v", err) return commands, fmt.Errorf("error creating Lua state: %v", err)
} }
L.SetGlobal("file", lua.LString(filename))
// Hmm... Maybe we don't want to defer this.. // Hmm... Maybe we don't want to defer this..
// Maybe we want to close them every iteration // Maybe we want to close them every iteration
// We'll leave it as is for now // We'll leave it as is for now
@@ -153,7 +160,11 @@ func ProcessRegex(content string, command utils.ModifyCommand) ([]utils.ReplaceC
} }
} }
captureGroups = deduplicateGroups(captureGroups) // Use the DeduplicateGroups flag to control whether to deduplicate capture groups
if !command.NoDedup {
logger.Debug("Deduplicating capture groups as specified in command settings")
captureGroups = deduplicateGroups(captureGroups)
}
if err := toLua(L, captureGroups); err != nil { if err := toLua(L, captureGroups); err != nil {
logger.Error("Failed to set Lua variables: %v", err) logger.Error("Failed to set Lua variables: %v", err)
@@ -205,7 +216,7 @@ func ProcessRegex(content string, command utils.ModifyCommand) ([]utils.ReplaceC
for _, capture := range captureGroups { for _, capture := range captureGroups {
if capture.Value == capture.Updated { if capture.Value == capture.Updated {
logger.Info("Capture group unchanged: %s", capture.Value) logger.Info("Capture group unchanged: %s", LimitString(capture.Value, 50))
continue continue
} }
@@ -250,7 +261,7 @@ func deduplicateGroups(captureGroups []*CaptureGroup) []*CaptureGroup {
} }
if overlaps { if overlaps {
// We CAN just continue despite this fuckup // We CAN just continue despite this fuckup
logger.Error("Overlapping capture group: %s", group.Name) logger.Warning("Overlapping capture group: %s", group.Name)
continue continue
} }
logger.Debug("No overlap detected for capture group: %s. Adding to deduplicated groups.", group.Name) logger.Debug("No overlap detected for capture group: %s. Adding to deduplicated groups.", group.Name)
@@ -268,8 +279,6 @@ func resolveRegexPlaceholders(pattern string) string {
// Handle special pattern modifications // Handle special pattern modifications
if !strings.HasPrefix(pattern, "(?s)") { if !strings.HasPrefix(pattern, "(?s)") {
pattern = "(?s)" + pattern pattern = "(?s)" + pattern
// Use fmt.Printf for test compatibility
fmt.Printf("Pattern modified to include (?s): %s\n", pattern)
} }
namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`) namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`)

View File

@@ -2,9 +2,8 @@ package processor
import ( import (
"bytes" "bytes"
"fmt" "cook/utils"
"io" "io"
"modify/utils"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@@ -38,7 +37,7 @@ func ApiAdaptor(content string, regex string, lua string) (string, int, int, err
LogLevel: "TRACE", LogLevel: "TRACE",
} }
commands, err := ProcessRegex(content, command) commands, err := ProcessRegex(content, command, "test")
if err != nil { if err != nil {
return "", 0, 0, err return "", 0, 0, err
} }
@@ -979,29 +978,12 @@ func TestPatternWithoutPrefixGetsModified(t *testing.T) {
// Setup // Setup
pattern := "some.*pattern" pattern := "some.*pattern"
// Redirect stdout to capture fmt.Printf output
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Execute function // Execute function
result := resolveRegexPlaceholders(pattern) result := resolveRegexPlaceholders(pattern)
// Restore stdout
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify results // Verify results
expectedPattern := "(?s)some.*pattern" expectedPattern := "(?s)some.*pattern"
assert.Equal(t, expectedPattern, result, "Expected pattern to be %q, got %q", expectedPattern, result) assert.Equal(t, expectedPattern, result, "Expected pattern to be %q, got %q", expectedPattern, result)
expectedOutput := fmt.Sprintf("Pattern modified to include (?s): %s\n", expectedPattern)
assert.Equal(t, expectedOutput, output, "Expected output message %q, got %q", expectedOutput, output)
} }
// Empty string input returns "(?s)" // Empty string input returns "(?s)"
@@ -1009,29 +991,12 @@ func TestEmptyStringReturnsWithPrefix(t *testing.T) {
// Setup // Setup
pattern := "" pattern := ""
// Redirect stdout to capture fmt.Printf output
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
// Execute function // Execute function
result := resolveRegexPlaceholders(pattern) result := resolveRegexPlaceholders(pattern)
// Restore stdout
w.Close()
os.Stdout = oldStdout
// Read captured output
var buf bytes.Buffer
io.Copy(&buf, r)
output := buf.String()
// Verify results // Verify results
expectedPattern := "(?s)" expectedPattern := "(?s)"
assert.Equal(t, expectedPattern, result, "Expected pattern to be %q, got %q", expectedPattern, result) assert.Equal(t, expectedPattern, result, "Expected pattern to be %q, got %q", expectedPattern, result)
expectedOutput := fmt.Sprintf("Pattern modified to include (?s): %s\n", expectedPattern)
assert.Equal(t, expectedOutput, output, "Expected output message %q, got %q", expectedOutput, output)
} }
// Named group with "!num" pattern gets replaced with proper regex for numbers // Named group with "!num" pattern gets replaced with proper regex for numbers

View File

@@ -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
} }
} }
} }

View File

@@ -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"
@@ -15,7 +15,7 @@ func ApiAdaptor(content string, regex string, lua string) (string, int, int, err
LogLevel: "TRACE", LogLevel: "TRACE",
} }
commands, err := processor.ProcessRegex(content, command) commands, err := processor.ProcessRegex(content, command, "test")
if err != nil { if err != nil {
return "", 0, 0, err return "", 0, 0, err
} }

120
utils/db.go Normal file
View 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
}

96
utils/file.go Normal file
View File

@@ -0,0 +1,96 @@
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 {
log.Warning("Failed to write file %q: %v", fileSnapshot.FilePath, err)
continue
}
log.Debug("File %q written to disk", fileSnapshot.FilePath)
}
log.Debug("Done")
return nil
}

View File

@@ -5,11 +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("f", "", "Filter commands before running them")
) )

View File

@@ -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
}

View File

@@ -2,9 +2,11 @@ package utils
import ( import (
"fmt" "fmt"
"modify/logger"
"os" "os"
"path/filepath"
"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"
) )
@@ -14,9 +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"`
NoDedup bool `yaml:"nodedup"`
Disabled bool `yaml:"disable"`
} }
type CookFile []ModifyCommand type CookFile []ModifyCommand
@@ -36,29 +40,89 @@ func (c *ModifyCommand) Validate() error {
return nil return nil
} }
func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string][]ModifyCommand, error) { // Ehh.. Not much better... Guess this wasn't the big deal
var matchesMemoTable map[string]bool = make(map[string]bool)
func Matches(path string, glob string) (bool, error) {
key := fmt.Sprintf("%s:%s", path, glob)
if matches, ok := matchesMemoTable[key]; ok {
logger.Debug("Found match for file %q and glob %q in memo table", path, glob)
return matches, nil
}
matches, err := doublestar.Match(glob, path)
if err != nil {
return false, fmt.Errorf("failed to match glob %s with file %s: %w", glob, path, err)
}
matchesMemoTable[key] = matches
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 {
File string
IsolateCommands []ModifyCommand
Commands []ModifyCommand
}
func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string]FileCommandAssociation, error) {
associationCount := 0 associationCount := 0
fileCommands := make(map[string][]ModifyCommand) fileCommands := make(map[string]FileCommandAssociation)
for _, file := range files { for _, file := range files {
file = strings.ReplaceAll(file, "\\", "/")
fileCommands[file] = FileCommandAssociation{
File: file,
IsolateCommands: []ModifyCommand{},
Commands: []ModifyCommand{},
}
for _, command := range commands { for _, command := range commands {
for _, glob := range command.Files { for _, glob := range command.Files {
// TODO: Maybe memoize this function call glob = strings.ReplaceAll(glob, "\\", "/")
matches, err := doublestar.Match(glob, file) 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
} }
if matches { if matches {
logger.Debug("Found match for file %q and command %q", file, command.Regex) logger.Debug("Found match for file %q and command %q", file, command.Regex)
fileCommands[file] = append(fileCommands[file], command) association := fileCommands[file]
if command.Isolate {
association.IsolateCommands = append(association.IsolateCommands, command)
} else {
association.Commands = append(association.Commands, command)
}
fileCommands[file] = association
associationCount++ associationCount++
} }
} }
} }
logger.Debug("Found %d commands for file %q", len(fileCommands[file]), file) logger.Debug("Found %d commands for file %q", len(fileCommands[file].Commands), file)
if len(fileCommands[file]) == 0 { if len(fileCommands[file].Commands) == 0 {
logger.Info("No commands found for file %q", file) logger.Info("No commands found for file %q", file)
} }
if len(fileCommands[file].IsolateCommands) > 0 {
logger.Info("Found %d isolate commands for file %q", len(fileCommands[file].IsolateCommands), file)
}
} }
logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands)) logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands))
return fileCommands, nil return fileCommands, nil
@@ -69,6 +133,8 @@ func AggregateGlobs(commands []ModifyCommand) map[string]struct{} {
globs := make(map[string]struct{}) globs := make(map[string]struct{})
for _, command := range commands { for _, command := range commands {
for _, glob := range command.Files { for _, glob := range command.Files {
glob = strings.Replace(glob, "~", os.Getenv("HOME"), 1)
glob = strings.ReplaceAll(glob, "\\", "/")
globs[glob] = struct{}{} globs[glob] = struct{}{}
} }
} }
@@ -88,9 +154,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)
@@ -112,69 +180,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 {
if err != nil { newcommands, err := LoadCommandsFromCookFiles(arg)
return nil, fmt.Errorf("failed to load commands from cook files: %w", err) 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 {
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) logger.Info("Successfully loaded %d commands from cook files", len(newcommands))
for _, cmd := range newcommands {
if cmd.Disabled {
logger.Info("Skipping disabled command: %s", cmd.Name)
continue
}
commands = append(commands, cmd)
}
logger.Info("Now total commands: %d", len(commands))
} }
logger.Info("Successfully loaded %d commands from args", len(newcommands))
commands = append(commands, newcommands...)
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)
@@ -206,3 +246,16 @@ func CountGlobsBeforeDedup(commands []ModifyCommand) int {
} }
return count return count
} }
func FilterCommands(commands []ModifyCommand, filter string) []ModifyCommand {
filteredCommands := []ModifyCommand{}
filters := strings.Split(filter, ",")
for _, cmd := range commands {
for _, filter := range filters {
if strings.Contains(cmd.Name, filter) {
filteredCommands = append(filteredCommands, cmd)
}
}
}
return filteredCommands
}

View File

@@ -158,21 +158,24 @@ func TestAssociateFilesWithCommands(t *testing.T) {
// The associations expected depends on the implementation // The associations expected depends on the implementation
// Let's check the actual associations and verify they make sense // Let's check the actual associations and verify they make sense
for file, cmds := range associations { for file, assoc := range associations {
t.Logf("File %s is associated with %d commands", file, len(cmds)) t.Logf("File %s is associated with %d commands and %d isolate commands", file, len(assoc.Commands), len(assoc.IsolateCommands))
for i, cmd := range cmds { for i, cmd := range assoc.Commands {
t.Logf(" Command %d: Pattern=%s, Files=%v", i, cmd.Regex, cmd.Files) t.Logf(" Command %d: Pattern=%s, Files=%v", i, cmd.Regex, cmd.Files)
} }
for i, cmd := range assoc.IsolateCommands {
t.Logf(" Isolate Command %d: Pattern=%s, Files=%v", i, cmd.Regex, cmd.Files)
}
// Specific validation based on our file types // Specific validation based on our file types
switch file { switch file {
case "file1.xml": case "file1.xml":
if len(cmds) < 1 { if len(assoc.Commands) < 1 {
t.Errorf("Expected at least 1 command for file1.xml, got %d", len(cmds)) t.Errorf("Expected at least 1 command for file1.xml, got %d", len(assoc.Commands))
} }
// Verify at least one command with *.xml pattern // Verify at least one command with *.xml pattern
hasXmlGlob := false hasXmlGlob := false
for _, cmd := range cmds { for _, cmd := range assoc.Commands {
for _, glob := range cmd.Files { for _, glob := range cmd.Files {
if glob == "*.xml" { if glob == "*.xml" {
hasXmlGlob = true hasXmlGlob = true
@@ -187,12 +190,12 @@ func TestAssociateFilesWithCommands(t *testing.T) {
t.Errorf("Expected command with *.xml glob for file1.xml") t.Errorf("Expected command with *.xml glob for file1.xml")
} }
case "file2.txt": case "file2.txt":
if len(cmds) < 1 { if len(assoc.Commands) < 1 {
t.Errorf("Expected at least 1 command for file2.txt, got %d", len(cmds)) t.Errorf("Expected at least 1 command for file2.txt, got %d", len(assoc.Commands))
} }
// Verify at least one command with *.txt pattern // Verify at least one command with *.txt pattern
hasTxtGlob := false hasTxtGlob := false
for _, cmd := range cmds { for _, cmd := range assoc.Commands {
for _, glob := range cmd.Files { for _, glob := range cmd.Files {
if glob == "*.txt" { if glob == "*.txt" {
hasTxtGlob = true hasTxtGlob = true
@@ -207,12 +210,12 @@ func TestAssociateFilesWithCommands(t *testing.T) {
t.Errorf("Expected command with *.txt glob for file2.txt") t.Errorf("Expected command with *.txt glob for file2.txt")
} }
case "subdir/file3.xml": case "subdir/file3.xml":
if len(cmds) < 1 { if len(assoc.Commands) < 1 {
t.Errorf("Expected at least 1 command for subdir/file3.xml, got %d", len(cmds)) t.Errorf("Expected at least 1 command for subdir/file3.xml, got %d", len(assoc.Commands))
} }
// Should match both *.xml and subdir/* patterns // Should match both *.xml and subdir/* patterns
matches := 0 matches := 0
for _, cmd := range cmds { for _, cmd := range assoc.Commands {
for _, glob := range cmd.Files { for _, glob := range cmd.Files {
if glob == "*.xml" || glob == "subdir/*" { if glob == "*.xml" || glob == "subdir/*" {
matches++ matches++
@@ -266,137 +269,15 @@ 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
yamlData := []byte(` yamlData := []byte(`
- name: command1 - name: command1
pattern: "*.txt" regex: "*.txt"
lua: replace lua: replace
- name: command2 - name: command2
pattern: "*.go" regex: "*.go"
lua: delete lua: delete
`) `)
@@ -420,11 +301,11 @@ func TestLoadCommandsFromCookFileWithComments(t *testing.T) {
yamlData := []byte(` yamlData := []byte(`
# This is a comment # This is a comment
- name: command1 - name: command1
pattern: "*.txt" regex: "*.txt"
lua: replace lua: replace
# Another comment # Another comment
- name: command2 - name: command2
pattern: "*.go" regex: "*.go"
lua: delete lua: delete
`) `)
@@ -445,7 +326,7 @@ func TestLoadCommandsFromCookFileWithComments(t *testing.T) {
// Handle different YAML formatting styles (flow vs block) // Handle different YAML formatting styles (flow vs block)
func TestLoadCommandsFromCookFileWithFlowStyle(t *testing.T) { func TestLoadCommandsFromCookFileWithFlowStyle(t *testing.T) {
// Arrange // Arrange
yamlData := []byte(`[ { name: command1, pattern: "*.txt", lua: replace }, { name: command2, pattern: "*.go", lua: delete } ]`) yamlData := []byte(`[ { name: command1, regex: "*.txt", lua: replace }, { name: command2, regex: "*.go", lua: delete } ]`)
// Act // Act
commands, err := LoadCommandsFromCookFile(yamlData) commands, err := LoadCommandsFromCookFile(yamlData)
@@ -496,13 +377,13 @@ func TestLoadCommandsFromCookFileWithMultipleEntries(t *testing.T) {
// Arrange // Arrange
yamlData := []byte(` yamlData := []byte(`
- name: command1 - name: command1
pattern: "*.txt" regex: "*.txt"
lua: replace lua: replace
- name: command2 - name: command2
pattern: "*.go" regex: "*.go"
lua: delete lua: delete
- name: command3 - name: command3
pattern: "*.md" regex: "*.md"
lua: append lua: append
`) `)
@@ -553,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

View File

@@ -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 {