Week 19 of 2026
Development log of Devlog Excavator
3 items
Make weekly entries, group them by project
On by
I've realized that what I want is for each project to have it's devlog in a separate directory. Also devlog entries should span a week, not a day. So the new structure will be like this:
project-alpha/2026-W01.md
The document will contain all the commits from week 1 of 2026.
Evaluating spec based on Nushell output was difficult and flaky. I want to rewrite the assetions in spec to introspect the directory structure and markdown output. This should be easier, since recent addition of Markdown parser to Nushell. But the spec is currently outdated. Evil me went ahead with the implementation without rewriting it.
Internally, now all the data is collected, processed and prepared to be
written out. With the --dry-run option, all this data is returned,
presumably for further processing, because it's too much to eyeball.
In a real run, the markdown files are written and only short stats are returned.
As I was working on this, I refactored and cleaned up some code.
index 82e781f..1924ff3 100755
--- a/excavate
+++ b/excavate
@@ -78,6 +78,29 @@ export def main [
= }
= }
=
+ ## Set file path and markdown content to write
+ let prepare_to_write = { |entry|
+
+ $entry
+ | get project
+ | str kebab-case
+ | { parent: $out_dir, stem: $in }
+ | path join
+ | { parent: $in, stem: $entry.week, extension: "md" }
+ | path join
+ | let out_path
+
+ let title = $"($entry.project) ($entry.week)"
+
+ $entry
+ | format entry $title $config
+ | let markdown
+
+ $entry
+ | insert path $out_path
+ | insert markdown $markdown
+ }
+
= # The main pipeline:
= $projects_dir
= | project list --ignore $config.ignore.paths
@@ -88,18 +111,19 @@ export def main [
= | insert project $commit_project
= | move day project author email subject time sha path --first
= | let commits
- | group-by --to-table day project
- | sort-by day
+ | insert week { |row| $row.day | format date "%Y-W%W" }
+ | group-by project week --prune --to-table
+ | sort-by week
+ | each $prepare_to_write
= | let entries
=
= let stats = {
= commits: ($commits | length),
= projects: ($entries | get project | uniq | length),
- days: ($entries | get day | uniq | length)
+ weeks: ($entries | get week | uniq | length)
+ entries: ($entries | length)
= }
=
- log info $"Excavated ($stats.commits) commits from ($stats.projects) projects across ($stats.days) days"
-
= # Warn about unlisted projects
= $commits
= | where { is-empty project }
@@ -108,30 +132,23 @@ export def main [
= | uniq-by path
= | each { log warning $"Activity in an unlisted project on ($in.day): ($in.path)" }
=
+ log debug $"Excavated ($stats.commits) commits from ($stats.projects) projects across ($stats.weeks) weeks"
+
= if $dry_run {
- $commits
- # 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
- }
-
- # TODO: Allow setting in a config file
- mkdir $out_dir
+ # Return all the collected data, maybe for further processing
+ return $entries
+ } else {
+ for entry in $entries {
+ log info $"Writing to ($entry.path) …"
=
+ $entry.path
+ | path dirname
+ | mkdir $in
=
- for entry in $entries {
- $entry
- | get day project
- | str join "-"
- | str kebab-case
- | { parent: $out_dir, stem: $in, extension: "md" }
- | path join
- | let out_path
-
- log info $"Writing to ($out_path)..."
- $entry
- | format entry $in.project $config
- | save --force $out_path
+ $entry.markdown
+ | save --force $entry.path
+ }
+ return $stats
= }
=}
=
@@ -245,31 +262,33 @@ def "format entry" [
= title: string
= config: record
=] {
- let substitute = { |entry|
+ let apply_substitutions = { |text|
= $config.substitute
- | reduce --fold $entry { |substitute, memo|
+ | reduce --fold $text { |substitute, memo|
= $memo | str replace --all $substitute.what $substitute.with
= }
= }
=
+ let entry = $in
+
+ # # TODO: Write this in front-matter (extra.stats.commits or something)
+ # let stats = { commits: ($in.items | length) }
+
= let frontmatter = { title: $title }
- | insert $config.frontmatter.projects_cell_path [ $title ]
+ # TODO: | insert $config.frontmatter.stats_cell_path [ $stats ]
=
= [
= $"---"
= ($frontmatter | to yaml)
= $"---"
= $""
- # TODO: Write this in front-matter (extra.stats.commits or something)
- $"Commits: ($in.items | length)"
- $""
= $""
= (
- $in.items
+ $entry.items
= | each { | commit | $commit | format commit }
- | each $substitute
+ | each $apply_substitutions
= | str join "\n\n"
- )
+ )
= ] | str join "\n"
=}
=
@@ -289,9 +308,11 @@ def "format commit" [] {
=
= [
= $"# ($in.subject | str trim)"
- ""
+ $""
+ $"<time datetime=\"($in.day)\">($in.day | format date '%A, %F')</time>"
+ $""
= ($body | str trim)
- ""
+ $""
= ($patch | each { format diff } | str join "\n\n" )
= ] | str join "\n"
=}Add an author to the signature of each commit
On by
After the time, on the same line.
index 1924ff3..ab4e743 100755
--- a/excavate
+++ b/excavate
@@ -305,16 +305,24 @@ def "format commit" [] {
= | split row --regex "(?m)^diff --git .+" | skip 1
= }
= | let patch
-
+
+ [
+ $"On <time datetime=\"($in.day)\">($in.day | format date '%A, %F')</time>"
+ $"by <span class=\"author\">($in.author)</span>"
+ ]
+ | str join " "
+ | let signature
+
= [
= $"# ($in.subject | str trim)"
= $""
- $"<time datetime=\"($in.day)\">($in.day | format date '%A, %F')</time>"
+ $signature
= $""
= ($body | str trim)
= $""
= ($patch | each { format diff } | str join "\n\n" )
- ] | str join "\n"
+ ]
+ | str join "\n"
=}
=
=def "format diff" [] {Let the excavator write stats for each entry
On by
In the front-matter, under a configurable cell-path.
index bcc83b7..a75c053 100644
--- a/devlog-sample.toml
+++ b/devlog-sample.toml
@@ -3,10 +3,9 @@ projects_stubs_dir = "content/works/"
=
=[frontmatter]
=
-# By default projects are listed under extra.projects. This example shows how
-# to use custom cell path to list them under taxonomies, for example to use with
-# Zola (https://www.getzola.org/documentation/content/taxonomies/).
-projects_cell_path = [ "taxonomies", "projects" ]
+# By default stats are listed in front-matter under <extra>. This example shows how
+# to use custom cell path to list them under <extra.stats>.
+stats_cell_path = [ "extra", "stats" ]
=
=# Each project you want to excavate should have it's own section, like this:
=[[projects]]index ab4e743..233c18a 100755
--- a/excavate
+++ b/excavate
@@ -271,11 +271,19 @@ def "format entry" [
=
= let entry = $in
=
- # # TODO: Write this in front-matter (extra.stats.commits or something)
- # let stats = { commits: ($in.items | length) }
+ let stats = {
+ commits: ($entry.items | length)
+ authors: ($entry.items | get author | uniq )
+ }
=
= let frontmatter = { title: $title }
- # TODO: | insert $config.frontmatter.stats_cell_path [ $stats ]
+ | upsert $config.frontmatter.stats_cell_path { |row|
+ $row
+ | get --optional $config.frontmatter.stats_cell_path
+ | default {}
+ | merge $stats
+ }
+
=
= [
= $"---"
@@ -338,7 +346,7 @@ def "format diff" [] {
=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 frontmatter.stats_cell_path { default { [ extra ] } | into cell-path }
= | upsert ignore.paths { default [] | each { path expand } }
= | upsert projects_dir { default "~/Projects" | path expand }
= | upsert projects_stubs_dir { default "./drafts/projects" | path expand }