diff --git a/.gitignore b/.gitignore index 3a550dc..5a806ae 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,6 @@ Temporary Items *.icloud # End of https://www.toptal.com/developers/gitignore/api/go,linux,emacs,macos -my-log +/my-log cover.html cmd/test.go diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c10b6e7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,47 @@ +# Agent Guidelines for my-log + +## Build/Test/Lint Commands +- Build: `make build` or `go build -o my-log` +- Test all: `make test` (runs fmt + test with race + coverage) +- Test single: `go test ./path/to/package -run TestName` +- Format: `make fmt` or `go fmt ./...` +- Coverage report: `make report` (generates cover.html) + +## Code Style +- **Module**: `codeberg.org/danjones000/my-log` +- **Go version**: 1.21.5 +- **Imports**: Standard library first, then external packages, then local packages. Use short aliases for common imports (e.g., `fp` for `path/filepath`, `mapst` for `mitchellh/mapstructure`) +- **Formatting**: Always run `go fmt` before commits (included in test target) +- **Types**: Use explicit types (e.g., `int64`, `float64`). Convert numbers appropriately when unmarshaling JSON +- **Naming**: PascalCase for exported, camelCase for unexported. Use descriptive names +- **Error handling**: Return wrapped errors with context. Define custom errors in models/errors.go. Use `ErrorIs` for error checking +- **Testing**: Use testify/assert. Table-driven tests with helper functions. Test both marshal/unmarshal for encoding types +- **Concurrency**: Use channels and goroutines with WaitGroups for parallel processing (see entry.go patterns) +- **Comments**: Include license header on cmd files. Document exported functions and types + +## Git Commit Guidelines +- **Format**: Prepend commit messages with a gitmoji emoji (see https://gitmoji.dev) +- **Style**: Write detailed commit messages that explain what changed and why +- **Examples**: `✨ Add JSON export functionality for log entries`, `🐛 Fix date parsing for RFC3339 timestamps`, `📝 Update README with configuration examples` + +## Git Flow Workflow +- **Main branches**: `stable` (production-ready), `develop` (integration branch) +- **Development**: Always commit new features/fixes to `develop` branch or appropriate feature branches +- **Branch prefixes**: + - `feat/feature-name` - New features, merge to `develop` when complete + - `bug/bug-name` - Bug fixes (non-urgent), merge to `develop` when complete + - `hot/version` - Hotfixes for production issues, merge to **both** `stable` and `develop` + - `rel/version` - Release preparation branches +- **Version tags**: Prefix all version tags with `v` (e.g., `v1.0.2`, `v0.0.6`) +- **Releases**: Update CHANGELOG.md with a summary of changes for each new version +- **Never commit directly to**: `stable` branch (only merge from `rel/` or `hot/` branches) +- **Before starting work**: Ensure you're on `develop` branch or create an appropriate feature branch from it + +## Project Structure +- `cmd/my-log/`: Main application entrypoint +- `internal/cmd/`: Cobra commands (root, drop, config) +- `models/`: Core types (Entry, Log, Meta) with marshal/unmarshal implementations +- `config/`: TOML-based configuration with env overrides +- `formatters/`: Output formatters (plain, json, null) +- `files/`: File operations (append) +- `tools/`: Utilities (date parsing, string parsing, write buffers) diff --git a/CHANGELOG.md b/CHANGELOG.md index 092e30b..46990e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.0.7] - 2025-11-05 + +- 🚚 Refactor project structure to follow standard Go layout (cmd/my-log/ and internal/cmd/) +- 📝 Add AGENTS.md with comprehensive guidelines for coding agents +- ⚡️ Performance improvement: compile regexes only once +- ✨ Add support for bang prefix to skip value parsing +- 🐛 Fix bang trimming in output +- 🔨 Fix Makefile for Darwin compatibility +- ✏️ Fix Makefile to avoid unnecessary reruns +- ✅ Fix TestMkdirErr on Darwin + +## [0.0.6] - 2024-10-07 + +- Update external dependency: go-dateparser + ## [0.0.5] - 2024-10-07 - Small change: adds --output_json to drop command. diff --git a/Makefile b/Makefile index 35491dc..aaf684f 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,12 @@ OUT=my-log GOBIN=$(shell go env GOBIN) COVEROUT=cover.out COVERHTML=cover.html +OPEN=xdg-open +OS=$(shell uname -s) + +ifeq ($(OS),Darwin) +OPEN=open +endif .PHONY: help help: ## Show help for documented recipes @@ -23,7 +29,8 @@ test: ## Test application and generate coverage report $(MAKE) clean $(MAKE) $(COVEROUT) -$(COVEROUT): $(SOURCES) fmt +$(COVEROUT): $(SOURCES) + $(MAKE) fmt go test ./... -race -cover -coverprofile $@ $(COVERHTML): $(COVEROUT) @@ -33,11 +40,11 @@ $(COVERHTML): $(COVEROUT) report: $(COVERHTML) ## Generate a coverage report .PHONY: open-report -open-report: report ## Open the coverage report in the default browser - xdg-open $(COVERHTML) +open-report: $(COVERHTML) ## Open the coverage report in the default browser + $(OPEN) $< .PHONY: build build: $(OUT) ## Builds the application $(OUT): $(SOURCES) fmt - go build -o $@ + go build -o $@ ./cmd/my-log diff --git a/main.go b/cmd/my-log/main.go similarity index 93% rename from main.go rename to cmd/my-log/main.go index a23ee3c..c7f2435 100644 --- a/main.go +++ b/cmd/my-log/main.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ package main -import "codeberg.org/danjones000/my-log/cmd" +import "codeberg.org/danjones000/my-log/internal/cmd" func main() { cmd.Execute() diff --git a/files/append_test.go b/files/append_test.go index 0976363..03f0253 100644 --- a/files/append_test.go +++ b/files/append_test.go @@ -211,7 +211,7 @@ func (s *AppendTestSuite) TestConfLoadErr() { func (s *AppendTestSuite) TestMkdirErr() { // Don't run this test as root - config.Overrides["input.path"] = "/root/my-logs-test" + config.Overrides["input.path"] = "/var/my-logs-test" defer func(path string) { config.Overrides["input.path"] = path }(s.dir) diff --git a/go.mod b/go.mod index 4928514..b67cb17 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.21.5 require ( github.com/BurntSushi/toml v1.3.2 github.com/caarlos0/env/v10 v10.0.0 - github.com/markusmobius/go-dateparser v1.2.1 + github.com/markusmobius/go-dateparser v1.2.3 github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cobra v1.8.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 ) require ( @@ -25,9 +25,7 @@ require ( github.com/tetratelabs/wazero v1.2.1 // indirect github.com/wasilibs/go-re2 v1.3.0 // indirect golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -replace github.com/markusmobius/go-dateparser => github.com/goodevilgenius/go-dateparser v1.2.2 diff --git a/go.sum b/go.sum index 350aa50..f782cde 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= -github.com/goodevilgenius/go-dateparser v1.2.2 h1:Up9KokPx/h07mesQGAZQg3Xi/8yrDVn1638h3k/lRyk= -github.com/goodevilgenius/go-dateparser v1.2.2/go.mod h1:5xYsZ1h7iB3sE1BSu8bkjYpbFST7EU1/AFxcyO3mgYg= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= @@ -24,6 +22,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= +github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -36,8 +36,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs= github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw= @@ -46,8 +46,8 @@ github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2e github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo= golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE= golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/cmd/config.go b/internal/cmd/config.go similarity index 100% rename from cmd/config.go rename to internal/cmd/config.go diff --git a/cmd/drop.go b/internal/cmd/drop.go similarity index 100% rename from cmd/drop.go rename to internal/cmd/drop.go diff --git a/cmd/root.go b/internal/cmd/root.go similarity index 100% rename from cmd/root.go rename to internal/cmd/root.go diff --git a/tools/parse.go b/tools/parse.go index 021c6a2..618737e 100644 --- a/tools/parse.go +++ b/tools/parse.go @@ -11,24 +11,25 @@ func ParseBytes(in []byte) any { return ParseString(string(in)) } +var yesno = regexp.MustCompile("^(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$") +var yes = regexp.MustCompile("^(y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON)$") +var null = regexp.MustCompile("^(~|null|Null|NULL|none|None|NONE|nil|Nil|NIL)$") + func ParseString(in string) any { s := strings.TrimSpace(in) if s == "" { return s } - yesno := regexp.MustCompile("^(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$") - yes := regexp.MustCompile("^(y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON)$") - null := regexp.MustCompile("^(~|null|Null|NULL|none|None|NONE|nil|Nil|NIL)$") + if strings.HasPrefix(s, "!") { + return s + } + var j json.RawMessage if null.MatchString(s) { return nil } else if yesno.MatchString(s) { - if yes.MatchString(s) { - return true - } else { - return false - } + return yes.MatchString(s) } else if i, err := strconv.Atoi(s); err == nil { return i } else if f, err := strconv.ParseFloat(s, 64); err == nil { diff --git a/tools/parse_date.go b/tools/parse_date.go index d060299..0fa117a 100644 --- a/tools/parse_date.go +++ b/tools/parse_date.go @@ -1,6 +1,7 @@ package tools import ( + "strconv" "time" dp "github.com/markusmobius/go-dateparser" @@ -23,6 +24,19 @@ func ParseDate(in string) (t time.Time, err error) { return MaxTime, nil } + var er error + for _, format := range []string{time.RFC3339, DateFormat} { + if t, er = time.ParseInLocation(format, in, nil); er == nil { + return + } + } + + var ts int64 + if ts, er = strconv.ParseInt(in, 10, 0); er == nil { + t = time.Unix(ts, 0) + return + } + conf := dp.Configuration{ CurrentTime: time.Now().Local(), ReturnTimeAsPeriod: true, @@ -32,12 +46,11 @@ func ParseDate(in string) (t time.Time, err error) { d, err := dp.Parse(&conf, in) t = d.Time if err != nil { - d, err = dp.Parse(&conf, in, DateFormat) - t = d.Time return } y, mon, day, h, loc := t.Year(), t.Month(), t.Day(), t.Hour(), t.Location() + switch d.Period { case date.Second: t = t.Truncate(time.Second) diff --git a/tools/parse_date_test.go b/tools/parse_date_test.go index 7aa30b2..3099b88 100644 --- a/tools/parse_date_test.go +++ b/tools/parse_date_test.go @@ -23,7 +23,7 @@ func TestParseDate(t *testing.T) { firstMonth := time.Date(y, mon, 1, 0, 0, 0, 0, loc) firstYear := time.Date(y, 1, 1, 0, 0, 0, 0, loc) exact := "2075-02-12T12:13:54.536-02:00" - exactd, _ := time.ParseInLocation(time.RFC3339, exact, time.FixedZone("UTC-02:00", -7200)) + exactd, _ := time.ParseInLocation(time.RFC3339, exact, nil) var ts int64 = 1708876012 tsd := time.Unix(ts, 0) ent := "February 25, 2024 at 04:00:13AM +0230" diff --git a/tools/parse_test.go b/tools/parse_test.go index 826af9b..72fc50d 100644 --- a/tools/parse_test.go +++ b/tools/parse_test.go @@ -38,6 +38,10 @@ func TestParse(t *testing.T) { {"on-value", "on", true}, {"no-value", "no", false}, {"off-value", "off", false}, + {"skip-parsing-num", "!42", "!42"}, + {"skip-parsing-bool", "!false", "!false"}, + {"skip-parsing-time", "!" + when.Format(time.RFC3339), "!" + when.Format(time.RFC3339)}, + {"skip-parsing-duration", "!15 mins", "!15 mins"}, } for _, tt := range tests { diff --git a/tools/write_buffer.go b/tools/write_buffer.go index 8ae9386..b9626bc 100644 --- a/tools/write_buffer.go +++ b/tools/write_buffer.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "time" ) @@ -27,6 +28,9 @@ func WriteValue(buff *bytes.Buffer, val any) (n int, err error) { return buff.Write(o) } case string: + if strings.HasPrefix(v, "!") { + return buff.WriteString(strings.TrimPrefix(v, "!")) + } return buff.WriteString(v) case int: return buff.WriteString(strconv.Itoa(v)) @@ -39,6 +43,9 @@ func WriteValue(buff *bytes.Buffer, val any) (n int, err error) { case json.RawMessage: return buff.Write(v) case []byte: + if v[0] == '!' { + return buff.Write(v[1:]) + } return buff.Write(v) case byte: err = buff.WriteByte(v) diff --git a/tools/write_buffer_test.go b/tools/write_buffer_test.go index aa7e203..8d3ab1f 100644 --- a/tools/write_buffer_test.go +++ b/tools/write_buffer_test.go @@ -33,6 +33,14 @@ func TestWriteBuffer(t *testing.T) { {"slice", []any{1, 2, "foo"}, `[1,2,"foo"]`, nil}, {"map", map[string]any{"baz": 42, "foo": "bar"}, `{"baz":42,"foo":"bar"}`, nil}, {"struct", struct{}{}, "", errors.New("Unsupported type struct {}")}, + {"skip-bang-num", "!42", "42", nil}, + {"skip-bang-bool", "!false", "false", nil}, + {"skip-bang-time", "!" + when.Format(time.RFC3339), when.Format(time.RFC3339), nil}, + {"skip-bang-duration", "!15 mins", "15 mins", nil}, + {"skip-bang-bytes-num", []byte("!42"), "42", nil}, + {"skip-bang-bytes-bool", []byte("!false"), "false", nil}, + {"skip-bang-bytes-time", []byte("!" + when.Format(time.RFC3339)), when.Format(time.RFC3339), nil}, + {"skip-bang-bytes-duration", []byte("!15 mins"), "15 mins", nil}, } for _, tt := range tests {