Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package klog for openSUSE:Factory checked in at 2026-10-20 12:43:57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/klog (Old) and /work/SRC/openSUSE:Factory/.klog.new.3698736 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "klog" Tue Oct 20 12:43:57 2026 rev:7 rq:1386422 version:7.2 Changes: -------- --- /work/SRC/openSUSE:Factory/klog/klog.changes 2026-02-25 21:21:31.569224800 +0100 +++ /work/SRC/openSUSE:Factory/.klog.new.3698736/klog.changes 2026-10-20 12:44:03.875907571 +0200 @@ -1,0 +2,25 @@ +Tue Oct 20 07:06:17 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 7.2: + * [ NOTICE ] Some (potentially build-breaking!) updates around + the build process: + - The naming scheme of the zipped release artifacts have + changed, it’s now: klog-${OS}-${ARCH}64.zip, e.g.: + klog-linux-amd64.zip. (The binary inside the zip archive is + still named klog / klog.exe, just as before.) There are now + also ARM64 builds for both Linux and Windows available. + - There is a new Go build flag BinaryBuildDate, which embeds + and renders the build date in the klog version subcommand. + - All 3 Go build flags (BinaryVersion, BinaryBuildHash, + BinaryBuildDate) now undergo some basic validation – see + klog.go to learn more. Malformed or absent values don’t break + the build, but are gracefully substituted by placeholder + values, e.g. v?.?. + * [ FEATURE ] Add new klog note subcommand for appending text to + existing entry summaries. (See klog note --help to learn more.) + * [ FEATURE ] Support --now flag on klog print subcommand + (primarily useful in conjunction with the --with-totals flag). + * [ FIX ] Tab completion now works reliable for file paths + containing spaces or other special characters. + +------------------------------------------------------------------- Old: ---- klog-7.1.obscpio New: ---- klog-7.2.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ klog.spec ++++++ --- /var/tmp/diff_new_pack.eunrgv/_old 2026-10-20 12:44:05.805988314 +0200 +++ /var/tmp/diff_new_pack.eunrgv/_new 2026-10-20 12:44:05.806988355 +0200 @@ -17,7 +17,7 @@ Name: klog -Version: 7.1 +Version: 7.2 Release: 0 Summary: Time tracking in a human-readable, plain-text file format License: MIT @@ -27,7 +27,7 @@ BuildRequires: bash-completion BuildRequires: fish BuildRequires: zsh -BuildRequires: golang(API) >= 1.26 +BuildRequires: golang(API) >= 1.27 %description klog is a plain-text file format and a command line tool for time tracking. @@ -76,11 +76,15 @@ %build COMMIT_HASH="$(sed -n 's/commit: \(.*\)/\1/p' %_sourcedir/%{name}.obsinfo)" +DATE_FMT="+%%Y-%%m-%%dT%%H:%%M:%%SZ" +BUILD_DATE=$(date -u -d "@${SOURCE_DATE_EPOCH}" "${DATE_FMT}" 2>/dev/null || date -u -r "${SOURCE_DATE_EPOCH}" "${DATE_FMT}" 2>/dev/null || date -u "${DATE_FMT}") + go build \ -mod=vendor \ -buildmode=pie \ -ldflags=" \ -X main.BinaryVersion=v%{version} \ + -X main.BinaryBuildDate=${BUILD_DATE} \ -X main.BinaryBuildHash=${COMMIT_HASH}" \ -o bin/%{name} ./%{name}.go ++++++ _service ++++++ --- /var/tmp/diff_new_pack.eunrgv/_old 2026-10-20 12:44:05.838989694 +0200 +++ /var/tmp/diff_new_pack.eunrgv/_new 2026-10-20 12:44:05.841989820 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/jotaen/klog</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v7.1</param> + <param name="revision">v7.2</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> @@ -11,7 +11,7 @@ <service name="set_version" mode="manual"> </service> <service name="go_modules" mode="manual"> - <param name="basename">klog-7.1</param> + <param name="basename">klog-7.2</param> </service> <!-- services below are running at buildtime --> <service name="tar" mode="buildtime"> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.eunrgv/_old 2026-10-20 12:44:05.867990907 +0200 +++ /var/tmp/diff_new_pack.eunrgv/_new 2026-10-20 12:44:05.871991075 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/jotaen/klog</param> - <param name="changesrevision">7a4f7b6b0749adb164aa6ac67dee9ba6556f261e</param></service></servicedata> + <param name="changesrevision">66d62de2091364685ffd5226bf35f11104f9246b</param></service></servicedata> (No newline at EOF) ++++++ klog-7.1.obscpio -> klog-7.2.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/CHANGELOG.md new/klog-7.2/CHANGELOG.md --- old/klog-7.1/CHANGELOG.md 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/CHANGELOG.md 2026-10-19 19:23:47.000000000 +0200 @@ -1,6 +1,25 @@ # Changelog **Summary of changes of the command line tool** +## v7.2 (2026-10-26) +- **[ NOTICE ]** Some (potentially build-breaking!) updates around the build process: + - The naming scheme of the zipped release artifacts have changed, + it’s now: `klog-${OS}-${ARCH}64.zip`, e.g.: `klog-linux-amd64.zip`. (The binary + inside the zip archive is still named `klog` / `klog.exe`, just as before.) + There are now also ARM64 builds for both Linux and Windows available. + - There is a new Go build flag `BinaryBuildDate`, which embeds and renders the + build date in the `klog version` subcommand. + - All 3 Go build flags (`BinaryVersion`, `BinaryBuildHash`, `BinaryBuildDate`) + now undergo some basic validation – see `klog.go` to learn more. Malformed or + absent values don’t break the build, but are gracefully substituted by placeholder + values, e.g. `v?.?`. +- **[ FEATURE ]** Add new `klog note` subcommand for appending text to existing + entry summaries. (See `klog note --help` to learn more.) +- **[ FEATURE ]** Support `--now` flag on `klog print` subcommand + (primarily useful in conjunction with the `--with-totals` flag). +- **[ FIX ]** Tab completion now works reliable for file paths containing spaces + or other special characters. + ## v7.1 (2026-02-22) - **[ FEATURE ]** Include warnings in JSON output of `klog json` subcommand. - **[ FEATURE ]** When using `klog report --fill` and combining this with a periodic diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/README.md new/klog-7.2/README.md --- old/klog-7.1/README.md 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/README.md 2026-10-19 19:23:47.000000000 +0200 @@ -23,6 +23,8 @@ If you have questions, feedback, feature ideas, or want to report something that’s not working properly, feel invited to [start a discussion](https://github.com/jotaen/klog/discussions). +To learn how to compile klog from sources, please refer to the `run::build` shell function in [the `run.sh` file](./run.sh). + ## About klog was created by [Jan Heuermann](https://www.jotaen.net). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/go.mod new/klog-7.2/go.mod --- old/klog-7.1/go.mod 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/go.mod 2026-10-19 19:23:47.000000000 +0200 @@ -1,22 +1,20 @@ module github.com/jotaen/klog -go 1.26 +go 1.27 require ( cloud.google.com/go v0.123.0 - github.com/alecthomas/kong v1.14.0 + github.com/alecthomas/kong v1.16.1 github.com/jotaen/genie v0.0.3 - github.com/jotaen/kong-completion v0.0.12 + github.com/jotaen/kong-completion v0.0.15 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/posener/complete v1.2.3 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/go.sum new/klog-7.2/go.sum --- old/klog-7.1/go.sum 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/go.sum 2026-10-19 19:23:47.000000000 +0200 @@ -2,13 +2,11 @@ cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s= -github.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= +github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8E= +github.com/alecthomas/kong v1.16.1/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -21,11 +19,10 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/jotaen/genie v0.0.3 h1:YyyEn4PMnmVwu9BYm3qCCW6mzlMzw6hNeAKzUmAP0HY= github.com/jotaen/genie v0.0.3/go.mod h1:5v0pWbZ+yHWL8QIfTq1PSuUuoGQ1enQZ4XTtV/PnJos= -github.com/jotaen/kong-completion v0.0.12 h1:a9jmSaWgkdAUMQT583UxLIJrO9tfdSmYqcIxrBByjPc= -github.com/jotaen/kong-completion v0.0.12/go.mod h1:dyIG20e3qq128SUBtF8jzI7YtkfzjWMlgbqkAJd6xHQ= +github.com/jotaen/kong-completion v0.0.15 h1:ygxUE1S2Ec4OOnJxQEXDf8UdSQryrQzYksgEGjd8jVE= +github.com/jotaen/kong-completion v0.0.15/go.mod h1:dyIG20e3qq128SUBtF8jzI7YtkfzjWMlgbqkAJd6xHQ= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -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/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= @@ -33,10 +30,9 @@ github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab/go.mod h1:/PfPXh0EntGc3QAAyUaviy4S9tzy4Zp0e2ilq4voC6E= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/args/now.go new/klog-7.2/klog/app/cli/args/now.go --- old/klog-7.1/klog/app/cli/args/now.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/args/now.go 2026-10-19 19:23:47.000000000 +0200 @@ -9,35 +9,35 @@ ) type NowArgs struct { - Now bool `name:"now" short:"n" help:"Assume open ranges to be closed at this moment."` - hadOpenRange bool // Field only for internal use + Now bool `name:"now" short:"n" help:"Assume open ranges to be closed at this moment."` + closedEntries map[klog.Record]int // Field only for internal use } -func (args *NowArgs) ApplyNow(reference gotime.Time, rs ...klog.Record) app.Error { +func (args *NowArgs) ApplyNow(reference gotime.Time, rs ...klog.Record) (map[klog.Record]int, app.Error) { if args.Now { - hasClosedAnyRange, err := service.CloseOpenRanges(reference, rs...) + closedEntries, err := service.CloseOpenRanges(reference, rs...) if err != nil { - return app.NewErrorWithCode( + return nil, app.NewErrorWithCode( app.LOGICAL_ERROR, "Cannot apply --now flag", "There are records with uncloseable time ranges", err, ) } - args.hadOpenRange = hasClosedAnyRange - return nil + args.closedEntries = closedEntries + return closedEntries, nil } - return nil + return nil, nil } func (args *NowArgs) HadOpenRange() bool { - return args.hadOpenRange + return len(args.closedEntries) > 0 } // GetWarning warns the user that they specified the --now flag but there actually // weren’t any closable ranges in the data. func (args *NowArgs) GetWarning() service.UsageWarning { - if args.Now && !args.hadOpenRange { + if args.Now && !args.HadOpenRange() { return service.PointlessNowWarning } return service.UsageWarning{} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/index.go new/klog-7.2/klog/app/cli/index.go --- old/klog-7.1/klog/app/cli/index.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/index.go 2026-10-19 19:23:47.000000000 +0200 @@ -36,6 +36,7 @@ Pause Pause `cmd:"" name:"pause" group:"Manipulate Files" help:"Pause the open time range."` Switch Switch `cmd:"" name:"switch" group:"Manipulate Files" help:"Close open range and starts a new one."` Create Create `cmd:"" name:"create" group:"Manipulate Files" help:"Create a new, empty record."` + Note Note `cmd:"" name:"note" group:"Manipulate Files" help:"Append text to an existing entry summary."` // Manage Files Bookmarks Bookmarks `cmd:"" name:"bookmarks" group:"Manage Files" aliases:"bk" help:"Named aliases for often-used files."` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/json.go new/klog-7.2/klog/app/cli/json.go --- old/klog-7.1/klog/app/cli/json.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/json.go 2026-10-19 19:23:47.000000000 +0200 @@ -42,7 +42,7 @@ return err } now := ctx.Now() - nErr := opt.ApplyNow(now, records...) + _, nErr := opt.ApplyNow(now, records...) if nErr != nil { return nErr } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/note.go new/klog-7.2/klog/app/cli/note.go --- old/klog-7.1/klog/app/cli/note.go 1970-01-01 01:00:00.000000000 +0100 +++ new/klog-7.2/klog/app/cli/note.go 2026-10-19 19:23:47.000000000 +0200 @@ -0,0 +1,55 @@ +package cli + +import ( + "github.com/jotaen/klog/klog" + "github.com/jotaen/klog/klog/app" + "github.com/jotaen/klog/klog/app/cli/args" + "github.com/jotaen/klog/klog/app/cli/helper" + "github.com/jotaen/klog/klog/parser/reconciling" +) + +type Note struct { + Text klog.EntrySummary `arg:"" required:"" placeholder:"TEXT" help:"The text to add."` + Entry int `name:"entry" short:"e" placeholder:"INT" help:"Target the nth entry instead of the open time range. If INT is positive, it counts from the start (beginning with '1'); if negative, it counts from the end (beginning with '-1'). If absent, it tries to find and target the open-ended entry."` + AppendMode string `name:"append-mode" short:"a" placeholder:"MODE" help:"How to add the text: MODE can be 'inline' (default) or 'newline'." enum:"inline,newline" default:"inline"` + args.AtDateArgs + args.NoStyleArgs + args.WarnArgs + args.OutputFileArgs +} + +func (opt *Note) Help() string { + return ` +By default, it targets the open time range (e.g. '18:00 - ?'). Alternatively, you can choose any entry via '--entry'. + +The '--append-mode' flag controls how the text is added: + - '--append-mode=inline' (default) adds the text to the last line of the summary, delimited by a space character. + - '--append-mode=newline' adds the text as separate line underneath the summary. + +By default, the command targets the record at today’s date, or yesterday’s if there is no record today. +You can otherwise specify a date with '--date'. +` +} + +func (opt *Note) Run(ctx app.Context) app.Error { + opt.NoStyleArgs.Apply(&ctx) + date := opt.AtDate(ctx.Now()) + creators := []reconciling.Creator{reconciling.NewReconcilerAtRecord(date)} + if opt.Date == nil { + // If no date had been specified explicitly, try to fall back to yesterday’s record. + creators = append(creators, reconciling.NewReconcilerAtRecord(date.PlusDays(-1))) + } + summary := opt.Text + if opt.AppendMode == "newline" && summary[0] != "" { + summary = append(klog.EntrySummary{""}, summary...) + } + return helper.Reconcile(ctx, helper.ReconcileOpts{OutputFileArgs: opt.OutputFileArgs, WarnArgs: opt.WarnArgs}, + creators, + func(reconciler *reconciling.Reconciler) error { + if opt.Entry != 0 { + return reconciler.AppendToNthEntry(opt.Entry, summary) + } + return reconciler.AppendToOpenRange(summary) + }, + ) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/note_test.go new/klog-7.2/klog/app/cli/note_test.go --- old/klog-7.1/klog/app/cli/note_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/klog-7.2/klog/app/cli/note_test.go 2026-10-19 19:23:47.000000000 +0200 @@ -0,0 +1,202 @@ +package cli + +import ( + "testing" + + "github.com/jotaen/klog/klog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNoteAppendsInlineToOpenRange(t *testing.T) { + t.Run("no existing summary", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00 - ? + 1h Other +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Working on something #tag"), + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 9:00 - ? Working on something #tag + 1h Other +`, state.writtenFileContents) + }) + + t.Run("existing single-line summary", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00-? Working + 1h Other +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("on something #tag"), + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 9:00-? Working on something #tag + 1h Other +`, state.writtenFileContents) + }) + + t.Run("existing multiline summary", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00-? Working + on something + 1h Other +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("#tag"), + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 9:00-? Working + on something #tag + 1h Other +`, state.writtenFileContents) + }) +} + +func TestNoteAddsNewLineToOpenRange(t *testing.T) { + t.Run("no existing summary", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00-? + 1h Other +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Did something #tag"), + AppendMode: "newline", + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 9:00-? + Did something #tag + 1h Other +`, state.writtenFileContents) + }) + + t.Run("existing single-line summary", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00-? Working + 1h Other +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Did something #tag"), + AppendMode: "newline", + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 9:00-? Working + Did something #tag + 1h Other +`, state.writtenFileContents) + }) + + t.Run("existing multiline summary", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00-? Working + More Working + 1h Other +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Did something #tag"), + AppendMode: "newline", + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 9:00-? Working + More Working + Did something #tag + 1h Other +`, state.writtenFileContents) + }) +} + +func TestNoteTargetsNthEntry(t *testing.T) { + records := ` +1920-02-02 + 8:00-9:00 First + 9:00-? Second + 1h Third +` + t.Run("Counting from the start", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(records)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Note"), + Entry: 1, + AppendMode: "newline", + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 8:00-9:00 First + Note + 9:00-? Second + 1h Third +`, state.writtenFileContents) + }) + + t.Run("Counting from the end", func(t *testing.T) { + state, err := NewTestingContext()._SetRecords(records)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("and more"), + Entry: -1, + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 8:00-9:00 First + 9:00-? Second + 1h Third and more +`, state.writtenFileContents) + }) + + t.Run("Out of range", func(t *testing.T) { + for _, entry := range []int{4, -4} { + _, err := NewTestingContext()._SetRecords(records)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Note"), + Entry: entry, + }).Run) + require.Error(t, err) + } + }) +} + +func TestNoteFallsBackToYesterday(t *testing.T) { + state, err := NewTestingContext()._SetRecords(` +1920-02-02 + 22:00-? +`)._SetNow(1920, 2, 3, 1, 10)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Still working"), + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +1920-02-02 + 22:00-? Still working +`, state.writtenFileContents) +} + +func TestNoteDoesNotFallBackToYesterdayIfDateSpecified(t *testing.T) { + _, err := NewTestingContext()._SetRecords(` +1920-02-02 + 22:00-? +`)._SetNow(1920, 2, 3, 1, 10)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Still working"), + Date: klog.Ɀ_Date_(1920, 2, 3), + }).Run) + require.Error(t, err) +} + +func TestNoteFailsWithoutOpenRange(t *testing.T) { + _, err := NewTestingContext()._SetRecords(` +1920-02-02 + 9:00-10:00 +`)._SetNow(1920, 2, 2, 15, 24)._Run((&Note{ + Text: klog.Ɀ_EntrySummary_("Text"), + }).Run) + require.Error(t, err) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/print.go new/klog-7.2/klog/app/cli/print.go --- old/klog-7.1/klog/app/cli/print.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/print.go 2026-10-19 19:23:47.000000000 +0200 @@ -15,6 +15,7 @@ WithTotals bool `name:"with-totals" help:"Amend output with evaluated total times."` args.FilterArgs args.SortArgs + args.NowArgs args.WarnArgs args.NoStyleArgs args.InputFilesArgs @@ -26,6 +27,10 @@ Note that the output doesn’t resemble the file verbatim, but it may apply some minor formatting. You can optionally also sort the records, or print out the total times for each record and entry. + +Open-ended time ranges (e.g., '8:00 - ?') are printed as they are, and they count as '0m' in the totals when using the '--with-totals' flag. +With the '--now' flag, they are printed as if they were closed “right now”, and their elapsed time is factored into the totals. +In the totals column, the provisional durations are marked with an asterisk, e.g. '2h17m*'. ` } @@ -45,22 +50,29 @@ return nil } records = opt.ApplySort(records) + closedEntries, nErr := opt.ApplyNow(now, records...) + if nErr != nil { + return nErr + } serialisedRecords := parser.SerialiseRecords(serialser, records...) output := func() string { if opt.WithTotals { - return printWithDurations(styler, serialisedRecords) + return printWithDurations(styler, serialisedRecords, closedEntries) } return "\n" + serialisedRecords.ToString() }() ctx.Print(output + "\n") - opt.WarnArgs.PrintWarnings(ctx, records, nil) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning()}) return nil } -func printWithDurations(styler tf.Styler, ls parser.Lines) string { +// printWithDurations prefixes each line with its total duration. `closedEntries` +// denotes the entries whose open range was closed via `--now`; the auto-closed durations +// are suffixed with an asterisk, to indicate that they are provisional. +func printWithDurations(styler tf.Styler, ls parser.Lines, closedEntries map[klog.Record]int) string { type Prefix struct { - d klog.Duration + text string isSub bool } var prefixes []*Prefix @@ -76,18 +88,39 @@ } if previousRecord == nil { previousRecord = l.Record - return &Prefix{service.Total(l.Record), false} + return &Prefix{ + text: func() string { + suffix := "" + _, hasAutoClosedEntry := closedEntries[l.Record] + if hasAutoClosedEntry { + suffix = "*" + } + return service.Total(l.Record).ToString() + suffix + }(), + isSub: false, + } } if l.EntryI != -1 && l.EntryI != previousEntry { previousEntry = l.EntryI - return &Prefix{l.Record.Entries()[l.EntryI].Duration(), true} + return &Prefix{ + text: func() string { + openI, hasAutoClosedEntry := closedEntries[l.Record] + entryTotal := l.Record.Entries()[l.EntryI].Duration() + suffix := "" + if hasAutoClosedEntry && openI == l.EntryI { + suffix = "*" + } + return entryTotal.ToString() + suffix + }(), + isSub: true, + } } else { return nil } }() prefixes = append(prefixes, prefix) - if prefix != nil && len(prefix.d.ToString()) > maxColumnLength { - maxColumnLength = len(prefix.d.ToString()) + if prefix != nil && len(prefix.text) > maxColumnLength { + maxColumnLength = len(prefix.text) } } @@ -102,12 +135,12 @@ if p == nil { return strings.Repeat(" ", maxColumnLength+1) } - length := len(p.d.ToString()) + length := len(p.text) value := "" if p.isSub { - value += styler.Props(tf.StyleProps{Color: tf.TEXT_SUBDUED}).Format(p.d.ToString()) + value += styler.Props(tf.StyleProps{Color: tf.TEXT_SUBDUED}).Format(p.text) } else { - value += styler.Props(tf.StyleProps{IsUnderlined: true}).Format(p.d.ToString()) + value += styler.Props(tf.StyleProps{IsUnderlined: true}).Format(p.text) } return strings.Repeat(" ", maxColumnLength-length+1) + value }() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/print_test.go new/klog-7.2/klog/app/cli/print_test.go --- old/klog-7.1/klog/app/cli/print_test.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/print_test.go 2026-10-19 19:23:47.000000000 +0200 @@ -80,7 +80,7 @@ assert.Equal(t, "\n2018-02-01\n\n2018-01-31\n\n2018-01-30\n\n", stateSortedDesc.printBuffer) } -func TestPrintRecordsWithDurations(t *testing.T) { +func TestPrintRecordsWithTotals(t *testing.T) { state, err := NewTestingContext()._SetNow(2018, 02, 07, 19, 00)._SetRecords(` 2018-01-31 Hello #world @@ -122,3 +122,83 @@ `, state.printBuffer) } + +func TestPrintRecordsWithNow(t *testing.T) { + records := ` +2018-02-05 +No open entry + 5h + +2018-02-06 + 22:00 - ? Late shift + +2018-02-07 + 35m + 18:00 - ? I just + started something +` + + t.Run("without --with-totals", func(t *testing.T) { + state, err := NewTestingContext()._SetNow(2018, 02, 07, 19, 00)._SetRecords(records)._Run((&Print{ + NowArgs: args.NowArgs{Now: true}, + }).Run) + require.Nil(t, err) + assert.Equal(t, ` +2018-02-05 +No open entry + 5h + +2018-02-06 + 22:00 - 19:00> Late shift + +2018-02-07 + 35m + 18:00 - 19:00 I just + started something + +`, state.printBuffer) + }) + + t.Run("together with --with-totals", func(t *testing.T) { + state, err := NewTestingContext()._SetNow(2018, 02, 07, 19, 00)._SetRecords(records)._Run((&Print{ + WithTotals: true, + NowArgs: args.NowArgs{Now: true}, + }).Run) + require.Nil(t, err) + assert.Equal(t, ` + 5h | 2018-02-05 + | No open entry + 5h | 5h + + 21h* | 2018-02-06 + 21h* | 22:00 - 19:00> Late shift + + 1h35m* | 2018-02-07 + 35m | 35m + 1h* | 18:00 - 19:00 I just + | started something + +`, state.printBuffer) + }) +} + +func TestPrintWithNowWarnsIfNoOpenRange(t *testing.T) { + state, err := NewTestingContext()._SetNow(2018, 02, 07, 19, 00)._SetRecords(` +2018-02-07 + 35m +`)._Run((&Print{ + NowArgs: args.NowArgs{Now: true}, + }).Run) + require.Nil(t, err) + assert.Contains(t, state.printBuffer, "--now") +} + +func TestPrintWithNowFailsOnUncloseableRange(t *testing.T) { + _, err := NewTestingContext()._SetNow(2018, 02, 07, 19, 00)._SetRecords(` +2018-01-01 + 8:00 - ? +`)._Run((&Print{ + NowArgs: args.NowArgs{Now: true}, + }).Run) + require.Error(t, err) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/report.go new/klog-7.2/klog/app/cli/report.go --- old/klog-7.1/klog/app/cli/report.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/report.go 2026-10-19 19:23:47.000000000 +0200 @@ -57,7 +57,7 @@ if len(records) == 0 { return nil } - nErr := opt.ApplyNow(now, records...) + _, nErr := opt.ApplyNow(now, records...) if nErr != nil { return nErr } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/tags.go new/klog-7.2/klog/app/cli/tags.go --- old/klog-7.1/klog/app/cli/tags.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/tags.go 2026-10-19 19:23:47.000000000 +0200 @@ -47,7 +47,7 @@ if fErr != nil { return fErr } - nErr := opt.ApplyNow(now, records...) + _, nErr := opt.ApplyNow(now, records...) if nErr != nil { return nErr } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/today.go new/klog-7.2/klog/app/cli/today.go --- old/klog-7.1/klog/app/cli/today.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/today.go 2026-10-19 19:23:47.000000000 +0200 @@ -66,7 +66,7 @@ return err } now := ctx.Now() - nErr := opt.ApplyNow(now, records...) + _, nErr := opt.ApplyNow(now, records...) if nErr != nil { return nErr } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/total.go new/klog-7.2/klog/app/cli/total.go --- old/klog-7.1/klog/app/cli/total.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/total.go 2026-10-19 19:23:47.000000000 +0200 @@ -40,7 +40,7 @@ if fErr != nil { return fErr } - nErr := opt.ApplyNow(now, records...) + _, nErr := opt.ApplyNow(now, records...) if nErr != nil { return nErr } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/cli/version.go new/klog-7.2/klog/app/cli/version.go --- old/klog-7.1/klog/app/cli/version.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/cli/version.go 2026-10-19 19:23:47.000000000 +0200 @@ -45,7 +45,7 @@ return nil } ctx.Print("Command line tool: " + ctx.Meta().Version) - ctx.Print(" [" + ctx.Meta().SrcHash + "]\n") + ctx.Print(fmt.Sprintf(" [%s, %s]\n", ctx.Meta().SrcHash, ctx.Meta().BuildDate)) ctx.Print("File format: version " + klog.SPEC_VERSION + "\n") if opt.NoCheck { @@ -75,7 +75,7 @@ if v.Version() == ctx.Meta().Version && ctx.Meta().SrcHash == v.SrcHash() { ctx.Print("You already have the latest version!\n") } else { - ctx.Print(fmt.Sprintf("New version available: %s [%s]\n", v.Version(), v.SrcHash())) + ctx.Print(fmt.Sprintf("New version available: %s [%s, %s]\n", v.Version(), v.SrcHash(), v.BuildDate())) downloadLinkPath := "" if v.DownloadLinkPath() != "" { downloadLinkPath = "/" + v.DownloadLinkPath() @@ -88,6 +88,7 @@ type versionInfo interface { Version() string SrcHash() string + BuildDate() string DownloadLinkPath() string IsValid() bool } @@ -138,10 +139,11 @@ return nil, nil } -// versionFromJotaen checks the version from klog.jotaen.net +// versionFromJotaen checks the version via klog.jotaen.net type versionFromJotaen struct { Version_ string `json:"version"` BuildHash_ string `json:"src_hash"` + ReleaseDate_ string `json:"release_date"` DownloadLinkPath_ string `json:"download_link_path"` } @@ -156,11 +158,13 @@ func (v *versionFromJotaen) IsValid() bool { return strings.HasPrefix(v.Version_, "v") && len(v.BuildHash_) >= 7 } +func (v *versionFromJotaen) BuildDate() string { return v.ReleaseDate_ } -// versionFromGithub checks the version from github.com +// versionFromGithub checks the version via github.com type versionFromGithub struct { Tag string `json:"tag_name"` CommitHash string `json:"target_commitish"` + CreatedAt string `json:"created_at"` // Refers to the build date, not the publishing date. } func (v *versionFromGithub) Version() string { return v.Tag } @@ -176,3 +180,10 @@ func (v *versionFromGithub) IsValid() bool { return strings.HasPrefix(v.Tag, "v") && len(v.CommitHash) >= 7 } +func (v *versionFromGithub) BuildDate() string { + t, err := time.Parse(time.RFC3339, v.CreatedAt) + if err != nil { + return "????-??-??" + } + return t.UTC().Format(time.DateOnly) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/context.go new/klog-7.2/klog/app/context.go --- old/klog-7.1/klog/app/context.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/context.go 2026-10-19 19:23:47.000000000 +0200 @@ -87,22 +87,6 @@ Config() Config } -// Meta holds miscellaneous information about the klog binary. -type Meta struct { - - // Specification contains the file format specification in full text. - Specification string - - // License contains the license text. - License string - - // Version contains the release version, e.g. `v2.7`. - Version string - - // SrcHash contains the hash of the sources that the binary was built from. - SrcHash string -} - // NewContext creates a new Context object. func NewContext(klogFolder File, meta Meta, styler tf.Styler, cfg Config) Context { parserEngine := parser.NewSerialParser() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/main/decoder.go new/klog-7.2/klog/app/main/decoder.go --- old/klog-7.1/klog/app/main/decoder.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/app/main/decoder.go 2026-10-19 19:23:47.000000000 +0200 @@ -148,7 +148,7 @@ return err } if value == "" { - return errors.New("Please provide a valid record summary") + return errors.New("Please provide a valid entry summary") } // Normalize potential double-escaping (from CLI) value = strings.ReplaceAll(value, "\\n", "\n") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/meta.go new/klog-7.2/klog/app/meta.go --- old/klog-7.1/klog/app/meta.go 1970-01-01 01:00:00.000000000 +0100 +++ new/klog-7.2/klog/app/meta.go 2026-10-19 19:23:47.000000000 +0200 @@ -0,0 +1,59 @@ +package app + +import ( + "regexp" + "strings" +) + +// Meta holds miscellaneous information about the klog binary. +type Meta struct { + + // Specification contains the file format specification in full text. + Specification string + + // License contains the license text. + License string + + // Version contains the release version, e.g. `v2.7`. + Version string + + // SrcHash contains the hash of the sources that the binary was built from. + SrcHash string + + // BuildDate is the date at which the binary was built, e.g. `2026-10-19`. + BuildDate string +} + +// NewMeta creates a meta info object from input strings. For invalid values, it +// replaces them with ?-placeholders instead of failing. The validation is mostly +// structural, not logical. +func NewMeta(specification string, license string, version string, srcHash string, buildDate string) Meta { + return Meta{ + Specification: specification, + License: license, + Version: func() string { + version = strings.TrimSpace(version) + pattern := regexp.MustCompile(`^v\d+\.\d+([.-].+)?$`) + if !pattern.MatchString(version) { + return "v?.?" + } + return version + }(), + SrcHash: func() string { + srcHash = strings.TrimSpace(srcHash) + pattern := regexp.MustCompile(`^[a-fA-F0-9]{7,}$`) + if !pattern.MatchString(srcHash) { + return "???????" + } + return strings.ToLower(srcHash[:7]) + }(), + BuildDate: func() string { + buildDate = strings.TrimSpace(buildDate) + pattern := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}(T.*)?$`) + if !pattern.MatchString(buildDate) { + return "????-??-??" + } + return buildDate[:10] + }(), + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/app/meta_test.go new/klog-7.2/klog/app/meta_test.go --- old/klog-7.1/klog/app/meta_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/klog-7.2/klog/app/meta_test.go 2026-10-19 19:23:47.000000000 +0200 @@ -0,0 +1,129 @@ +package app + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateMeta(t *testing.T) { + m := NewMeta("Specification", "License", "v1.0", "1234abc", "2000-12-31") + assert.Equal(t, "Specification", m.Specification) + assert.Equal(t, "License", m.License) + assert.Equal(t, "v1.0", m.Version) + assert.Equal(t, "1234abc", m.SrcHash) + assert.Equal(t, "2000-12-31", m.BuildDate) +} + +func TestCreateMetaTrimsWhiteSpace(t *testing.T) { + m := NewMeta("Specification", "License", " v1.0 ", " 1234abc ", " 2000-12-31 ") + assert.Equal(t, "Specification", m.Specification) + assert.Equal(t, "License", m.License) + assert.Equal(t, "v1.0", m.Version) + assert.Equal(t, "1234abc", m.SrcHash) + assert.Equal(t, "2000-12-31", m.BuildDate) +} + +func TestAcceptsValidVersion(t *testing.T) { + for _, in := range []string{ + "v0.0", + "v000.000", + "v001.002", + "v1.1", + "v183.5", + "v2.5910", + "v28372.5910", + "v5.23-alpha", + "v5.23.42-alpha", + "v5.23.42-alpha_SOMETHING&$^%!*@&#", + } { + t.Run(in, func(t *testing.T) { + m := NewMeta("Specification", "License", in, "1234abc", "2000-12-31") + assert.Equal(t, in, m.Version) + }) + } +} + +func TestRejectsInvalidVersion(t *testing.T) { + for _, in := range []string{ + "", + "foo", + "0.0", + "v1", + "v1.", + "v1.2.", + "v1.2-", + "v.12", + "v1.22alpha", + } { + t.Run(in, func(t *testing.T) { + m := NewMeta("Specification", "License", in, "1234abc", "2000-12-31") + assert.Equal(t, "v?.?", m.Version) + }) + } +} + +func TestAcceptsValidSrcHash(t *testing.T) { + for _, in := range []string{ + "1a2b3c4", + "1A2B3C4", + "1590acf", + "1234567890abcdef", + "1234567890ABCDEF", + "7a4f7b6b0749adb164aa6ac67dee9ba6556f261e", + } { + t.Run(in, func(t *testing.T) { + m := NewMeta("Specification", "License", "v1.2", in, "2000-12-31") + assert.Equal(t, strings.ToLower(in[:7]), m.SrcHash) + }) + } +} + +func TestRejectsInvalidSrcHash(t *testing.T) { + for _, in := range []string{ + "", + "0", + "123456", + "123456g", + "abcdefghijklmnopqrst", + "&^@!#@(*#&!*@&", + } { + t.Run(in, func(t *testing.T) { + m := NewMeta("Specification", "License", "v1.2", in, "2000-12-31") + assert.Equal(t, "???????", m.SrcHash) + }) + } +} + +func TestAcceptsValidBuildDate(t *testing.T) { + for _, in := range []string{ + "2000-12-31", + "3617-05-15", + "9999-99-99", + "0001-01-01", + "2000-12-31T12:00:23Z", + "2000-12-31Tanything", + } { + t.Run(in, func(t *testing.T) { + m := NewMeta("Specification", "License", "v1.2", "1234567", in) + assert.Equal(t, in[:10], m.BuildDate) + }) + } +} + +func TestRejectsInvalidBuildDate(t *testing.T) { + for _, in := range []string{ + "", + "153728182883", + "2000_12_31", + "2000 12 31", + "2000/12/31", + "abcd-ef-gh", + } { + t.Run(in, func(t *testing.T) { + m := NewMeta("Specification", "License", "v1.2", "1234567", in) + assert.Equal(t, "????-??-??", m.BuildDate) + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/parser/reconciling/append_to_summary.go new/klog-7.2/klog/parser/reconciling/append_to_summary.go --- old/klog-7.1/klog/parser/reconciling/append_to_summary.go 1970-01-01 01:00:00.000000000 +0100 +++ new/klog-7.2/klog/parser/reconciling/append_to_summary.go 2026-10-19 19:23:47.000000000 +0200 @@ -0,0 +1,37 @@ +package reconciling + +import ( + "errors" + "github.com/jotaen/klog/klog" +) + +// AppendToOpenRange adds text to the summary of the open time range. +func (r *Reconciler) AppendToOpenRange(additionalSummary klog.EntrySummary) error { + openRangeEntryIndex := r.findOpenRangeIndex() + if openRangeEntryIndex == -1 { + return errors.New("No open time range") + } + r.appendToSummary(openRangeEntryIndex, additionalSummary) + return nil +} + +// AppendToNthEntry adds text to the summary of the nth entry. If nth is positive, it +// counts from the start (beginning with 1); if negative, it counts from the end +// (beginning with -1). +func (r *Reconciler) AppendToNthEntry(nth int, additionalSummary klog.EntrySummary) error { + entriesCount := len(r.Record.Entries()) + entryIndex := nth - 1 + if nth < 0 { + entryIndex = entriesCount + nth + } + if nth == 0 || entryIndex < 0 || entryIndex >= entriesCount { + return errors.New("No such entry") + } + r.appendToSummary(entryIndex, additionalSummary) + return nil +} + +func (r *Reconciler) appendToSummary(entryIndex int, additionalSummary klog.EntrySummary) { + entryValueLineIndex := r.lastLinePointer - countLines(r.Record.Entries()[entryIndex:]) + r.concatenateSummary(entryIndex, entryValueLineIndex, additionalSummary) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/parser/reconciling/append_to_summary_test.go new/klog-7.2/klog/parser/reconciling/append_to_summary_test.go --- old/klog-7.1/klog/parser/reconciling/append_to_summary_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/klog-7.2/klog/parser/reconciling/append_to_summary_test.go 2026-10-19 19:23:47.000000000 +0200 @@ -0,0 +1,101 @@ +package reconciling + +import ( + "strconv" + "testing" + + "github.com/jotaen/klog/klog" + "github.com/jotaen/klog/klog/parser" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReconcilerAppendsToOpenRangeSummary(t *testing.T) { + original := ` +2018-01-01 + 8:00 - 9:00 Before + 9:00 - ? First line + Second line + 1h After +` + rs, bs, _ := parser.NewSerialParser().Parse(original) + reconciler := NewReconcilerAtRecord(klog.Ɀ_Date_(2018, 1, 1))(rs, bs) + require.NotNil(t, reconciler) + err := reconciler.AppendToOpenRange(klog.Ɀ_EntrySummary_("", "Third line #tag")) + require.Nil(t, err) + + result := assertResult(t, reconciler) + assert.Equal(t, ` +2018-01-01 + 8:00 - 9:00 Before + 9:00 - ? First line + Second line + Third line #tag + 1h After +`, result.AllSerialised) +} + +func TestReconcilerAppendToOpenRangeFailsWithoutOpenRange(t *testing.T) { + original := ` +2018-01-01 + 8:00 - 9:00 +` + rs, bs, _ := parser.NewSerialParser().Parse(original) + reconciler := NewReconcilerAtRecord(klog.Ɀ_Date_(2018, 1, 1))(rs, bs) + require.NotNil(t, reconciler) + err := reconciler.AppendToOpenRange(klog.Ɀ_EntrySummary_("", "Text")) + require.Error(t, err) +} + +func TestReconcilerAppendsToNthEntrySummary(t *testing.T) { + original := ` +2018-01-01 + 8:00 - 9:00 First + continued + 2h Second + 1h Third +` + rs, bs, _ := parser.NewSerialParser().Parse(original) + reconciler := NewReconcilerAtRecord(klog.Ɀ_Date_(2018, 1, 1))(rs, bs) + require.NotNil(t, reconciler) + + t.Run("Add 1st line", func(t *testing.T) { + require.Nil(t, reconciler.AppendToNthEntry(2, klog.Ɀ_EntrySummary_("entry"))) + result := assertResult(t, reconciler) + assert.Equal(t, ` +2018-01-01 + 8:00 - 9:00 First + continued + 2h Second entry + 1h Third +`, result.AllSerialised) + }) + + t.Run("Add 2nd lines", func(t *testing.T) { + require.Nil(t, reconciler.AppendToNthEntry(-3, klog.Ɀ_EntrySummary_("", "more"))) + result := assertResult(t, reconciler) + assert.Equal(t, ` +2018-01-01 + 8:00 - 9:00 First + continued + more + 2h Second entry + 1h Third +`, result.AllSerialised) + }) +} + +func TestReconcilerAppendToNthEntryFailsForMissingEntry(t *testing.T) { + original := ` +2018-01-01 + 8:00 - 9:00 +` + rs, bs, _ := parser.NewSerialParser().Parse(original) + reconciler := NewReconcilerAtRecord(klog.Ɀ_Date_(2018, 1, 1))(rs, bs) + require.NotNil(t, reconciler) + for _, nth := range []int{0, 2, 20, -2, -20} { + t.Run("For entry nr: "+strconv.Itoa(nth), func(t *testing.T) { + require.Error(t, reconciler.AppendToNthEntry(nth, klog.Ɀ_EntrySummary_("Text"))) + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/service/record.go new/klog-7.2/klog/service/record.go --- old/klog-7.1/klog/service/record.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/service/record.go 2026-10-19 19:23:47.000000000 +0200 @@ -2,18 +2,19 @@ import ( "errors" - "github.com/jotaen/klog/klog" gotime "time" + + "github.com/jotaen/klog/klog" ) // CloseOpenRanges closes open ranges at the time of `endTime`. Returns an error // if a range is not closeable at that point in time. // This method alters the provided records! -// The bool return value indicates whether any open ranges have been closed. -func CloseOpenRanges(endTime gotime.Time, rs ...klog.Record) (bool, error) { +// The `map` return value indicates which open ranges have been closed. +func CloseOpenRanges(endTime gotime.Time, rs ...klog.Record) (map[klog.Record]int, error) { thisDay := klog.NewDateFromGo(endTime) theDayBefore := thisDay.PlusDays(-1) - hasClosedAnyRange := false + closedEntries := map[klog.Record]int{} for _, r := range rs { if r.OpenRange() == nil { continue @@ -29,13 +30,28 @@ return nil, errors.New("Encountered uncloseable open range") }() if tErr != nil { - return false, tErr + return nil, tErr } + closedEntries[r] = openRangeIndex(r) eErr := r.EndOpenRange(end) - hasClosedAnyRange = true if eErr != nil { - return false, errors.New("Encountered uncloseable open range") + return nil, errors.New("Encountered uncloseable open range") } } - return hasClosedAnyRange, nil + return closedEntries, nil +} + +func openRangeIndex(r klog.Record) int { + openEndedEntry := -1 + for entryI, e := range r.Entries() { + klog.Unbox[any](&e, + func(klog.Range) any { return nil }, + func(klog.Duration) any { return nil }, + func(klog.OpenRange) any { + openEndedEntry = entryI + return nil + }, + ) + } + return openEndedEntry } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog/service/record_test.go new/klog-7.2/klog/service/record_test.go --- old/klog-7.1/klog/service/record_test.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog/service/record_test.go 2026-10-19 19:23:47.000000000 +0200 @@ -1,11 +1,12 @@ package service import ( + "testing" + gotime "time" + "github.com/jotaen/klog/klog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" - gotime "time" ) func TestDoesNotTouchRecordsIfNoOpenRange(t *testing.T) { @@ -13,9 +14,9 @@ r.AddDuration(klog.NewDuration(1, 0), nil) r.AddRange(klog.Ɀ_Range_(klog.Ɀ_Time_(1, 0), klog.Ɀ_Time_(2, 0)), nil) - hasClosedAnyRange, err := CloseOpenRanges(gotime.Now(), r) + closedEntries, err := CloseOpenRanges(gotime.Now(), r) require.Nil(t, err) - assert.False(t, hasClosedAnyRange) + assert.Len(t, closedEntries, 0) assert.Equal(t, klog.NewDuration(2, 0), Total(r)) } @@ -26,9 +27,10 @@ r.Start(klog.NewOpenRange(klog.Ɀ_Time_(3, 0)), nil) endTime, _ := gotime.Parse("2006-01-02T15:04:05-0700", "2020-01-01T05:30:00-0000") - hasClosedAnyRange, err := CloseOpenRanges(endTime, r) + closedEntries, err := CloseOpenRanges(endTime, r) require.Nil(t, err) - assert.True(t, hasClosedAnyRange) + require.Len(t, closedEntries, 1) + assert.Equal(t, 2, closedEntries[r]) assert.Equal(t, klog.NewDuration(2+2, 30), Total(r)) } @@ -39,9 +41,10 @@ r.Start(klog.NewOpenRange(klog.Ɀ_Time_(3, 0)), nil) endTime, _ := gotime.Parse("2006-01-02T15:04:05-0700", "2020-01-02T05:30:00-0000") - hasClosedAnyRange, err := CloseOpenRanges(endTime, r) + closedEntries, err := CloseOpenRanges(endTime, r) require.Nil(t, err) - assert.True(t, hasClosedAnyRange) + require.Len(t, closedEntries, 1) + assert.Equal(t, 2, closedEntries[r]) assert.Equal(t, klog.NewDuration(2+24+2, 30), Total(r)) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/klog.go new/klog-7.2/klog.go --- old/klog-7.1/klog.go 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/klog.go 2026-10-19 19:23:47.000000000 +0200 @@ -17,13 +17,29 @@ //go:embed LICENSE.txt var license string -var BinaryVersion string // Set via build flag -var BinaryBuildHash string // Set via build flag +// BinaryVersion is the version identifier that is embedded in the binary. +// It should be provided via a compile/build flag. The value must start with a +// `v`, followed by 2 dot-separated integers; e.g., `v2.12`. Optionally, there +// can be a dot- or hyphen-separated suffix (containing arbitrary characters), +// e.g., `v12.9-alpha` or `v8.1.0-alpha_2*`. If the build flag is absent, a value +// of `v?.?` is used. +var BinaryVersion string + +// BinaryBuildHash is a computed hash of the underlying sources of the build. +// It should be provided via a compile/build flag. This is usually the git commit +// hash, e.g. `7ab819c`. The value must consist of at least 7 hexadecimal digits. +// Longer values are truncated; the value will also be normalised to lower-case. If +// the build flag is absent, a value of `???????` is used. +var BinaryBuildHash string + +// BinaryBuildDate denotes the date that the build refers to. It should be provided +// via a compile/build flag. This should be the date of the latest commit, or +// alternatively the date at which the build occurred. It must be a valid calendar +// date, formatted `YYYY-MM-DD`; e.g. `2026-10-26`. If the build flag is absent, a +// value of `????-??-??` is used. +var BinaryBuildDate string func main() { - if len(BinaryBuildHash) > 7 { - BinaryBuildHash = BinaryBuildHash[:7] - } klogFolder := func() app.File { f, err := determineKlogConfigFolder() @@ -49,12 +65,12 @@ return c }() - code, err := klog.Run(klogFolder, app.Meta{ - Specification: specification, - License: license, - Version: BinaryVersion, - SrcHash: BinaryBuildHash, - }, config, os.Args[1:]) + code, err := klog.Run( + klogFolder, + app.NewMeta(specification, license, BinaryVersion, BinaryBuildHash, BinaryBuildDate), + config, + os.Args[1:], + ) if err != nil { fail(err, code) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.1/run.sh new/klog-7.2/run.sh --- old/klog-7.1/run.sh 2026-02-22 11:46:19.000000000 +0100 +++ new/klog-7.2/run.sh 2026-10-19 19:23:47.000000000 +0200 @@ -1,4 +1,5 @@ #!/bin/bash +# Use this script with https://run.jotaen.net # Install all dependencies run::install() { @@ -7,15 +8,17 @@ } # Compile to ./out/klog -# Takes two positional arguments: -# - The version (e.g.: v1.2) -# - The build hash (7 chars hex) +# Takes three positional arguments (see `klog.go` file): +# - Version string (e.g.: v1.2) +# - Build hash (7 chars hex) +# - Build date (formatted YYYY-MM-DD) run::build() { go build \ -ldflags "\ -s -w \ - -X 'main.BinaryVersion=${1:-v?.?}' \ - -X 'main.BinaryBuildHash=${2:-???????}' \ + -X 'main.BinaryVersion=$1' \ + -X 'main.BinaryBuildHash=$2' \ + -X 'main.BinaryBuildDate=$3' \ " \ -o ./out/klog \ klog.go ++++++ klog.obsinfo ++++++ --- /var/tmp/diff_new_pack.eunrgv/_old 2026-10-20 12:44:06.091000237 +0200 +++ /var/tmp/diff_new_pack.eunrgv/_new 2026-10-20 12:44:06.094000362 +0200 @@ -1,5 +1,5 @@ name: klog -version: 7.1 -mtime: 1771757179 -commit: 7a4f7b6b0749adb164aa6ac67dee9ba6556f261e +version: 7.2 +mtime: 1792430627 +commit: 66d62de2091364685ffd5226bf35f11104f9246b ++++++ vendor.tar.gz ++++++ ++++ 31270 lines of diff (skipped)
