All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.5.0 - 2026-08-26
- Hidden files and directories are scanned by default. Being hidden is no longer a reason to skip anything: the ignore files and the
excludeconfig key decide alone, which is what "everything git would track" already claimed. A tag in.github/workflows(a pinned action version, a step commented out until a fix lands) was silently never checked, which is the exact failure this tool exists to prevent. Repositories that keep tags in dotfiles will see findings they did not see before, andexcludeis how to quiet a large dotted directory that.gitignoredoes not cover. Thetodo-by . .githubidiom, which named the dotted directory because that was the only way to reach it, is now redundant and harmless: overlapping roots report their shared files once. --hiddenasks for what now always happens, so it does nothing. It is still accepted, so a CI job already passing it keeps working, and it no longer appears in--help. Passing it prints a deprecation notice on stderr naming the release that removes it, 1.0. Notices are collected during parsing and printed once, before any finding, so the next retired flag costs a line rather than a design.
- Overlapping roots no longer report their shared files once per covering root.
todo-by . srcwalkedsrctwice and printed every finding under it twice; each file is now reported exactly once, under the deepest root that covers it, in either argument order. A root named explicitly keeps its root semantics through the overlap: a gitignored directory passed alongside its ancestor is still scanned in full, exactly as it is alone. An exact repeat of the same place collapses to one walk, and when the repeated spellings differ, a spelling the scan would refuse as version control metadata (a symlink under.git, say) never wins the collapse over one it will actually walk, so which spelling came first cannot decide whether the directory is scanned at all. todo-by --files -exits 2 and says why, instead of printing nothing and exiting 0.--fileslists walked paths and never reads stdin, so a run given only-has nothing it could list, and its empty output must not pass for a clean listing.- A file is recognized as binary before it is read in full, not after.
scan_fileread the whole file and only then asked whether its first 8 KiB looked binary, so a multi gigabyte artifact was fully resident before the check that exists to skip it, and with the walk running one scan per worker thread, several could be resident at once. The detection prefix is read first now, and the rest of the file only once that prefix reads as text, so peak memory tracks the largest text file rather than the largest file. Scanning a 680 MB Rusttargetdirectory falls from 88 MB peak resident to 11 MB, and a tree holding one 2 GB binary from 2.05 GB to 3.8 MB. Piped input goes through the same reader now, socat big.bin | todo-by -falls from 207 MB to 3 MB on a 200 MB input instead of keeping the old behavior. It also stops reading there, the wayheaddoes, so underset -o pipefailthe writer'sSIGPIPEbecomes the pipeline's status even thoughtodo-byexited cleanly. Only a binary source larger than the pipe buffer reaches that, since text is still read to the end. What counts as binary is unchanged, and so is throughput on the benchmark corpus. - A reader that stops reading no longer crashes the run.
todo-by --files | head -4,lessquit on the first page,grep -qthat already matched: each closes the pipe, and the nextprintln!panicked, printing a crash report and exiting 101 (or aborting, in a release build). It fired only once the output outgrew the pipe buffer, so a small tree never saw it and a large one always did. Output now stops where the reader stopped, and the run keeps the exit code it had already earned, sotodo-by | head -1underset -o pipefailstill fails a job that has overdue tags. Any other write failure, a full disk among them, now exits 2 instead of panicking as well, which is the code the contract already gave it. Diagnostics on stderr are covered too, so2>&1 | headis safe; one that cannot be written is dropped rather than escalated, since a run must not be judged on whether anyone was still listening to it. - Version control metadata is never walked, with or without
--hidden..gitaccounted for 97% of the files a--hiddenscan read in this repository, and worse,.git/COMMIT_EDITMSGand.git/logshold commit messages: a commit that merely discussed a tag read to the scanner exactly like the tag itself..hgand.jjkeep the same text, and encoding it (as.jjdoes) does not hide it, since a description sits uncompressed inside its record and reads to a scanner working on plain text exactly like the tag it was describing..svn/pristinekeeps whole copies of tracked files on top of that, as does.jjon its non-default backend, which turns every real finding into a duplicate at a path nobody can edit. All four are matched by name, so a submodule's or a nested checkout's metadata goes too, as does the.gitfile a worktree gets in place of a directory. Naming one as a path argument does not reach it either, nor does running from inside it, nor does a symlink pointing into one or sitting inside one. A root dropped for this reason is named on stderr, and a run left with nothing else to read exits 2, since a scan that could not run must not look like a scan that found nothing. Stdin counts as something to read, sotodo-by - .gitstill reports what it found on stdin and exits on that.
0.4.0 - 2026-08-26
- Issue triggers: a tag can fire when a GitHub issue or pull request closes. Write
#123for the repository the git remote points at, or a full issue or pull request URL for any other repository or host.owner/repo#123is rejected rather than accepted, since the URL already says the same thing and can also name a host. Any state other than open fires,mergedincluded; the close reason is never requested and never inspected. - The issue spellings other tools accept (
owner/repo#123andGH-123, which GitHub autolinks, andrepo#123, which phpstan-todo-by takes) are reported rather than ignored, each naming the accepted spelling; the cross repository form quotes the exact URL to write instead. A token carrying neither marker (a#with a digit behind it, or theGH-form) stays prose, so a project matching ontododoes not have every undated TODO reported. - Issue URLs may carry a fragment or a query, so a comment permalink pasted straight from GitHub (
.../issues/123#issuecomment-456) names issue 123 rather than being ignored as prose. --online(and theonlineconfig key, with--offlineto override it) gates every network call. Without it, issue tags are left unchecked and reported once on stderr, with no findings and no change to the exit code. The check runs only when the scan actually found an issue tag, so a tree of date tags stays hermetic.- Issue lookups go through
curlwhenGH_TOKENorGITHUB_TOKENis set and the target is github.com (the token travels in curl's config file on stdin, never in argv or on disk, with a timeout of 30 seconds), and throughghotherwise, which authenticates from its own keyring. An environment token is never sent to any other host, since a tag names its own host and a tag is repository content. References are batched into one GraphQL request per host, up to 100 at a time. repoconfig key (owner/name): the repository bare#123references resolve against. Without it the git remote decides, and two remotes that disagree (a fork checkout) are reported rather than guessed at.onlineandrepoconfig keys, plus boolean values in the config parser.
- Minimum supported Rust version raised from 1.85 to 1.88. The
ignoredependency declaresrust-version = "1.88"from 0.4.31 on, so 1.85 no longer resolves a working dependency set.
- Flags that take no value now reject one instead of discarding it.
--exit-zero=falseparsed as the flag plus an ignored"false"and then did the opposite of what was written; the same held for--hidden,--filesand--dump-config. - A trigger written flush against an HTML comment closer, with no space before the
-->, left a stray->at the head of the finding's message. The closer is now stripped from the message the way it was already kept out of the trigger span.
0.3.0 - 2026-07-26
- Version triggers: a tag can fire when the project reaches a version instead of on a date. Write the version with a lowercase
v(v2.0, orv2026.01for calendar versions) to mean "that version or later", or with an explicit comparator (>=v2.0,>v2.0).<,<=,=,==,^, and~are recognized and reported as invalid rather than silently ignored, since this tool cannot fire on a version that is never released or one held below a ceiling. - Current version resolution, run once per scan and only when a version tag is actually found:
--current-version, thenTODO_BY_VERSION, then theversion-cmdconfig key (a shell command, run in the config file's directory), thengit describe --tags --abbrev=0. A resolved version keeps only its tag: the markersgit describeadds for commits past the tag (-4-gabc123) and for a dirty tree (-dirty) are stripped, since semver reads them as a pre-release sorting below the release they came from, which would make a commit pastv1.2.3count as never having reached it. version-cmdconfig key, for projects whose version lives inpackage.json,composer.json, or anywhere else git tags do not cover.
- Breaking. A year on its own (a tag reading just
2026) is no longer a deadline meaning December 31 of that year. Deadlines now need at least a month, written with dashes (2026-12). A bare digit-leading token reads as a version constraint now, and a lone year cannot be told apart from a one-component version. Existing year-only tags are reported as errors naming both replacements (2026-12for the deadline,v2026for the version), so upgrading surfaces every one of them instead of quietly changing when they fire. - Breaking. A version written in a tag needs a lowercase
v. A bare number (2.0,>=2.0,2026.09.01) is no longer a version trigger, and is reported as an error quoting the marked spelling to write instead. The marker is what separates a version from a date, and without it a tag sitting in prose reads two ways at once:2026.09.01is both a dotted deadline and a calendar version,12.5.2026is both a day-first date and a three-component version,3.5is both a constraint and the start of "3.5 hours of work". Guessing wrong on any of those fails silently, since a constraint the project never reaches produces no finding at all, which would bury the chore instead of surfacing it. Dates keep their own marking, the dashes they always had. - The current version is unaffected by that rule:
--current-version,TODO_BY_VERSION,version-cmd, andgit describeall still accept a bare1.2.3, since those strings come from the project rather than from a tag author.
0.2.1 - 2026-07-12
- Homebrew install:
brew install alies-dev/todo-by/todo-by. The repository doubles as its own tap, and the formula is regenerated from the release checksums after each release. - Declared minimum supported Rust version (
rust-version = "1.85"), enforced in CI.
0.2.0 - 2026-07-10
--warn <days>(orwarn = Nin the config file): tags due within N days are reported as warnings (yellow in text output,::warningannotations in GitHub Actions) and exit 0, so deadlines appear in CI before they start failing it.--exit-zero: report-only mode for scheduled inventory jobs and gradual adoption.--format json: JSON Lines output, one object per finding plus a trailing summary record. The schema is additive-stable.- GitHub Actions auto-detect: when
GITHUB_ACTIONS=trueand no--formatis given, the github format is selected automatically. --color auto|always|never, honoringNO_COLOR,TERM=dumb, and TTY detection.- stdin scanning:
todo-by -scans standard input (for examplegit diff | todo-by -). - Config file
todo-by.toml(or.todo-by.toml), discovered from the current directory upward. Keys:warn,exclude(gitignore-style globs on top of.gitignore),tags(replaces the default tag list). Precedence: flags, thenTODO_BY_FORMAT/TODO_BY_WARN, then the config file. - Introspection flags:
--fileslists what would be scanned,--dump-configprints the effective config and its source.
- The exit code contract is now documented: 0 clean (warnings alone stay 0), 1 findings, 2 usage, config, or IO error.
0.1.0 - 2026-07-09
Initial release.
- Scanner for
todo-by <date>tags in any file type: byte-level, case-insensitive, no language grammars. - Three date precisions:
2026(due Dec 31),2026-09(due last day of month),2026-09-01. Impossible dates such as2026-02-30are reported asinvalid-datefindings so typos cannot silently postpone a deadline. - Parallel directory walking with full gitignore semantics (nested
.gitignorefiles, negation,**globs), also outside a git repository. Hidden files, binaries, and symlinks are skipped; explicitly named files are always scanned. - Output formats:
textfor humans,githubfor workflow annotations. - Exit codes: 0 clean, 1 findings, 2 error.
--todayto override the clock for testing and dry runs.