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-02-25 21:12:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/klog (Old) and /work/SRC/openSUSE:Factory/.klog.new.1977 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "klog" Wed Feb 25 21:12:09 2026 rev:6 rq:1335043 version:7.1 Changes: -------- --- /work/SRC/openSUSE:Factory/klog/klog.changes 2026-02-09 15:35:50.453893652 +0100 +++ /work/SRC/openSUSE:Factory/.klog.new.1977/klog.changes 2026-02-25 21:21:31.569224800 +0100 @@ -1,0 +2,17 @@ +Wed Feb 25 09:31:45 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 7.1: + * [ FEATURE ] Include warnings in JSON output of klog json + subcommand. + * [ FEATURE ] When using klog report --fill and combining this + with a periodic date filter such as --this-week, --last-month + or --period 2021-05, fill up the entire queried period, rather + than just the gaps in between. + * [ FEATURE ] Display warning when using --diff together with + entry-level filtering, as that likely yields nonsensical + results. + * [ FIX ] Revise and define behaviour when applying filters to + empty records (i.e., records that don’t contain any time + entries). + +------------------------------------------------------------------- Old: ---- klog-7.0.obscpio New: ---- klog-7.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ klog.spec ++++++ --- /var/tmp/diff_new_pack.1cSCwN/_old 2026-02-25 21:21:32.081245897 +0100 +++ /var/tmp/diff_new_pack.1cSCwN/_new 2026-02-25 21:21:32.085246062 +0100 @@ -17,7 +17,7 @@ Name: klog -Version: 7.0 +Version: 7.1 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.25 +BuildRequires: golang(API) >= 1.26 %description klog is a plain-text file format and a command line tool for time tracking. ++++++ _service ++++++ --- /var/tmp/diff_new_pack.1cSCwN/_old 2026-02-25 21:21:32.121247545 +0100 +++ /var/tmp/diff_new_pack.1cSCwN/_new 2026-02-25 21:21:32.125247710 +0100 @@ -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.0</param> + <param name="revision">v7.1</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.0</param> + <param name="basename">klog-7.1</param> </service> <!-- services below are running at buildtime --> <service name="tar" mode="buildtime"> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.1cSCwN/_old 2026-02-25 21:21:32.145248534 +0100 +++ /var/tmp/diff_new_pack.1cSCwN/_new 2026-02-25 21:21:32.149248699 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/jotaen/klog</param> - <param name="changesrevision">76fdc21899f3547d28f6e80e4179959a92bcd999</param></service></servicedata> + <param name="changesrevision">7a4f7b6b0749adb164aa6ac67dee9ba6556f261e</param></service></servicedata> (No newline at EOF) ++++++ klog-7.0.obscpio -> klog-7.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/CHANGELOG.md new/klog-7.1/CHANGELOG.md --- old/klog-7.0/CHANGELOG.md 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/CHANGELOG.md 2026-02-22 11:46:19.000000000 +0100 @@ -1,6 +1,16 @@ # Changelog **Summary of changes of the command line tool** +## 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 + date filter such as `--this-week`, `--last-month` or `--period 2021-05`, fill + up the entire queried period, rather than just the gaps in between. +- **[ FEATURE ]** Display warning when using `--diff` together with entry-level + filtering, as that likely yields nonsensical results. +- **[ FIX ]** Revise and define behaviour when applying filters to empty records + (i.e., records that don’t contain any time entries). + ## v7.0 (2026-02-06) - **[ META ]** klog is 5 years old today – happy birthday! 🥳 - **[ FEATURE ]** Introduce `--filter` flag for applying generic and more complex diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/README.md new/klog-7.1/README.md --- old/klog-7.0/README.md 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/README.md 2026-02-22 11:46:19.000000000 +0100 @@ -23,8 +23,6 @@ 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). -If you’d like to contribute code, please discuss your intended change beforehand. Please refrain from submitting pull requests proactively. - ## About klog was created by [Jan Heuermann](https://www.jotaen.net). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/go.mod new/klog-7.1/go.mod --- old/klog-7.0/go.mod 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/go.mod 2026-02-22 11:46:19.000000000 +0100 @@ -1,12 +1,12 @@ module github.com/jotaen/klog -go 1.25 +go 1.26 require ( cloud.google.com/go v0.123.0 - github.com/alecthomas/kong v1.13.0 + github.com/alecthomas/kong v1.14.0 github.com/jotaen/genie v0.0.3 - github.com/jotaen/kong-completion v0.0.11 + github.com/jotaen/kong-completion v0.0.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/posener/complete v1.2.3 github.com/stretchr/testify v1.11.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/go.sum new/klog-7.1/go.sum --- old/klog-7.0/go.sum 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/go.sum 2026-02-22 11:46:19.000000000 +0100 @@ -2,8 +2,8 @@ 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.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA= -github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= +github.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s= +github.com/alecthomas/kong v1.14.0/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= @@ -21,8 +21,8 @@ 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.11 h1:ZRyQt+IwjcAObbiyxJZ3YR7r/o/f6HYidTK1+7YNtnE= -github.com/jotaen/kong-completion v0.0.11/go.mod h1:dyIG20e3qq128SUBtF8jzI7YtkfzjWMlgbqkAJd6xHQ= +github.com/jotaen/kong-completion v0.0.12 h1:a9jmSaWgkdAUMQT583UxLIJrO9tfdSmYqcIxrBByjPc= +github.com/jotaen/kong-completion v0.0.12/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= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/args/filter.go new/klog-7.1/klog/app/cli/args/filter.go --- old/klog-7.0/klog/app/cli/args/filter.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/args/filter.go 2026-02-22 11:46:19.000000000 +0100 @@ -11,31 +11,34 @@ type FilterArgs struct { // Date-related filters: - Date klog.Date `name:"date" placeholder:"DATE" group:"Filter" help:"Entries at this date. DATE has to be in format YYYY-MM-DD or YYYY/MM/DD. E.g., '2024-01-31' or '2024/01/31'."` - Since klog.Date `name:"since" placeholder:"DATE" group:"Filter" help:"Entries since this date (inclusive)."` - Until klog.Date `name:"until" placeholder:"DATE" group:"Filter" help:"Entries until this date (inclusive)."` - Period period.Period `name:"period" placeholder:"PERIOD" group:"Filter" help:"Entries within a calendar period. PERIOD has to be in format YYYY, YYYY-MM, YYYY-Www or YYYY-Qq. E.g., '2024', '2024-04', '2022-W21' or '2024-Q1'."` + Date klog.Date `name:"date" placeholder:"DATE" group:"Filter Flags:" help:"Records at this date. DATE has to be in format YYYY-MM-DD or YYYY/MM/DD. E.g., '2024-01-31' or '2024/01/31'."` + Since klog.Date `name:"since" placeholder:"DATE" group:"Filter Flags:" help:"Records since this date (inclusive)."` + Until klog.Date `name:"until" placeholder:"DATE" group:"Filter Flags:" help:"Records until this date (inclusive)."` + Period period.Period `name:"period" placeholder:"PERIOD" group:"Filter Flags:" help:"Records within a calendar period. PERIOD has to be in format YYYY, YYYY-MM, YYYY-Www or YYYY-Qq. E.g., '2024', '2024-04', '2022-W21' or '2024-Q1'."` // Filter shortcuts: // The two `XXX` ones are dummy entries just for the help output, they also aren’t available // for tab completion. The other ones are not shown in the help output (because that would be // too verbose then), but they are still available for tab completion. - Today bool `name:"today" group:"Filter" help:"Records at today’s date."` - Yesterday bool `name:"yesterday" group:"Filter" help:"Records at yesterday’s date."` - ThisXXX bool `name:"this-***" group:"Filter" help:"Records of this week/month/quarter/year, e.g. '--this-week' or '--this-quarter'." completion-enabled:"false"` - LastXXX bool `name:"last-***" group:"Filter" help:"Records of last week/month/quarter/year, e.g. '--last-month' or '--last-year'." completion-enabled:"false"` - ThisWeek bool `hidden:"" name:"this-week" group:"Filter" completion-enabled:"true"` - LastWeek bool `hidden:"" name:"last-week" group:"Filter" completion-enabled:"true"` - ThisMonth bool `hidden:"" name:"this-month" group:"Filter" completion-enabled:"true"` - LastMonth bool `hidden:"" name:"last-month" group:"Filter" completion-enabled:"true"` - ThisQuarter bool `hidden:"" name:"this-quarter" group:"Filter" completion-enabled:"true"` - LastQuarter bool `hidden:"" name:"last-quarter" group:"Filter" completion-enabled:"true"` - ThisYear bool `hidden:"" name:"this-year" group:"Filter" completion-enabled:"true"` - LastYear bool `hidden:"" name:"last-year" group:"Filter" completion-enabled:"true"` + Today bool `name:"today" group:"Filter Flags:" help:"Records at today’s date."` + Yesterday bool `name:"yesterday" group:"Filter Flags:" help:"Records at yesterday’s date."` + ThisXXX bool `name:"this-***" group:"Filter Flags:" help:"Records of this week/month/quarter/year, e.g. '--this-week' or '--this-quarter'." completion-enabled:"false"` + LastXXX bool `name:"last-***" group:"Filter Flags:" help:"Records of last week/month/quarter/year, e.g. '--last-month' or '--last-year'." completion-enabled:"false"` + ThisWeek bool `hidden:"" name:"this-week" group:"Filter Flags:" completion-enabled:"true"` + LastWeek bool `hidden:"" name:"last-week" group:"Filter Flags:" completion-enabled:"true"` + ThisMonth bool `hidden:"" name:"this-month" group:"Filter Flags:" completion-enabled:"true"` + LastMonth bool `hidden:"" name:"last-month" group:"Filter Flags:" completion-enabled:"true"` + ThisQuarter bool `hidden:"" name:"this-quarter" group:"Filter Flags:" completion-enabled:"true"` + LastQuarter bool `hidden:"" name:"last-quarter" group:"Filter Flags:" completion-enabled:"true"` + ThisYear bool `hidden:"" name:"this-year" group:"Filter Flags:" completion-enabled:"true"` + LastYear bool `hidden:"" name:"last-year" group:"Filter Flags:" completion-enabled:"true"` // General filters: - Tags []klog.Tag `name:"tag" placeholder:"TAG" group:"Filter" help:"Entries that match these tags (either in the record summary or the entry summary). You can omit the leading '#'."` - Filter string `name:"filter" placeholder:"EXPR" group:"Filter" help:"Entries that match this filter expression. Run 'klog info --filtering' to learn how expressions works."` + Tags []klog.Tag `name:"tag" placeholder:"TAG" group:"Filter Flags:" help:"Records or entries that match these tags (either in the record summary or the entry summary). You can omit the leading '#'."` + Filter string `name:"filter" placeholder:"EXPR" group:"Filter Flags:" help:"Records or entries that match this filter expression. Run 'klog info --filtering' to learn how expressions works."` + + hasPartialRecordsWithShouldTotal bool // Field only for internal use + singleShortHandFilter period.Period // Field only for internal use } func (args *FilterArgs) ApplyFilter(now gotime.Time, rs []klog.Record) ([]klog.Record, app.Error) { @@ -83,6 +86,9 @@ } return res }() + if len(dateRanges) == 1 { + args.singleShortHandFilter = dateRanges[0] + } for _, d := range dateRanges { predicates = append(predicates, filter.IsInDateRange{ From: d.Since(), @@ -125,7 +131,16 @@ // Apply filters, if applicable: if len(predicates) > 0 { - rs = filter.Filter(filter.And{Predicates: predicates}, rs) + hprws := false + rs, hprws = filter.Filter(filter.And{Predicates: predicates}, rs) + args.hasPartialRecordsWithShouldTotal = hprws + } return rs, nil } + +// SinglePeriodRequested returns the corresponding period if a single short-hand +// periodic filter (such as --this-month, --period) was used. +func (args *FilterArgs) SinglePeriodRequested() period.Period { + return args.singleShortHandFilter +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/args/misc.go new/klog-7.1/klog/app/cli/args/misc.go --- old/klog-7.0/klog/app/cli/args/misc.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/args/misc.go 2026-02-22 11:46:19.000000000 +0100 @@ -13,6 +13,16 @@ Diff bool `name:"diff" short:"d" help:"Show difference between actual and should-total time."` } +// GetWarning returns a warning if the user applied entry-level filtering (partial +// records) *and* requested to compute the should-total diff, as that may yield +// nonsensical results. +func (args *DiffArgs) GetWarning(filterArgs FilterArgs) service.UsageWarning { + if args.Diff && filterArgs.hasPartialRecordsWithShouldTotal { + return service.EntryFilteredDiffWarning + } + return service.UsageWarning{} +} + type NoStyleArgs struct { NoStyle bool `name:"no-style" help:"Do not style or colour the values."` } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/args/now.go new/klog-7.1/klog/app/cli/args/now.go --- old/klog-7.0/klog/app/cli/args/now.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/args/now.go 2026-02-22 11:46:19.000000000 +0100 @@ -34,9 +34,11 @@ return args.hadOpenRange } -func (args *NowArgs) GetNowWarnings() []string { +// 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 { - return []string{"You specified --now, but there was no open-ended time range."} + return service.PointlessNowWarning } - return nil + return service.UsageWarning{} } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/args/warn.go new/klog-7.1/klog/app/cli/args/warn.go --- old/klog-7.0/klog/app/cli/args/warn.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/args/warn.go 2026-02-22 11:46:19.000000000 +0100 @@ -11,16 +11,24 @@ NoWarn bool `name:"no-warn" help:"Suppress warnings about potential mistakes or logical errors."` } -func (args *WarnArgs) PrintWarnings(ctx app.Context, records []klog.Record, additionalWarnings []string) { +func (args *WarnArgs) PrintWarnings(ctx app.Context, records []klog.Record, additionalWarnings []service.UsageWarning) { styler, _ := ctx.Serialise() - if args.NoWarn { - return + warnings := args.GatherWarnings(ctx, records, additionalWarnings) + for _, w := range warnings { + ctx.Print(prettify.PrettifyWarning(w, styler)) } - for _, msg := range additionalWarnings { - ctx.Print(prettify.PrettifyGeneralWarning(msg, styler)) +} + +func (args *WarnArgs) GatherWarnings(ctx app.Context, records []klog.Record, additionalWarnings []service.UsageWarning) []string { + if args.NoWarn { + return nil } disabledCheckers := ctx.Config().NoWarnings.UnwrapOr(service.NewDisabledCheckers()) - service.CheckForWarnings(func(w service.Warning) { - ctx.Print(prettify.PrettifyWarning(w, styler)) - }, ctx.Now(), records, disabledCheckers) + warnings := service.CheckForWarnings(ctx.Now(), records, disabledCheckers) + for _, warn := range additionalWarnings { + if warn != (service.UsageWarning{}) && !disabledCheckers[warn.Name] { + warnings = append(warnings, warn.Message) + } + } + return warnings } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/info.go new/klog-7.1/klog/app/cli/info.go --- old/klog-7.0/klog/app/cli/info.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/info.go 2026-02-22 11:46:19.000000000 +0100 @@ -25,7 +25,9 @@ klog total --filter='2025-04 && #work' mytimes.klg -This would evaluate all entries in April 2025 that match the tag #work. Wrap the filter expression in single quotes to avoid undesired shell word splitting or substitution. Filter expressions consist of operands for matching the data that shall be included in the filter result. Operands can be combined via logical operators and grouped via parentheses. +This would evaluate all records and entries in April 2025 that match the tag #work. Wrap the filter expression in single quotes to avoid undesired shell word splitting or substitution. Filter expressions consist of operands for matching the data that shall be included in the filter result. Operands can be combined via logical operators and grouped via parentheses. + +Filters can match at record-level and/or at entry-level. It only keeps the data that satisfies the filter condition. For entry-level filters, this means that all non-matching entries are stripped from the record. Examples: 2025-04-20 || 2020-04-21 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/json.go new/klog-7.1/klog/app/cli/json.go --- old/klog-7.0/klog/app/cli/json.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/json.go 2026-02-22 11:46:19.000000000 +0100 @@ -4,6 +4,7 @@ "github.com/jotaen/klog/klog/app" "github.com/jotaen/klog/klog/app/cli/args" "github.com/jotaen/klog/klog/parser/json" + "github.com/jotaen/klog/klog/service" ) type Json struct { @@ -11,16 +12,21 @@ args.NowArgs args.FilterArgs args.SortArgs + args.WarnArgs args.InputFilesArgs } func (opt *Json) Help() string { return ` -The output structure is a JSON object which contains two properties at the top level: 'records' and 'errors'. -If the file is valid, 'records' is an array containing a JSON object for each record, and 'errors' is 'null'. -If the file has syntax errors, 'records' is 'null', and 'errors' contains an array of error objects. +The output structure is a JSON object which contains three properties at the top level: 'records', 'warnings' and 'errors': -The structure of the 'record' and 'error' objects is always uniform and should be self-explanatory. +- If the file is valid, 'records' is an array containing a JSON object for each record, and 'errors' is 'null'. + +- If the file is valid, but contains potential problems, 'warnings' is an array of strings with the respective messages. + +- If the file has syntax errors, 'records' and 'warnings' are 'null', and 'errors' contains an array of error objects. + +The 'record', 'warnings' and 'error' data structures are always uniform and should be self-explanatory. You can best explore it by running the command with the --pretty flag. ` } @@ -30,7 +36,7 @@ if err != nil { parserErrs, isParserErr := err.(app.ParserErrors) if isParserErr { - ctx.Print(json.ToJson(nil, parserErrs.All(), opt.Pretty) + "\n") + ctx.Print(json.ToJson(nil, parserErrs.All(), nil, opt.Pretty) + "\n") return nil } return err @@ -45,6 +51,7 @@ return fErr } records = opt.ApplySort(records) - ctx.Print(json.ToJson(records, nil, opt.Pretty) + "\n") + warnings := opt.GatherWarnings(ctx, records, []service.UsageWarning{opt.GetWarning()}) + ctx.Print(json.ToJson(records, nil, warnings, opt.Pretty) + "\n") return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/prettify/prettifier.go new/klog-7.1/klog/app/cli/prettify/prettifier.go --- old/klog-7.0/klog/app/cli/prettify/prettifier.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/prettify/prettifier.go 2026-02-22 11:46:19.000000000 +0100 @@ -6,7 +6,6 @@ "strings" "github.com/jotaen/klog/klog/app" - "github.com/jotaen/klog/klog/service" "github.com/jotaen/klog/klog/service/filter" tf "github.com/jotaen/klog/lib/terminalformat" ) @@ -81,13 +80,8 @@ ) } -// PrettifyWarning formats a warning about a record. -func PrettifyWarning(w service.Warning, styler tf.Styler) string { - return PrettifyGeneralWarning(w.Date().ToString()+": "+w.Warning(), styler) -} - -// PrettifyGeneralWarning formats a general warning message. -func PrettifyGeneralWarning(message string, styler tf.Styler) string { +// PrettifyWarning formats a warning message. +func PrettifyWarning(message string, styler tf.Styler) string { result := "" result += styler.Props(tf.StyleProps{Background: tf.YELLOW, Color: tf.YELLOW}).Format("[") result += styler.Props(tf.StyleProps{Background: tf.YELLOW, Color: tf.TEXT_INVERSE}).Format("WARNING") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/print.go new/klog-7.1/klog/app/cli/print.go --- old/klog-7.0/klog/app/cli/print.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/print.go 2026-02-22 11:46:19.000000000 +0100 @@ -23,9 +23,8 @@ func (opt *Print) Help() string { return ` Outputs data on the terminal, by default with syntax-highlighting turned on. -Note that the output doesn’t resemble the file byte by byte, but the command may apply some minor clean-ups of the formatting. +Note that the output doesn’t resemble the file verbatim, but it may apply some minor formatting. -If run with filter flags, it only outputs those entries that match the filter clauses. You can optionally also sort the records, or print out the total times for each record and entry. ` } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/report.go new/klog-7.1/klog/app/cli/report.go --- old/klog-7.0/klog/app/cli/report.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/report.go 2026-02-22 11:46:19.000000000 +0100 @@ -65,7 +65,12 @@ aggregator := opt.aggregator() recordGroups, dates := groupByDate(aggregator.DateHash, records) if opt.Fill { - dates = allDatesRange(records[0].Date(), records[len(records)-1].Date()) + singlePeriod := opt.FilterArgs.SinglePeriodRequested() + if singlePeriod != nil { + dates = allDatesRange(singlePeriod.Since(), singlePeriod.Until()) + } else { + dates = allDatesRange(records[0].Date(), records[len(records)-1].Date()) + } } // Table setup @@ -145,7 +150,7 @@ } table.Collect(ctx.Print) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning(), opt.DiffArgs.GetWarning(opt.FilterArgs)}) return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/report_test.go new/klog-7.1/klog/app/cli/report_test.go --- old/klog-7.0/klog/app/cli/report_test.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/report_test.go 2026-02-22 11:46:19.000000000 +0100 @@ -282,6 +282,29 @@ `, state.printBuffer) } +func TestPeriodicalReportWithFill(t *testing.T) { + state, err := NewTestingContext()._SetNow(2018, 8, 24, 0, 0)._SetRecords(` +2018-04-02 (8h!) + 8h + +2018-08-10 (5h30m!) + 2h + +2018-08-23 (2h!) + 5h20m +`)._Run((&Report{AggregateBy: "quarter", DiffArgs: args.DiffArgs{Diff: true}, Fill: true, FilterArgs: args.FilterArgs{ThisYear: true}}).Run) + require.Nil(t, err) + assert.Equal(t, ` + Total Should Diff +2018 Q1 + Q2 8h 8h! 0m + Q3 7h20m 7h30m! -10m + Q4 + ======== ========= ======== + 15h20m 15h30m! -10m +`, state.printBuffer) +} + func TestReportWithChart(t *testing.T) { t.Run("Daily (default) aggregation", func(t *testing.T) { state, err := NewTestingContext()._SetRecords(` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/tags.go new/klog-7.1/klog/app/cli/tags.go --- old/klog-7.0/klog/app/cli/tags.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/tags.go 2026-02-22 11:46:19.000000000 +0100 @@ -94,6 +94,6 @@ } } table.Collect(ctx.Print) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning()}) return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/today.go new/klog-7.1/klog/app/cli/today.go --- old/klog-7.0/klog/app/cli/today.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/today.go 2026-02-22 11:46:19.000000000 +0100 @@ -191,7 +191,7 @@ } } table.Collect(ctx.Print) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning()}) return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/cli/total.go new/klog-7.1/klog/app/cli/total.go --- old/klog-7.0/klog/app/cli/total.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/cli/total.go 2026-02-22 11:46:19.000000000 +0100 @@ -59,6 +59,6 @@ return "s" }())) - opt.WarnArgs.PrintWarnings(ctx, records, opt.GetNowWarnings()) + opt.WarnArgs.PrintWarnings(ctx, records, []service.UsageWarning{opt.NowArgs.GetWarning(), opt.DiffArgs.GetWarning(opt.FilterArgs)}) return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/app/config.go new/klog-7.1/klog/app/config.go --- old/klog-7.0/klog/app/config.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/app/config.go 2026-02-22 11:46:19.000000000 +0100 @@ -239,7 +239,14 @@ Name: "no_warnings", Help: Help{ Summary: "Whether klog should suppress certain warnings when processing files.", - Value: "The config property must be one (or several comma-separated) of: `UNCLOSED_OPEN_RANGE` (for unclosed open ranges in past records), `FUTURE_ENTRIES` (for records/entries in the future), `OVERLAPPING_RANGES` (for time ranges that overlap), `MORE_THAN_24H` (if there is a record with more than 24h total). Multiple values must be separated by a comma, e.g.: `UNCLOSED_OPEN_RANGE, MORE_THAN_24H`.", + Value: "The config property must be one or several (comma-separated) of: " + + "`UNCLOSED_OPEN_RANGE` (for unclosed open ranges in past records), " + + "`FUTURE_ENTRIES` (for records/entries in the future), " + + "`OVERLAPPING_RANGES` (for time ranges that overlap), " + + "`MORE_THAN_24H` (if there is a record with more than 24h total), " + + "`POINTLESS_NOW` (when using --now without any open ranges), " + + "`ENTRY_FILTERED_DIFFING` (when combining --diff and entry-level filtering). " + + "Multiple values must be separated by a comma, e.g.: `UNCLOSED_OPEN_RANGE, MORE_THAN_24H`.", Default: "If absent/empty, klog prints all available warnings.", }, read: func(value string, config *Config) error { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/parser/json/serialiser.go new/klog-7.1/klog/parser/json/serialiser.go --- old/klog-7.0/klog/parser/json/serialiser.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/parser/json/serialiser.go 2026-02-22 11:46:19.000000000 +0100 @@ -6,27 +6,30 @@ import ( "bytes" "encoding/json" + "sort" + "strings" + "github.com/jotaen/klog/klog" "github.com/jotaen/klog/klog/parser" "github.com/jotaen/klog/klog/parser/txt" "github.com/jotaen/klog/klog/service" - "sort" - "strings" ) // ToJson serialises records into their JSON representation. The output // structure is RecordView at the top level. -func ToJson(rs []klog.Record, errs []txt.Error, prettyPrint bool) string { +func ToJson(rs []klog.Record, errs []txt.Error, warnings []string, prettyPrint bool) string { envelop := func() Envelop { if errs == nil { return Envelop{ - Records: toRecordViews(rs), - Errors: nil, + Records: toRecordViews(rs), + Warnings: warnings, + Errors: nil, } } else { return Envelop{ - Records: nil, - Errors: toErrorViews(errs), + Records: nil, + Warnings: nil, + Errors: toErrorViews(errs), } } }() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/parser/json/serialiser_test.go new/klog-7.1/klog/parser/json/serialiser_test.go --- old/klog-7.0/klog/parser/json/serialiser_test.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/parser/json/serialiser_test.go 2026-02-22 11:46:19.000000000 +0100 @@ -1,27 +1,29 @@ package json import ( + "testing" + "github.com/jotaen/klog/klog" "github.com/jotaen/klog/klog/parser" "github.com/jotaen/klog/klog/parser/txt" "github.com/stretchr/testify/assert" - "testing" ) func TestSerialiseEmptyRecords(t *testing.T) { - json := ToJson([]klog.Record{}, nil, false) - assert.Equal(t, `{"records":[],"errors":null}`, json) + json := ToJson([]klog.Record{}, nil, nil, false) + assert.Equal(t, `{"records":[],"warnings":null,"errors":null}`, json) } func TestSerialiseEmptyArrayIfNoErrors(t *testing.T) { - json := ToJson(nil, nil, false) - assert.Equal(t, `{"records":[],"errors":null}`, json) + json := ToJson(nil, nil, nil, false) + assert.Equal(t, `{"records":[],"warnings":null,"errors":null}`, json) } func TestSerialisePrettyPrinted(t *testing.T) { - json := ToJson(nil, nil, true) + json := ToJson(nil, nil, nil, true) assert.Equal(t, `{ "records": [], + "warnings": null, "errors": null }`, json) } @@ -30,7 +32,7 @@ json := ToJson(func() []klog.Record { r := klog.NewRecord(klog.Ɀ_Date_(2000, 12, 31)) return []klog.Record{r} - }(), nil, false) + }(), nil, nil, false) assert.Equal(t, `{"records":[{`+ `"date":"2000-12-31",`+ `"summary":"",`+ @@ -42,7 +44,7 @@ `"diff_mins":0,`+ `"tags":[],`+ `"entries":[]`+ - `}],"errors":null}`, json) + `}],"warnings":null,"errors":null}`, json) } func TestSerialiseFullBlownRecord(t *testing.T) { @@ -54,7 +56,7 @@ r.AddRange(klog.Ɀ_Range_(klog.Ɀ_TimeYesterday_(23, 44), klog.Ɀ_Time_(5, 23)), nil) r.Start(klog.NewOpenRange(klog.Ɀ_TimeTomorrow_(0, 28)), klog.Ɀ_EntrySummary_("Started #todo=nr4", "still on #it")) return []klog.Record{r} - }(), nil, false) + }(), nil, nil, false) assert.Equal(t, `{"records":[{`+ `"date":"2000-12-31",`+ `"summary":"Hello #World\nWhat’s up?",`+ @@ -90,7 +92,7 @@ `"start":"0:28>",`+ `"start_mins":1468`+ `}]`+ - `}],"errors":null}`, json) + `}],"warnings":null,"errors":null}`, json) } func TestSerialiseParserErrors(t *testing.T) { @@ -98,8 +100,8 @@ json := ToJson(nil, []txt.Error{ parser.ErrorInvalidDate().New(block, 0, 0, 10), parser.ErrorMalformedSummary().New(block, 1, 3, 5).SetOrigin("/a/b/c/file.klg"), - }, false) - assert.Equal(t, `{"records":null,"errors":[{`+ + }, nil, false) + assert.Equal(t, `{"records":null,"warnings":null,"errors":[{`+ `"line":7,`+ `"column":1,`+ `"length":10,`+ @@ -115,3 +117,24 @@ `"file":"/a/b/c/file.klg"`+ `}]}`, json) } + +func TestSerialiseWarnings(t *testing.T) { + t.Run("include warnings if records are ok", func(t *testing.T) { + json := ToJson(nil, nil, []string{"Caution!", "Beware!"}, false) + assert.Equal(t, `{"records":[],"warnings":["Caution!","Beware!"],"errors":null}`, json) + }) + t.Run("ignore warnings if records are not ok", func(t *testing.T) { + block, _ := txt.ParseBlock("2018-99-99", 6) + json := ToJson(nil, []txt.Error{ + parser.ErrorInvalidDate().New(block, 0, 0, 10), + }, []string{"Caution!", "Beware!"}, false) + assert.Equal(t, `{"records":null,"warnings":null,"errors":[{`+ + `"line":7,`+ + `"column":1,`+ + `"length":10,`+ + `"title":"Invalid date",`+ + `"details":"Please make sure that the date format is either YYYY-MM-DD or YYYY/MM/DD, and that its value represents a valid day in the calendar.",`+ + `"file":""`+ + `}]}`, json) + }) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/parser/json/view.go new/klog-7.1/klog/parser/json/view.go --- old/klog-7.0/klog/parser/json/view.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/parser/json/view.go 2026-02-22 11:46:19.000000000 +0100 @@ -1,10 +1,14 @@ package json // Envelop is the top level data structure of the JSON output. -// It contains two nodes, `records` and `errors`, one of which is always `null`. +// It contains three nodes: +// - `records`: is `null` if there are errors +// - `warnings`: only if applicable and only unless there are errors +// - `errors`: only unless there are records type Envelop struct { - Records []RecordView `json:"records"` - Errors []ErrorView `json:"errors"` + Records []RecordView `json:"records"` + Warnings []string `json:"warnings"` + Errors []ErrorView `json:"errors"` } // RecordView is the JSON representation of a record. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/service/filter/filter.go new/klog-7.1/klog/service/filter/filter.go --- old/klog-7.0/klog/service/filter/filter.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/service/filter/filter.go 2026-02-22 11:46:19.000000000 +0100 @@ -4,20 +4,33 @@ "github.com/jotaen/klog/klog" ) -func Filter(p Predicate, rs []klog.Record) []klog.Record { +// Filter goes through a list of records and only keeps those that match the +// given predicate. The records may be returned partially, keeping only those +// entries that match the predicate. +// The second return value indicates whether there are partial records with a +// should-total set, as this may yield nonsensical results in a subsequent evaluation. +func Filter(p Predicate, rs []klog.Record) ([]klog.Record, bool) { var res []klog.Record + hasPartialRecordsWithShouldTotal := false for _, r := range rs { - var es []klog.Entry - for i, e := range r.Entries() { - if p.Matches(queriedEntry{r, r.Entries()[i]}) { - es = append(es, e) + if len(r.Entries()) == 0 && p.MatchesEmptyRecord(r) { + res = append(res, r) + } else { + var es []klog.Entry + for i, e := range r.Entries() { + if p.Matches(r, r.Entries()[i]) { + es = append(es, e) + } } + if len(es) == 0 { + continue + } + if len(es) != len(r.Entries()) && r.ShouldTotal() != nil { + hasPartialRecordsWithShouldTotal = true + } + r.SetEntries(es) + res = append(res, r) } - if len(es) == 0 { - continue - } - r.SetEntries(es) - res = append(res, r) } - return res + return res, hasPartialRecordsWithShouldTotal } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/service/filter/filter_test.go new/klog-7.1/klog/service/filter/filter_test.go --- old/klog-7.0/klog/service/filter/filter_test.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/service/filter/filter_test.go 2026-02-22 11:46:19.000000000 +0100 @@ -4,151 +4,272 @@ "testing" "github.com/jotaen/klog/klog" - "github.com/jotaen/klog/klog/service" + "github.com/jotaen/klog/klog/parser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func sampleRecordsForQuerying() []klog.Record { - return []klog.Record{ - func() klog.Record { - // Note that records without entries never match any query. - r := klog.NewRecord(klog.Ɀ_Date_(1999, 12, 30)) - r.SetSummary(klog.Ɀ_RecordSummary_("Hello World", "#foo")) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(1999, 12, 31)) - r.AddDuration(klog.NewDuration(5, 0), klog.Ɀ_EntrySummary_("#bar")) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(2000, 1, 1)) - r.SetSummary(klog.Ɀ_RecordSummary_("#foo")) - r.AddDuration(klog.NewDuration(0, 15), nil) - r.AddDuration(klog.NewDuration(6, 0), klog.Ɀ_EntrySummary_("#bar")) - r.AddDuration(klog.NewDuration(0, -30), nil) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(2000, 1, 2)) - r.SetSummary(klog.Ɀ_RecordSummary_("#foo")) - r.AddDuration(klog.NewDuration(7, 0), nil) - return r - }(), func() klog.Record { - r := klog.NewRecord(klog.Ɀ_Date_(2000, 1, 3)) - r.SetSummary(klog.Ɀ_RecordSummary_("#foo=a")) - r.AddDuration(klog.NewDuration(4, 0), klog.Ɀ_EntrySummary_("test", "foo #bar=1")) - r.AddDuration(klog.NewDuration(4, 0), klog.Ɀ_EntrySummary_("#bar=2")) - r.Start(klog.NewOpenRange(klog.Ɀ_Time_(12, 00)), nil) - return r - }(), + rs, _, err := parser.NewSerialParser().Parse(` +1999-12-29 +No tags here + +1999-12-30 +Hello World #foo #first + +1999-12-31 + 5h #bar [300] + +2000-01-01 +#foo #third + 1:30-1:45 [15] + 6h #bar [360] + -30m [-30] + +2000-01-02 +#foo #fourth + 7h #xyz [420] + +2000-01-03 +#foo=a #fifth + 12:00-16:00 [240] + #bar=1 + 3h #bar=2 [180] + 12:00-? [0] +`) + if err != nil { + panic(err) + } + return rs +} + +type expect struct { + date klog.Date + durations []int +} + +func assertResult(t *testing.T, es []expect, rs []klog.Record) { + require.Equal(t, len(es), len(rs), "unexpected number of records") + for i, expct := range es { + assert.Equal(t, expct.date, rs[i].Date(), "unexpected date") + require.Equal(t, len(expct.durations), len(rs[i].Entries()), "unexpected number of entries") + actualDurations := make([]int, len(rs[i].Entries())) + for j, e := range rs[i].Entries() { + actualDurations[j] = e.Duration().InMinutes() + } + assert.Equal(t, expct.durations, actualDurations, "unexpected duration") } } func TestQueryWithNoClauses(t *testing.T) { - rs := Filter(And{}, sampleRecordsForQuerying()) - require.Len(t, rs, 4) - assert.Equal(t, klog.NewDuration(5+6+7+8, -30+15), service.Total(rs...)) + rs, hprws := Filter(And{}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 29), []int{}}, + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) +} + +func TestQueryWithNoMatches(t *testing.T) { + rs, hprws := Filter(IsInDateRange{ + From: klog.Ɀ_Date_(2002, 1, 1), + To: klog.Ɀ_Date_(2002, 1, 1), + }, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{}, rs) +} + +func TestQueryAgainstEmptyInput(t *testing.T) { + rs, hprws := Filter(IsInDateRange{ + From: klog.Ɀ_Date_(2002, 1, 1), + To: klog.Ɀ_Date_(2002, 1, 1), + }, nil) + assert.False(t, hprws) + assertResult(t, []expect{}, rs) } func TestQueryWithAtDate(t *testing.T) { - rs := Filter(IsInDateRange{ + rs, hprws := Filter(IsInDateRange{ From: klog.Ɀ_Date_(2000, 1, 2), To: klog.Ɀ_Date_(2000, 1, 2), }, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, klog.NewDuration(7, 0), service.Total(rs...)) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + }, rs) } func TestQueryWithAfter(t *testing.T) { - rs := Filter(IsInDateRange{ + rs, hprws := Filter(IsInDateRange{ From: klog.Ɀ_Date_(2000, 1, 1), To: nil, }, sampleRecordsForQuerying()) - require.Len(t, rs, 3) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, 2, rs[1].Date().Day()) - assert.Equal(t, 3, rs[2].Date().Day()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } func TestQueryWithBefore(t *testing.T) { - rs := Filter(IsInDateRange{ + rs, hprws := Filter(IsInDateRange{ From: nil, To: klog.Ɀ_Date_(2000, 1, 1), }, sampleRecordsForQuerying()) - require.Len(t, rs, 2) - assert.Equal(t, 31, rs[0].Date().Day()) - assert.Equal(t, 1, rs[1].Date().Day()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 29), []int{}}, + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + }, rs) } -func TestQueryWithTagOnEntries(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("bar", "")}, sampleRecordsForQuerying()) - require.Len(t, rs, 3) - assert.Equal(t, 31, rs[0].Date().Day()) - assert.Equal(t, 1, rs[1].Date().Day()) - assert.Equal(t, 3, rs[2].Date().Day()) - assert.Equal(t, klog.NewDuration(5+8+6, 0), service.Total(rs...)) +func TestQueryWithTagOnOverallSummary(t *testing.T) { + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("foo", "")}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15, 360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } -func TestQueryWithTagOnOverallSummary(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("foo", "")}, sampleRecordsForQuerying()) - require.Len(t, rs, 3) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, 2, rs[1].Date().Day()) - assert.Equal(t, 3, rs[2].Date().Day()) - assert.Equal(t, klog.NewDuration(6+7+8, -30+15), service.Total(rs...)) +func TestQueryWithTagOnEntries(t *testing.T) { + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("bar", "")}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{360}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180}}, + }, rs) } func TestQueryWithTagOnEntriesAndInSummary(t *testing.T) { - rs := Filter(And{[]Predicate{HasTag{klog.NewTagOrPanic("foo", "")}, HasTag{klog.NewTagOrPanic("bar", "")}}}, sampleRecordsForQuerying()) - require.Len(t, rs, 2) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, 3, rs[1].Date().Day()) - assert.Equal(t, klog.NewDuration(8+6, 0), service.Total(rs...)) + rs, hprws := Filter(And{[]Predicate{HasTag{klog.NewTagOrPanic("foo", "")}, HasTag{klog.NewTagOrPanic("bar", "")}}}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{360}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180}}, + }, rs) } func TestQueryWithTagValues(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("foo", "a")}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, 3, rs[0].Date().Day()) - assert.Equal(t, klog.NewDuration(8, 0), service.Total(rs...)) + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("foo", "a")}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } func TestQueryWithTagValuesInEntries(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("bar", "1")}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, 3, rs[0].Date().Day()) - assert.Equal(t, klog.NewDuration(4, 0), service.Total(rs...)) + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("bar", "1")}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{240}}, + }, rs) } func TestQueryWithTagNonMatchingValues(t *testing.T) { - rs := Filter(HasTag{klog.NewTagOrPanic("bar", "3")}, sampleRecordsForQuerying()) - require.Len(t, rs, 0) + rs, hprws := Filter(HasTag{klog.NewTagOrPanic("bar", "3")}, sampleRecordsForQuerying()) + assert.False(t, hprws) + assertResult(t, []expect{}, rs) } func TestQueryWithEntryTypes(t *testing.T) { { - rs := Filter(IsEntryType{ENTRY_TYPE_DURATION}, sampleRecordsForQuerying()) - require.Len(t, rs, 4) - assert.Equal(t, klog.NewDuration(0, 1545), service.Total(rs...)) + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_DURATION}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{360, -30}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{180}}, + }, rs) + } + { + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_DURATION_NEGATIVE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{-30}}, + }, rs) + } + { + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_DURATION_POSITIVE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 31), []int{300}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{360}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{180}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_DURATION_NEGATIVE}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, 1, rs[0].Date().Day()) - assert.Equal(t, klog.NewDuration(0, -30), service.Total(rs...)) + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_RANGE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 1), []int{15}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_DURATION_POSITIVE}, sampleRecordsForQuerying()) - require.Len(t, rs, 4) - assert.Equal(t, klog.NewDuration(0, 1575), service.Total(rs...)) + rs, hprws := Filter(IsEntryType{ENTRY_TYPE_OPEN_RANGE}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{0}}, + }, rs) + } +} + +func TestComplexFilterQueries(t *testing.T) { + { + rs, hprws := Filter(Or{[]Predicate{ + IsInDateRange{From: klog.Ɀ_Date_(2000, 1, 2), To: nil}, + HasTag{klog.NewTagOrPanic("first", "")}, + And{[]Predicate{ + Not{HasTag{klog.NewTagOrPanic("something", "1")}}, + IsEntryType{ENTRY_TYPE_RANGE}, + }}, + }}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 30), []int{}}, + {klog.Ɀ_Date_(2000, 1, 1), []int{15}}, + {klog.Ɀ_Date_(2000, 1, 2), []int{420}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 180, 0}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_RANGE}, sampleRecordsForQuerying()) - require.Len(t, rs, 0) - assert.Equal(t, klog.NewDuration(0, 0), service.Total(rs...)) + rs, hprws := Filter(And{[]Predicate{ + IsInDateRange{From: klog.Ɀ_Date_(2000, 1, 1), To: klog.Ɀ_Date_(2000, 1, 3)}, + HasTag{klog.NewTagOrPanic("bar", "")}, + Not{HasTag{klog.NewTagOrPanic("third", "")}}, + IsEntryType{ENTRY_TYPE_RANGE}, + }}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(2000, 1, 3), []int{240}}, + }, rs) } { - rs := Filter(IsEntryType{ENTRY_TYPE_OPEN_RANGE}, sampleRecordsForQuerying()) - require.Len(t, rs, 1) - assert.Equal(t, klog.NewDuration(0, 0), service.Total(rs...)) + rs, hprws := Filter(Not{Or{[]Predicate{ + IsInDateRange{From: klog.Ɀ_Date_(1999, 12, 30), To: klog.Ɀ_Date_(2000, 1, 1)}, + HasTag{klog.NewTagOrPanic("xyz", "")}, + And{[]Predicate{ + IsEntryType{ENTRY_TYPE_DURATION_POSITIVE}, + HasTag{klog.NewTagOrPanic("bar", "")}, + }}, + }}}, sampleRecordsForQuerying()) + assert.True(t, hprws) + assertResult(t, []expect{ + {klog.Ɀ_Date_(1999, 12, 29), []int{}}, + {klog.Ɀ_Date_(2000, 1, 3), []int{240, 0}}, + }, rs) } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/service/filter/predicate.go new/klog-7.1/klog/service/filter/predicate.go --- old/klog-7.0/klog/service/filter/predicate.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/service/filter/predicate.go 2026-02-22 11:46:19.000000000 +0100 @@ -7,32 +7,37 @@ "github.com/jotaen/klog/klog" ) -type queriedEntry struct { - parent klog.Record - entry klog.Entry -} - +// Predicate is the generic base type for all predicates. The caller is responsible for +// selecting the applicable match function (it’s meant to be either/or). type Predicate interface { - Matches(queriedEntry) bool + // Matches returns true if the record’s entry satisfies the predicate. + Matches(klog.Record, klog.Entry) bool + // MatchesEmptyRecord returns true if an empty record (without any entries) + // satisfies the predicate. + MatchesEmptyRecord(klog.Record) bool } type IsInDateRange struct { - From klog.Date - To klog.Date + From klog.Date // May be nil to denote open range. + To klog.Date // May be nil to denote open range. +} + +func (i IsInDateRange) Matches(r klog.Record, e klog.Entry) bool { + return i.MatchesEmptyRecord(r) } -func (i IsInDateRange) Matches(e queriedEntry) bool { +func (i IsInDateRange) MatchesEmptyRecord(r klog.Record) bool { isAfter := func() bool { if i.From == nil { return true } - return e.parent.Date().IsAfterOrEqual(i.From) + return r.Date().IsAfterOrEqual(i.From) }() isBefore := func() bool { if i.To == nil { return true } - return i.To.IsAfterOrEqual(e.parent.Date()) + return i.To.IsAfterOrEqual(r.Date()) }() return isAfter && isBefore } @@ -41,17 +46,30 @@ Tag klog.Tag } -func (h HasTag) Matches(e queriedEntry) bool { - return e.parent.Summary().Tags().Contains(h.Tag) || e.entry.Summary().Tags().Contains(h.Tag) +func (h HasTag) Matches(r klog.Record, e klog.Entry) bool { + return h.MatchesEmptyRecord(r) || e.Summary().Tags().Contains(h.Tag) +} + +func (h HasTag) MatchesEmptyRecord(r klog.Record) bool { + return r.Summary().Tags().Contains(h.Tag) } type And struct { Predicates []Predicate } -func (a And) Matches(e queriedEntry) bool { +func (a And) Matches(r klog.Record, e klog.Entry) bool { + for _, p := range a.Predicates { + if !p.Matches(r, e) { + return false + } + } + return true +} + +func (a And) MatchesEmptyRecord(r klog.Record) bool { for _, p := range a.Predicates { - if !p.Matches(e) { + if !p.MatchesEmptyRecord(r) { return false } } @@ -62,9 +80,18 @@ Predicates []Predicate } -func (o Or) Matches(e queriedEntry) bool { +func (o Or) Matches(r klog.Record, e klog.Entry) bool { for _, p := range o.Predicates { - if p.Matches(e) { + if p.Matches(r, e) { + return true + } + } + return false +} + +func (o Or) MatchesEmptyRecord(r klog.Record) bool { + for _, p := range o.Predicates { + if p.MatchesEmptyRecord(r) { return true } } @@ -75,8 +102,12 @@ Predicate Predicate } -func (n Not) Matches(e queriedEntry) bool { - return !n.Predicate.Matches(e) +func (n Not) Matches(r klog.Record, e klog.Entry) bool { + return !n.Predicate.Matches(r, e) +} + +func (n Not) MatchesEmptyRecord(r klog.Record) bool { + return !n.Predicate.MatchesEmptyRecord(r) } type EntryType string @@ -108,17 +139,17 @@ Type EntryType } -func (t IsEntryType) Matches(e queriedEntry) bool { - return klog.Unbox[bool](&e.entry, func(r klog.Range) bool { +func (t IsEntryType) Matches(r klog.Record, e klog.Entry) bool { + return klog.Unbox[bool](&e, func(r klog.Range) bool { return t.Type == ENTRY_TYPE_RANGE }, func(duration klog.Duration) bool { if t.Type == ENTRY_TYPE_DURATION { return true } - if t.Type == ENTRY_TYPE_DURATION_POSITIVE && e.entry.Duration().InMinutes() >= 0 { + if t.Type == ENTRY_TYPE_DURATION_POSITIVE && e.Duration().InMinutes() >= 0 { return true } - if t.Type == ENTRY_TYPE_DURATION_NEGATIVE && e.entry.Duration().InMinutes() < 0 { + if t.Type == ENTRY_TYPE_DURATION_NEGATIVE && e.Duration().InMinutes() < 0 { return true } return false @@ -126,3 +157,7 @@ return t.Type == ENTRY_TYPE_OPEN_RANGE }) } + +func (t IsEntryType) MatchesEmptyRecord(r klog.Record) bool { + return false +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/service/warning.go new/klog-7.1/klog/service/warning.go --- old/klog-7.0/klog/service/warning.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/service/warning.go 2026-02-22 11:46:19.000000000 +0100 @@ -7,21 +7,22 @@ "github.com/jotaen/klog/klog" ) -// Warning contains information for helping locate an issue. -type Warning struct { - date klog.Date - origin checker +// UsageWarning contains information for avoiding potential usage issues. +type UsageWarning struct { + Name string + Message string } -// Date is the date of the record that the warning refers to. -func (w Warning) Date() klog.Date { - return w.date -} - -// Warning is a short description of the problem. -func (w Warning) Warning() string { - return w.origin.Message() -} +var ( + PointlessNowWarning = UsageWarning{ + Name: "POINTLESS_NOW", + Message: "You specified --now, but there was no open-ended time range", + } + EntryFilteredDiffWarning = UsageWarning{ + Name: "ENTRY_FILTERED_DIFFING", + Message: "Combining --diff and filtering at entry-level may yield nonsensical results", + } +) type checker interface { Warn(klog.Record) klog.Date @@ -39,6 +40,8 @@ (&futureEntriesChecker{}).Name(): false, (&overlappingTimeRangesChecker{}).Name(): false, (&moreThan24HoursChecker{}).Name(): false, + PointlessNowWarning.Name: false, + EntryFilteredDiffWarning.Name: false, } } @@ -47,7 +50,7 @@ // strict validation, but the main purpose is to help users spot accidental mistakes users // might have made. The checks are limited to record-level, because otherwise it would // need to make assumptions on how records are organised within or across files. -func CheckForWarnings(onWarn func(Warning), reference gotime.Time, rs []klog.Record, disabledCheckers DisabledCheckers) { +func CheckForWarnings(reference gotime.Time, rs []klog.Record, disabledCheckers DisabledCheckers) []string { now := NewDateTimeFromGo(reference) sortedRs := Sort(rs, false) checkers := []checker{ @@ -56,6 +59,7 @@ &overlappingTimeRangesChecker{}, &moreThan24HoursChecker{}, } + var warnings []string for _, r := range sortedRs { for _, c := range checkers { if disabledCheckers[c.Name()] { @@ -63,13 +67,11 @@ } d := c.Warn(r) if d != nil { - onWarn(Warning{ - date: d, - origin: c, - }) + warnings = append(warnings, d.ToString()+": "+c.Message()) } } } + return warnings } type unclosedOpenRangeChecker struct { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/klog-7.0/klog/service/warning_test.go new/klog-7.1/klog/service/warning_test.go --- old/klog-7.0/klog/service/warning_test.go 2026-02-06 22:46:44.000000000 +0100 +++ new/klog-7.1/klog/service/warning_test.go 2026-02-22 11:46:19.000000000 +0100 @@ -1,6 +1,7 @@ package service import ( + "strings" "testing" gotime "time" @@ -8,22 +9,18 @@ "github.com/stretchr/testify/assert" ) -func countWarningsOfKind(c checker, ws []Warning) int { +func countWarningsOfKind(c checker, ws []string) int { count := 0 for _, w := range ws { - if w.Warning() == c.Message() { + if strings.HasSuffix(w, c.Message()) { count++ } } return count } -func collectWarnings(reference gotime.Time, rs []klog.Record) []Warning { - var ws []Warning - CheckForWarnings(func(w Warning) { - ws = append(ws, w) - }, reference, rs, NewDisabledCheckers()) - return ws +func collectWarnings(reference gotime.Time, rs []klog.Record) []string { + return CheckForWarnings(reference, rs, NewDisabledCheckers()) } func TestNoWarnForOpenRanges(t *testing.T) { @@ -79,8 +76,8 @@ } ws := collectWarnings(timestamp, rs) assert.Equal(t, 2, countWarningsOfKind(&unclosedOpenRangeChecker{}, ws)) - assert.Equal(t, today.PlusDays(-1), ws[0].Date()) - assert.Equal(t, today.PlusDays(-2), ws[1].Date()) + assert.Equal(t, today.PlusDays(-1).ToString(), ws[0][0:10]) + assert.Equal(t, today.PlusDays(-2).ToString(), ws[1][0:10]) } func TestNoWarningForFutureEntries(t *testing.T) { @@ -313,11 +310,7 @@ }(), } - var ws []Warning - CheckForWarnings(func(w Warning) { - ws = append(ws, w) - }, timestamp, rs, x.dc) - + ws := CheckForWarnings(timestamp, rs, x.dc) assert.Len(t, ws, x.exp) } } ++++++ klog.obsinfo ++++++ --- /var/tmp/diff_new_pack.1cSCwN/_old 2026-02-25 21:21:32.445260896 +0100 +++ /var/tmp/diff_new_pack.1cSCwN/_new 2026-02-25 21:21:32.453261225 +0100 @@ -1,5 +1,5 @@ name: klog -version: 7.0 -mtime: 1770414404 -commit: 76fdc21899f3547d28f6e80e4179959a92bcd999 +version: 7.1 +mtime: 1771757179 +commit: 7a4f7b6b0749adb164aa6ac67dee9ba6556f261e ++++++ vendor.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/alecthomas/kong/mapper.go new/vendor/github.com/alecthomas/kong/mapper.go --- old/vendor/github.com/alecthomas/kong/mapper.go 2026-02-06 22:46:44.000000000 +0100 +++ new/vendor/github.com/alecthomas/kong/mapper.go 2026-02-22 11:46:19.000000000 +0100 @@ -541,8 +541,12 @@ if ctx.Value.Flag != nil { t := ctx.Scan.Pop() // If decoding a flag, we need a value. + tail := "" + if sep != -1 { + tail += string(sep) + "..." + } if t.IsEOL() { - return fmt.Errorf("missing value, expecting \"<arg>%c...\"", sep) + return fmt.Errorf("missing value, expecting \"<arg>%s\"", tail) } switch v := t.Value.(type) { case string: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/alecthomas/kong/tag.go new/vendor/github.com/alecthomas/kong/tag.go --- old/vendor/github.com/alecthomas/kong/tag.go 2026-02-06 22:46:44.000000000 +0100 +++ new/vendor/github.com/alecthomas/kong/tag.go 2026-02-22 11:46:19.000000000 +0100 @@ -166,13 +166,13 @@ return d, nil } -func getTagInfo(ft reflect.StructField) (string, tagChars) { - s, ok := ft.Tag.Lookup("kong") +func getTagInfo(tag reflect.StructTag) (string, tagChars) { + s, ok := tag.Lookup("kong") if ok { return s, kongChars } - return string(ft.Tag), bareChars + return string(tag), bareChars } func newEmptyTag() *Tag { @@ -204,10 +204,26 @@ t.Ignored = true return t, nil } - items, err := parseTagItems(getTagInfo(ft)) + items := map[string][]string{} + // First use a [Signature] if present + signatureTag, ok := maybeGetSignature(ft.Type) + if ok { + signatureItems, err := parseTagItems(getTagInfo(signatureTag)) + if err != nil { + return nil, err + } + items = signatureItems + } + // Next overlay the field's tags. + fieldItems, err := parseTagItems(getTagInfo(ft.Tag)) if err != nil { return nil, err } + for key, value := range fieldItems { + // Prepend field tag values + items[key] = append(value, items[key]...) + } + t := &Tag{ items: items, } @@ -384,3 +400,34 @@ } return r, nil } + +// Signature allows flags, args and commands to supply a default set of tags, +// that can be overridden by the field itself. +type Signature interface { + // Signature returns default tags for the flag, arg or command. + // + // eg. `name:"migrate" help:"Run migrations" aliases:"mig,mg"`. + Signature() string +} + +var signatureOverrideType = reflect.TypeOf((*Signature)(nil)).Elem() + +func maybeGetSignature(t reflect.Type) (reflect.StructTag, bool) { + ut := t + if ut.Kind() == reflect.Pointer { + ut = ut.Elem() + } + ptr := reflect.New(ut) + var sig string + for _, v := range []reflect.Value{ptr, ptr.Elem()} { + if v.Type().Implements(signatureOverrideType) { + sig = v.Interface().(Signature).Signature() //nolint:forcetypeassert + break + } + } + sig = strings.TrimSpace(sig) + if sig == "" { + return "", false + } + return reflect.StructTag(sig), true +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/jotaen/kong-completion/positional_predictor.go new/vendor/github.com/jotaen/kong-completion/positional_predictor.go --- old/vendor/github.com/jotaen/kong-completion/positional_predictor.go 2026-02-06 22:46:44.000000000 +0100 +++ new/vendor/github.com/jotaen/kong-completion/positional_predictor.go 2026-02-22 11:46:19.000000000 +0100 @@ -8,9 +8,10 @@ // PositionalPredictor is a predictor for positional arguments type PositionalPredictor struct { - Predictors []complete.Predictor - ArgFlags []string - BoolFlags []string + Predictors []complete.Predictor + ArgFlags []string + BoolFlags []string + LastFlagIsCumulative bool // “Cumulative” means the last flag can appear multiple times. } // Predict implements complete.Predict @@ -26,6 +27,9 @@ position := p.predictorIndex(a) complete.Log("predicting positional argument(%d)", position) if position < 0 || position > len(p.Predictors)-1 { + if p.LastFlagIsCumulative && len(p.Predictors) > 0 { + return p.Predictors[len(p.Predictors)-1] + } return nil } return p.Predictors[position] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/jotaen/kong-completion/prediction.go new/vendor/github.com/jotaen/kong-completion/prediction.go --- old/vendor/github.com/jotaen/kong-completion/prediction.go 2026-02-06 22:46:44.000000000 +0100 +++ new/vendor/github.com/jotaen/kong-completion/prediction.go 2026-02-22 11:46:19.000000000 +0100 @@ -161,10 +161,12 @@ if err != nil { return nil, err } + lastIsCumulative := len(node.Positional) > 0 && node.Positional[len(node.Positional)-1].IsCumulative() cmd.Args = &PositionalPredictor{ - Predictors: pps, - ArgFlags: flagNamesWithHyphens(flags.argFlags...), - BoolFlags: flagNamesWithHyphens(flags.boolFlags...), + Predictors: pps, + ArgFlags: flagNamesWithHyphens(flags.argFlags...), + BoolFlags: flagNamesWithHyphens(flags.boolFlags...), + LastFlagIsCumulative: lastIsCumulative, } return &cmd, nil diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt --- old/vendor/modules.txt 2026-02-06 22:46:44.000000000 +0100 +++ new/vendor/modules.txt 2026-02-22 11:46:19.000000000 +0100 @@ -1,7 +1,7 @@ # cloud.google.com/go v0.123.0 ## explicit; go 1.24.0 cloud.google.com/go/civil -# github.com/alecthomas/kong v1.13.0 +# github.com/alecthomas/kong v1.14.0 ## explicit; go 1.20 github.com/alecthomas/kong # github.com/davecgh/go-spew v1.1.1 @@ -16,7 +16,7 @@ # github.com/jotaen/genie v0.0.3 ## explicit; go 1.25 github.com/jotaen/genie -# github.com/jotaen/kong-completion v0.0.11 +# github.com/jotaen/kong-completion v0.0.12 ## explicit; go 1.25 github.com/jotaen/kong-completion # github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
