Week 17 of 2026

Development log of Devlog Excavator

13 items
  1. Rename devlog.nu to excavate
  2. The program will warn on activity in unlisted projects
  3. Do not format time if running as a Nushell command
  4. Refine the spec regarding ignore
  5. Implement the ignore feature
  6. Export project sub-commands: list and log
  7. Implement the stub projects sub-command
  8. Let the program die on broken projects
  9. Let project stubs and devlog go to ./drafts/
  10. More configuration options
  11. Implement the substitution feature
  12. Write a comment about smarted diff output
  13. Write about the report feature

Rename devlog.nu to excavate

On by Tad Lispy

A simple verb without the .nu extension. Nice.

similarity index 100%
rename from devlog.nu
rename to excavate
index 209b098..658718a 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -2,7 +2,7 @@
=interpreter: nu --stdin spec/interpreters/basic.nu
=---
=
-# The Dev Log Excavator
+# The Devlog Excavator
=
=This program excavates a devlog (i.e. a bunch of .md files) from commit messages in multiple repositories.
=
@@ -122,9 +122,7 @@ TODO: Setup a second project.
=  
=    Leave the repository and go back to where the config file is.
=
-  * Run the `devlog.nu --projects-dir . --dry-run` command
-  
-    TODO: Make it `devlog list`, without the extension,
+  * Run the `excavate --projects-dir . --dry-run` command
=  
=  * Expect the output to contain `the header row`
=
@@ -166,7 +164,7 @@ When using `--dry-run` it might be useful to see the commits grouped by project
=With `--since` and `--until` flags you can select a span of time from which commits are excavated.
=
=
-## The `excavate` Subcommand
+## The `excavate` command
=
=For each day run `git log` in each project. If there is any output, dump it to `devlog/<date>/<project-name>.md`. So each file should contain a log from a single project that was developed on a given day.
=

The program will warn on activity in unlisted projects

On by Tad Lispy

Maybe the user would like to include them? Otherwise they should be ignored. BTW, the ignore flag doesn't do anything yet. It's a regression. Let's fix that!

