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 90a3f53..46990e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 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 diff --git a/Makefile b/Makefile index a3d8581..aaf684f 100644 --- a/Makefile +++ b/Makefile @@ -47,4 +47,4 @@ open-report: $(COVERHTML) ## Open the coverage report in the default browser 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/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 e69c476..618737e 100644 --- a/tools/parse.go +++ b/tools/parse.go @@ -11,15 +11,20 @@ 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 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 {