index 0b8fa4c..14d0d03 100755
--- a/excavate
+++ b/excavate
@@ -46,6 +46,7 @@ export def main [
=        }
=    }
=
+    # The main pipeline:
=    $projects_dir
=    | project list
=    | each $extract_log
@@ -67,6 +68,14 @@ export def main [
=
=    log info $"Excavated ($stats.commits) commits from ($stats.projects) projects across ($stats.days) days"
=
+    # Warn about unlisted projects
+    $commits
+    | where { is-empty project }
+    | select path day
+    | sort-by day --reverse
+    | uniq-by path
+    | each { log warning $"Activity in an unlisted project on ($in.day): ($in.path)" }
+
=    if $dry_run {
=        $commits
=        | update time { format date } # For the output, format date

Do not format time if running as a Nushell command

On by Tad Lispy

I mostly use excavate as a command in Nushell, so I'd rather has the time column as a proper datetime. But if running as a script, it would get converted to a humanized string, like "Yesterday" or "6 years ago". This is not very useful. So previously the date was always formatted into a string. But now I found a way to have it as a datetime value when running in Nushell and as a string otherwise.

index 14d0d03..8a37e16 100755
--- a/excavate
+++ b/excavate
@@ -1,4 +1,4 @@
-#!/usr/bin/env nu
+#!/usr/bin/env -S running_as_a_script=true nu
=
=use std/log
=
@@ -78,7 +78,8 @@ export def main [
=
=    if $dry_run {
=        $commits
-        | update time { format date } # For the output, format date
+        # If not running as a Nushell command, format time in the output
+        | if running_as_a_script in $env { update time { format date } } else { $in } 
=        | return $in
=    }
=

Refine the spec regarding ignore

On by Tad Lispy

The previous aproach with ignore field under the project is not very ergonomic, as it requires every ignored path to have it's own section and a name. In my ~/Projects/ directory I have hundreds of little experiments with one or two commits each and I don't want to give them all names just to avoid excavating them.

So now in the config there is the [ignore] section with paths - an array of strings. Easy to maintain.

index 9a8d531..eb670a5 100644
--- a/devlog-sample.toml
+++ b/devlog-sample.toml
@@ -5,6 +5,7 @@
=# Zola (https://www.getzola.org/documentation/content/taxonomies/).
=projects_cell_path = [ "taxonomies", "projects" ]
=
+# Each project you want to excavate should have it's own section, like this:
=[[projects]]
=name = "Tad Better Behavior"
=path = "~/Projects/tad-better-behavior"
@@ -16,3 +17,10 @@ path = "~/Projects/better-tech-club/bettertech.eu"
=[[projects]]
=name = "DevLog Extracavator"
=path = "."
+
+[ignore]
+# List all the paths to ignore when excavating
+paths = [
+    "~/Projects/manhattan",
+    "~/Projects/for-your-eyes-only",
+]
index 658718a..7a7364f 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -214,7 +214,17 @@ This seems to be specific to `skip`. Other commands fail as expected.
=
=By default `./devlog.toml` shall be used, but it can be changed with `--config-file` flag. The file must list all the projects for which devlog is to be generated. The name of the project comes from there. See the devlog-sample.toml file.
=
-A project can be flagged to be ignored with the `ignore` key.
+Any path under "ignore.paths" will be ignored, i.e. no entries will be excavated from it and there will be no warning.
+
+``` toml
+[ignore]
+paths = [
+    "~/Projects/manhattan",
+    "~/Projects/for-your-eyes-only",
+]
+```
+
+In the future there might be more things to ignore, like authors, files in patches (globs), etc.
=
-Any project that is not listed, but had activity within the period for which a devlog is being generated should produce a warning with useful instructions.
+Any project that is not listed and not ignored, but had activity within the period for which a devlog is being generated, will produce a warning with useful instructions.
=

Implement the ignore feature

On by Tad Lispy

As described in the spec.

index 8a37e16..da4d957 100755
--- a/excavate
+++ b/excavate
@@ -16,6 +16,7 @@ export def main [
=    open $config_file
=    | update projects { update path { path expand } } # Resolve all project paths
=    | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path }
+    | upsert ignore.paths { default [] | each { path expand } }
=    | let config
=
=    $until | default (date now) | let until
@@ -48,7 +49,7 @@ export def main [
=
=    # The main pipeline:
=    $projects_dir
-    | project list
+    | project list --ignore $config.ignore.paths
=    | each $extract_log
=    | flatten
=    | update time { |commit| $commit.time | into datetime }
@@ -103,11 +104,15 @@ export def main [
=    }
=}
=
-def "project list" []: path -> list<path> {
+def "project list" [
+    --ignore: list<path> = [] # Paths to ignore
+]: path -> list<path> {
=    $in
+    | path expand
=    | path join "**/.git"
=    | glob $in
=    | path dirname
+    | where { |project_path| not ($ignore | any { |ignored_path| $ignored_path in $project_path }) }
=}
=
=def "project log" [

Export project sub-commands: list and log

On by Tad Lispy

I finally figured out how to seamlessly export sub-commands both from a Nushell module and a script.

Excavator can be run in two ways. As a Nushell command:

use ./excavate
excavate --dry-run

and as a stand-alone program (in any other shell, like Bash):

$ ./excavate --dry-run

I want to have a as much feature parity between the two as possible. One thing I was struggling with until today were sub-commands. For a script they need to be prefixed with main, like main project list. For the module, they need to be exported.

I tried something like export def "main project list [...] {}" but then when imported I have to call them like this:

excavate main project list

Not elegant at all! But now, thanks to my yesterday's fruitless research on aliasing ls, I've found a trick. Name the commands in a natural way (without the main prefix) and then alias them with the prefix. Nice!

One more thing to remember is enabling stdin when invoking the nu interpreter. In a script this is best done in the shebang line. Just don't forget to add the -S or --split-string.

NOTE: The portability of this solution (e.g. with BSD and OSX) is questionable. Maybe we need to do something smarted here? I need to investigate it.

index da4d957..ae9964a 100755
--- a/excavate
+++ b/excavate
@@ -1,4 +1,4 @@
-#!/usr/bin/env -S running_as_a_script=true nu
+#!/usr/bin/env -S running_as_a_script=true nu --stdin
=
=use std/log
=
@@ -104,7 +104,8 @@ export def main [
=    }
=}
=
-def "project list" [
+# List all projects under a given path
+export def "project list" [
=    --ignore: list<path> = [] # Paths to ignore
=]: path -> list<path> {
=    $in
@@ -112,10 +113,13 @@ def "project list" [
=    | path join "**/.git"
=    | glob $in
=    | path dirname
-    | where { |project_path| not ($ignore | any { |ignored_path| $ignored_path in $project_path }) }
+    | where { |project_path| not ($ignore | any { path expand | $in in $project_path }) }
=}
=
-def "project log" [
+# Excavate a devlog from a single project
+# 
+# NOTE: This command does not write to any files, only lists commits.
+export def "project log" [
=    since: datetime,
=    until: datetime,
=]: path -> list {
@@ -161,6 +165,8 @@ def "project log" [
=    | insert path $project_path
=}
=
+alias "main project log" = project log
+alias "main project list" = project list
=
=def "format entry" [
=    title: string

Implement the stub projects sub-command

On by Tad Lispy

When starting a devlog (like I do now) it's useful to write out markdown document for each project.

Stubbing is driven by the config file, which needs to be normalized before it can be processed. This logic is shared with the main command, so now there is a normalize config helper just for that.

By default stubs are written to the ./projects/ directory, so it shouldn't be checked into version control. The name of this directory seems a bit too generic. I might change it later.

index 74bb444..e91585b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
=/drafts/
+/projects/
=/devlog.toml
=.direnv
=.devenv
index ae9964a..b4e2059 100755
--- a/excavate
+++ b/excavate
@@ -14,9 +14,7 @@ export def main [
=    # Read the config file, process and set some defaults
=    # TODO: DRY with `main list`
=    open $config_file
-    | update projects { update path { path expand } } # Resolve all project paths
-    | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path }
-    | upsert ignore.paths { default [] | each { path expand } }
+    | normalize config
=    | let config
=
=    $until | default (date now) | let until
@@ -104,6 +102,44 @@ export def main [
=    }
=}
=
+export def "stub projects" [
+    --config-file: path = "./devlog.toml"
+] {
+    open $config_file
+    | normalize config
+    | let config
+
+    mkdir $config.projects_stub_dir
+
+    for project in $config.projects {
+        $project.name
+        | str kebab-case
+        | {
+            parent: $config.projects_stub_dir,
+            stem: $in,
+            extension: "md"
+        }
+        | path join
+        | let out_path
+
+
+        {
+            title: $project.name
+        }
+        | to yaml
+        | str trim
+        | [
+            $"---"
+            $in
+            $"---"
+            $""
+            $"# ($project.name)"
+        ]
+        | str join "\n"
+        | save --force $out_path
+    }
+}
+
=# List all projects under a given path
=export def "project list" [
=    --ignore: list<path> = [] # Paths to ignore
@@ -167,6 +203,7 @@ export def "project log" [
=
=alias "main project log" = project log
=alias "main project list" = project list
+alias "main stub projects" = stub projects
=
=def "format entry" [
=    title: string
@@ -214,3 +251,12 @@ def "format commit" [] {
=def "format diff" [] {
=    $"``` diff\n($in | str trim)\n```"
=}
+
+# Standardize some elements of the config so we don't have to worry about it later
+def "normalize config" []: record -> record {
+    $in
+    | update projects { update path { path expand } } # Resolve all project paths
+    | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path }
+    | upsert ignore.paths { default [] | each { path expand } }
+    | upsert projects_stub_dir { default "./projects" | path expand }
+}
index 7a7364f..8d136da 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -228,3 +228,6 @@ In the future there might be more things to ignore, like authors, files in patch
=
=Any project that is not listed and not ignored, but had activity within the period for which a devlog is being generated, will produce a warning with useful instructions.
=
+## Stubbing projects 
+
+Use the `excavate stub projects` command to write a markdown file for each project listed in the configuration file. The output path is controlled by the `projects_stubs_dir` key.

Let the program die on broken projects

On by Tad Lispy

It will log each failure, including stderr from git status and then return an error listing each failed project.

index b4e2059..5b1c6b0 100755
--- a/excavate
+++ b/excavate
@@ -19,6 +19,36 @@ export def main [
=
=    $until | default (date now) | let until
=
+    # Check if all listed projects have proper git repositories
+    $config.projects
+    | insert status {
+        cd $in.path
+        $in.path
+        | path join ".git"
+        | git --git-dir $in status --porcelain
+        | complete
+    }
+    | each {
+        if ($in | get status.stdout | is-not-empty) {
+            log warning $"Repository not clean at ($in.name) ($in.path)"
+            $in
+        } else {
+            $in
+        }
+    }
+    | where { $in.status.exit_code != 0 }
+    | let broken_projects
+    | each {
+        log error $"Project ($in.name) at ($in.path) seems broken:\n($in.status.stderr)"
+    }
+
+    if ($broken_projects | is-not-empty) {
+        $broken_projects
+        | each { $"  ($in.name) \(($in.path)\)" }
+        | str join "\n"
+        | error make --unspanned $"Some projects are broken. See errors logged above.\n\n($in)"
+    }
+
=    let commit_day = { |commit|
=        $commit
=        | get time
index 8d136da..39a6a0c 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -181,6 +181,11 @@ If some work was done on such a project, there should be a warning about it.
=
=If a project is then added to the list, running the command again should not override existing drafts, but create new files for the newly listed projects.
=
+
+## Broken projects
+
+Projects listed in the configuration file, with path not pointing to the **root of a git** repository will result in program terminating early with an error.
+
=## Binary files issue
=
=A non-unicode fragment in the diff breaks my script. For example in a repo with some PDFs checked in I'm getting this:

Let project stubs and devlog go to ./drafts/

On by Tad Lispy

By default excavate stub projects will write to drafts/projects (this can be changed in the configuration file) and the main excavate command will write to ./drafts/devlog (this can be changed using the --out-dir cli flag). That way there is only one directory to manage, and also it's more obvious that these are all drafts.

index e91585b..74bb444 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
=/drafts/
-/projects/
=/devlog.toml
=.direnv
=.devenv
index 5b1c6b0..8ec75ef 100755
--- a/excavate
+++ b/excavate
@@ -7,7 +7,7 @@ export def main [
=    --until: datetime                       # When did you last committed anything good?
=    --projects-dir: path = "~/Projects",    # Where do you keep your projects?
=    --day-start: string = "04:00"           # Until how late can you code?
-    --out-dir: path = "drafts"              # Where to write excavated posts
+    --out-dir: path = "drafts/devlog"       # Where to write excavated posts
=    --config-file: path = "devlog.toml"     # The configuration file
=    --dry-run                               # Just list the commits to excavate
=] {
@@ -288,5 +288,5 @@ def "normalize config" []: record -> record {
=    | update projects { update path { path expand } } # Resolve all project paths
=    | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path }
=    | upsert ignore.paths { default [] | each { path expand } }
-    | upsert projects_stub_dir { default "./projects" | path expand }
+    | upsert projects_stub_dir { default "./drafts/projects" | path expand }
=}

More configuration options

On by Tad Lispy

Directory paths for

are now configurable.

Also, the default configuration file path is now a constant (no more repeated strings).

One stale TODO comment removed.

index eb670a5..bcc83b7 100644
--- a/devlog-sample.toml
+++ b/devlog-sample.toml
@@ -1,3 +1,6 @@
+drafts_dir = "content/devlog"
+projects_stubs_dir = "content/works/"
+
=[frontmatter]
=
=# By default projects are listed under extra.projects.  This example shows how
index 8ec75ef..fe9310f 100755
--- a/excavate
+++ b/excavate
@@ -2,22 +2,25 @@
=
=use std/log
=
+const default_config_file = "devlog.toml"
+
=export def main [
-    --since: datetime = 1970-01-01          # How far back should we look?
-    --until: datetime                       # When did you last committed anything good?
-    --projects-dir: path = "~/Projects",    # Where do you keep your projects?
-    --day-start: string = "04:00"           # Until how late can you code?
-    --out-dir: path = "drafts/devlog"       # Where to write excavated posts
-    --config-file: path = "devlog.toml"     # The configuration file
-    --dry-run                               # Just list the commits to excavate
+    --since: datetime = 1970-01-01              # How far back should we look?
+    --until: datetime                           # When did you last committed anything good?
+    --day-start: string = "04:00"               # Until how late can you code?
+    --config-file: path = $default_config_file  # The configuration file
+    --out-dir: path                             # Where to write excavated posts (default: $config.drafts_dir)
+    --projects-dir: path                        # Where do you keep your projects (default: $config.projects_dir)
+    --dry-run                                   # Just list the commits to excavate
=] {
=    # Read the config file, process and set some defaults
-    # TODO: DRY with `main list`
=    open $config_file
=    | normalize config
=    | let config
=
=    $until | default (date now) | let until
+    $out_dir | default $config.drafts_dir | let out_dir
+    $projects_dir | default $config.projects_dir | let projects_dir
=
=    # Check if all listed projects have proper git repositories
=    $config.projects
@@ -133,19 +136,22 @@ export def main [
=}
=
=export def "stub projects" [
-    --config-file: path = "./devlog.toml"
+    --config-file: path = $default_config_file
+    --out-dir: path                             # Where to write the stubs (default: $config.projects_stubs_dir)
=] {
=    open $config_file
=    | normalize config
=    | let config
=
-    mkdir $config.projects_stub_dir
+    $out_dir | default $config.projects_stubs_dir | let out_dir
+
+    mkdir $out_dir
=
=    for project in $config.projects {
=        $project.name
=        | str kebab-case
=        | {
-            parent: $config.projects_stub_dir,
+            parent: $out_dir,
=            stem: $in,
=            extension: "md"
=        }
@@ -288,5 +294,7 @@ def "normalize config" []: record -> record {
=    | update projects { update path { path expand } } # Resolve all project paths
=    | upsert frontmatter.projects_cell_path { default { [ extra projects ] } | into cell-path }
=    | upsert ignore.paths { default [] | each { path expand } }
-    | upsert projects_stub_dir { default "./drafts/projects" | path expand }
+    | upsert projects_dir { default "~/Projects" | path expand }
+    | upsert projects_stubs_dir { default "./drafts/projects" | path expand }
+    | upsert drafts_dir { default "./drafts/devlog" | path expand }
=}
index 39a6a0c..5025af6 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -181,10 +181,11 @@ If some work was done on such a project, there should be a warning about it.
=
=If a project is then added to the list, running the command again should not override existing drafts, but create new files for the newly listed projects.
=
+Any project that is not listed and not ignored, but had activity within the period for which a devlog is being generated, will produce a warning with useful instructions.
=
=## Broken projects
=
-Projects listed in the configuration file, with path not pointing to the **root of a git** repository will result in program terminating early with an error.
+Projects listed in the configuration file, with path not pointing to the **root of a git** repository will result in program terminating early with an error. All broken projects should be listed in the error output.
=
=## Binary files issue
=
@@ -217,9 +218,14 @@ This seems to be specific to `skip`. Other commands fail as expected.
=
=## Configuration file
=
-By default `./devlog.toml` shall be used, but it can be changed with `--config-file` flag. The file must list all the projects for which devlog is to be generated. The name of the project comes from there. See the devlog-sample.toml file.
+By default `./devlog.toml` shall be used, but it can be changed with `--config-file` flag. The file must list all the projects for which devlog is to be generated. The name of each project comes from there.
+
+In addition it can specify:
=
-Any path under "ignore.paths" will be ignored, i.e. no entries will be excavated from it and there will be no warning.
+  - `drafts_dir`: where devlog entries are written (default: `./drafts/devlog`)
+  - `projects_stubs_dir`: where projects' are written by `excavate stub projects`(defautl: `./drafts/projects`)
+
+It can also include the `ignore` section. Any path under `ignore.paths` will be ignored, i.e. no entries will be excavated from it and there will be no warning.
=
=``` toml
=[ignore]
@@ -231,7 +237,12 @@ paths = [
=
=In the future there might be more things to ignore, like authors, files in patches (globs), etc.
=
-Any project that is not listed and not ignored, but had activity within the period for which a devlog is being generated, will produce a warning with useful instructions.
+A configuration file can also contain `frontmatter` section with configuration related to the entries front-matter.
+
+  - `projects_cell_path`: where to write the name of a project that the entry belongs to
+
+See the `devlog-sample.toml` file.
+
=
=## Stubbing projects 
=

Implement the substitution feature

On by Tad Lispy

The rationale is given in the spec.

index fe9310f..e869fe7 100755
--- a/excavate
+++ b/excavate
@@ -245,6 +245,14 @@ def "format entry" [
=    title: string
=    config: record
=] {
+    let substitute = { |entry|
+        $config.substitute
+        | reduce --fold  $entry { |substitute, memo|
+            log info $"Substituting ($substitute.what) with ($substitute.with)"
+            $memo | str replace --all $substitute.what $substitute.with
+        }
+    }
+
=    let frontmatter = { title: $title }
=    | insert $config.frontmatter.projects_cell_path [ $title ]
=
@@ -257,7 +265,12 @@ def "format entry" [
=        $"Commits: ($in.items | length)"
=        $""
=        $""
-        ($in.items | each { | commit | $commit | format commit } | str join "\n\n")
+        (
+            $in.items
+            | each { | commit | $commit | format commit }
+            | each $substitute
+            | str join "\n\n"
+            )
=    ] | str join "\n"
=}
=
@@ -297,4 +310,5 @@ def "normalize config" []: record -> record {
=    | upsert projects_dir { default "~/Projects" | path expand }
=    | upsert projects_stubs_dir { default "./drafts/projects" | path expand }
=    | upsert drafts_dir { default "./drafts/devlog" | path expand }
+    | upsert substitute { default []  }
=}
index 5025af6..87b6215 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -243,6 +243,31 @@ A configuration file can also contain `frontmatter` section with configuration r
=
=See the `devlog-sample.toml` file.
=
+## Substitutions patches
+
+Sometimes patches can contain text that triggers other programs in the devlog pipeline. Often these are templating or rendering systems. For example I use Zola (a static site generator) a lot, so many of the patches include markdown documents written for Zola, like `{{ do_something(with=this) }}`. Code like this ends up in devlog entries. And because I also use Zola to render the devlog on my website, it leads to trouble. Zola "thinks" that I'm trying to use a shortcode, where in fact it's just a piece of code from a different project.
+
+The only reliable way I know to prevent it is to escape the problematic code in the Markdown files and then unescape it in the rendered HTML. It's not perfect, but works well enough. To facilitate it without a ton of manual edits, Excavator has a mechanism to replace any number of sequences with substitutes.
+
+Each substitution has to be a section with two keys: `what` and `with`, like this:
+
+``` toml
+[[substitute]]
+what = "{{"
+with = "{\\{"
+
+[[substitute]]
+what = "}}"
+with = "\\}}"
+
+[[substitute]]
+what = "{%"
+with = "{\\%"
+
+[[substitute]]
+what = "%}"
+with = "\\%}"
+```
=
=## Stubbing projects 
=

Write a comment about smarted diff output

On by Tad Lispy

index e869fe7..c59d1b0 100755
--- a/excavate
+++ b/excavate
@@ -298,7 +298,12 @@ def "format commit" [] {
=}
=
=def "format diff" [] {
-    $"``` diff\n($in | str trim)\n```"
+    # TODO: Smarter diffs
+    # If file is created, write the code in a file's language
+    # If file is an image, export it and insert an image tag
+    # If image was modified, insert both for comparison
+    str trim
+    | $"``` diff\n($in)\n```"
=}
=
=# Standardize some elements of the config so we don't have to worry about it later

Write about the report feature

On by Tad Lispy

Not implemented yet.

index 87b6215..e2c4b25 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -154,6 +154,13 @@ TODO: Setup a second project.
=    │ +3 +│ +2026-04-01 +│ +Project Alpha +│ +Bob +│ +bob@example.com +│ +I've lawyered up +│ +Wed, 1 Apr 2026 09:03:00 \+\d{4} +│ +[0-9a-f]+ +│ +.+/project-alpha +│
=    ```
=
+## The report
+
+The output should contain a report, including
+
+1. Used configuration (after normalization and application of command line flags)
+2. Total number of processed projects, commits,  and days
+
=## Grouped view
=
=When using `--dry-run` it might be useful to see the commits grouped by project and day. Maybe there should be another flag? Or value?