diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/000ddc5eaf58e781c945d6321970ec40/description b/000ddc5eaf58e781c945d6321970ec40/description new file mode 100644 index 0000000..8e7da78 --- /dev/null +++ b/000ddc5eaf58e781c945d6321970ec40/description @@ -0,0 +1,13 @@ +add an option to `ent list` to show just the ids of filtered issues + +This would be useful in scripts, like: +``` +for ISSUE_ID in $(ent list --ids-only ${MY_FILTER}); do + # do thing with ${ISSUE_ID} +done +``` + +For now you can sort of emulate this with: +``` +ent list state=done done-time=2026-01-01T00:00:00-06:00.. | grep ' ' | cut -f 1 -d ' ' +``` diff --git a/000ddc5eaf58e781c945d6321970ec40/state b/000ddc5eaf58e781c945d6321970ec40/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/000ddc5eaf58e781c945d6321970ec40/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/01b9499f6a927ca6326b91037bfe0821/comments/02ecad639e8781f007f24fec4fb85a84/description b/01b9499f6a927ca6326b91037bfe0821/comments/02ecad639e8781f007f24fec4fb85a84/description new file mode 100644 index 0000000..92b7372 --- /dev/null +++ b/01b9499f6a927ca6326b91037bfe0821/comments/02ecad639e8781f007f24fec4fb85a84/description @@ -0,0 +1 @@ +Allow issues to move within collections. diff --git a/01b9499f6a927ca6326b91037bfe0821/description b/01b9499f6a927ca6326b91037bfe0821/description new file mode 100644 index 0000000..4dba385 --- /dev/null +++ b/01b9499f6a927ca6326b91037bfe0821/description @@ -0,0 +1,11 @@ +collections + +introduce a new data structure - a "collection". + +a collection is a grouping of: +1. ent repositories +2. other collections + +a collection is defined by some user-producible, machine readable file (maybe a TOML or something similar) and points to: +1. locations of other ent repositories (local or remote) +2. locations of other collection files (local or remote) diff --git a/01b9499f6a927ca6326b91037bfe0821/tags b/01b9499f6a927ca6326b91037bfe0821/tags new file mode 100644 index 0000000..a7453f0 --- /dev/null +++ b/01b9499f6a927ca6326b91037bfe0821/tags @@ -0,0 +1 @@ +feature diff --git a/08f0d7ee7842c439382816d21ec1dea2/assignee b/08f0d7ee7842c439382816d21ec1dea2/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/tools/set-done-time b/08f0d7ee7842c439382816d21ec1dea2/comments/0c39fa626ffff7e2f3a9b704e2eb7ec7/description old mode 100755 new mode 100644 similarity index 56% rename from tools/set-done-time rename to 08f0d7ee7842c439382816d21ec1dea2/comments/0c39fa626ffff7e2f3a9b704e2eb7ec7/description index 6a29fd9..b1c4818 --- a/tools/set-done-time +++ b/08f0d7ee7842c439382816d21ec1dea2/comments/0c39fa626ffff7e2f3a9b704e2eb7ec7/description @@ -1,19 +1,14 @@ -#!/bin/bash -# -# This script finds all issues with state=Done which do not have a -# `done_time`. -# -# It sets each issue's `done_time` to the most recent time that the -# `state` was updated from the git log. -# +The done-time thing is implemented and merged, but many issues in our +database were marked Done before this change, so they have no `done_time`. +I fixed this by setting a done-time for each of them using this script: +``` +#!/bin/bash set -e for ISSUE_ID in $(ent list state=done done-time=9999-01-01T00:00:00-06:00.. | grep ' ' | cut -f 1 -d ' '); do - echo ${ISSUE_ID} UTIME=$(PAGER='' git log -n1 --pretty=format:%at%n entomologist-data -- ${ISSUE_ID}/state) - echo ${UTIME} DATETIME=$(date --rfc-3339=seconds --date="@${UTIME}") - echo ${DATETIME} ent done-time ${ISSUE_ID} "${DATETIME}" done +``` diff --git a/08f0d7ee7842c439382816d21ec1dea2/comments/3eb43ce8ab338850fb6e5005ab5fee8d/description b/08f0d7ee7842c439382816d21ec1dea2/comments/3eb43ce8ab338850fb6e5005ab5fee8d/description new file mode 100644 index 0000000..fc4ece5 --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/comments/3eb43ce8ab338850fb6e5005ab5fee8d/description @@ -0,0 +1,26 @@ +maybe there's a delineation that's starting to be drawn here between an "issue" and what the database on the backend uses. + +for the filesystem DB, i think it might make sense to have a hashmap that stores everything as a key-value pair, where each key is a file, and each value is the contents of that file. + +once we go up the stack, i think it makes sense to have things in concrete structs, since that's easier to reason about. + +this also frees up the filesystem DB to get used for other things potentially, not just issues. + +so you'd have a system where + +``` +filesystem +^ +| +v +db layer: key/value pair +^ +| +v +application layer: concrete structs (like `Issue` etc) +^ +| +v +presentation layer: (CLI / TUI / etc.) +``` + diff --git a/08f0d7ee7842c439382816d21ec1dea2/comments/89e04463c36f5cb8d82b66995541c1ce/description b/08f0d7ee7842c439382816d21ec1dea2/comments/89e04463c36f5cb8d82b66995541c1ce/description new file mode 100644 index 0000000..f2c0eb7 --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/comments/89e04463c36f5cb8d82b66995541c1ce/description @@ -0,0 +1,5 @@ +Or maybe things that required special handling in the code should live +directly as fields in the Issue struct? With their own getter/setter +API & CLI? + +Not sure what's cleaner. diff --git a/08f0d7ee7842c439382816d21ec1dea2/comments/b725410a5c0ff4bd65fc242f760cc0d4/description b/08f0d7ee7842c439382816d21ec1dea2/comments/b725410a5c0ff4bd65fc242f760cc0d4/description new file mode 100644 index 0000000..808e094 --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/comments/b725410a5c0ff4bd65fc242f760cc0d4/description @@ -0,0 +1,44 @@ +I like the idea of a layered architecture, with a db layer that manages +filesystem i/o. + + +# API + +* Read a filesystem object into a struct Issue (e.g. `ent show`) + +* Write a whole struct Issue into a filesystem object (e.g. `ent new` + or `ent comment`). + +* Write a single field from a struct Issue into an existing filesystem + object (e.g. `ent state ISSUE Done`). + +On write operations, the git commit message should be meaningful to +the application. Maybe that can be done generically by the db library, +or maybe the application needs to supply the commit message. + + +# Design + +A filesystem stores two kinds of things: directories and files. +A directory contains files, and other directories. + +Git stores two kinds of things: trees and blobs. Trees contain blobs, +and other trees. + +Maybe this DB tracks two kinds of things: databases and key/value objects. +Databases store key/value objects, and other databases. + +Some things we'd want from this DB layer: + +* Filesystem objects correspond to structs, like how we have each struct + Issue in its own issue directory. + +* Structs are nested, like how struct Issue contains struct Comment + +* Some fields are simple types (`author` is String), some are + less simple (`timestamp` is chrono::DateTime), some are custom + (`state` is enum State), and some are complicated (`dependencies` + is Option>, `comments` is Vec) + +* Filesystem objects are optimized for getting tracked by git - minimize + merge conflicts. diff --git a/08f0d7ee7842c439382816d21ec1dea2/comments/bc45a23fdb019a5837711231dbace000/description b/08f0d7ee7842c439382816d21ec1dea2/comments/bc45a23fdb019a5837711231dbace000/description new file mode 100644 index 0000000..c3010a7 --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/comments/bc45a23fdb019a5837711231dbace000/description @@ -0,0 +1,38 @@ +The normal `date` command is helpful here: +``` +$ date --iso-8601=seconds +2025-07-19T12:22:56-06:00 + +$ date --iso-8601=seconds --date='last monday' +2025-07-14T00:00:00-06:00 + +$ date --iso-8601=seconds --date='last monday - 1 week' +2025-07-07T00:00:00-06:00 +``` + + +Given that, here's a "what did we finish last week" command: +``` +$ ent list state=done done-time=$(date --iso-8601=seconds --date='last monday - 1 week')..$(date --iso-8601=seconds --date='last monday') +failed to parse issue 87fa3146b90db61c4ea0de182798a0e5, skipping +ignoring error: StdIoError(Os { code: 21, kind: IsADirectory, message: "Is a directory" }) +Done: +8c73c9fd5bc4f551ee5069035ae6e866 migrate the Todo list into entomologist +75cefad80aacbf23fc7b9c24a75aa236 🗨️ 4 # implement `ent comment ISSUE [DESCRIPTION]` (👉 seb) +7da3bd5b72de0a05936b094db5d24304 🗨️ 1 implement `ent edit ${COMMENT}` (👉 seb) +198a7d56a19f0579fbc04f2ee9cc234f fix ignoring unknown file in issues directory: "README.md" +e089400e8a9e11fe9bf10d50b2f889d7 add `ent sync` to keep local `entomologist-data` branch in sync with remote +a26da230276d317e85f9fcca41c19d2e `ent edit ${ISSUE}` with no change fails (👉 seb) +317ea8ccac1d414cde55771321bdec30 🗨️ 2 allow multiple read-only ent processes simultaneously (👉 seb) +da435e5e298b28dc223f9dcfe62a9140 add user control over state transitions (👉 lex) +fd81241f795333b64e7911cfb1b57c8f commit messages in the `entomologist-data` branch could be better (👉 seb) +093e87e8049b93bfa2d8fcd544cae75f add optional 'assignee' to issue (👉 seb) +793bda8b9726b0336d97e856895907f8 `ent list` should have a consistent sort order (👉 seb) +af53c561b36e9b2709b939f81daee534 use git author info to attribute issues and comments to people (👉 seb) +9e69a30ad6965d7488514584c97ac63c teach `ent list FILTER` to filter by assignee (👉 seb) +a5ac277614ea4d13f78031abb25ea7d6 `ent new` and `ent comment`: detect empty issue descriptions & comments (👉 seb) +7d2d236668872cf11f167ac0462f8751 🗨️ 1 add `ent tag ISSUE [[-]TAG]` (👉 seb) +54f0eb67b05aa10763c86869ce840f33 `ent sync` should report what changes got fetched & what changes will be pushed (👉 seb) +4e314a8590864fa76d22758e1785ae35 don't spawn an editor if stdin & stdout aren't a terminal (👉 seb) +d3a705245bd69aa56524b80b5ae0bc26 🗨️ 1 move IssuesDatabase out of binary and into library (👉 sigil-03) +``` diff --git a/08f0d7ee7842c439382816d21ec1dea2/comments/bd8d26478108fde4e1a9f32507957e5f/description b/08f0d7ee7842c439382816d21ec1dea2/comments/bd8d26478108fde4e1a9f32507957e5f/description new file mode 100644 index 0000000..a1b03ce --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/comments/bd8d26478108fde4e1a9f32507957e5f/description @@ -0,0 +1,17 @@ +I'm liking the idea of a per-issue key/value store. + +I think keys and values are both strings, with possible further +parsing/interpretation for some keys? + +For the application in this issue we'd teach ent to create a "finished" +key, maybe when setting the state to Done, with a value of an +ISO8601-style datetime stamp (e.g. "2025-07-12T15:08:58-06:00"). + +There would be a documented spec of reserved keys and their special +meanings and handling. + +Full user access via CLI, maybe something like `ent get VARIABLE` and +`ent set VARIABLE VALUE`? Or `ent variable [NAME[=VALUE]]`? + +Or maybe merge this with the tags somehow? Would that be simpler, +or more complicated (for the user)? diff --git a/08f0d7ee7842c439382816d21ec1dea2/description b/08f0d7ee7842c439382816d21ec1dea2/description new file mode 100644 index 0000000..c99db9c --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/description @@ -0,0 +1,77 @@ +figure out how to make useful reports + +In task-warrior we had a pair of custom reports, one to list things we +did last week and another to list things we were planning to do this +coming week. + +Docs here: +* custom reports: +* end date (brief): + + +# Done last week + +> report.weekly.prev.description='Tasks Completed Last Week' +> report.weekly.prev.columns=uuid,description.desc,project,assignee,end +> report.weekly.prev.sort=end+ +> report.weekly.prev.filter=+COMPLETED and -hidden and (end.after=sow - 1 week) and (end.by=eow - 1 week) + +The "done last week" report filters on a column called `end`, which is +a datetime made up by tw indicating when the ticket became COMPLETED. + + +# To do this week + +> report.selected.external.description='Tasks Selected for This Week' +> report.selected.external.columns=project,description.desc,assignee,due +> report.selected.external.sort=assignee+/,urgency- +> report.selected.external.filter=-COMPLETED and -internal and -hidden and (due.by=eow or +selected or +interrupt) + +The "to do this week" report looks for the tags "selected" or "interrupt", +or a due date this week. + + +# What is to be done + +## Done last week + +I can imagine finding the issues with state=done, running `git log` on +their state file, finding the datetime of the transition to done, and +selecting the ones where that datetime is in a particular time window. + +This has the drawback that it's hard to lie about... With taskwarrior +I often found myself finishing a task on Week X, but forgetting to mark +it complete. Then the Monday of Week X+1 i would mark it complete. +If i didn't override the completion-date the task would look like it +was completed on Week X+1, not Week X like i wanted. + +In git we have `git commit --date=DATE`, but that's only usable at +commit-time (and awkward to express to ent). We can rewrite history with +`git rebase`, but only until we `ent sync`. + +Maybe the `state` file should have a date in it, in addition to the +state word? Or maybe `completion-date` should be a key in a per-issue +key-value store? Is that kv store related to tags? Idk... + +Not sure how to express to ent what completion-dates i want to see. +Maybe a new filter type? `ent list finished=2025-07-01..now`? + +Maybe we can use git tags in the entomologist-data branch somehow? +`git log` between tags and look for "state: _ -> done". But how to +position the tags on the commits i want? Seems cumbersome. + + +## To do this week + +entomologist doesn't have due dates yet, but we should probably add that. + +entomologist has tags so it's be easy to list "tags=selected,interrupt". +Or maybe we should just use "state=inprogress"? + + +## Misc thoughts + +It's nice that we can add whatever files we want to the +`entomologist-data` branch. We could add a directory of report +definitions, and have them be shared across the project, and not have +to type them out every time. diff --git a/08f0d7ee7842c439382816d21ec1dea2/done_time b/08f0d7ee7842c439382816d21ec1dea2/done_time new file mode 100644 index 0000000..198df67 --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/done_time @@ -0,0 +1 @@ +2025-07-19T20:31:35.010603718-06:00 \ No newline at end of file diff --git a/08f0d7ee7842c439382816d21ec1dea2/state b/08f0d7ee7842c439382816d21ec1dea2/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/08f0d7ee7842c439382816d21ec1dea2/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/093e87e8049b93bfa2d8fcd544cae75f/assignee b/093e87e8049b93bfa2d8fcd544cae75f/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/093e87e8049b93bfa2d8fcd544cae75f/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/093e87e8049b93bfa2d8fcd544cae75f/description b/093e87e8049b93bfa2d8fcd544cae75f/description new file mode 100644 index 0000000..642810e --- /dev/null +++ b/093e87e8049b93bfa2d8fcd544cae75f/description @@ -0,0 +1,7 @@ +add optional 'assignee' to issue + +This is probably just another free-form text string for now, maybe +something fancier in the future. + +NOTE: Teaching `ent list FILTER` to filter by assignee is a separate +issue. diff --git a/093e87e8049b93bfa2d8fcd544cae75f/done_time b/093e87e8049b93bfa2d8fcd544cae75f/done_time new file mode 100644 index 0000000..98c8b92 --- /dev/null +++ b/093e87e8049b93bfa2d8fcd544cae75f/done_time @@ -0,0 +1 @@ +2025-07-08T18:48:13-06:00 \ No newline at end of file diff --git a/093e87e8049b93bfa2d8fcd544cae75f/state b/093e87e8049b93bfa2d8fcd544cae75f/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/093e87e8049b93bfa2d8fcd544cae75f/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/0f8b0c982bcfe7d5406bea58301014bc/description b/0f8b0c982bcfe7d5406bea58301014bc/description new file mode 100644 index 0000000..c3e0ce5 --- /dev/null +++ b/0f8b0c982bcfe7d5406bea58301014bc/description @@ -0,0 +1,17 @@ +we need some way to search the text of issues and comments + +Is this a normal filter? Maybe `ent list text=REGEX`? + +Or maybe we want more detailed output than just a list? Showing the +actual Issue or Comment that matches. Maybe `ent search REGEX` producing +something like: +``` +$ ent search REGEX +issue: issue1 +blah blah + +issue: issue2 +blah + comment 1 + comment 2 +``` diff --git a/0f8b0c982bcfe7d5406bea58301014bc/state b/0f8b0c982bcfe7d5406bea58301014bc/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/0f8b0c982bcfe7d5406bea58301014bc/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/131504dd52f3a9b358bfb4701e656fbe/description b/131504dd52f3a9b358bfb4701e656fbe/description new file mode 100644 index 0000000..3c53fbd --- /dev/null +++ b/131504dd52f3a9b358bfb4701e656fbe/description @@ -0,0 +1,15 @@ +teach `ent sync` to scan `main` branch commits for special commands + +I'm thinking primarily of "Fixes: abc123" but maybe there are others? + +ent could be informed of the main branch name via +entomologist-data:config.toml, and use a special tag pointing at the +most recent commit in that branch that it has processed. When you run +`ent sync`, after it runs `git fetch` it could log the main branch from +its tagged commit to the just-fetched branch head, read all the commit +messages, and find and process all the ent commands. Then finally move +its tag to the new main branch head. + +This way you could put an ent command like "Fixes: abc123" in a bugfix +commit, and after you merge that branch to main, the next ent sync will +mark issue abc123 as done for you. diff --git a/131504dd52f3a9b358bfb4701e656fbe/state b/131504dd52f3a9b358bfb4701e656fbe/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/131504dd52f3a9b358bfb4701e656fbe/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/198a7d56a19f0579fbc04f2ee9cc234f/description b/198a7d56a19f0579fbc04f2ee9cc234f/description new file mode 100644 index 0000000..65775af --- /dev/null +++ b/198a7d56a19f0579fbc04f2ee9cc234f/description @@ -0,0 +1,8 @@ +fix ignoring unknown file in issues directory: "README.md" +ignoring unknown file in issues directory: ".git" +b738f2842db428df1b4aad0192a7f36c write a manpage (New) +75cefad80aacbf23fc7b9c24a75aa236 # implement `ent comment ${ISSUE} [-m ${MESSAGE}]` (New) +7da3bd5b72de0a05936b094db5d24304 implement `ent edit ${COMMENT}` (New) +1f85dfac686d5ea2417b2b07f7e1ff01 # implement `ent attach ${ISSUE} ${FILE}` (New) +da435e5e298b28dc223f9dcfe62a914 add user control over state transitions (New) +8c73c9fd5bc4f551ee5069035ae6e866 migrate the Todo list into entomologist (New) emitting warnings for unknown files \ No newline at end of file diff --git a/198a7d56a19f0579fbc04f2ee9cc234f/done_time b/198a7d56a19f0579fbc04f2ee9cc234f/done_time new file mode 100644 index 0000000..8d26bab --- /dev/null +++ b/198a7d56a19f0579fbc04f2ee9cc234f/done_time @@ -0,0 +1 @@ +2025-07-07T16:49:13-06:00 \ No newline at end of file diff --git a/198a7d56a19f0579fbc04f2ee9cc234f/state b/198a7d56a19f0579fbc04f2ee9cc234f/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/198a7d56a19f0579fbc04f2ee9cc234f/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/1c771196c49c732932caacfa79ad56dc/description b/1c771196c49c732932caacfa79ad56dc/description new file mode 100644 index 0000000..2b392da --- /dev/null +++ b/1c771196c49c732932caacfa79ad56dc/description @@ -0,0 +1 @@ +add get() and get_mut() API to database diff --git a/1c771196c49c732932caacfa79ad56dc/state b/1c771196c49c732932caacfa79ad56dc/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/1c771196c49c732932caacfa79ad56dc/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/1c771196c49c732932caacfa79ad56dc/tags b/1c771196c49c732932caacfa79ad56dc/tags new file mode 100644 index 0000000..65eef93 --- /dev/null +++ b/1c771196c49c732932caacfa79ad56dc/tags @@ -0,0 +1 @@ +db diff --git a/1ebdee0502937bf934bb0d72256dbdd1/assignee b/1ebdee0502937bf934bb0d72256dbdd1/assignee new file mode 100644 index 0000000..680727a --- /dev/null +++ b/1ebdee0502937bf934bb0d72256dbdd1/assignee @@ -0,0 +1 @@ +03 \ No newline at end of file diff --git a/1ebdee0502937bf934bb0d72256dbdd1/description b/1ebdee0502937bf934bb0d72256dbdd1/description new file mode 100644 index 0000000..e4bbac3 --- /dev/null +++ b/1ebdee0502937bf934bb0d72256dbdd1/description @@ -0,0 +1 @@ +add delete subcommand to delete ENTries (heh) \ No newline at end of file diff --git a/1ebdee0502937bf934bb0d72256dbdd1/state b/1ebdee0502937bf934bb0d72256dbdd1/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/1ebdee0502937bf934bb0d72256dbdd1/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/1f85dfac686d5ea2417b2b07f7e1ff01/assignee b/1f85dfac686d5ea2417b2b07f7e1ff01/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/1f85dfac686d5ea2417b2b07f7e1ff01/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/1f85dfac686d5ea2417b2b07f7e1ff01/comments/52c8960ce61a41cd1b6e1a79cc059cfb/description b/1f85dfac686d5ea2417b2b07f7e1ff01/comments/52c8960ce61a41cd1b6e1a79cc059cfb/description new file mode 100644 index 0000000..69154c9 --- /dev/null +++ b/1f85dfac686d5ea2417b2b07f7e1ff01/comments/52c8960ce61a41cd1b6e1a79cc059cfb/description @@ -0,0 +1 @@ +will need an attachments manager, since we probably want to be passing around handles and not actual file data... unless we move the struct remotely? will have to think about it diff --git a/1f85dfac686d5ea2417b2b07f7e1ff01/description b/1f85dfac686d5ea2417b2b07f7e1ff01/description new file mode 100644 index 0000000..8118186 --- /dev/null +++ b/1f85dfac686d5ea2417b2b07f7e1ff01/description @@ -0,0 +1,4 @@ +# implement `ent attach ${ISSUE} ${FILE}` + +- each issue has its own independent namespace for attached files +- issue description & comments can reference attached files via standard md links diff --git a/1f85dfac686d5ea2417b2b07f7e1ff01/state b/1f85dfac686d5ea2417b2b07f7e1ff01/state new file mode 100644 index 0000000..505c028 --- /dev/null +++ b/1f85dfac686d5ea2417b2b07f7e1ff01/state @@ -0,0 +1 @@ +inprogress \ No newline at end of file diff --git a/1f85dfac686d5ea2417b2b07f7e1ff01/tags b/1f85dfac686d5ea2417b2b07f7e1ff01/tags new file mode 100644 index 0000000..a7453f0 --- /dev/null +++ b/1f85dfac686d5ea2417b2b07f7e1ff01/tags @@ -0,0 +1 @@ +feature diff --git a/20649f4f970201e04ea50e2b460cff17/comments/2a9e1d6e52d025992bc7c8c22fd20474/description b/20649f4f970201e04ea50e2b460cff17/comments/2a9e1d6e52d025992bc7c8c22fd20474/description new file mode 100644 index 0000000..1cb68a4 --- /dev/null +++ b/20649f4f970201e04ea50e2b460cff17/comments/2a9e1d6e52d025992bc7c8c22fd20474/description @@ -0,0 +1,8 @@ +Oops, i stupidly ran: ent new "add `ent sync --dry-run`" + +Because i used double-quotes, bash saw the `...` and ran that as a +command, which failed, but then bash ran the result anyway ("ent new +"add "), and here we are. + +This would be a good time for `ent rm` as suggested by issue +1ebdee0502937bf934bb0d72256dbdd1. diff --git a/20649f4f970201e04ea50e2b460cff17/description b/20649f4f970201e04ea50e2b460cff17/description new file mode 100644 index 0000000..282b2d6 --- /dev/null +++ b/20649f4f970201e04ea50e2b460cff17/description @@ -0,0 +1 @@ +add \ No newline at end of file diff --git a/20649f4f970201e04ea50e2b460cff17/state b/20649f4f970201e04ea50e2b460cff17/state new file mode 100644 index 0000000..9e3b09d --- /dev/null +++ b/20649f4f970201e04ea50e2b460cff17/state @@ -0,0 +1 @@ +wontdo \ No newline at end of file diff --git a/31144ca83f6f3de1a9e3db651b70a8b4/assignee b/31144ca83f6f3de1a9e3db651b70a8b4/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/31144ca83f6f3de1a9e3db651b70a8b4/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/31144ca83f6f3de1a9e3db651b70a8b4/description b/31144ca83f6f3de1a9e3db651b70a8b4/description new file mode 100644 index 0000000..d9f805b --- /dev/null +++ b/31144ca83f6f3de1a9e3db651b70a8b4/description @@ -0,0 +1 @@ +`ent assign` and maybe other commands needlessly read the database twice diff --git a/31144ca83f6f3de1a9e3db651b70a8b4/done_time b/31144ca83f6f3de1a9e3db651b70a8b4/done_time new file mode 100644 index 0000000..637f6c2 --- /dev/null +++ b/31144ca83f6f3de1a9e3db651b70a8b4/done_time @@ -0,0 +1 @@ +2025-07-22T20:37:03.829541278-06:00 \ No newline at end of file diff --git a/31144ca83f6f3de1a9e3db651b70a8b4/state b/31144ca83f6f3de1a9e3db651b70a8b4/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/31144ca83f6f3de1a9e3db651b70a8b4/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/31144ca83f6f3de1a9e3db651b70a8b4/tags b/31144ca83f6f3de1a9e3db651b70a8b4/tags new file mode 100644 index 0000000..bd14107 --- /dev/null +++ b/31144ca83f6f3de1a9e3db651b70a8b4/tags @@ -0,0 +1 @@ +perf diff --git a/317ea8ccac1d414cde55771321bdec30/assignee b/317ea8ccac1d414cde55771321bdec30/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/317ea8ccac1d414cde55771321bdec30/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/317ea8ccac1d414cde55771321bdec30/comments/5ef6900a2353a7223a324e3bd2891bdf/description b/317ea8ccac1d414cde55771321bdec30/comments/5ef6900a2353a7223a324e3bd2891bdf/description new file mode 100644 index 0000000..2f3535e --- /dev/null +++ b/317ea8ccac1d414cde55771321bdec30/comments/5ef6900a2353a7223a324e3bd2891bdf/description @@ -0,0 +1,5 @@ +It's easy to make worktrees with detached heads: +``` +$ git worktree add --detach /tmp/ent1 entomologist-data +$ git worktree add --detach /tmp/ent2 entomologist-data +``` diff --git a/317ea8ccac1d414cde55771321bdec30/comments/cb48ee85eb91c4061d60043d53fa68f8/description b/317ea8ccac1d414cde55771321bdec30/comments/cb48ee85eb91c4061d60043d53fa68f8/description new file mode 100644 index 0000000..0f940a7 --- /dev/null +++ b/317ea8ccac1d414cde55771321bdec30/comments/cb48ee85eb91c4061d60043d53fa68f8/description @@ -0,0 +1,4 @@ +Currently the `ent` program makes a worktree before considering what +command it's supposed to run. Which is nice because it simplifies each +command handler, but it's bad because different command handlers will +want different kinds of worktrees to implement this issue. diff --git a/317ea8ccac1d414cde55771321bdec30/description b/317ea8ccac1d414cde55771321bdec30/description new file mode 100644 index 0000000..429e75e --- /dev/null +++ b/317ea8ccac1d414cde55771321bdec30/description @@ -0,0 +1,15 @@ +allow multiple read-only ent processes simultaneously + +Currently every time `ent` makes a worktree to read its issues database +branch, it checks out the local branch `entomologist-data`. This means +only one worktree can exist at a time, because each branch can only be +checked out into at most one worktree. + +Some ent operations are read-only which means we could `git worktree +add --detached` the `entomologist-data` branch instead, which uses a +detached HEAD checkout, which is sufficient for read-only operations like +`ent list` and `ent show`. + +This would be useful if you're sitting in `ent edit ${ISSUE_1}` and want +to look at another issue with `ent show ${ISSUE_2}` or list the issues +with `ent list`. diff --git a/317ea8ccac1d414cde55771321bdec30/done_time b/317ea8ccac1d414cde55771321bdec30/done_time new file mode 100644 index 0000000..be44435 --- /dev/null +++ b/317ea8ccac1d414cde55771321bdec30/done_time @@ -0,0 +1 @@ +2025-07-11T11:54:56-06:00 \ No newline at end of file diff --git a/317ea8ccac1d414cde55771321bdec30/state b/317ea8ccac1d414cde55771321bdec30/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/317ea8ccac1d414cde55771321bdec30/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/3b35e0324d3e5bf863ef93c2f64a6add/assignee b/3b35e0324d3e5bf863ef93c2f64a6add/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/3b35e0324d3e5bf863ef93c2f64a6add/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/3b35e0324d3e5bf863ef93c2f64a6add/description b/3b35e0324d3e5bf863ef93c2f64a6add/description new file mode 100644 index 0000000..28e1169 --- /dev/null +++ b/3b35e0324d3e5bf863ef93c2f64a6add/description @@ -0,0 +1,3 @@ +include issue ID in the issue + +currently the issue ID is not contained by the issue data structure. update the issue data structure so it contains the issue ID. diff --git a/3b35e0324d3e5bf863ef93c2f64a6add/done_time b/3b35e0324d3e5bf863ef93c2f64a6add/done_time new file mode 100644 index 0000000..c710694 --- /dev/null +++ b/3b35e0324d3e5bf863ef93c2f64a6add/done_time @@ -0,0 +1 @@ +2025-07-18T16:26:06.414392436-06:00 \ No newline at end of file diff --git a/3b35e0324d3e5bf863ef93c2f64a6add/state b/3b35e0324d3e5bf863ef93c2f64a6add/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/3b35e0324d3e5bf863ef93c2f64a6add/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/46317396704f388542daa7cb56d50076/description b/46317396704f388542daa7cb56d50076/description new file mode 100644 index 0000000..3b47fb6 --- /dev/null +++ b/46317396704f388542daa7cb56d50076/description @@ -0,0 +1,20 @@ +put ent issue database in a dir in the main branches? + +Currently entomologist can access an issue database in a branch (via +temporary worktrees), or in a directory (via direct access). + +The directory-based issue database mode is kind of broken, because +entomologist blindly runs `git commit` after modifying the database +(and isn't careful about managing the git index when doing so). + +(Tangent: The IssueDatabase and IssueDatabaseSource structs of the `ent` +binary should maybe move into the lib Issues struct, and the code could +make smarter choices about how to access the database.) + +Putting the entomologist issue database in the main branch(es) of the +repo has some appealing qualities. For example, a bug-fix branch could +both fix the bug in the code, and change the bug's issue status from +"InProgress" to "Done", so that when the branch is merged to main, +the bug gets fixed there. + +The branch can be rebased on top of main to pick up changes to the bug. diff --git a/46317396704f388542daa7cb56d50076/state b/46317396704f388542daa7cb56d50076/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/46317396704f388542daa7cb56d50076/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/478ac34c204be06b1da5b4f0b5a2532d/assignee b/478ac34c204be06b1da5b4f0b5a2532d/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/478ac34c204be06b1da5b4f0b5a2532d/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/478ac34c204be06b1da5b4f0b5a2532d/comments/af965d012390c5c18c16a94f0714e720/description b/478ac34c204be06b1da5b4f0b5a2532d/comments/af965d012390c5c18c16a94f0714e720/description new file mode 100644 index 0000000..3b1391b --- /dev/null +++ b/478ac34c204be06b1da5b4f0b5a2532d/comments/af965d012390c5c18c16a94f0714e720/description @@ -0,0 +1,18 @@ +I added a simple performance-measuring tool, `tools/time-ent`. It runs +ent a couple of different ways, against a known entomologist-data branch +(specified by a commit in the regular entomologist-data branch in this +repo). That script gives a rough, over-all measurement of how long +different ent commands take. + +More detailed information, including the call tree and flame graph, +can be gathered like this: + +``` +$ perf record -g -F 999 ent tag 7e2a3a59fb6b77403ff1035255367607 +$ perf script -F +pid > /tmp/test.perf +``` + +Then upload the `test.perf` file to the firefox visualizer at +. + +A local visualizer would be nice, i'm sure there is one somewhere. diff --git a/478ac34c204be06b1da5b4f0b5a2532d/description b/478ac34c204be06b1da5b4f0b5a2532d/description new file mode 100644 index 0000000..1b771b3 --- /dev/null +++ b/478ac34c204be06b1da5b4f0b5a2532d/description @@ -0,0 +1 @@ +profile ent to determine where slowdowns occur diff --git a/478ac34c204be06b1da5b4f0b5a2532d/done_time b/478ac34c204be06b1da5b4f0b5a2532d/done_time new file mode 100644 index 0000000..3c3b9a9 --- /dev/null +++ b/478ac34c204be06b1da5b4f0b5a2532d/done_time @@ -0,0 +1 @@ +2025-07-22T20:47:14.363750382-06:00 \ No newline at end of file diff --git a/478ac34c204be06b1da5b4f0b5a2532d/state b/478ac34c204be06b1da5b4f0b5a2532d/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/478ac34c204be06b1da5b4f0b5a2532d/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/478ac34c204be06b1da5b4f0b5a2532d/tags b/478ac34c204be06b1da5b4f0b5a2532d/tags new file mode 100644 index 0000000..68a51eb --- /dev/null +++ b/478ac34c204be06b1da5b4f0b5a2532d/tags @@ -0,0 +1,2 @@ +debug +perf diff --git a/4a9118e5e06956e0b0766ace15174297/comments/4fae444c41579442b412d8b2de72a8d8/description b/4a9118e5e06956e0b0766ace15174297/comments/4fae444c41579442b412d8b2de72a8d8/description new file mode 100644 index 0000000..c26403d --- /dev/null +++ b/4a9118e5e06956e0b0766ace15174297/comments/4fae444c41579442b412d8b2de72a8d8/description @@ -0,0 +1,10 @@ +Yeah that red blob 🛑 is super heavy, and the octagonal shape is pretty +subtle - it looks more like a stop light than a stop sign. + +I like your thought of somehow showing the state of the issues we +dependend on... How about showing the count of not-Done dependencies? +And once all the dependencies are Done, not showing the ↖ or ⌛ or +whatever at all. + +In the `ent show` output we could list all dependencies with a ↖ or +⌛ for the not-Done ones and a ✅ for the Done ones. diff --git a/4a9118e5e06956e0b0766ace15174297/comments/b132544d85794eaf48e25955a431ab5a/description b/4a9118e5e06956e0b0766ace15174297/comments/b132544d85794eaf48e25955a431ab5a/description new file mode 100644 index 0000000..e823c1a --- /dev/null +++ b/4a9118e5e06956e0b0766ace15174297/comments/b132544d85794eaf48e25955a431ab5a/description @@ -0,0 +1,3 @@ +stop sign is an interesting thought, does definitely represent that the issue is blocked... although it feels like a very visually heavy emoji to use, and if we use a color to represent issue state later on Stop Sign is similar to Red Circle and might get visually confusing. + +and yeah, that's exactly what i'm envisioning - i'd like to be able to see at a glance that an issue has dependencies. it might be nice to see the state of those dependencies? but that starts to feel off in the weeds. for now just seeing the count would be nice i think. diff --git a/4a9118e5e06956e0b0766ace15174297/comments/b65c9fd75be26d9153bbafb07ca1fc61/description b/4a9118e5e06956e0b0766ace15174297/comments/b65c9fd75be26d9153bbafb07ca1fc61/description new file mode 100644 index 0000000..7d11ed2 --- /dev/null +++ b/4a9118e5e06956e0b0766ace15174297/comments/b65c9fd75be26d9153bbafb07ca1fc61/description @@ -0,0 +1 @@ +i think my preference is ↖ since it more concisely represents that this issue is waiting on other issues in the DAG / flow... diff --git a/4a9118e5e06956e0b0766ace15174297/comments/d6a3fb287621b6d08bb9ab8cbad7cf53/description b/4a9118e5e06956e0b0766ace15174297/comments/d6a3fb287621b6d08bb9ab8cbad7cf53/description new file mode 100644 index 0000000..aaf6056 --- /dev/null +++ b/4a9118e5e06956e0b0766ace15174297/comments/d6a3fb287621b6d08bb9ab8cbad7cf53/description @@ -0,0 +1,10 @@ +Of the three options you suggested i like "Hourglass Not Done" (⏳) +best, because it most clearly shows that we're waiting for something. + +Or how about Stop Sign, 🛑? + +Do you want `ent list` to show the number of issues that this issue is +blocked waiting on? Something like: + + 012345 This issues stands alone + abc123 ⏳4 This other issue depends on four things diff --git a/4a9118e5e06956e0b0766ace15174297/description b/4a9118e5e06956e0b0766ace15174297/description new file mode 100644 index 0000000..ffe685f --- /dev/null +++ b/4a9118e5e06956e0b0766ace15174297/description @@ -0,0 +1,8 @@ +add dependency count to `ent list` + + +candidate emojis: +↖ - represents the "dependency tree" DAG edge that points to this issue +⌛- shows that the issue is waiting on something else +🔗- shows that the issue is linked to something else + diff --git a/4a9118e5e06956e0b0766ace15174297/state b/4a9118e5e06956e0b0766ace15174297/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/4a9118e5e06956e0b0766ace15174297/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/4e314a8590864fa76d22758e1785ae35/assignee b/4e314a8590864fa76d22758e1785ae35/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/4e314a8590864fa76d22758e1785ae35/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/4e314a8590864fa76d22758e1785ae35/description b/4e314a8590864fa76d22758e1785ae35/description new file mode 100644 index 0000000..9687fdf --- /dev/null +++ b/4e314a8590864fa76d22758e1785ae35/description @@ -0,0 +1,8 @@ +don't spawn an editor if stdin & stdout aren't a terminal + +I sometimes run `ent show ISSUE | more` because ISSUE doesn't fit on my +screen and we don't have built-in pager support yet. + +Then i see something i want to change, or i want to add a comment, so +i up-arrow in the shell and change `show` to `edit` or `comment`, and +ent opens what is in effect `vi | more` which isn't what i want at all. diff --git a/4e314a8590864fa76d22758e1785ae35/done_time b/4e314a8590864fa76d22758e1785ae35/done_time new file mode 100644 index 0000000..1fd5373 --- /dev/null +++ b/4e314a8590864fa76d22758e1785ae35/done_time @@ -0,0 +1 @@ +2025-07-13T10:39:17-06:00 \ No newline at end of file diff --git a/4e314a8590864fa76d22758e1785ae35/state b/4e314a8590864fa76d22758e1785ae35/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/4e314a8590864fa76d22758e1785ae35/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/50012ba39d8dac21ac122affe92c4160/assignee b/50012ba39d8dac21ac122affe92c4160/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/50012ba39d8dac21ac122affe92c4160/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/50012ba39d8dac21ac122affe92c4160/comments/8e95fd44281f27fff6d9eaed68f4a6d8/description b/50012ba39d8dac21ac122affe92c4160/comments/8e95fd44281f27fff6d9eaed68f4a6d8/description new file mode 100644 index 0000000..52ecfa4 --- /dev/null +++ b/50012ba39d8dac21ac122affe92c4160/comments/8e95fd44281f27fff6d9eaed68f4a6d8/description @@ -0,0 +1,7 @@ +i have some work which might take care of this which is happening here: + +entomologist:cffd805252f9a2b4373abf7852d9e750 + +this creates a new type Entry, which has the basic author / creation-time / id as files, and then stores T in the filesystem however it likes to be stored. + +it's still a ways off, but maybe it's worth waiting for so we don't have duplicate work? and for now we can just live with the git log penalty. diff --git a/50012ba39d8dac21ac122affe92c4160/description b/50012ba39d8dac21ac122affe92c4160/description new file mode 100644 index 0000000..b3817b3 --- /dev/null +++ b/50012ba39d8dac21ac122affe92c4160/description @@ -0,0 +1 @@ +store Author and Creation-time of Issue & Comment as files, don't run `git log` \ No newline at end of file diff --git a/50012ba39d8dac21ac122affe92c4160/state b/50012ba39d8dac21ac122affe92c4160/state new file mode 100644 index 0000000..505c028 --- /dev/null +++ b/50012ba39d8dac21ac122affe92c4160/state @@ -0,0 +1 @@ +inprogress \ No newline at end of file diff --git a/50012ba39d8dac21ac122affe92c4160/tags b/50012ba39d8dac21ac122affe92c4160/tags new file mode 100644 index 0000000..bd14107 --- /dev/null +++ b/50012ba39d8dac21ac122affe92c4160/tags @@ -0,0 +1 @@ +perf diff --git a/54e366c80dfc6fc2dd5d52eb36023386/comments/7ee4a01771a0cfa20d4c536f8f54573b/description b/54e366c80dfc6fc2dd5d52eb36023386/comments/7ee4a01771a0cfa20d4c536f8f54573b/description new file mode 100644 index 0000000..91e4373 --- /dev/null +++ b/54e366c80dfc6fc2dd5d52eb36023386/comments/7ee4a01771a0cfa20d4c536f8f54573b/description @@ -0,0 +1,8 @@ +We can determine something like an "mtime" for an Issue by running a +command like: + +`git log --pretty=format:%ai --max-count 1 entomologist-data -- ${ISSUE_ID}` + +Running it on `${ISSUE_ID}/comments` gives you the most recent time +there was a commit in the comments directory, indicating a new or +edited comment. diff --git a/54e366c80dfc6fc2dd5d52eb36023386/description b/54e366c80dfc6fc2dd5d52eb36023386/description new file mode 100644 index 0000000..9a55450 --- /dev/null +++ b/54e366c80dfc6fc2dd5d52eb36023386/description @@ -0,0 +1 @@ +filter issues with comment activity in a particular timeframe diff --git a/54e366c80dfc6fc2dd5d52eb36023386/state b/54e366c80dfc6fc2dd5d52eb36023386/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/54e366c80dfc6fc2dd5d52eb36023386/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/54f0eb67b05aa10763c86869ce840f33/assignee b/54f0eb67b05aa10763c86869ce840f33/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/54f0eb67b05aa10763c86869ce840f33/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/54f0eb67b05aa10763c86869ce840f33/description b/54f0eb67b05aa10763c86869ce840f33/description new file mode 100644 index 0000000..14218f1 --- /dev/null +++ b/54f0eb67b05aa10763c86869ce840f33/description @@ -0,0 +1,19 @@ +`ent sync` should report what changes got fetched & what changes will be pushed + +Currently `ent sync` does: + +1. `git fetch origin` +2. `git merge origin/entomologist-data` +3. `git push origin entomologist-data` + +I think after (1), before (2), we can do some easy, partially useful things: + +1. Show what we have locally that's not on the remote yet: + `git log -p entomologist-data ^origin/entomologist-data` + +2. Show what the remote has that we don't have locally: + `git log -p origin/entomologist-data ^entomologist-data ` + +Eventually (maybe after issue 5fe71e27727f4243dc997e63b9a02971 and/or +issue fd81241f795333b64e7911cfb1b57c8f) we can display the changes in +a simpler way. diff --git a/54f0eb67b05aa10763c86869ce840f33/done_time b/54f0eb67b05aa10763c86869ce840f33/done_time new file mode 100644 index 0000000..9527745 --- /dev/null +++ b/54f0eb67b05aa10763c86869ce840f33/done_time @@ -0,0 +1 @@ +2025-07-11T20:32:59-06:00 \ No newline at end of file diff --git a/54f0eb67b05aa10763c86869ce840f33/state b/54f0eb67b05aa10763c86869ce840f33/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/54f0eb67b05aa10763c86869ce840f33/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/5e1a860b3ab12ee297492d70d68711d8/assignee b/5e1a860b3ab12ee297492d70d68711d8/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/5e1a860b3ab12ee297492d70d68711d8/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/5e1a860b3ab12ee297492d70d68711d8/comments/4e8ba061e3643ed2be8d1f448badfbc3/description b/5e1a860b3ab12ee297492d70d68711d8/comments/4e8ba061e3643ed2be8d1f448badfbc3/description new file mode 100644 index 0000000..21acc15 --- /dev/null +++ b/5e1a860b3ab12ee297492d70d68711d8/comments/4e8ba061e3643ed2be8d1f448badfbc3/description @@ -0,0 +1,5 @@ +have initial rough copy of the TUI binary. i had initially started developing it as a standalone application but then decided it would be better to include it as part of the entomologist repository. + +currently the TUI can only list issues and show a preview, and scaling is broken. + +i also need to update it so that the widget implementations are done as part of the library instead of using the horrible, cursed wrappers that are currently around the types... (since it was a seperate binary, and the trait was from a different crate and the type was from a different crate, rust won't let you `impl` a trait) diff --git a/5e1a860b3ab12ee297492d70d68711d8/description b/5e1a860b3ab12ee297492d70d68711d8/description new file mode 100644 index 0000000..b0c38ae --- /dev/null +++ b/5e1a860b3ab12ee297492d70d68711d8/description @@ -0,0 +1,14 @@ +TUI v0.1 + +build a TUI binary for ent which mirrors the CLI functionality + +- [ ] issues list (88d5111fd6e59802d0b839ff1fd6bf71) + - [ ] filtering(?) +- [ ] show issue preview (bd00a62f9d7c77fd8dd0da5d20aa803d) +- [ ] show issue detail +- [ ] show issue comments +- [ ] commands + - [ ] new ($EDITOR) + - [ ] edit ($EDITOR) + - [ ] comment ($EDITOR) + - [ ] state diff --git a/5e1a860b3ab12ee297492d70d68711d8/state b/5e1a860b3ab12ee297492d70d68711d8/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/5e1a860b3ab12ee297492d70d68711d8/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/5e1a860b3ab12ee297492d70d68711d8/tags b/5e1a860b3ab12ee297492d70d68711d8/tags new file mode 100644 index 0000000..3813c94 --- /dev/null +++ b/5e1a860b3ab12ee297492d70d68711d8/tags @@ -0,0 +1,2 @@ +tui +tui/v0.1 diff --git a/5f59a02e5320b62a68ad704da17e0ff4/comments/6681d522c81311810f09248751bd47cc/description b/5f59a02e5320b62a68ad704da17e0ff4/comments/6681d522c81311810f09248751bd47cc/description new file mode 100644 index 0000000..24e19d2 --- /dev/null +++ b/5f59a02e5320b62a68ad704da17e0ff4/comments/6681d522c81311810f09248751bd47cc/description @@ -0,0 +1,3 @@ +yeah exactly that. just a convenience thing to be able to (from any directory) run `ent update`. + +i could just make it an alias as well... not sure which is more idiomatic diff --git a/5f59a02e5320b62a68ad704da17e0ff4/comments/ef62c5e84ed7e3571f9d9c8ced81f49d/description b/5f59a02e5320b62a68ad704da17e0ff4/comments/ef62c5e84ed7e3571f9d9c8ced81f49d/description new file mode 100644 index 0000000..42a7bac --- /dev/null +++ b/5f59a02e5320b62a68ad704da17e0ff4/comments/ef62c5e84ed7e3571f9d9c8ced81f49d/description @@ -0,0 +1,3 @@ +What do you mean by ent updating itself? + +Do you mean it git clones itself and cargo builds and installs itself? diff --git a/5f59a02e5320b62a68ad704da17e0ff4/description b/5f59a02e5320b62a68ad704da17e0ff4/description new file mode 100644 index 0000000..3956415 --- /dev/null +++ b/5f59a02e5320b62a68ad704da17e0ff4/description @@ -0,0 +1,3 @@ +add `ent update` + +allow `ent` to update itself when commanded to diff --git a/5fe71e27727f4243dc997e63b9a02971/description b/5fe71e27727f4243dc997e63b9a02971/description new file mode 100644 index 0000000..9f741d2 --- /dev/null +++ b/5fe71e27727f4243dc997e63b9a02971/description @@ -0,0 +1,7 @@ +add a way to see how an issue has changed over time + +Maybe `ent log ISSUE` (and `ent log COMMENT`)? + +This could be a thinly veiled wrapper around: + +`git log entomologist-data -- ISSUE` diff --git a/5fe71e27727f4243dc997e63b9a02971/state b/5fe71e27727f4243dc997e63b9a02971/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/5fe71e27727f4243dc997e63b9a02971/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/63bca383372acb1070f9b2416b2a84f6/description b/63bca383372acb1070f9b2416b2a84f6/description new file mode 100644 index 0000000..bb2561d --- /dev/null +++ b/63bca383372acb1070f9b2416b2a84f6/description @@ -0,0 +1,5 @@ +refactor IssuesDatabaseSource / IssuesDatabase / Issues + +If feels like there's a lot of overlap and intimate sharing of +responsibility among these three, and i'm not sure we have the best +abstraction to describe it. diff --git a/63bca383372acb1070f9b2416b2a84f6/state b/63bca383372acb1070f9b2416b2a84f6/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/63bca383372acb1070f9b2416b2a84f6/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/6418c1d923b740057a6138e16664b3c6/description b/6418c1d923b740057a6138e16664b3c6/description new file mode 100644 index 0000000..a9ddf25 --- /dev/null +++ b/6418c1d923b740057a6138e16664b3c6/description @@ -0,0 +1,7 @@ +add `ent modify ISSUE CHANGE...` + +CHANGE is one or more changes to make to the issue, that looks a bit +like the FILTER of `ent list`. Something like: +``` +$ ent modify 123abc state=inprogress assignee=seb tag=bug,cli +``` diff --git a/6418c1d923b740057a6138e16664b3c6/state b/6418c1d923b740057a6138e16664b3c6/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/6418c1d923b740057a6138e16664b3c6/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/75cefad80aacbf23fc7b9c24a75aa236/assignee b/75cefad80aacbf23fc7b9c24a75aa236/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/75cefad80aacbf23fc7b9c24a75aa236/comments/397a3034f4330129d10d4379e94e7279/description b/75cefad80aacbf23fc7b9c24a75aa236/comments/397a3034f4330129d10d4379e94e7279/description new file mode 100644 index 0000000..ff7c1e9 --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/comments/397a3034f4330129d10d4379e94e7279/description @@ -0,0 +1,5 @@ +Mostly implemented: +- each issue has a `comments/` directory +- each comment is a subdirectory of `comments/` +- each comment-directory is named with a 128-bit nonce +- each comment has only a single "field": `description` diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/3fa5bfd93317ad25772680071d5ac325 b/75cefad80aacbf23fc7b9c24a75aa236/comments/9e81dce91164a3ee26c3214ee2d9d809/description similarity index 100% rename from test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/3fa5bfd93317ad25772680071d5ac325 rename to 75cefad80aacbf23fc7b9c24a75aa236/comments/9e81dce91164a3ee26c3214ee2d9d809/description diff --git a/75cefad80aacbf23fc7b9c24a75aa236/comments/acb06210852087afceaf4d9e015e3c6b/description b/75cefad80aacbf23fc7b9c24a75aa236/comments/acb06210852087afceaf4d9e015e3c6b/description new file mode 100644 index 0000000..8c82c0f --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/comments/acb06210852087afceaf4d9e015e3c6b/description @@ -0,0 +1,5 @@ +> should we also add timestamps to the comments in text? or should we just rely on the filesystem/git time? + +I think the "creation time" of a comment is the commit time of the oldest +commit that touches the comment. We can get this from something like: +$ git log --pretty=format:%at entomologist-data -- ${ISSUE}/comments/${COMMENT} diff --git a/75cefad80aacbf23fc7b9c24a75aa236/comments/ee87637887476815b189a0d19b9d9a4b/description b/75cefad80aacbf23fc7b9c24a75aa236/comments/ee87637887476815b189a0d19b9d9a4b/description new file mode 100644 index 0000000..6229ffd --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/comments/ee87637887476815b189a0d19b9d9a4b/description @@ -0,0 +1 @@ +should we also add timestamps to the comments in text? or should we just rely on the filesystem/git time? \ No newline at end of file diff --git a/75cefad80aacbf23fc7b9c24a75aa236/description b/75cefad80aacbf23fc7b9c24a75aa236/description new file mode 100644 index 0000000..6bb20e9 --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/description @@ -0,0 +1,6 @@ +# implement `ent comment ISSUE [DESCRIPTION]` + +- each issue dir has a `comments` subdir +- each comment is identified by a 128-bit nonce +- each comment is a directory under `${ISSUE}/comments` +- each comment has a single "field": `description` diff --git a/75cefad80aacbf23fc7b9c24a75aa236/done_time b/75cefad80aacbf23fc7b9c24a75aa236/done_time new file mode 100644 index 0000000..63ac4c4 --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/done_time @@ -0,0 +1 @@ +2025-07-09T11:03:45-06:00 \ No newline at end of file diff --git a/75cefad80aacbf23fc7b9c24a75aa236/state b/75cefad80aacbf23fc7b9c24a75aa236/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/75cefad80aacbf23fc7b9c24a75aa236/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/779724be18a248a7f13fb52243b1e779/description b/779724be18a248a7f13fb52243b1e779/description new file mode 100644 index 0000000..6aa6a07 --- /dev/null +++ b/779724be18a248a7f13fb52243b1e779/description @@ -0,0 +1,12 @@ +`git clone; ent list` fails + +There's no local entomologist branch: +``` +$ ent list +stdout: +stderr: fatal: invalid reference: entomologist-data + +Error: Oops, something went wrong +``` + +Running `ent sync` fixes is, but entomologist should be more resilient. diff --git a/781360ade670846ed0ccdbfd19ffa8fd/description b/781360ade670846ed0ccdbfd19ffa8fd/description new file mode 100644 index 0000000..30b9ed7 --- /dev/null +++ b/781360ade670846ed0ccdbfd19ffa8fd/description @@ -0,0 +1,114 @@ +add a database layer to the architecture between the FS and ent? + +This issue is to consider adding a database abstraction between the +entomologist library and the git-backed filesystem. + +Currently the entomologist library code directly reads and writes files +and directories, and calls `git commit` when it wants. The directory +is generally an ephemeral worktree containing a (possibly detached) +checkout of the `entomologist-data` branch. + +We may be able to simplify the ent internals (and the application) +by adding a "database" API between ent and the filesystem. + +(This issue grew out of the discussion in issue +08f0d7ee7842c439382816d21ec1dea2. I've distilled the discussion in that +issue here.) + +for the filesystem DB, i think it might make sense to have a hashmap that stores everything as a key-value pair, where each key is a file, and each value is the contents of that file. + +once we go up the stack, i think it makes sense to have things in concrete structs, since that's easier to reason about. + +this also frees up the filesystem DB to get used for other things potentially, not just issues. + +So we'd have a system like this: +``` +git-backed filesystem +^ +| +v +db layer: key/value pair +^ +| +v +entomologist library layer: concrete structs (like `Issue` etc) +^ +| +v +presentation layer: (CLI / TUI / etc.) +``` + + +# Entomologist library API (presented up to the applicationA) + +Very similar to current entomologist library API. + +* Open the database at this DatabaseSource (filesystem path or git branch, + read-only or read-write, returns an opaque "entdb"(?) handle) + +* List issues in entdb + +* Add/edit issue + +* Get/set issue state/tags/assignee/done-time/etc + +* Add/edit comment on issue + + +# Database API (presented up to entomologist library) + +* Open the database at this DB Source (filesystem path or git branch, + read-only or read-write, returns an opaque "db" handle) + +* Read a db object into a key/value store. + + - Keys are filenames. Values are the file contents of that file, + or a database if the filename refers to a directory. + + - The read is by default *not* recursive for performance reasons; + the application may choose to read a "sub-database" referred to + by a key in the "parent database" if it wants, when it wants. + + - The application receives a k/v store and is responsible for + unpacking/interpreting/parsing that into some app-level struct + that is meaningful to the application. + +* Write a key-value store to a db. + + - Commits by default (the application supplies the commit message), + though maybe we want a way to stage multiple changes and commit + at the end? + + - The application transcodes its internal struct into a generic k/v + store for the db library. + +On write operations, the git commit message should be meaningful to +the application. Maybe that can be done generically by the db library, +or maybe the application needs to supply the commit message. + + +# Design + +A filesystem stores two kinds of things: directories and files. +A directory contains files, and other directories. + +Git stores two kinds of things: trees and blobs. Trees contain blobs, +and other trees. + +This DB tracks two kinds of things: databases and key/value objects. +Databases store key/value objects, and other databases. + +Some things we'd want from this DB layer: + +* Filesystem objects correspond to structs, like how we have each struct + Issue in its own issue directory. + +* Structs are nested, like how struct Issue contains struct Comment + +* Some fields are simple types (`author` is String), some are + less simple (`timestamp` is chrono::DateTime), some are custom + (`state` is enum State), and some are complicated (`dependencies` + is Option>, `comments` is Vec) + +* Filesystem objects are optimized for getting tracked by git - minimize + merge conflicts. diff --git a/781360ade670846ed0ccdbfd19ffa8fd/state b/781360ade670846ed0ccdbfd19ffa8fd/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/781360ade670846ed0ccdbfd19ffa8fd/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/781360ade670846ed0ccdbfd19ffa8fd/tags b/781360ade670846ed0ccdbfd19ffa8fd/tags new file mode 100644 index 0000000..65eef93 --- /dev/null +++ b/781360ade670846ed0ccdbfd19ffa8fd/tags @@ -0,0 +1 @@ +db diff --git a/793bda8b9726b0336d97e856895907f8/assignee b/793bda8b9726b0336d97e856895907f8/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/793bda8b9726b0336d97e856895907f8/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/793bda8b9726b0336d97e856895907f8/description b/793bda8b9726b0336d97e856895907f8/description new file mode 100644 index 0000000..82c1c84 --- /dev/null +++ b/793bda8b9726b0336d97e856895907f8/description @@ -0,0 +1,4 @@ +`ent list` should have a consistent sort order + +Maybe first sort and group by State, and sort chronologically (by Issue +creation time) within each State grouping? diff --git a/793bda8b9726b0336d97e856895907f8/done_time b/793bda8b9726b0336d97e856895907f8/done_time new file mode 100644 index 0000000..2dd5851 --- /dev/null +++ b/793bda8b9726b0336d97e856895907f8/done_time @@ -0,0 +1 @@ +2025-07-08T18:48:23-06:00 \ No newline at end of file diff --git a/793bda8b9726b0336d97e856895907f8/state b/793bda8b9726b0336d97e856895907f8/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/793bda8b9726b0336d97e856895907f8/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/7bf773d64437d6b92b7ffe6932531533/assignee b/7bf773d64437d6b92b7ffe6932531533/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/7bf773d64437d6b92b7ffe6932531533/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/7bf773d64437d6b92b7ffe6932531533/description b/7bf773d64437d6b92b7ffe6932531533/description new file mode 100644 index 0000000..9defc62 --- /dev/null +++ b/7bf773d64437d6b92b7ffe6932531533/description @@ -0,0 +1,9 @@ +fix parse error in `ent done-time` + +Got this error when i typoed the RFC 3339 time (missing `:` in timezone +offset): + $ ent done-time 8c73c9fd5bc4f551ee5069035ae6e866 2025-07-07T22:29:25-0600 + + thread 'main' panicked at src/bin/ent/main.rs:503:26: + called `Result::unwrap()` on an `Err` value: ParseError(Invalid) + note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/7bf773d64437d6b92b7ffe6932531533/done_time b/7bf773d64437d6b92b7ffe6932531533/done_time new file mode 100644 index 0000000..d467d60 --- /dev/null +++ b/7bf773d64437d6b92b7ffe6932531533/done_time @@ -0,0 +1 @@ +2025-07-22T10:03:25.567072982-06:00 \ No newline at end of file diff --git a/7bf773d64437d6b92b7ffe6932531533/state b/7bf773d64437d6b92b7ffe6932531533/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/7bf773d64437d6b92b7ffe6932531533/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/7d2d236668872cf11f167ac0462f8751/assignee b/7d2d236668872cf11f167ac0462f8751/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/7d2d236668872cf11f167ac0462f8751/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/7d2d236668872cf11f167ac0462f8751/comments/9bafbb1050f33fd31c4c112c06eea4d9/description b/7d2d236668872cf11f167ac0462f8751/comments/9bafbb1050f33fd31c4c112c06eea4d9/description new file mode 100644 index 0000000..e224997 --- /dev/null +++ b/7d2d236668872cf11f167ac0462f8751/comments/9bafbb1050f33fd31c4c112c06eea4d9/description @@ -0,0 +1,2 @@ +Tags as described above seem useful, but also key/value pairs seem useful, +and similar. Maybe a tag is a key with a Null value? diff --git a/7d2d236668872cf11f167ac0462f8751/description b/7d2d236668872cf11f167ac0462f8751/description new file mode 100644 index 0000000..cce35df --- /dev/null +++ b/7d2d236668872cf11f167ac0462f8751/description @@ -0,0 +1,25 @@ +add `ent tag ISSUE [[-]TAG]` + +# Usage + +`ent tag ISSUE`: List tags on ISSUE. + +`ent tag ISSUE TAG`: Add TAG to ISSUE. + +`ent tag ISSUE -TAG`: Remove TAG from ISSUE. + + +# Notes + +Tags are short text strings without white space, and not beginning with +"-". + +The Issue directory will gain a file named `tags`, containing the sorted +list of tags. + +The `ent list` output will add a bit to the end of each issue displayed, +something like "[tag1, tag2]", or nothing if the issue has no tags +(like how we don't show assignee if it is None). + +As part of this issue, the `ent list FILTER` will gain a new filter +chunk type like "tags=v2.31,ux-papercut". diff --git a/7d2d236668872cf11f167ac0462f8751/done_time b/7d2d236668872cf11f167ac0462f8751/done_time new file mode 100644 index 0000000..b4df3d5 --- /dev/null +++ b/7d2d236668872cf11f167ac0462f8751/done_time @@ -0,0 +1 @@ +2025-07-12T16:14:46-06:00 \ No newline at end of file diff --git a/7d2d236668872cf11f167ac0462f8751/state b/7d2d236668872cf11f167ac0462f8751/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/7d2d236668872cf11f167ac0462f8751/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/7da3bd5b72de0a05936b094db5d24304/assignee b/7da3bd5b72de0a05936b094db5d24304/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/7da3bd5b72de0a05936b094db5d24304/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/7da3bd5b72de0a05936b094db5d24304/comments/87919ac6e79521732b0ec65fa7f79853/description b/7da3bd5b72de0a05936b094db5d24304/comments/87919ac6e79521732b0ec65fa7f79853/description new file mode 100644 index 0000000..cfe450f --- /dev/null +++ b/7da3bd5b72de0a05936b094db5d24304/comments/87919ac6e79521732b0ec65fa7f79853/description @@ -0,0 +1,4 @@ +I think this will be easy. We'll teach `ent edit ID` to search not only +issue IDs but also comment IDs, then reuse the existing edit code. + +Yup, turned out to be exactly that easy. :-) diff --git a/7da3bd5b72de0a05936b094db5d24304/description b/7da3bd5b72de0a05936b094db5d24304/description new file mode 100644 index 0000000..f23a78e --- /dev/null +++ b/7da3bd5b72de0a05936b094db5d24304/description @@ -0,0 +1 @@ +implement `ent edit ${COMMENT}` \ No newline at end of file diff --git a/7da3bd5b72de0a05936b094db5d24304/done_time b/7da3bd5b72de0a05936b094db5d24304/done_time new file mode 100644 index 0000000..946bd9b --- /dev/null +++ b/7da3bd5b72de0a05936b094db5d24304/done_time @@ -0,0 +1 @@ +2025-07-11T20:32:23-06:00 \ No newline at end of file diff --git a/7da3bd5b72de0a05936b094db5d24304/state b/7da3bd5b72de0a05936b094db5d24304/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/7da3bd5b72de0a05936b094db5d24304/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/7e2a3a59fb6b77403ff1035255367607/assignee b/7e2a3a59fb6b77403ff1035255367607/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/7e2a3a59fb6b77403ff1035255367607/comments/192f34c4f21e4c6badc0e52881c83fa3/description b/7e2a3a59fb6b77403ff1035255367607/comments/192f34c4f21e4c6badc0e52881c83fa3/description new file mode 100644 index 0000000..995e859 --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/comments/192f34c4f21e4c6badc0e52881c83fa3/description @@ -0,0 +1 @@ +had to update the file structure as well, but this is done, and up in !25 diff --git a/7e2a3a59fb6b77403ff1035255367607/comments/4910a6080a1dee6586ad57827f8774fb/description b/7e2a3a59fb6b77403ff1035255367607/comments/4910a6080a1dee6586ad57827f8774fb/description new file mode 100644 index 0000000..4e69377 --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/comments/4910a6080a1dee6586ad57827f8774fb/description @@ -0,0 +1,3 @@ +pushed new commit to the MR to fix the test that broke with the new dependencies layout. + +IRL discussion landed on this being OK because the CLI was never released with the old dependencies layout, so unless someone has manually created a dependencies file, this should not have an impact. diff --git a/7e2a3a59fb6b77403ff1035255367607/comments/76eee0c6f69981b5c698208725bdd689/description b/7e2a3a59fb6b77403ff1035255367607/comments/76eee0c6f69981b5c698208725bdd689/description new file mode 100644 index 0000000..2a2a658 --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/comments/76eee0c6f69981b5c698208725bdd689/description @@ -0,0 +1,12 @@ +We also need to implement dependency handling, not sure how that should work. + +* If we add a dependency to an Issue, to we put the Issue into state + Blocked? + +* If an Issue is marked Done, do we search all the other issues to see + if the Done issue was a dependency, and remove it? + +* What happens when the last dependency of an Issue is removed? + +(Maybe this comment should be a new issue, not a comment on the parent +issue) diff --git a/7e2a3a59fb6b77403ff1035255367607/comments/939decab121b6b4b3095a3b20d5d13f4/description b/7e2a3a59fb6b77403ff1035255367607/comments/939decab121b6b4b3095a3b20d5d13f4/description new file mode 100644 index 0000000..38f265e --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/comments/939decab121b6b4b3095a3b20d5d13f4/description @@ -0,0 +1,3 @@ +for now, let's just add the ability to mark dependencies, and then later we can build functionality on top of it which allows an application to decide what to do based on the state of those dependencies. + +i think regardless we should always keep the dependencies, unless they get removed intentionally, and should instead operate off of the `state` of those dependencies. diff --git a/7e2a3a59fb6b77403ff1035255367607/description b/7e2a3a59fb6b77403ff1035255367607/description new file mode 100644 index 0000000..c778f10 --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/description @@ -0,0 +1,3 @@ +allow adding / removing dependencies from CLI + +allow user to add / remove dependencies from the CLI diff --git a/7e2a3a59fb6b77403ff1035255367607/state b/7e2a3a59fb6b77403ff1035255367607/state new file mode 100644 index 0000000..505c028 --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/state @@ -0,0 +1 @@ +inprogress \ No newline at end of file diff --git a/7e2a3a59fb6b77403ff1035255367607/tags b/7e2a3a59fb6b77403ff1035255367607/tags new file mode 100644 index 0000000..573c0c4 --- /dev/null +++ b/7e2a3a59fb6b77403ff1035255367607/tags @@ -0,0 +1 @@ +cli diff --git a/87fa3146b90db61c4ea0de182798a0e5/assignee b/87fa3146b90db61c4ea0de182798a0e5/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/87fa3146b90db61c4ea0de182798a0e5/comments/6ce4f668904c7016c846bcc9c0429aca/description b/87fa3146b90db61c4ea0de182798a0e5/comments/6ce4f668904c7016c846bcc9c0429aca/description new file mode 100644 index 0000000..f0fdef8 --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/comments/6ce4f668904c7016c846bcc9c0429aca/description @@ -0,0 +1,18 @@ +`perf` shows that the majority of the time it takes to run "ent anything" +is spent reading the issue database, and the majority of the time it +takes to read the database is spend running `git log` to find the author +and ctime of issues and comments. + +I've reducted this a bit (in the `speed-test` git branch) by running +`git log` half as many times, but the shape of the flame graph remains +the same. + +We've discussed lazy-loading things to avoid doing this work when we don't +need to. For example, if we're trying to look up the tags of issue A, +there's no need to ingest all the comments of issue B. + +Another option is to store Author and Creation-time the normal way, as +files in the issue directory. We'd never have to run `git log` at all. +I've verified that this makes ent *very* fast. This is a much easier +change than lazy-loading. I opened issue 50012ba39d8dac21ac122affe92c4160 +to make this switch. diff --git a/87fa3146b90db61c4ea0de182798a0e5/comments/783786c019dc6dff582b173dfd1e1d10/description b/87fa3146b90db61c4ea0de182798a0e5/comments/783786c019dc6dff582b173dfd1e1d10/description new file mode 100644 index 0000000..41a9dca --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/comments/783786c019dc6dff582b173dfd1e1d10/description @@ -0,0 +1 @@ +i like storing the author / etc. as files - this was an interesting thread to read RE performance. :) diff --git a/87fa3146b90db61c4ea0de182798a0e5/comments/8df399ccd57f285a2edb3b058560c1a4/description b/87fa3146b90db61c4ea0de182798a0e5/comments/8df399ccd57f285a2edb3b058560c1a4/description new file mode 100644 index 0000000..edc2d3b --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/comments/8df399ccd57f285a2edb3b058560c1a4/description @@ -0,0 +1,5 @@ +noticing the following two commands are very slow: +* `ent comment` +* `ent tag` + +`ent tag` in particular is _very_ slow diff --git a/87fa3146b90db61c4ea0de182798a0e5/comments/9a99ad629c31320056d85b96113cf5e2/description b/87fa3146b90db61c4ea0de182798a0e5/comments/9a99ad629c31320056d85b96113cf5e2/description new file mode 100644 index 0000000..8c01121 --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/comments/9a99ad629c31320056d85b96113cf5e2/description @@ -0,0 +1,18 @@ +I ran ent under perf with this command: +``` +$ perf record -g -F 999 ent tag 7e2a3a59fb6b77403ff1035255367607 +``` + +This produced `perf.data`, which i converted to the format supported by the firefox profile tool: +``` +$ perf script -F +pid > /tmp/test.perf +``` + +I uploaded this to the firefox visualizer at . + +If i'm interpreting this correctly i think it's showing that we spend +most of our time running `git log` in different ways to determine the +author and creation-time of the issues. + +If so, lazy load (95a8190f4bbdcafbb4c72db81dfc2aa6) should speed up ent +a lot. diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/dd79c8cfb8beeacd0460429944b4ecbe b/87fa3146b90db61c4ea0de182798a0e5/dependencies/478ac34c204be06b1da5b4f0b5a2532d similarity index 100% rename from test/0002/a85f81fc5f14cb5d4851dd445dc9744c/dependencies/dd79c8cfb8beeacd0460429944b4ecbe rename to 87fa3146b90db61c4ea0de182798a0e5/dependencies/478ac34c204be06b1da5b4f0b5a2532d diff --git a/87fa3146b90db61c4ea0de182798a0e5/dependencies/50012ba39d8dac21ac122affe92c4160 b/87fa3146b90db61c4ea0de182798a0e5/dependencies/50012ba39d8dac21ac122affe92c4160 new file mode 100644 index 0000000..e69de29 diff --git a/87fa3146b90db61c4ea0de182798a0e5/description b/87fa3146b90db61c4ea0de182798a0e5/description new file mode 100644 index 0000000..fb01efe --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/description @@ -0,0 +1 @@ +large ent repositories are slow diff --git a/87fa3146b90db61c4ea0de182798a0e5/state b/87fa3146b90db61c4ea0de182798a0e5/state new file mode 100644 index 0000000..505c028 --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/state @@ -0,0 +1 @@ +inprogress \ No newline at end of file diff --git a/87fa3146b90db61c4ea0de182798a0e5/tags b/87fa3146b90db61c4ea0de182798a0e5/tags new file mode 100644 index 0000000..767ce86 --- /dev/null +++ b/87fa3146b90db61c4ea0de182798a0e5/tags @@ -0,0 +1,2 @@ +issue +perf diff --git a/88d5111fd6e59802d0b839ff1fd6bf71/assignee b/88d5111fd6e59802d0b839ff1fd6bf71/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/88d5111fd6e59802d0b839ff1fd6bf71/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/88d5111fd6e59802d0b839ff1fd6bf71/description b/88d5111fd6e59802d0b839ff1fd6bf71/description new file mode 100644 index 0000000..58ef8ac --- /dev/null +++ b/88d5111fd6e59802d0b839ff1fd6bf71/description @@ -0,0 +1,9 @@ +issues list + +add issues list to TUI + +REQ: +- [ ] list issues + +STRETCH: +- [ ] filter issues diff --git a/88d5111fd6e59802d0b839ff1fd6bf71/state b/88d5111fd6e59802d0b839ff1fd6bf71/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/88d5111fd6e59802d0b839ff1fd6bf71/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/88d5111fd6e59802d0b839ff1fd6bf71/tags b/88d5111fd6e59802d0b839ff1fd6bf71/tags new file mode 100644 index 0000000..3813c94 --- /dev/null +++ b/88d5111fd6e59802d0b839ff1fd6bf71/tags @@ -0,0 +1,2 @@ +tui +tui/v0.1 diff --git a/89a4c085d5202f0be7d9c6d263c4c4b8/description b/89a4c085d5202f0be7d9c6d263c4c4b8/description new file mode 100644 index 0000000..8089d24 --- /dev/null +++ b/89a4c085d5202f0be7d9c6d263c4c4b8/description @@ -0,0 +1,3 @@ +manage whitespace in issues and comments + +Strip leading and trailing whitespace when loading and saving descriptions. diff --git a/89a4c085d5202f0be7d9c6d263c4c4b8/state b/89a4c085d5202f0be7d9c6d263c4c4b8/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/89a4c085d5202f0be7d9c6d263c4c4b8/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/8c73c9fd5bc4f551ee5069035ae6e866/description b/8c73c9fd5bc4f551ee5069035ae6e866/description new file mode 100644 index 0000000..9876137 --- /dev/null +++ b/8c73c9fd5bc4f551ee5069035ae6e866/description @@ -0,0 +1 @@ +migrate the Todo list into entomologist \ No newline at end of file diff --git a/8c73c9fd5bc4f551ee5069035ae6e866/done_time b/8c73c9fd5bc4f551ee5069035ae6e866/done_time new file mode 100644 index 0000000..f730d86 --- /dev/null +++ b/8c73c9fd5bc4f551ee5069035ae6e866/done_time @@ -0,0 +1 @@ +2025-07-07T22:29:25-06:00 \ No newline at end of file diff --git a/8c73c9fd5bc4f551ee5069035ae6e866/state b/8c73c9fd5bc4f551ee5069035ae6e866/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/8c73c9fd5bc4f551ee5069035ae6e866/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/8edf884dbde5828a30a4dccad503f28a/comments/7db06053e24489963aec82a4084302de/description b/8edf884dbde5828a30a4dccad503f28a/comments/7db06053e24489963aec82a4084302de/description new file mode 100644 index 0000000..a2e859c --- /dev/null +++ b/8edf884dbde5828a30a4dccad503f28a/comments/7db06053e24489963aec82a4084302de/description @@ -0,0 +1 @@ +This is a duplicate of issue e089400e8a9e11fe9bf10d50b2f889d7 diff --git a/8edf884dbde5828a30a4dccad503f28a/description b/8edf884dbde5828a30a4dccad503f28a/description new file mode 100644 index 0000000..213599b --- /dev/null +++ b/8edf884dbde5828a30a4dccad503f28a/description @@ -0,0 +1 @@ +add sync subcommand to sync entomologist-data branch \ No newline at end of file diff --git a/8edf884dbde5828a30a4dccad503f28a/state b/8edf884dbde5828a30a4dccad503f28a/state new file mode 100644 index 0000000..9e3b09d --- /dev/null +++ b/8edf884dbde5828a30a4dccad503f28a/state @@ -0,0 +1 @@ +wontdo \ No newline at end of file diff --git a/8ef792a2092e5ba84da3cb4efc3c88c6/assignee b/8ef792a2092e5ba84da3cb4efc3c88c6/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/8ef792a2092e5ba84da3cb4efc3c88c6/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/8ef792a2092e5ba84da3cb4efc3c88c6/description b/8ef792a2092e5ba84da3cb4efc3c88c6/description new file mode 100644 index 0000000..b39a250 --- /dev/null +++ b/8ef792a2092e5ba84da3cb4efc3c88c6/description @@ -0,0 +1 @@ +teach `ent show` to include tags \ No newline at end of file diff --git a/8ef792a2092e5ba84da3cb4efc3c88c6/done_time b/8ef792a2092e5ba84da3cb4efc3c88c6/done_time new file mode 100644 index 0000000..52af538 --- /dev/null +++ b/8ef792a2092e5ba84da3cb4efc3c88c6/done_time @@ -0,0 +1 @@ +2025-07-22T13:46:21.421286034-06:00 \ No newline at end of file diff --git a/8ef792a2092e5ba84da3cb4efc3c88c6/state b/8ef792a2092e5ba84da3cb4efc3c88c6/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/8ef792a2092e5ba84da3cb4efc3c88c6/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/9502937b139b797f94eb422651417757/description b/9502937b139b797f94eb422651417757/description new file mode 100644 index 0000000..2452fce --- /dev/null +++ b/9502937b139b797f94eb422651417757/description @@ -0,0 +1,13 @@ +`ent show`: visually distinguish issues and comments from each other + +`git log` does it well: + +* The first line of each commit (that says `commit SHA`) is in a + different color + +* The commit message is indented, making it visually distinctive from + the commit header which is not indented + +* There's a blank line after each commit, before the next one starts + +Something similar in `ent show` would be good. diff --git a/9502937b139b797f94eb422651417757/state b/9502937b139b797f94eb422651417757/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/9502937b139b797f94eb422651417757/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/95a8190f4bbdcafbb4c72db81dfc2aa6/description b/95a8190f4bbdcafbb4c72db81dfc2aa6/description new file mode 100644 index 0000000..758040b --- /dev/null +++ b/95a8190f4bbdcafbb4c72db81dfc2aa6/description @@ -0,0 +1,5 @@ +lazy read of DB + +when running `list` don't load the entire object, instead only load the relevant metadata used to display in the list + +Similarly, when running e.g. `ent show ISSUE_ID`, don't load the other issues. diff --git a/95a8190f4bbdcafbb4c72db81dfc2aa6/state b/95a8190f4bbdcafbb4c72db81dfc2aa6/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/95a8190f4bbdcafbb4c72db81dfc2aa6/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/95a8190f4bbdcafbb4c72db81dfc2aa6/tags b/95a8190f4bbdcafbb4c72db81dfc2aa6/tags new file mode 100644 index 0000000..bd14107 --- /dev/null +++ b/95a8190f4bbdcafbb4c72db81dfc2aa6/tags @@ -0,0 +1 @@ +perf diff --git a/9e69a30ad6965d7488514584c97ac63c/assignee b/9e69a30ad6965d7488514584c97ac63c/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/9e69a30ad6965d7488514584c97ac63c/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/9e69a30ad6965d7488514584c97ac63c/description b/9e69a30ad6965d7488514584c97ac63c/description new file mode 100644 index 0000000..0c241bc --- /dev/null +++ b/9e69a30ad6965d7488514584c97ac63c/description @@ -0,0 +1 @@ +teach `ent list FILTER` to filter by assignee diff --git a/9e69a30ad6965d7488514584c97ac63c/done_time b/9e69a30ad6965d7488514584c97ac63c/done_time new file mode 100644 index 0000000..82b181f --- /dev/null +++ b/9e69a30ad6965d7488514584c97ac63c/done_time @@ -0,0 +1 @@ +2025-07-09T11:03:14-06:00 \ No newline at end of file diff --git a/9e69a30ad6965d7488514584c97ac63c/state b/9e69a30ad6965d7488514584c97ac63c/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/9e69a30ad6965d7488514584c97ac63c/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/9f0d77fa44e265a4e316f94be42554a5/description b/9f0d77fa44e265a4e316f94be42554a5/description new file mode 100644 index 0000000..4e1687d --- /dev/null +++ b/9f0d77fa44e265a4e316f94be42554a5/description @@ -0,0 +1,11 @@ +`ent list` should take a git-like --pretty=format: + +The immediate use is: +* list all issues in state=inprogress, but only tell me the assignees of each + +I currently do this with a slow and awkward shell script: +``` +for ISSUE_ID in $(ent list state=inprogress | grep ' ' | cut -f 1 -d ' '); do + ent show "${ISSUE_ID}" | grep -i assign +done +``` diff --git a/9f0d77fa44e265a4e316f94be42554a5/state b/9f0d77fa44e265a4e316f94be42554a5/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/9f0d77fa44e265a4e316f94be42554a5/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 4d2d2c5..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "entomologist" -version = "0.1.0" -edition = "2024" - -[features] -default = [] -log = ["dep:log", "dep:simple_logger"] - -[dev-dependencies] -pretty_assertions = "1.4.1" - -[dependencies] -anyhow = "1.0.95" -chrono = "0.4.41" -clap = { version = "4.5.26", features = ["derive", "wrap_help"] } -log = { version = "0.4.27", optional = true } -rand = "0.9.1" -serde = { version = "1.0.217", features = ["derive"] } -simple_logger = { version = "5.0.0", optional = true } -tempfile = "3.20.0" -thiserror = "2.0.11" -toml = "0.8.19" diff --git a/README.md b/README.md index 71a2f8e..2bd9d23 100644 --- a/README.md +++ b/README.md @@ -1,81 +1 @@ -Entomologist is a distributed, collaborative, offline-first issue tracker, -backed by git. - - -# Quick start - -Entomologist provides a single executable called `ent` which performs -all interaction with the issues database. `ent --help` provides terse -usage info. - -No initialization is needed, just start using `ent` inside your git repo: - -``` -$ git clone git@server:my-repo.git -$ cd my-repo -$ ent list -# no issues shown, unless my-repo contained some already -``` - -Create an issue: -``` -$ ent new -# Starts your $EDITOR. Type in the issue description, "git-commit -# style" with a title line, optionally followed by an empty line and -# free form text. -``` - -List issues with `ent list`. Optionally takes a filter argument that -controls which issues are shown, see `ent list --help` for details. -For example, to show only new and backlog issues assigned to me or -unassigned, run `ent list state=new,backlog:assignee=$(whoami),`. - -Show all details of an issue with `ent show`. - -Modify the state of an issue using `ent state`. Supported states are New, -Backlog, InProgress, Done, and WontDo. - -Assign an issue to a person using `ent assign`. The person is just -a free-form text field for now. Make it a name, or an email address, -or whatever you want. - -Add a comment on an issue with `ent comment`. - -Edit an issue or a comment with `ent edit`. - -Add or remove tags on an issue using `ent tag`. - - -# Synchronization - -Synchronize your local issue database with the server using `ent sync`. -This will: - -1. Fetch the remote issue database branch into your local repo. - -2. Show the list of local changes not yet on the remote. - -3. Show the list of remote changes not yet incorporated into the local - branch. - -4. Merge the branches. - -5. Push the result back to the remote. - -Step 4 might fail if (for example) both sides edited the same issue in -a way that git can't merge automatically. In this case, check out the -`entomologist-data` branch, merge by hand and resolve the conflicts, -and run `ent sync` again. - - -# Git storage - -Issues are stored in a normal orphan branch in a git repo, next to but -independent of whatever else is stored in the repo. The default branch -name is `entomologist-data`. - -Anyone who has a clone of the repo has the complete issue database. - -Anyone who has write-access to the repo can modify the issue database. -The issue database branch can be modified by pull request, same as any -other branch. +This branch is used by entomologist to track issues. \ No newline at end of file diff --git a/a26da230276d317e85f9fcca41c19d2e/assignee b/a26da230276d317e85f9fcca41c19d2e/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/a26da230276d317e85f9fcca41c19d2e/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/a26da230276d317e85f9fcca41c19d2e/description b/a26da230276d317e85f9fcca41c19d2e/description new file mode 100644 index 0000000..9d2f2b3 --- /dev/null +++ b/a26da230276d317e85f9fcca41c19d2e/description @@ -0,0 +1,17 @@ +`ent edit ${ISSUE}` with no change fails + +After the user closes the editor, ent blindly tries to commit, which +fails like this: +``` +stdout: On branch entomologist-data +nothing to commit, working tree clean + +stderr: +Error: Failed to run git + +Caused by: + Oops, something went wrong +``` + +Probably we should run `git status` after the editor finishes, to see +if there's anything to commit. diff --git a/a26da230276d317e85f9fcca41c19d2e/done_time b/a26da230276d317e85f9fcca41c19d2e/done_time new file mode 100644 index 0000000..9f12039 --- /dev/null +++ b/a26da230276d317e85f9fcca41c19d2e/done_time @@ -0,0 +1 @@ +2025-07-10T09:49:17-06:00 \ No newline at end of file diff --git a/a26da230276d317e85f9fcca41c19d2e/state b/a26da230276d317e85f9fcca41c19d2e/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/a26da230276d317e85f9fcca41c19d2e/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/a5ac277614ea4d13f78031abb25ea7d6/assignee b/a5ac277614ea4d13f78031abb25ea7d6/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/a5ac277614ea4d13f78031abb25ea7d6/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/a5ac277614ea4d13f78031abb25ea7d6/description b/a5ac277614ea4d13f78031abb25ea7d6/description new file mode 100644 index 0000000..9c6996e --- /dev/null +++ b/a5ac277614ea4d13f78031abb25ea7d6/description @@ -0,0 +1,7 @@ +`ent new` and `ent comment`: detect empty issue descriptions & comments + +If the user changes their mind and saves an empty file, don't make a +new empty comment. + +For an embarrassing example, see comment 9e81dce91164a3ee26c3214ee2d9d809 +on issue 75cefad80aacbf23fc7b9c24a75aa236. diff --git a/a5ac277614ea4d13f78031abb25ea7d6/done_time b/a5ac277614ea4d13f78031abb25ea7d6/done_time new file mode 100644 index 0000000..359ef30 --- /dev/null +++ b/a5ac277614ea4d13f78031abb25ea7d6/done_time @@ -0,0 +1 @@ +2025-07-10T09:49:24-06:00 \ No newline at end of file diff --git a/a5ac277614ea4d13f78031abb25ea7d6/state b/a5ac277614ea4d13f78031abb25ea7d6/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/a5ac277614ea4d13f78031abb25ea7d6/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/a97c817024233be0e34536dfb1323070/comments/047869e6714d6d31cd3dd55de47b0871/description b/a97c817024233be0e34536dfb1323070/comments/047869e6714d6d31cd3dd55de47b0871/description new file mode 100644 index 0000000..d4468c9 --- /dev/null +++ b/a97c817024233be0e34536dfb1323070/comments/047869e6714d6d31cd3dd55de47b0871/description @@ -0,0 +1,2 @@ +This will probably require `struct Issue` to incorporate the issue uuid, +like `struct Comment` does. diff --git a/a97c817024233be0e34536dfb1323070/comments/55507ab98176d4d7cf12f0c794246aaf/description b/a97c817024233be0e34536dfb1323070/comments/55507ab98176d4d7cf12f0c794246aaf/description new file mode 100644 index 0000000..f0f9795 --- /dev/null +++ b/a97c817024233be0e34536dfb1323070/comments/55507ab98176d4d7cf12f0c794246aaf/description @@ -0,0 +1,4 @@ +This was fixed in +. + +I will fudge the done-time of this issue to match the merge of that PR. diff --git a/a97c817024233be0e34536dfb1323070/description b/a97c817024233be0e34536dfb1323070/description new file mode 100644 index 0000000..a20505b --- /dev/null +++ b/a97c817024233be0e34536dfb1323070/description @@ -0,0 +1 @@ +update `ent new` to output the issue ID \ No newline at end of file diff --git a/a97c817024233be0e34536dfb1323070/done_time b/a97c817024233be0e34536dfb1323070/done_time new file mode 100644 index 0000000..df0d40a --- /dev/null +++ b/a97c817024233be0e34536dfb1323070/done_time @@ -0,0 +1 @@ +2025-07-17T12:02:42-06:00 \ No newline at end of file diff --git a/a97c817024233be0e34536dfb1323070/state b/a97c817024233be0e34536dfb1323070/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/a97c817024233be0e34536dfb1323070/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/ae56208b6db64ccd9da31ce2bb1aad64/assignee b/ae56208b6db64ccd9da31ce2bb1aad64/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/ae56208b6db64ccd9da31ce2bb1aad64/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/ae56208b6db64ccd9da31ce2bb1aad64/comments/2419a8f74876a6100e0aa9de80c387e6/description b/ae56208b6db64ccd9da31ce2bb1aad64/comments/2419a8f74876a6100e0aa9de80c387e6/description new file mode 100644 index 0000000..3eaaaf3 --- /dev/null +++ b/ae56208b6db64ccd9da31ce2bb1aad64/comments/2419a8f74876a6100e0aa9de80c387e6/description @@ -0,0 +1,6 @@ +While many "stray worktree" bugs got fixed by commit +ca353352f8dced335506ffb4bd839213b64afefb, the case of Control-C still +leaves a stray worktree behind because it doesn't shut down the process +cleanly, so `Worktree::drop()` doesn't get run. + +Not sure what the best way to fix this is. diff --git a/ae56208b6db64ccd9da31ce2bb1aad64/comments/4527a79c3802573d72f513c2f9cc9eda/description b/ae56208b6db64ccd9da31ce2bb1aad64/comments/4527a79c3802573d72f513c2f9cc9eda/description new file mode 100644 index 0000000..e00a7f8 --- /dev/null +++ b/ae56208b6db64ccd9da31ce2bb1aad64/comments/4527a79c3802573d72f513c2f9cc9eda/description @@ -0,0 +1,15 @@ +``` +$ ent sync +Enter passphrase for key '/home/seb/.ssh/id_ed25519': +^C + +$ ent sync +stdout: +stderr: Preparing worktree (checking out 'entomologist-data') +fatal: 'entomologist-data' is already used by worktree at '/tmp/.tmpgKK2gw' + +Error: Oops, something went wrong + +$ rm -rf /tmp/.tmpgKK2gw/ +$ git worktree prune +``` diff --git a/ae56208b6db64ccd9da31ce2bb1aad64/comments/f432474d9d62b6a3f23026b58dbc9045/description b/ae56208b6db64ccd9da31ce2bb1aad64/comments/f432474d9d62b6a3f23026b58dbc9045/description new file mode 100644 index 0000000..eca3473 --- /dev/null +++ b/ae56208b6db64ccd9da31ce2bb1aad64/comments/f432474d9d62b6a3f23026b58dbc9045/description @@ -0,0 +1,4 @@ +There was a bug in entomologist::git::Worktree::drop() which would make +it fail to clean up the worktree if it was dirty. + +This was fixed in commit ca353352f8dced335506ffb4bd839213b64afefb. diff --git a/ae56208b6db64ccd9da31ce2bb1aad64/description b/ae56208b6db64ccd9da31ce2bb1aad64/description new file mode 100644 index 0000000..0187de1 --- /dev/null +++ b/ae56208b6db64ccd9da31ce2bb1aad64/description @@ -0,0 +1,7 @@ +`ent sync`, ^C, now there's a stray worktree + +The worktree has the `entomologist-data` branch checked out in it, +so ent won't make another. + +Probably want a crash handler in `ent` to clean up any worktree we +have open. diff --git a/ae56208b6db64ccd9da31ce2bb1aad64/state b/ae56208b6db64ccd9da31ce2bb1aad64/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/ae56208b6db64ccd9da31ce2bb1aad64/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/af53c561b36e9b2709b939f81daee534/assignee b/af53c561b36e9b2709b939f81daee534/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/af53c561b36e9b2709b939f81daee534/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/af53c561b36e9b2709b939f81daee534/description b/af53c561b36e9b2709b939f81daee534/description new file mode 100644 index 0000000..e7da839 --- /dev/null +++ b/af53c561b36e9b2709b939f81daee534/description @@ -0,0 +1 @@ +use git author info to attribute issues and comments to people \ No newline at end of file diff --git a/af53c561b36e9b2709b939f81daee534/done_time b/af53c561b36e9b2709b939f81daee534/done_time new file mode 100644 index 0000000..df6fb87 --- /dev/null +++ b/af53c561b36e9b2709b939f81daee534/done_time @@ -0,0 +1 @@ +2025-07-08T18:48:37-06:00 \ No newline at end of file diff --git a/af53c561b36e9b2709b939f81daee534/state b/af53c561b36e9b2709b939f81daee534/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/af53c561b36e9b2709b939f81daee534/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/b62c30d419fb7727d2faaee20dfed1be/description b/b62c30d419fb7727d2faaee20dfed1be/description new file mode 100644 index 0000000..19ac498 --- /dev/null +++ b/b62c30d419fb7727d2faaee20dfed1be/description @@ -0,0 +1,5 @@ +decrease write window scope + +decrease the amount of time that the `ent` binary has a mutable reference to the database object by only checking out a worktree on a commit operation. + +alternatively (or maybe in addition to?) possibly use branching to allow for simultaneous edits in a safer way. diff --git a/b62c30d419fb7727d2faaee20dfed1be/state b/b62c30d419fb7727d2faaee20dfed1be/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/b62c30d419fb7727d2faaee20dfed1be/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/b738f2842db428df1b4aad0192a7f36c/comments/3fb80cb5ffed0d536568c8470bbe011a/description b/b738f2842db428df1b4aad0192a7f36c/comments/3fb80cb5ffed0d536568c8470bbe011a/description new file mode 100644 index 0000000..26124b8 --- /dev/null +++ b/b738f2842db428df1b4aad0192a7f36c/comments/3fb80cb5ffed0d536568c8470bbe011a/description @@ -0,0 +1,2 @@ +I've used asciidoc to write docs, it's got a tolerable syntax and +generates usable manpages, html, and pdf. diff --git a/b738f2842db428df1b4aad0192a7f36c/comments/91b801a9b6b77cca66ae5ab0b7d97cca/description b/b738f2842db428df1b4aad0192a7f36c/comments/91b801a9b6b77cca66ae5ab0b7d97cca/description new file mode 100644 index 0000000..b3fd943 --- /dev/null +++ b/b738f2842db428df1b4aad0192a7f36c/comments/91b801a9b6b77cca66ae5ab0b7d97cca/description @@ -0,0 +1,38 @@ +# `ent list FILTER` filter format + +I'm not sure about the filter format, but here are some notes... + +There are (as of df7b5c6aa4a00dfca66dc802f1e813f23f27a5b9) two independent +filters: "state" and "assignee". The "state" filter defaults to +including issues whose state is InProgress, Blocked, Backlog, or New. +The "assignee" filter defaults to including all issues, assigned or not. + +The two filters can be independently overridden by the `ent list +FILTER` command. FILTER is a string containing "chunks" separated by +":", like the PATH environment variable. Each chunk is of the form +"name=value[,value...]". "name" can currently be either "state" or +"assignee", and we can add more in the future (e.g "tag" or "ctime"). + +The "value" arguments to the "state" filter must be one of the valid +states, or it's a parse error. + +The "value" arguments to the "assignee" filter are used to +string-compare against the issues "assignee" field, exact matches are +accepted and everything else is rejected. A special assignee filter of +the empty string matches issues that don't have an assignee. + +Some examples: + +* `ent list` shows issues in the states listed above, and don't filter + based on assignee at all. + +* `ent list assignee=seb` shows issues in the states listed above, + but only if the assignee is "seb". + +* `ent list assignee=seb,` shows issues in the states listed above, + but only if the assignee is "seb" or if there is no assignee. + +* `ent list state=done` shows all issues in the Done state. + +* `ent list state=inprogress:assignee=seb` shows issues in the InProgress + state that are assigned to "seb". diff --git a/b738f2842db428df1b4aad0192a7f36c/description b/b738f2842db428df1b4aad0192a7f36c/description new file mode 100644 index 0000000..097bfa4 --- /dev/null +++ b/b738f2842db428df1b4aad0192a7f36c/description @@ -0,0 +1 @@ +write a manpage \ No newline at end of file diff --git a/b738f2842db428df1b4aad0192a7f36c/state b/b738f2842db428df1b4aad0192a7f36c/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/b738f2842db428df1b4aad0192a7f36c/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/b738f2842db428df1b4aad0192a7f36c/tags b/b738f2842db428df1b4aad0192a7f36c/tags new file mode 100644 index 0000000..d8f8d46 --- /dev/null +++ b/b738f2842db428df1b4aad0192a7f36c/tags @@ -0,0 +1 @@ +docs diff --git a/bd00a62f9d7c77fd8dd0da5d20aa803d/assignee b/bd00a62f9d7c77fd8dd0da5d20aa803d/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/bd00a62f9d7c77fd8dd0da5d20aa803d/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/bd00a62f9d7c77fd8dd0da5d20aa803d/description b/bd00a62f9d7c77fd8dd0da5d20aa803d/description new file mode 100644 index 0000000..509ca4b --- /dev/null +++ b/bd00a62f9d7c77fd8dd0da5d20aa803d/description @@ -0,0 +1,7 @@ +issue preview + +show a preview of the issue: +- [ ] author +- [ ] ID +- [ ] tags +- [ ] description preview diff --git a/bd00a62f9d7c77fd8dd0da5d20aa803d/state b/bd00a62f9d7c77fd8dd0da5d20aa803d/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/bd00a62f9d7c77fd8dd0da5d20aa803d/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/bd00a62f9d7c77fd8dd0da5d20aa803d/tags b/bd00a62f9d7c77fd8dd0da5d20aa803d/tags new file mode 100644 index 0000000..3813c94 --- /dev/null +++ b/bd00a62f9d7c77fd8dd0da5d20aa803d/tags @@ -0,0 +1,2 @@ +tui +tui/v0.1 diff --git a/bd71d3896b8fa3e19cde4b5de9d9ac78/description b/bd71d3896b8fa3e19cde4b5de9d9ac78/description new file mode 100644 index 0000000..aa451d9 --- /dev/null +++ b/bd71d3896b8fa3e19cde4b5de9d9ac78/description @@ -0,0 +1 @@ +figure out how to do tab completion, including for issue ids diff --git a/bd71d3896b8fa3e19cde4b5de9d9ac78/state b/bd71d3896b8fa3e19cde4b5de9d9ac78/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/bd71d3896b8fa3e19cde4b5de9d9ac78/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/bdf44bd55fdecdfc1badcb7123d2a606/assignee b/bdf44bd55fdecdfc1badcb7123d2a606/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/bdf44bd55fdecdfc1badcb7123d2a606/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/bdf44bd55fdecdfc1badcb7123d2a606/description b/bdf44bd55fdecdfc1badcb7123d2a606/description new file mode 100644 index 0000000..33e41d0 --- /dev/null +++ b/bdf44bd55fdecdfc1badcb7123d2a606/description @@ -0,0 +1,6 @@ +issue detail + +show details about the issue +* potentially seperate screen? +* allow scrolling +* show comments diff --git a/bdf44bd55fdecdfc1badcb7123d2a606/state b/bdf44bd55fdecdfc1badcb7123d2a606/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/bdf44bd55fdecdfc1badcb7123d2a606/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/bdf44bd55fdecdfc1badcb7123d2a606/tags b/bdf44bd55fdecdfc1badcb7123d2a606/tags new file mode 100644 index 0000000..3813c94 --- /dev/null +++ b/bdf44bd55fdecdfc1badcb7123d2a606/tags @@ -0,0 +1,2 @@ +tui +tui/v0.1 diff --git a/bed47b2be016cc41eb43ef6d0acf1f8f/comments/d35ca7f72272a79938f4189828c36662/description b/bed47b2be016cc41eb43ef6d0acf1f8f/comments/d35ca7f72272a79938f4189828c36662/description new file mode 100644 index 0000000..d1e0870 --- /dev/null +++ b/bed47b2be016cc41eb43ef6d0acf1f8f/comments/d35ca7f72272a79938f4189828c36662/description @@ -0,0 +1,4 @@ +just came to the repo to make this issue if it didn't already exist, so +1 to this + + +it would be very useful to be able to assign things to multiple people diff --git a/bed47b2be016cc41eb43ef6d0acf1f8f/description b/bed47b2be016cc41eb43ef6d0acf1f8f/description new file mode 100644 index 0000000..18967cd --- /dev/null +++ b/bed47b2be016cc41eb43ef6d0acf1f8f/description @@ -0,0 +1,20 @@ +make issue assignment more flexible + +I think there are two things i want that don't currently work: +* add some way to un-assign a person from an issue +* support multiple people assigned to the same issue + +Maybe the `assignee` field of the Issue struct should be a Vec +instead of String? And the `assignee` file of the issue directory should +be a list, one assignee per line? + +Possible new CLI ui: + +* `ent assign ISSUE PERSON[,PERSON...]` replaces the `assignee` list + with the new user-specified list. +* `ent assign ISSUE +PERSON` adds PERSON to the list +* `ent assign ISSUE -PERSON` removes the PERSON from the list + +Also support removing the `assignee` file from the issue directory if the +list is empty. Or maybe allow the file to exist but with 0 bytes in it? +Probably either one is fine. diff --git a/bed47b2be016cc41eb43ef6d0acf1f8f/state b/bed47b2be016cc41eb43ef6d0acf1f8f/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/bed47b2be016cc41eb43ef6d0acf1f8f/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/c1f6bcd5727bf3e0e5a4f4b79d8242ab/comments/b4d084918bb227469cd80a9a1b190833/description b/c1f6bcd5727bf3e0e5a4f4b79d8242ab/comments/b4d084918bb227469cd80a9a1b190833/description new file mode 100644 index 0000000..cc96c5e --- /dev/null +++ b/c1f6bcd5727bf3e0e5a4f4b79d8242ab/comments/b4d084918bb227469cd80a9a1b190833/description @@ -0,0 +1 @@ +This issue duplicates 0f8b0c982bcfe7d5406bea58301014bc diff --git a/c1f6bcd5727bf3e0e5a4f4b79d8242ab/description b/c1f6bcd5727bf3e0e5a4f4b79d8242ab/description new file mode 100644 index 0000000..c93f050 --- /dev/null +++ b/c1f6bcd5727bf3e0e5a4f4b79d8242ab/description @@ -0,0 +1 @@ +teach `ent list FILTER` a new filter type that searches issue & comment descriptions diff --git a/c1f6bcd5727bf3e0e5a4f4b79d8242ab/state b/c1f6bcd5727bf3e0e5a4f4b79d8242ab/state new file mode 100644 index 0000000..9e3b09d --- /dev/null +++ b/c1f6bcd5727bf3e0e5a4f4b79d8242ab/state @@ -0,0 +1 @@ +wontdo \ No newline at end of file diff --git a/c9b5bd61e7e103c8ab3e7920fcbc67e1/description b/c9b5bd61e7e103c8ab3e7920fcbc67e1/description new file mode 100644 index 0000000..db8679b --- /dev/null +++ b/c9b5bd61e7e103c8ab3e7920fcbc67e1/description @@ -0,0 +1,3 @@ +sometimes the output should go to a pager + +`ent show` definitely should go in a pager. `ent list` maybe not? diff --git a/c9b5bd61e7e103c8ab3e7920fcbc67e1/state b/c9b5bd61e7e103c8ab3e7920fcbc67e1/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/c9b5bd61e7e103c8ab3e7920fcbc67e1/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/ca164f570ea3d62f2ff4a001200e0a00/description b/ca164f570ea3d62f2ff4a001200e0a00/description new file mode 100644 index 0000000..4a0f820 --- /dev/null +++ b/ca164f570ea3d62f2ff4a001200e0a00/description @@ -0,0 +1 @@ +add `ent sync --dry-run` \ No newline at end of file diff --git a/ca164f570ea3d62f2ff4a001200e0a00/state b/ca164f570ea3d62f2ff4a001200e0a00/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/ca164f570ea3d62f2ff4a001200e0a00/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/cb41626330c86b0d67690022266212e4/description b/cb41626330c86b0d67690022266212e4/description new file mode 100644 index 0000000..7f9a503 --- /dev/null +++ b/cb41626330c86b0d67690022266212e4/description @@ -0,0 +1,3 @@ +add updates screen to TUI + +updates pane will allow user to see all updates that occurred on a refresh (with a preview maybe?) and mark them as read diff --git a/cb41626330c86b0d67690022266212e4/tags b/cb41626330c86b0d67690022266212e4/tags new file mode 100644 index 0000000..ba0da07 --- /dev/null +++ b/cb41626330c86b0d67690022266212e4/tags @@ -0,0 +1 @@ +tui diff --git a/cc6607ad7565902b149e9836dd4f029c/assignee b/cc6607ad7565902b149e9836dd4f029c/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/cc6607ad7565902b149e9836dd4f029c/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/cc6607ad7565902b149e9836dd4f029c/description b/cc6607ad7565902b149e9836dd4f029c/description new file mode 100644 index 0000000..b44bbef --- /dev/null +++ b/cc6607ad7565902b149e9836dd4f029c/description @@ -0,0 +1,10 @@ +commands + +allow calling commands from the TUI. commands that open an editor will open the system editor and then return to the TUI when complete. + +- [ ] new +- [ ] edit +- [ ] commant +- [ ] state +- [ ] assign +- [ ] ...? diff --git a/cc6607ad7565902b149e9836dd4f029c/state b/cc6607ad7565902b149e9836dd4f029c/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/cc6607ad7565902b149e9836dd4f029c/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/cc6607ad7565902b149e9836dd4f029c/tags b/cc6607ad7565902b149e9836dd4f029c/tags new file mode 100644 index 0000000..3813c94 --- /dev/null +++ b/cc6607ad7565902b149e9836dd4f029c/tags @@ -0,0 +1,2 @@ +tui +tui/v0.1 diff --git a/cc7c13a6220f963436dcf29274dc45c5/comments/91846caf2947046990227faa1c8cf501/description b/cc7c13a6220f963436dcf29274dc45c5/comments/91846caf2947046990227faa1c8cf501/description new file mode 100644 index 0000000..fba99d7 --- /dev/null +++ b/cc7c13a6220f963436dcf29274dc45c5/comments/91846caf2947046990227faa1c8cf501/description @@ -0,0 +1,2 @@ +This is partially addressed by +. diff --git a/cc7c13a6220f963436dcf29274dc45c5/description b/cc7c13a6220f963436dcf29274dc45c5/description new file mode 100644 index 0000000..316867e --- /dev/null +++ b/cc7c13a6220f963436dcf29274dc45c5/description @@ -0,0 +1,12 @@ +changes to the ent filesystem break older versions of ent + + +recently with the dependency API, i updated the ent DB with an entry which contains a dependency, and has an updated method of storing them (files inside of a directory instead of a file with a list of dependencies) + +this has caused a problem with mainline `ent` showing the following error: + +because mainline `ent` expects there to be a file with a list of dependencies, and when it goes to open the file, it errors, since it's actually a directory with the same name as what it expects the file to be named. + +we should probably make it so if `ent` doesn't understand something, it does not completely exit... at least in some cases? + +or enforce some kind of versioning on the DB or something... not entirely sure. diff --git a/cc7c13a6220f963436dcf29274dc45c5/state b/cc7c13a6220f963436dcf29274dc45c5/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/cc7c13a6220f963436dcf29274dc45c5/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/cffd805252f9a2b4373abf7852d9e750/assignee b/cffd805252f9a2b4373abf7852d9e750/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/cffd805252f9a2b4373abf7852d9e750/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/cffd805252f9a2b4373abf7852d9e750/comments/aaae75bbc6cb3f687a6a7d84bd4567e5/description b/cffd805252f9a2b4373abf7852d9e750/comments/aaae75bbc6cb3f687a6a7d84bd4567e5/description new file mode 100644 index 0000000..17b5695 --- /dev/null +++ b/cffd805252f9a2b4373abf7852d9e750/comments/aaae75bbc6cb3f687a6a7d84bd4567e5/description @@ -0,0 +1,10 @@ +got the initial framework laid out tonight - some of the naming i'm still not happy with, so it'll take some staring at that to figure out what feels right. + +i'm not entirely sure the direction of... type responsibility..? that this is implementing is correct either, but we'll see. + +right now i have a two test cases, one which is passing, and the other which isn't passing yet, because i still have `todo!()`s for the vec implementation (needs to make a dir) + +dir naming is another thing i'm not totally happy about yet as well. + + +but it's a start! diff --git a/cffd805252f9a2b4373abf7852d9e750/description b/cffd805252f9a2b4373abf7852d9e750/description new file mode 100644 index 0000000..f46af6b --- /dev/null +++ b/cffd805252f9a2b4373abf7852d9e750/description @@ -0,0 +1,6 @@ +make generic Entry type which can get put in FS + +make a new type, Entry which allows things to get put into / taken out of the filesystem + + +branch: `03/entries` diff --git a/cffd805252f9a2b4373abf7852d9e750/state b/cffd805252f9a2b4373abf7852d9e750/state new file mode 100644 index 0000000..505c028 --- /dev/null +++ b/cffd805252f9a2b4373abf7852d9e750/state @@ -0,0 +1 @@ +inprogress \ No newline at end of file diff --git a/d3a705245bd69aa56524b80b5ae0bc26/assignee b/d3a705245bd69aa56524b80b5ae0bc26/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/d3a705245bd69aa56524b80b5ae0bc26/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/d3a705245bd69aa56524b80b5ae0bc26/comments/273273aa8822e304c60ddc160b2fae54/description b/d3a705245bd69aa56524b80b5ae0bc26/comments/273273aa8822e304c60ddc160b2fae54/description new file mode 100644 index 0000000..ba5b521 --- /dev/null +++ b/d3a705245bd69aa56524b80b5ae0bc26/comments/273273aa8822e304c60ddc160b2fae54/description @@ -0,0 +1,2 @@ +opened new MR: +https://git.glyphs.tech/taproot-manufacturing/entomologist/compare/main...03/issues-db-into-library diff --git a/d3a705245bd69aa56524b80b5ae0bc26/description b/d3a705245bd69aa56524b80b5ae0bc26/description new file mode 100644 index 0000000..0df3299 --- /dev/null +++ b/d3a705245bd69aa56524b80b5ae0bc26/description @@ -0,0 +1,5 @@ +move IssuesDatabase out of binary and into library + +currently the IssuesDatabase type lives in the binary application, which makes it impossible to use with other applications. + +since the IssuesDatabase type is needed for some of the git management related to the storage of issues, it should exist as part of the library diff --git a/d3a705245bd69aa56524b80b5ae0bc26/done_time b/d3a705245bd69aa56524b80b5ae0bc26/done_time new file mode 100644 index 0000000..de06dc9 --- /dev/null +++ b/d3a705245bd69aa56524b80b5ae0bc26/done_time @@ -0,0 +1 @@ +2025-07-13T21:56:29-06:00 \ No newline at end of file diff --git a/d3a705245bd69aa56524b80b5ae0bc26/state b/d3a705245bd69aa56524b80b5ae0bc26/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/d3a705245bd69aa56524b80b5ae0bc26/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/d9a22cb47b2cf0ef56f32674fa65f737/description b/d9a22cb47b2cf0ef56f32674fa65f737/description new file mode 100644 index 0000000..3a70b6d --- /dev/null +++ b/d9a22cb47b2cf0ef56f32674fa65f737/description @@ -0,0 +1,4 @@ +teach `ent tag ISSUE TAG` to add multiple tags at once + +Probably use a syntax like `ent tag ISSUE TAG[,TAG...]` to match the +filter syntax. diff --git a/d9a22cb47b2cf0ef56f32674fa65f737/state b/d9a22cb47b2cf0ef56f32674fa65f737/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/d9a22cb47b2cf0ef56f32674fa65f737/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/da435e5e298b28dc223f9dcfe62a9140/assignee b/da435e5e298b28dc223f9dcfe62a9140/assignee new file mode 100644 index 0000000..2140339 --- /dev/null +++ b/da435e5e298b28dc223f9dcfe62a9140/assignee @@ -0,0 +1 @@ +lex \ No newline at end of file diff --git a/da435e5e298b28dc223f9dcfe62a9140/description b/da435e5e298b28dc223f9dcfe62a9140/description new file mode 100644 index 0000000..a1ba09a --- /dev/null +++ b/da435e5e298b28dc223f9dcfe62a9140/description @@ -0,0 +1 @@ +add user control over state transitions \ No newline at end of file diff --git a/da435e5e298b28dc223f9dcfe62a9140/done_time b/da435e5e298b28dc223f9dcfe62a9140/done_time new file mode 100644 index 0000000..168647e --- /dev/null +++ b/da435e5e298b28dc223f9dcfe62a9140/done_time @@ -0,0 +1 @@ +2025-07-07T21:24:25-06:00 \ No newline at end of file diff --git a/da435e5e298b28dc223f9dcfe62a9140/state b/da435e5e298b28dc223f9dcfe62a9140/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/da435e5e298b28dc223f9dcfe62a9140/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/assignee b/dd20d3ddc86ee802fe7b15e2c91dc160/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/comments/0efebcbcf60417a54382a408b99dadf5/description b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/0efebcbcf60417a54382a408b99dadf5/description new file mode 100644 index 0000000..74afbd6 --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/0efebcbcf60417a54382a408b99dadf5/description @@ -0,0 +1,21 @@ +There's a fly in the ointment: we want to store tags as files in a +directory, but file names are not allowed to contain the character `/`, +and some of our current tags use `/` in the tag name. + +We could allow subdirectories of the `tags` directory, but it feels +like that breaks the budding database abstraction of "a directory is a +key/value store, filenames are the keys, file contents are the values". + +We could forbid `/` in tags and replace them with `.` or `_` or something, +but that's a bit sad because i like `/` as the "hierarchy separator". + +Wikipedia claims, and experiment confirms: +> The big solidus ⧸ (Unicode code point U+29F8) is permitted in Unix +> and Windows filenames. + + + +But that feels like madness. + +I think the least worst option is to replace `/` with `.` in tags. +Opinions? diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/comments/7fc0142ef391d3824de29d23ae807f2b/description b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/7fc0142ef391d3824de29d23ae807f2b/description new file mode 100644 index 0000000..dff86e5 --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/7fc0142ef391d3824de29d23ae807f2b/description @@ -0,0 +1,49 @@ +We've discussed three options for how to store tags: + +1. Each tag is a line in the file "${ISSUE}/tags". This is what we + use now. + + This one is bad because git conflicts are inevitable when people + add/remove tags near each other. This problem is what's prompting + this whole issue. + +2. Each tag is a file in the directory "${ISSUE}/tags/", with characters + that are illegal in filenames (notably '/') escaped. For example, + the tag "tui/v0.1" would live in "${ISSUE}/tags/tui,1v0.1". + + This one looks a bit weird. + +3. Each tag is a file somewhere under the directory "${ISSUE}/tags/", + and tags that include the character '/' live in subdirectories + matching the tag name. For example, the tag "tui/v0.1" would live in + "${ISSUE}/tags/tui/v0.1". + + I like this option but i'm a bit unsure of how some aspects would + work... + +How should entomologist handle "${ISSUE}/tags/tui/v0.1"? Does this +issue have one tag ("tui/v0.1") or two tags ("tui" and "tui/v0.1")? + +If a user runs `ent tag ${ISSUE} tui/v0.1`, i think it would be a bit +surprising if the issue now has both "tui/v0.1" like they asked for, +and also "tui" which they didn't ask for. But maybe that's just how +hierarchical tags work? + +What happens if the user runs "ent tag ${ISSUE} tui/v0.1", then "ent +tag ${ISSUE} -tui" to remove "tui"? Does entomologist refuse, or does +it also remove "tui/v0.1"? Probably it refuses, again because that's +how hierarchical tags work - you (obviously?) can't have "tui/v0.1" +without also having "tui". + +What happens if the user runs "ent tag ${ISSUE} tui/v0.1", then runs +"ent tag ${ISSUE} -tui/v0.1"? Let's assume "v0.1" is the only file in +the "tags/tui/" directory. Probably we remove the "tags/tui" directory, +and now the issue has no tags? Again a bit surprising imo. + +If a user adds a tag "tui", it creates the file "${ISSUE}/tags/tui". +If the user then adds "tui/v0.1" entomologist will have to delete the +"tui" file and replace it with a "tui/" directory, then add a "v0.1" +file to that directory, but that's no big deal. + +If one user adds the tag "tui", and another user adds the tag(s) +"tui/v0.1", there will be a git conflict on sync. diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/comments/9c4dc8aa38ba0a9a6f3fbc014b05f7ba/description b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/9c4dc8aa38ba0a9a6f3fbc014b05f7ba/description new file mode 100644 index 0000000..9918ec7 --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/9c4dc8aa38ba0a9a6f3fbc014b05f7ba/description @@ -0,0 +1,33 @@ +There are `entomologist-data` branches "in the wild" that have issues +with tags files, so we need to migrate those somehow. + +We discussed a couple of ways to do this: + +1. Have the ent library do it automatically and transparently when it + comes across an old-style tags file. In `Issue::new_from_dir()` we'd + detect the old format, parse it, then immediately delete the file, + write the new format directory, and commit the result. + + This is good: + - It's easy for the human + + This is bad: + - entomologist will need to carry along this crufty code until we + think there are no more old-style repos in existence + + +2. Make a stand-alone tool (like the `set-done-time` tool in + . + The ent binary does *not* know how to read the old tags file, and will + error out if it comes across an issue with such a file. The human + would then manually run the tags-representation updater tool. + This tool would make the git worktree, walk the issues, find the + old-style tags, rewrite them in the new format, and commit the result. + + This is good: + - Keeps the core entomologist code clean and "modern". + + This is bad: + - This tool does manual/direct manipulation of the on-disk format, + which means it duplicates a lot of what the entomologist library + does. diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/comments/d05f01c7b06ef3354ee22dc614560fdd/description b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/d05f01c7b06ef3354ee22dc614560fdd/description new file mode 100644 index 0000000..1fd3171 --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/comments/d05f01c7b06ef3354ee22dc614560fdd/description @@ -0,0 +1,16 @@ +Another option is to escape '/' in tags when turning them into filenames. + +Let's somewhat arbitrarily say we use ',' as the escape character. + +',' in a tag would be replaced with the two-character sequence ',0' +in the filename. + +'/' in a tag would be replaced with ',1' in the filename. + +(And so on for any other characters we need to escape.) + +This would give the user the behavior we want, which is to be free to use +'/' in tags. + +The implementation detail of escaped tags would be hidden from the user +unless they dug into the on-disk format by hand. diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/description b/dd20d3ddc86ee802fe7b15e2c91dc160/description new file mode 100644 index 0000000..0e51f85 --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/description @@ -0,0 +1,10 @@ +store tags as a dir of files, not a file of lines + +Currently we store all tags in a file (named `tags`) in the Issue dir. +The `tags` file has a line per tag. + +This is bad because `ent sync` will get a git merge conflict if we add +or remove a tag near where someone else added or removed a tag. + +Better to have a `tags` a directory, with an empty file per tag. +The filename is the tag. This use is conflict free in git. diff --git a/dd20d3ddc86ee802fe7b15e2c91dc160/state b/dd20d3ddc86ee802fe7b15e2c91dc160/state new file mode 100644 index 0000000..505c028 --- /dev/null +++ b/dd20d3ddc86ee802fe7b15e2c91dc160/state @@ -0,0 +1 @@ +inprogress \ No newline at end of file diff --git a/e089400e8a9e11fe9bf10d50b2f889d7/description b/e089400e8a9e11fe9bf10d50b2f889d7/description new file mode 100644 index 0000000..7b853a3 --- /dev/null +++ b/e089400e8a9e11fe9bf10d50b2f889d7/description @@ -0,0 +1,16 @@ +add `ent sync` to keep local `entomologist-data` branch in sync with remote + +My git workflow does not use `git pull`, instead i: +1. `git fetch` +2. `git merge` or `git rebase` + +When i run `git fetch` to fetch new issues, my local +`entomologist-data` branch does not get automatically synchronized with +`origin/entomologist-data`. + +My current clumsy manual workaround is: +1. `git fetch` +2. `git checkout entomologist-data` +3. `git merge origin/entomologist-data` +4. `git checkout ${BRANCH}` +5. `git push origin entomologist-data` diff --git a/e089400e8a9e11fe9bf10d50b2f889d7/done_time b/e089400e8a9e11fe9bf10d50b2f889d7/done_time new file mode 100644 index 0000000..1ca8397 --- /dev/null +++ b/e089400e8a9e11fe9bf10d50b2f889d7/done_time @@ -0,0 +1 @@ +2025-07-08T16:16:09-06:00 \ No newline at end of file diff --git a/e089400e8a9e11fe9bf10d50b2f889d7/state b/e089400e8a9e11fe9bf10d50b2f889d7/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/e089400e8a9e11fe9bf10d50b2f889d7/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/e43d29638fa3836087794c669cde18a9/description b/e43d29638fa3836087794c669cde18a9/description new file mode 100644 index 0000000..f32831d --- /dev/null +++ b/e43d29638fa3836087794c669cde18a9/description @@ -0,0 +1,7 @@ +add a Workflow section to the README + +This section will describe the typical workflows that ent does well. + +This was one thing i found lacking when investigating alternative issue +tracking systems before starting ent, and i think doing this well could +help people start using ent. diff --git a/e43d29638fa3836087794c669cde18a9/state b/e43d29638fa3836087794c669cde18a9/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/e43d29638fa3836087794c669cde18a9/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/eee4a129dacac9ddff2e50580e822cbf/comments/02ad89e0f5411d684136af9b98196b87/description b/eee4a129dacac9ddff2e50580e822cbf/comments/02ad89e0f5411d684136af9b98196b87/description new file mode 100644 index 0000000..053959e --- /dev/null +++ b/eee4a129dacac9ddff2e50580e822cbf/comments/02ad89e0f5411d684136af9b98196b87/description @@ -0,0 +1,3 @@ +I'm unable to reproduce this. Maybe this was a bug earlier but fixed now? + +If so let's move it to Done or WontDo. diff --git a/eee4a129dacac9ddff2e50580e822cbf/description b/eee4a129dacac9ddff2e50580e822cbf/description new file mode 100644 index 0000000..b2078c9 --- /dev/null +++ b/eee4a129dacac9ddff2e50580e822cbf/description @@ -0,0 +1 @@ +ent edit fails to find file/directory \ No newline at end of file diff --git a/eee4a129dacac9ddff2e50580e822cbf/state b/eee4a129dacac9ddff2e50580e822cbf/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/eee4a129dacac9ddff2e50580e822cbf/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/efc67db278f79a54a608723f4ce538b6/description b/efc67db278f79a54a608723f4ce538b6/description new file mode 100644 index 0000000..29cb508 --- /dev/null +++ b/efc67db278f79a54a608723f4ce538b6/description @@ -0,0 +1,4 @@ +smart ui scaling + +some thoughts: +make the preview pane hide under issues pane until task is selected when terminal size is too small to display both diff --git a/efc67db278f79a54a608723f4ce538b6/tags b/efc67db278f79a54a608723f4ce538b6/tags new file mode 100644 index 0000000..ba0da07 --- /dev/null +++ b/efc67db278f79a54a608723f4ce538b6/tags @@ -0,0 +1 @@ +tui diff --git a/f14bd410300d6d8802d873c6b584c4aa/assignee b/f14bd410300d6d8802d873c6b584c4aa/assignee new file mode 100644 index 0000000..284bb3b --- /dev/null +++ b/f14bd410300d6d8802d873c6b584c4aa/assignee @@ -0,0 +1 @@ +sigil-03 \ No newline at end of file diff --git a/f14bd410300d6d8802d873c6b584c4aa/description b/f14bd410300d6d8802d873c6b584c4aa/description new file mode 100644 index 0000000..b35ecdd --- /dev/null +++ b/f14bd410300d6d8802d873c6b584c4aa/description @@ -0,0 +1,4 @@ +make `ent new` output issue ID + + +when the `ent new` command is invoked, it should also return the issue ID to STDOUT diff --git a/f14bd410300d6d8802d873c6b584c4aa/done_time b/f14bd410300d6d8802d873c6b584c4aa/done_time new file mode 100644 index 0000000..8397ea2 --- /dev/null +++ b/f14bd410300d6d8802d873c6b584c4aa/done_time @@ -0,0 +1 @@ +2025-07-18T16:22:31.222641244-06:00 \ No newline at end of file diff --git a/f14bd410300d6d8802d873c6b584c4aa/state b/f14bd410300d6d8802d873c6b584c4aa/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/f14bd410300d6d8802d873c6b584c4aa/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/f14bd410300d6d8802d873c6b584c4aa/tags b/f14bd410300d6d8802d873c6b584c4aa/tags new file mode 100644 index 0000000..573c0c4 --- /dev/null +++ b/f14bd410300d6d8802d873c6b584c4aa/tags @@ -0,0 +1 @@ +cli diff --git a/f3b19da37751e1a77ec5d9580effc713/description b/f3b19da37751e1a77ec5d9580effc713/description new file mode 100644 index 0000000..2740204 --- /dev/null +++ b/f3b19da37751e1a77ec5d9580effc713/description @@ -0,0 +1,7 @@ +linking + +allows users to follow embedded links to other `ent` content, etc. directly from the TUI + +- allow users to follow comment / issue / etc. +- allow users to follow links +- allow users to open files(?) diff --git a/f3b19da37751e1a77ec5d9580effc713/tags b/f3b19da37751e1a77ec5d9580effc713/tags new file mode 100644 index 0000000..ba0da07 --- /dev/null +++ b/f3b19da37751e1a77ec5d9580effc713/tags @@ -0,0 +1 @@ +tui diff --git a/f57708cdac0607194ba61824e857b37c/comments/db3eca1d2312bae8e62d2a237a184aa9/description b/f57708cdac0607194ba61824e857b37c/comments/db3eca1d2312bae8e62d2a237a184aa9/description new file mode 100644 index 0000000..55f1a94 --- /dev/null +++ b/f57708cdac0607194ba61824e857b37c/comments/db3eca1d2312bae8e62d2a237a184aa9/description @@ -0,0 +1,36 @@ +One possible way to determine how much of the UUID we need to keep +for uniqueness: + +* Read the issue database into memory + +* Build a tree of all the issue ids: + + * Define a Node type for a tree datastructure as: + enum Node { + Empty, + IssueId(String), + SubNodes([&Node; 256]), + } + + * Create an empty root node = Node::Empty + + * For each issue: + issue_id_bytes = issue_id.iter() + + issue_id_byte = issue_id_bytes.next() + current_node = root + + while current_node == SubNodes: + current_node = current_node[issue_id_byte] + issue_id_byte = issue_id_bytes.next() + + if current_node == Empty: + current_node = Node::IssueId(issue_id) + else: # we know current_node == IssueId(original_issue_id) + replace current_node with a SubNodes initialized to [Node::Empty; 256] + current_node[original_issue_id[depth]] = Node::IssueId(original_issue_id) + recurse trying again to insert issue_id + +* Walk the entire tree, keeping track of the depth of each IssueId node (ie the number of SubNodes nodes above it) + +* The largest depth is the number of bytes of the issue ids neede to ensure uniqueness diff --git a/f57708cdac0607194ba61824e857b37c/description b/f57708cdac0607194ba61824e857b37c/description new file mode 100644 index 0000000..71cba23 --- /dev/null +++ b/f57708cdac0607194ba61824e857b37c/description @@ -0,0 +1,59 @@ +shorten uuids to uniqueness + +We currently use 128-bit uuids for all objects (Issue and Comment), +which is a bit cumbersome to work with on the command line. + +A typical issue list might look like this: +``` +$ ent list +093e87e8049b93bfa2d8fcd544cae75f add optional 'assignee' to issue (New) +198a7d56a19f0579fbc04f2ee9cc234f fix ignoring unknown file in issues directory: "README.md" (Done) +1ebdee0502937bf934bb0d72256dbdd1 add delete subcommand to delete ENTries (heh) (New) +1f85dfac686d5ea2417b2b07f7e1ff01 # implement `ent attach ${ISSUE} ${FILE}` (New) +317ea8ccac1d414cde55771321bdec30 allow multiple read-only ent processes simultaneously (New) +75cefad80aacbf23fc7b9c24a75aa236 # implement `ent comment ISSUE [DESCRIPTION]` (InProgress) +7da3bd5b72de0a05936b094db5d24304 implement `ent edit ${COMMENT}` (New) +8c73c9fd5bc4f551ee5069035ae6e866 migrate the Todo list into entomologist (Done) +8edf884dbde5828a30a4dccad503f28a add sync subcommand to sync entomologist-data branch (WontDo) +a26da230276d317e85f9fcca41c19d2e `ent edit ${ISSUE}` with no change fails (New) +a97c817024233be0e34536dfb1323070 update `ent new` to output the issue ID (New) +b738f2842db428df1b4aad0192a7f36c write a manpage (New) +da435e5e298b28dc223f9dcfe62a9140 add user control over state transitions (Done) +e089400e8a9e11fe9bf10d50b2f889d7 add `ent sync` to keep local `entomologist-data` branch in sync with remote (InProgress) +eee4a129dacac9ddff2e50580e822cbf ent edit fails to find file/directory (New) +f57708cdac0607194ba61824e857b37c shorten uuids to uniqueness (New) +fd81241f795333b64e7911cfb1b57c8f commit messages in the `entomologist-data` branch could be better (New) +``` + +After deserializing the `entomologist-data` branch into memory, examine +all uuids and determine how many nibbles we actually need to use to +ensure uniqueness. + +Per git convention, we'll use/keep the left-most, aka most significant +part of the uuid and ignore/discard the right-most/least-significant +part of the uuids. + +The issue list above would look like this: +``` +$ ent list +09 add optional 'assignee' to issue (New) +19 fix ignoring unknown file in issues directory: "README.md" (Done) +1e add delete subcommand to delete ENTries (heh) (New) +1f # implement `ent attach ${ISSUE} ${FILE}` (New) +31 allow multiple read-only ent processes simultaneously (New) +75 # implement `ent comment ISSUE [DESCRIPTION]` (InProgress) +7d implement `ent edit ${COMMENT}` (New) +8c migrate the Todo list into entomologist (Done) +8e add sync subcommand to sync entomologist-data branch (WontDo) +a2 `ent edit ${ISSUE}` with no change fails (New) +a9 update `ent new` to output the issue ID (New) +b7 write a manpage (New) +da add user control over state transitions (Done) +e0 add `ent sync` to keep local `entomologist-data` branch in sync with remote (InProgress) +ee ent edit fails to find file/directory (New) +f5 shorten uuids to uniqueness (New) +fd commit messages in the `entomologist-data` branch could be better (New) +``` + +And we could address issues by these shorter ids: +`$ ent edit f5` diff --git a/f57708cdac0607194ba61824e857b37c/state b/f57708cdac0607194ba61824e857b37c/state new file mode 100644 index 0000000..b6fe829 --- /dev/null +++ b/f57708cdac0607194ba61824e857b37c/state @@ -0,0 +1 @@ +backlog \ No newline at end of file diff --git a/fd81241f795333b64e7911cfb1b57c8f/assignee b/fd81241f795333b64e7911cfb1b57c8f/assignee new file mode 100644 index 0000000..d4596cc --- /dev/null +++ b/fd81241f795333b64e7911cfb1b57c8f/assignee @@ -0,0 +1 @@ +seb \ No newline at end of file diff --git a/fd81241f795333b64e7911cfb1b57c8f/description b/fd81241f795333b64e7911cfb1b57c8f/description new file mode 100644 index 0000000..32c6b39 --- /dev/null +++ b/fd81241f795333b64e7911cfb1b57c8f/description @@ -0,0 +1,11 @@ +commit messages in the `entomologist-data` branch could be better + +It'd be nice to be able to look at the `git log --oneline` of an issue dir and see: +* creating the issue +* editing the issue +* changing state or assignee +* adding a comment +* editing a comment + +I think each kind of log message we want will correspond to a matching +API function in the entomologist crate. diff --git a/fd81241f795333b64e7911cfb1b57c8f/done_time b/fd81241f795333b64e7911cfb1b57c8f/done_time new file mode 100644 index 0000000..e61c147 --- /dev/null +++ b/fd81241f795333b64e7911cfb1b57c8f/done_time @@ -0,0 +1 @@ +2025-07-11T20:32:31-06:00 \ No newline at end of file diff --git a/fd81241f795333b64e7911cfb1b57c8f/state b/fd81241f795333b64e7911cfb1b57c8f/state new file mode 100644 index 0000000..348ebd9 --- /dev/null +++ b/fd81241f795333b64e7911cfb1b57c8f/state @@ -0,0 +1 @@ +done \ No newline at end of file diff --git a/install.sh b/install.sh deleted file mode 100755 index ba8faf0..0000000 --- a/install.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -BINFILE="${SCRIPT_DIR}/target/release/ent" -INSTALL_DIR="/usr/local/bin" - -cargo build --release -echo "copying ent to ${INSTALL_DIR}" -sudo cp $BINFILE $INSTALL_DIR -echo "ent installed to ${INSTALL_DIR}" diff --git a/src/bin/ent/main.rs b/src/bin/ent/main.rs deleted file mode 100644 index e071233..0000000 --- a/src/bin/ent/main.rs +++ /dev/null @@ -1,597 +0,0 @@ -use clap::Parser; - -use entomologist::issue::State; -#[cfg(feature = "log")] -use simple_logger; - -#[derive(Debug, clap::Parser)] -#[command(version, about, long_about = None)] -struct Args { - /// Directory containing issues. - #[arg(short = 'd', long)] - issues_dir: Option, - - /// Branch containing issues. - #[arg(short = 'b', long)] - issues_branch: Option, - - /// Type of behavior/output. - #[command(subcommand)] - command: Commands, -} - -#[derive(clap::Subcommand, Debug)] -enum Commands { - /// List issues. - List { - /// Filter strings, describes issues to include in the list. - /// Each filter string is of the form "name=condition". - /// The supported names and their matching conditions are: - /// - /// "state": Comma-separated list of states to list. - /// Example: "state=new,backlog". Defaults to - /// "new,backlog,blocked,inprogress". - /// - /// "assignee": Comma-separated list of assignees to include in - /// the list. The empty string includes issues with no assignee. - /// Example: "assignee=seb," lists issues assigned to "seb" and - /// issues without an assignee. Defaults to include all issues. - /// - /// "tag": Comma-separated list of tags to include, or exclude - /// if prefixed with "-". Example: "tag=bug,-docs" shows issues - /// that are tagged "bug" and not tagged "docs". Defaults to - /// including all tags and excluding none. - /// - /// "done-time": Time range of issue completion, in the form - /// "[START]..[END]". Includes issues that were marked Done - /// between START and END. START and END are both in RFC 3339 - /// format, e.g. "YYYY-MM-DDTHH:MM:SS[+-]HH:MM". If START - /// is omitted, defaults to the beginning of time. If END is - /// omitted, defaults to the end of time. - filter: Vec, - }, - - /// Create a new issue. - New { description: Option }, - - /// Edit the description of an Issue or a Comment. - Edit { uuid: String }, - - /// Show the full description of an issue. - Show { issue_id: String }, - - /// Modify the state of an issue - State { - issue_id: String, - new_state: Option, - }, - - /// Create a new comment on an issue. - Comment { - issue_id: String, - description: Option, - }, - - /// Sync entomologist data with remote. This fetches from the remote, - /// merges the remote entomologist data branch with the local one, - /// and pushes the result back to the remote. - Sync { - /// Name of the git remote to sync with. - #[arg(default_value_t = String::from("origin"))] - remote: String, - }, - - /// Get or set the Assignee field of an Issue. - Assign { - issue_id: String, - new_assignee: Option, - }, - - /// Add or remove a Tag to/from an Issue, or list the Tags on an Issue. - Tag { - issue_id: String, - #[arg(allow_hyphen_values = true)] - tag: Option, - }, - - /// Get or set the `done_time` of the Issue. - DoneTime { - issue_id: String, - done_time: Option, - }, - - /// get or add a dependency to the issue - Depend { - issue_id: String, - dependency_id: Option, - }, -} - -fn handle_command( - args: &Args, - issues_database_source: &entomologist::database::IssuesDatabaseSource, -) -> anyhow::Result<()> { - match &args.command { - Commands::List { filter } => { - let issues = entomologist::database::read_issues_database(issues_database_source)?; - let filter = { - let mut f = entomologist::Filter::new(); - for filter_str in filter { - f.parse(filter_str)?; - } - f - }; - - let mut uuids_by_state = std::collections::HashMap::< - entomologist::issue::State, - Vec<&entomologist::issue::IssueHandle>, - >::new(); - for (uuid, issue) in issues.issues.iter() { - if !filter.include_states.contains(&issue.state) { - continue; - } - if filter.include_assignees.len() > 0 { - let assignee = match &issue.assignee { - Some(assignee) => assignee, - None => "", - }; - if !filter.include_assignees.contains(assignee) { - continue; - } - } - - if filter.include_tags.len() > 0 { - if !issue.has_any_tag(&filter.include_tags) { - continue; - } - } - if filter.exclude_tags.len() > 0 { - if issue.has_any_tag(&filter.exclude_tags) { - continue; - } - } - - if let Some(issue_done_time) = issue.done_time { - if let Some(start_done_time) = filter.start_done_time { - if start_done_time > issue_done_time { - continue; - } - } - if let Some(end_done_time) = filter.end_done_time { - if end_done_time < issue_done_time { - continue; - } - } - } - - // This issue passed all the filters, include it in list. - uuids_by_state - .entry(issue.state.clone()) - .or_default() - .push(uuid); - } - - use entomologist::issue::State; - for state in [ - State::InProgress, - State::Blocked, - State::Backlog, - State::New, - State::Done, - State::WontDo, - ] { - let these_uuids = uuids_by_state.entry(state.clone()).or_default(); - if these_uuids.len() == 0 { - continue; - } - these_uuids.sort_by(|a_id, b_id| { - let a = issues.issues.get(*a_id).unwrap(); - let b = issues.issues.get(*b_id).unwrap(); - a.creation_time.cmp(&b.creation_time) - }); - println!("{:?}:", state); - for uuid in these_uuids { - let issue = issues.issues.get(*uuid).unwrap(); - let comments = match issue.comments.len() { - 0 => String::from(" "), - n => format!("🗨️ {}", n), - }; - let assignee = match &issue.assignee { - Some(assignee) => format!(" (👉 {})", assignee), - None => String::from(""), - }; - let tags = match &issue.tags.len() { - 0 => String::from(""), - _ => { - // Could use `format!(" {:?}", issue.tags)` - // here, but that results in `["tag1", "TAG2", - // "i-am-also-a-tag"]` and i don't want the - // double-quotes around each tag. - let mut tags = String::from(" ["); - let mut separator = ""; - for tag in &issue.tags { - tags.push_str(separator); - tags.push_str(tag); - separator = ", "; - } - tags.push_str("]"); - tags - } - }; - println!( - "{} {} {}{}{}", - uuid, - comments, - issue.title(), - assignee, - tags - ); - } - println!(""); - } - } - - Commands::New { description } => { - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - match entomologist::issue::Issue::new(&issues_database.dir, description) { - Err(entomologist::issue::IssueError::EmptyDescription) => { - println!("no new issue created"); - return Ok(()); - } - Err(e) => { - return Err(e.into()); - } - Ok(issue) => { - println!("created new issue '{}'", issue.title()); - println!("ID: {}", issue.id); - return Ok(()); - } - } - } - - Commands::Edit { uuid } => { - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; - if let Some(issue) = issues.get_mut_issue(uuid) { - match issue.edit_description() { - Err(entomologist::issue::IssueError::EmptyDescription) => { - println!("aborted issue edit"); - return Ok(()); - } - Err(e) => return Err(e.into()), - Ok(()) => return Ok(()), - } - } - // No issue by that ID, check all the comments. - for (_, issue) in issues.issues.iter_mut() { - for comment in issue.comments.iter_mut() { - if comment.uuid == *uuid { - match comment.edit_description() { - Err(entomologist::comment::CommentError::EmptyDescription) => { - println!("aborted comment edit"); - return Ok(()); - } - Err(e) => return Err(e.into()), - Ok(()) => return Ok(()), - } - } - } - } - return Err(anyhow::anyhow!( - "no issue or comment with uuid {} found", - uuid - )); - } - - Commands::Show { issue_id } => { - let issues = entomologist::database::read_issues_database(issues_database_source)?; - let Some(issue) = issues.get_issue(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - println!("issue {}", issue_id); - println!("author: {}", issue.author); - if issue.tags.len() > 0 { - print!("tags: "); - let mut separator = ""; - for tag in &issue.tags { - print!("{}{}", separator, tag); - separator = ", "; - } - println!(""); - } - println!("creation_time: {}", issue.creation_time); - if let Some(done_time) = &issue.done_time { - println!("done_time: {}", done_time); - } - println!("state: {:?}", issue.state); - if let Some(dependencies) = &issue.dependencies { - println!("dependencies: {:?}", dependencies); - } - if let Some(assignee) = &issue.assignee { - println!("assignee: {}", assignee); - } - println!(""); - println!("{}", issue.description); - for comment in &issue.comments { - println!(""); - println!("comment: {}", comment.uuid); - println!("author: {}", comment.author); - println!("creation_time: {}", comment.creation_time); - println!(""); - println!("{}", comment.description); - } - } - - Commands::State { - issue_id, - new_state, - } => match new_state { - Some(new_state) => { - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; - match issues.issues.get_mut(issue_id) { - Some(issue) => { - let current_state = issue.state.clone(); - issue.set_state(new_state.clone())?; - println!("issue: {}", issue_id); - println!("state: {} -> {}", current_state, new_state); - } - None => { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - } - } - } - None => { - let issues = entomologist::database::read_issues_database(issues_database_source)?; - match issues.issues.get(issue_id) { - Some(issue) => { - println!("issue: {}", issue_id); - println!("state: {}", issue.state); - } - None => { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - } - } - } - }, - - Commands::Comment { - issue_id, - description, - } => { - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; - let Some(issue) = issues.get_mut_issue(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - match issue.add_comment(description) { - Err(entomologist::issue::IssueError::CommentError( - entomologist::comment::CommentError::EmptyDescription, - )) => { - println!("aborted new comment"); - return Ok(()); - } - Err(e) => { - return Err(e.into()); - } - Ok(comment) => { - println!( - "created new comment {} on issue {}", - &comment.uuid, &issue_id - ); - } - } - } - - Commands::Sync { remote } => { - if let entomologist::database::IssuesDatabaseSource::Branch(branch) = - issues_database_source - { - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - entomologist::git::sync(&issues_database.dir, remote, branch)?; - println!("synced {:?} with {:?}", branch, remote); - } else { - return Err(anyhow::anyhow!( - "`sync` operates on a branch, don't specify `issues_dir`" - )); - } - } - - Commands::Assign { - issue_id, - new_assignee, - } => match new_assignee { - Some(new_assignee) => { - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; - let Some(issue) = issues.get_mut_issue(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - let old_assignee: String = match &issue.assignee { - Some(assignee) => assignee.clone(), - None => String::from("None"), - }; - issue.set_assignee(new_assignee)?; - println!("issue: {}", issue_id); - println!("assignee: {} -> {}", old_assignee, new_assignee); - } - None => { - let issues = entomologist::database::read_issues_database(issues_database_source)?; - let Some(original_issue) = issues.issues.get(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - let old_assignee: String = match &original_issue.assignee { - Some(assignee) => assignee.clone(), - None => String::from("None"), - }; - println!("issue: {}", issue_id); - println!("assignee: {}", old_assignee); - } - }, - - Commands::Tag { issue_id, tag } => match tag { - Some(tag) => { - // Add or remove tag. - if tag.len() == 0 { - return Err(anyhow::anyhow!("invalid zero-length tag")); - } - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; - let Some(issue) = issues.get_mut_issue(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - if tag.chars().nth(0).unwrap() == '-' { - let tag = &tag[1..]; - issue.remove_tag(tag)?; - } else { - issue.add_tag(tag)?; - } - } - None => { - // Just list the tags. - let issues = entomologist::database::read_issues_database(issues_database_source)?; - let Some(issue) = issues.issues.get(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - match &issue.tags.len() { - 0 => println!("no tags"), - _ => { - // Could use `format!(" {:?}", issue.tags)` - // here, but that results in `["tag1", "TAG2", - // "i-am-also-a-tag"]` and i don't want the - // double-quotes around each tag. - for tag in &issue.tags { - println!("{}", tag); - } - } - } - } - }, - - Commands::DoneTime { - issue_id, - done_time, - } => match done_time { - Some(done_time) => { - // Add or remove tag. - let issues_database = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&issues_database.dir)?; - let Some(issue) = issues.get_mut_issue(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - let done_time = match chrono::DateTime::parse_from_rfc3339(done_time) { - Ok(done_time) => done_time.with_timezone(&chrono::Local), - Err(e) => { - eprintln!("failed to parse done-time from {}", done_time); - return Err(e.into()); - } - }; - issue.set_done_time(done_time)?; - } - None => { - let issues = entomologist::database::read_issues_database(issues_database_source)?; - let Some(issue) = issues.issues.get(issue_id) else { - return Err(anyhow::anyhow!("issue {} not found", issue_id)); - }; - match &issue.done_time { - Some(done_time) => println!("done_time: {}", done_time), - None => println!("None"), - }; - } - }, - - Commands::Depend { - issue_id, - dependency_id, - } => match dependency_id { - Some(dep_id) => { - let ent_db = entomologist::database::make_issues_database( - issues_database_source, - entomologist::database::IssuesDatabaseAccess::ReadWrite, - )?; - let mut issues = entomologist::issues::Issues::new_from_dir(&ent_db.dir)?; - if issues.issues.contains_key(dep_id) { - if let Some(issue) = issues.issues.get_mut(issue_id) { - issue.add_dependency(dep_id.clone())?; - } else { - Err(anyhow::anyhow!("issue {} not found", issue_id))?; - }; - } else { - Err(anyhow::anyhow!("dependency {} not found", dep_id))?; - }; - } - None => { - let ent_db = entomologist::database::read_issues_database(issues_database_source)?; - - let Some(issue) = ent_db.issues.get(issue_id) else { - Err(anyhow::anyhow!("issue {} not found", issue_id))? - }; - println!("DEPENDENCIES:"); - if let Some(list) = &issue.dependencies { - for dependency in list { - println!("{}", dependency); - } - } else { - println!("NONE"); - } - } - }, - } - - Ok(()) -} - -fn main() -> anyhow::Result<()> { - #[cfg(feature = "log")] - simple_logger::SimpleLogger::new().env().init().unwrap(); - - let args: Args = Args::parse(); - // println!("{:?}", args); - - let issues_database_source = match (&args.issues_dir, &args.issues_branch) { - (Some(dir), None) => { - entomologist::database::IssuesDatabaseSource::Dir(std::path::Path::new(dir)) - } - (None, Some(branch)) => entomologist::database::IssuesDatabaseSource::Branch(branch), - (None, None) => entomologist::database::IssuesDatabaseSource::Branch("entomologist-data"), - (Some(_), Some(_)) => { - return Err(anyhow::anyhow!( - "don't specify both `--issues-dir` and `--issues-branch`" - )); - } - }; - - if let entomologist::database::IssuesDatabaseSource::Branch(branch) = &issues_database_source { - if !entomologist::git::git_branch_exists(branch)? { - entomologist::git::create_orphan_branch(branch)?; - } - } - - handle_command(&args, &issues_database_source)?; - - Ok(()) -} diff --git a/src/comment.rs b/src/comment.rs deleted file mode 100644 index 1fa2e36..0000000 --- a/src/comment.rs +++ /dev/null @@ -1,230 +0,0 @@ -use std::io::{IsTerminal, Write}; - -#[derive(Debug, PartialEq)] -pub struct Comment { - pub uuid: String, - pub author: String, - pub creation_time: chrono::DateTime, - pub description: String, - - /// This is the directory that the comment lives in. Only used - /// internally by the entomologist library. - pub dir: std::path::PathBuf, -} - -#[derive(Debug, thiserror::Error)] -pub enum CommentError { - #[error(transparent)] - StdIoError(#[from] std::io::Error), - #[error(transparent)] - EnvVarError(#[from] std::env::VarError), - #[error("Failed to parse comment")] - CommentParseError, - #[error("Failed to run git")] - GitError(#[from] crate::git::GitError), - #[error("Failed to run editor")] - EditorError, - #[error("supplied description is empty")] - EmptyDescription, - #[error("stdin/stdout is not a terminal")] - StdioIsNotTerminal, -} - -impl Comment { - pub fn new_from_dir(comment_dir: &std::path::Path) -> Result { - let mut description: Option = None; - - for direntry in comment_dir.read_dir()? { - if let Ok(direntry) = direntry { - let file_name = direntry.file_name(); - if file_name == "description" { - description = Some(std::fs::read_to_string(direntry.path())?); - } else { - #[cfg(feature = "log")] - debug!( - "ignoring unknown file in comment directory: {:?}", - file_name - ); - } - } - } - let Some(description) = description else { - return Err(CommentError::CommentParseError); - }; - - let (author, creation_time) = crate::git::git_log_oldest_author_timestamp(comment_dir)?; - - let dir = std::path::PathBuf::from(comment_dir); - - Ok(Self { - uuid: String::from( - dir.file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ), - author, - creation_time, - description, - dir: std::path::PathBuf::from(comment_dir), - }) - } - - /// Create a new Comment on the specified Issue. Commits. - pub fn new( - issue: &crate::issue::Issue, - description: &Option, - ) -> Result { - let mut dir = std::path::PathBuf::from(&issue.dir); - dir.push("comments"); - if !dir.exists() { - std::fs::create_dir(&dir)?; - } - - let rnd: u128 = rand::random(); - let uuid = format!("{:032x}", rnd); - dir.push(&uuid); - std::fs::create_dir(&dir)?; - - let mut comment = crate::comment::Comment { - uuid, - author: String::from(""), // this will be updated from git when we re-read this comment - creation_time: chrono::Local::now(), - description: String::from(""), // this will be set immediately below - dir: dir.clone(), - }; - - match description { - Some(description) => { - if description.len() == 0 { - return Err(CommentError::EmptyDescription); - } - comment.description = String::from(description); - let description_filename = comment.description_filename(); - let mut description_file = std::fs::File::create(&description_filename)?; - write!(description_file, "{}", description)?; - } - None => comment.edit_description_file()?, - }; - - crate::git::add(&dir)?; - if crate::git::worktree_is_dirty(&dir.to_string_lossy())? { - crate::git::commit( - &dir, - &format!( - "add comment {} on issue {}", - comment.uuid, - issue - .dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ), - )?; - } - - Ok(comment) - } - - pub fn read_description(&mut self) -> Result<(), CommentError> { - let mut description_filename = std::path::PathBuf::from(&self.dir); - description_filename.push("description"); - self.description = std::fs::read_to_string(description_filename)?; - Ok(()) - } - - pub fn edit_description(&mut self) -> Result<(), CommentError> { - self.edit_description_file()?; - let description_filename = self.description_filename(); - crate::git::add(&description_filename)?; - if crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { - crate::git::commit( - &description_filename - .parent() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?, - &format!( - "edit comment {} on issue FIXME", // FIXME: name the issue that the comment is on - self.dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy() - ), - )?; - self.read_description()?; - } - Ok(()) - } - - /// Opens the Comment's `description` file in an editor. Validates - /// the editor's exit code. Updates the Comment's internal - /// description from what the user saved in the file. - /// - /// Used by Issue::add_comment() when no description is supplied, - /// and (FIXME: in the future) used by `ent edit COMMENT`. - pub fn edit_description_file(&mut self) -> Result<(), CommentError> { - if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() { - return Err(CommentError::StdioIsNotTerminal); - } - - let description_filename = self.description_filename(); - let exists = description_filename.exists(); - - let editor = match std::env::var("EDITOR") { - Ok(editor) => editor, - Err(std::env::VarError::NotPresent) => String::from("vi"), - Err(e) => return Err(e.into()), - }; - let result = std::process::Command::new(editor) - .arg(&description_filename.as_os_str()) - .spawn()? - .wait_with_output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(CommentError::EditorError); - } - - if !description_filename.exists() || description_filename.metadata()?.len() == 0 { - // User saved an empty file, which means they changed their - // mind and no longer want to edit the description. - if exists { - crate::git::restore_file(&description_filename)?; - } - return Err(CommentError::EmptyDescription); - } - self.read_description()?; - Ok(()) - } -} - -// This is the private, internal API. -impl Comment { - fn description_filename(&self) -> std::path::PathBuf { - let mut description_filename = std::path::PathBuf::from(&self.dir); - description_filename.push("description"); - description_filename - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn read_comment_0() { - let comment_dir = std::path::Path::new( - "test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347", - ); - let comment = Comment::new_from_dir(comment_dir).unwrap(); - let expected = Comment { - uuid: String::from("9055dac36045fe36545bed7ae7b49347"), - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T10:08:38-06:00") - .unwrap() - .with_timezone(&chrono::Local), - description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe\n\nIt has multiple lines\n"), - dir: std::path::PathBuf::from(comment_dir), - }; - assert_eq!(comment, expected); - } -} diff --git a/src/database.rs b/src/database.rs deleted file mode 100644 index 8afcd8d..0000000 --- a/src/database.rs +++ /dev/null @@ -1,94 +0,0 @@ -use thiserror::Error; -use crate::{git::GitError, issues::ReadIssuesError}; - -/// Errors that the DB can emit: -#[derive(Debug, Error)] -pub enum Error { - #[error(transparent)] - IssuesError(#[from] ReadIssuesError), - #[error(transparent)] - GitError(#[from] GitError), -} - - -/// The main function looks at the command-line arguments and determines -/// from there where to get the Issues Database to operate on. -/// -/// * If the user specified `--issues-dir` we use that. -/// -/// * If the user specified `--issues-branch` we make sure the branch -/// exists, then use that. -/// -/// * If the user specified neither, we use the default branch -/// `entomologist-data` (after ensuring that it exists). -/// -/// * If the user specified both, it's an operator error and we abort. -/// -/// The result of that code populates an IssuesDatabaseSource object, -/// that gets used later to access the database. -pub enum IssuesDatabaseSource<'a> { - Dir(&'a std::path::Path), - Branch(&'a str), -} - - - -/// The IssuesDatabase type is a "fat path". It holds a PathBuf pointing -/// at the issues database directory, and optionally a Worktree object -/// corresponding to that path. -/// -/// The worktree field itself is never read: we put its path in `dir` -/// and that's all that the calling code cares about. -/// -/// The Worktree object is included here *when* the IssuesDatabaseSource -/// is a branch. In this case a git worktree is created to hold the -/// checkout of the branch. When the IssueDatabase object is dropped, -/// the contained/owned Worktree object is dropped, which deletes the -/// worktree directory from the filesystem and prunes the worktree from -/// git's worktree list. - -pub struct IssuesDatabase { - pub dir: std::path::PathBuf, - - #[allow(dead_code)] - pub worktree: Option, -} - -pub enum IssuesDatabaseAccess { - ReadOnly, - ReadWrite, -} - -pub fn make_issues_database( - issues_database_source: &IssuesDatabaseSource, - access_type: IssuesDatabaseAccess, -) -> Result { - match issues_database_source { - IssuesDatabaseSource::Dir(dir) => Ok(IssuesDatabase { - dir: std::path::PathBuf::from(dir), - worktree: None, - }), - IssuesDatabaseSource::Branch(branch) => { - let worktree = match access_type { - IssuesDatabaseAccess::ReadOnly => { - crate::git::Worktree::new_detached(branch)? - } - IssuesDatabaseAccess::ReadWrite => crate::git::Worktree::new(branch)?, - }; - Ok(IssuesDatabase { - dir: std::path::PathBuf::from(worktree.path()), - worktree: Some(worktree), - }) - } - } -} - -pub fn read_issues_database( - issues_database_source: &IssuesDatabaseSource, -) -> Result { - let issues_database = - make_issues_database(issues_database_source, IssuesDatabaseAccess::ReadOnly)?; - Ok(crate::issues::Issues::new_from_dir( - &issues_database.dir, - )?) -} \ No newline at end of file diff --git a/src/git.rs b/src/git.rs deleted file mode 100644 index 6e70fa8..0000000 --- a/src/git.rs +++ /dev/null @@ -1,547 +0,0 @@ -use std::io::Write; - -#[derive(Debug, thiserror::Error)] -pub enum GitError { - #[error(transparent)] - StdIoError(#[from] std::io::Error), - #[error(transparent)] - ParseIntError(#[from] std::num::ParseIntError), - #[error("Oops, something went wrong")] - Oops, -} - -#[derive(Debug)] -/// `Worktree` is a struct that manages a temporary directory containing -/// a checkout of a specific branch. The worktree is removed and pruned -/// when the `Worktree` struct is dropped. -pub struct Worktree { - path: tempfile::TempDir, -} - -impl Drop for Worktree { - fn drop(&mut self) { - let result = std::process::Command::new("git") - .args([ - "worktree", - "remove", - "--force", - &self.path.path().to_string_lossy(), - ]) - .output(); - match result { - Err(e) => { - println!("failed to run git: {:#?}", e); - } - Ok(result) => { - if !result.status.success() { - println!("failed to remove git worktree: {:#?}", result); - } - } - } - } -} - -impl Worktree { - pub fn new(branch: &str) -> Result { - let path = tempfile::tempdir()?; - let result = std::process::Command::new("git") - .args(["worktree", "add", &path.path().to_string_lossy(), branch]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(Self { path }) - } - - pub fn new_detached(branch: &str) -> Result { - let path = tempfile::tempdir()?; - let result = std::process::Command::new("git") - .args([ - "worktree", - "add", - "--detach", - &path.path().to_string_lossy(), - branch, - ]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(Self { path }) - } - - pub fn path(&self) -> &std::path::Path { - self.path.as_ref() - } -} - -pub fn checkout_branch_in_worktree( - branch: &str, - worktree_dir: &std::path::Path, -) -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["worktree", "add", &worktree_dir.to_string_lossy(), branch]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(()) -} - -pub fn git_worktree_prune() -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["worktree", "prune"]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(()) -} - -pub fn git_remove_branch(branch: &str) -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["branch", "-D", branch]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(()) -} - -pub fn git_branch_exists(branch: &str) -> Result { - let result = std::process::Command::new("git") - .args(["show-ref", "--quiet", branch]) - .output()?; - return Ok(result.status.success()); -} - -pub fn worktree_is_dirty(dir: &str) -> Result { - // `git status --porcelain` prints a terse list of files added or - // modified (both staged and not), and new untracked files. So if - // says *anything at all* it means the worktree is dirty. - let result = std::process::Command::new("git") - .args(["status", "--porcelain", "--untracked-files=no"]) - .current_dir(dir) - .output()?; - return Ok(result.stdout.len() > 0); -} - -pub fn add(file: &std::path::Path) -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["add", &file.to_string_lossy()]) - .current_dir( - file.parent() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?, - ) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - return Ok(()); -} - -pub fn restore_file(file: &std::path::Path) -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["restore", &file.to_string_lossy()]) - .current_dir( - file.parent() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))?, - ) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - return Ok(()); -} - -pub fn commit(dir: &std::path::Path, msg: &str) -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["commit", "-m", msg]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(()) -} - -pub fn git_commit_file(file: &std::path::Path) -> Result<(), GitError> { - let mut git_dir = std::path::PathBuf::from(file); - git_dir.pop(); - - let result = std::process::Command::new("git") - .args([ - "add", - &file - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ]) - .current_dir(&git_dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - let result = std::process::Command::new("git") - .args([ - "commit", - "-m", - &format!( - "update '{}' in issue {}", - file.file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - git_dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy() - ), - ]) - .current_dir(&git_dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - Ok(()) -} - -pub fn git_fetch(dir: &std::path::Path, remote: &str) -> Result<(), GitError> { - let result = std::process::Command::new("git") - .args(["fetch", remote]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - Ok(()) -} - -pub fn sync(dir: &std::path::Path, remote: &str, branch: &str) -> Result<(), GitError> { - // We do all the work in a directory that's (FIXME) hopefully a - // worktree. If anything goes wrong we just fail out and ask the - // human to fix it by hand :-/ - // 1. `git fetch` - // 2. `git merge REMOTE/BRANCH` - // 3. `git push REMOTE BRANCH` - - git_fetch(dir, remote)?; - - // FIXME: Possible things to add: - // * `git log -p` shows diff - // * `git log --numstat` shows machine-readable diffstat - - // Show what we just fetched from the remote. - let result = std::process::Command::new("git") - .args([ - "log", - "--no-merges", - "--pretty=format:%an: %s", - &format!("{}/{}", remote, branch), - &format!("^{}", branch), - ]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!( - "Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}", - branch - ); - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - if result.stdout.len() > 0 { - println!("Changes fetched from remote {}:", remote); - println!("{}", &String::from_utf8_lossy(&result.stdout)); - println!(""); - } - - // Show what we are about to push to the remote. - let result = std::process::Command::new("git") - .args([ - "log", - "--no-merges", - "--pretty=format:%an: %s", - &format!("{}", branch), - &format!("^{}/{}", remote, branch), - ]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!( - "Sync failed! 'git log' error! Help, a human needs to fix the mess in {:?}", - branch - ); - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - if result.stdout.len() > 0 { - println!("Changes to push to remote {}:", remote); - println!("{}", &String::from_utf8_lossy(&result.stdout)); - println!(""); - } - - // Merge remote branch into local. - let result = std::process::Command::new("git") - .args(["merge", &format!("{}/{}", remote, branch)]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!( - "Sync failed! Merge error! Help, a human needs to fix the mess in {:?}", - branch - ); - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - // Push merged branch to remote. - let result = std::process::Command::new("git") - .args(["push", remote, branch]) - .current_dir(dir) - .output()?; - if !result.status.success() { - println!( - "Sync failed! Push error! Help, a human needs to fix the mess in {:?}", - branch - ); - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - Ok(()) -} - -pub fn git_log_oldest_timestamp( - path: &std::path::Path, -) -> Result, GitError> { - let mut git_dir = std::path::PathBuf::from(path); - git_dir.pop(); - let result = std::process::Command::new("git") - .args([ - "log", - "--pretty=format:%at", - "--", - &path - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ]) - .current_dir(&git_dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - let timestamp_str = std::str::from_utf8(&result.stdout).unwrap(); - let timestamp_last = timestamp_str.split("\n").last().unwrap(); - let timestamp_i64 = timestamp_last.parse::()?; - let timestamp = chrono::DateTime::from_timestamp(timestamp_i64, 0) - .unwrap() - .with_timezone(&chrono::Local); - Ok(timestamp) -} - -pub fn git_log_oldest_author(path: &std::path::Path) -> Result { - let mut git_dir = std::path::PathBuf::from(path); - git_dir.pop(); - let result = std::process::Command::new("git") - .args([ - "log", - "--pretty=format:%an <%ae>", - "--", - &path - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ]) - .current_dir(&git_dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - let author_str = std::str::from_utf8(&result.stdout).unwrap(); - let author_last = author_str.split("\n").last().unwrap(); - Ok(String::from(author_last)) -} - -pub fn git_log_oldest_author_timestamp( - path: &std::path::Path, -) -> Result<(String, chrono::DateTime), GitError> { - let mut git_dir = std::path::PathBuf::from(path); - git_dir.pop(); - let result = std::process::Command::new("git") - .args([ - "log", - "--pretty=format:%at %an <%ae>", - "--", - &path - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ]) - .current_dir(&git_dir) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - let raw_output_str = String::from_utf8_lossy(&result.stdout); - let Some(raw_output_last) = raw_output_str.split("\n").last() else { - return Err(GitError::Oops); - }; - let Some(index) = raw_output_last.find(' ') else { - return Err(GitError::Oops); - }; - let author_str = &raw_output_last[index + 1..]; - let timestamp_str = &raw_output_last[0..index]; - let timestamp_i64 = timestamp_str.parse::()?; - let timestamp = chrono::DateTime::from_timestamp(timestamp_i64, 0) - .unwrap() - .with_timezone(&chrono::Local); - - Ok((String::from(author_str), timestamp)) -} - -pub fn create_orphan_branch(branch: &str) -> Result<(), GitError> { - { - let tmp_worktree = tempfile::tempdir().unwrap(); - create_orphan_branch_at_path(branch, tmp_worktree.path())?; - } - // The temp dir is now removed / cleaned up. - - let result = std::process::Command::new("git") - .args(["worktree", "prune"]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - Ok(()) -} - -fn create_orphan_branch_at_path( - branch: &str, - worktree_path: &std::path::Path, -) -> Result<(), GitError> { - let worktree_dir = worktree_path.to_string_lossy(); - let result = std::process::Command::new("git") - .args(["worktree", "add", "--orphan", "-b", branch, &worktree_dir]) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - let mut readme_filename = std::path::PathBuf::from(worktree_path); - readme_filename.push("README.md"); - let mut readme = std::fs::File::create(readme_filename)?; - write!( - readme, - "This branch is used by entomologist to track issues." - )?; - - let result = std::process::Command::new("git") - .args(["add", "README.md"]) - .current_dir(worktree_path) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - let result = std::process::Command::new("git") - .args(["commit", "-m", "create entomologist issue branch"]) - .current_dir(worktree_path) - .output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(GitError::Oops); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn test_worktree() { - let mut p = std::path::PathBuf::new(); - { - let worktree = Worktree::new("origin/main").unwrap(); - - p.push(worktree.path()); - assert!(p.exists()); - - let mut p2 = p.clone(); - p2.push("README.md"); - assert!(p2.exists()); - } - // The temporary worktree directory is removed when the Temp variable is dropped. - assert!(!p.exists()); - } - - #[test] - fn test_create_orphan_branch() { - let rnd: u128 = rand::random(); - let mut branch = std::string::String::from("entomologist-test-branch-"); - branch.push_str(&format!("{:032x}", rnd)); - create_orphan_branch(&branch).unwrap(); - git_remove_branch(&branch).unwrap(); - } - - #[test] - fn test_branch_exists_0() { - let r = git_branch_exists("main").unwrap(); - assert_eq!(r, true); - } - - #[test] - fn test_branch_exists_1() { - let rnd: u128 = rand::random(); - let mut branch = std::string::String::from("entomologist-missing-branch-"); - branch.push_str(&format!("{:032x}", rnd)); - let r = git_branch_exists(&branch).unwrap(); - assert_eq!(r, false); - } -} diff --git a/src/issue.rs b/src/issue.rs deleted file mode 100644 index 06f959f..0000000 --- a/src/issue.rs +++ /dev/null @@ -1,603 +0,0 @@ -use core::fmt; -use std::io::{IsTerminal, Write}; -use std::str::FromStr; - -#[cfg(feature = "log")] -use log::debug; - -#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Deserialize)] -/// These are the states an issue can be in. -pub enum State { - New, - Backlog, - Blocked, - InProgress, - Done, - WontDo, -} - -pub type IssueHandle = String; - -#[derive(Debug, PartialEq)] -pub struct Issue { - pub id: String, - pub author: String, - pub creation_time: chrono::DateTime, - pub done_time: Option>, - pub tags: Vec, - pub state: State, - pub dependencies: Option>, - pub assignee: Option, - pub description: String, - pub comments: Vec, - - /// This is the directory that the issue lives in. Only used - /// internally by the entomologist library. - pub dir: std::path::PathBuf, -} - -#[derive(Debug, thiserror::Error)] -pub enum IssueError { - #[error(transparent)] - StdIoError(#[from] std::io::Error), - #[error(transparent)] - EnvVarError(#[from] std::env::VarError), - #[error(transparent)] - CommentError(#[from] crate::comment::CommentError), - #[error(transparent)] - ChronoParseError(#[from] chrono::format::ParseError), - #[error("Failed to parse issue")] - IssueParseError, - #[error("Failed to parse state")] - StateParseError, - #[error("Failed to run git")] - GitError(#[from] crate::git::GitError), - #[error("Failed to run editor")] - EditorError, - #[error("supplied description is empty")] - EmptyDescription, - #[error("tag {0} not found")] - TagNotFound(String), - #[error("stdin/stdout is not a terminal")] - StdioIsNotTerminal, - #[error("Failed to parse issue ID")] - IdError, - #[error("Dependency not found")] - DepNotFound, - #[error("Dependency already exists")] - DepExists, - #[error("Self-dependency not allowed")] - DepSelf, -} - -impl FromStr for State { - type Err = IssueError; - fn from_str(s: &str) -> Result { - let s = s.to_lowercase(); - if s == "new" { - Ok(State::New) - } else if s == "backlog" { - Ok(State::Backlog) - } else if s == "blocked" { - Ok(State::Blocked) - } else if s == "inprogress" { - Ok(State::InProgress) - } else if s == "done" { - Ok(State::Done) - } else if s == "wontdo" { - Ok(State::WontDo) - } else { - Err(IssueError::StateParseError) - } - } -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let fmt_str = match self { - State::New => "new", - State::Backlog => "backlog", - State::Blocked => "blocked", - State::InProgress => "inprogress", - State::Done => "done", - State::WontDo => "wontdo", - }; - write!(f, "{fmt_str}") - } -} - -// This is the public API of Issue. -impl Issue { - pub fn new_from_dir(dir: &std::path::Path) -> Result { - let mut description: Option = None; - let mut state = State::New; // default state, if not specified in the issue - let mut dependencies: Option> = None; - let mut comments = Vec::::new(); - let mut assignee: Option = None; - let mut tags = Vec::::new(); - let mut done_time: Option> = None; - - for direntry in dir.read_dir()? { - if let Ok(direntry) = direntry { - let file_name = direntry.file_name(); - if file_name == "description" { - description = Some(std::fs::read_to_string(direntry.path())?); - } else if file_name == "state" { - let state_string = std::fs::read_to_string(direntry.path())?; - state = State::from_str(state_string.trim())?; - } else if file_name == "assignee" { - assignee = Some(String::from( - std::fs::read_to_string(direntry.path())?.trim(), - )); - } else if file_name == "done_time" { - let raw_done_time = chrono::DateTime::<_>::parse_from_rfc3339( - std::fs::read_to_string(direntry.path())?.trim(), - )?; - done_time = Some(raw_done_time.into()); - } else if file_name == "dependencies" && direntry.metadata()?.is_dir() { - dependencies = Self::read_dependencies(&direntry.path())?; - } else if file_name == "tags" { - let contents = std::fs::read_to_string(direntry.path())?; - tags = contents - .lines() - .filter(|s| s.len() > 0) - .map(|tag| String::from(tag.trim())) - .collect(); - tags.sort(); - } else if file_name == "comments" && direntry.metadata()?.is_dir() { - Self::read_comments(&mut comments, &direntry.path())?; - } else { - #[cfg(feature = "log")] - debug!("ignoring unknown file in issue directory: {:?}", file_name); - } - } - } - - let Some(description) = description else { - return Err(IssueError::IssueParseError); - }; - - // parse the issue ID from the directory name - let id = if let Some(parsed_id) = match dir.file_name() { - Some(name) => name.to_str(), - None => Err(IssueError::IdError)?, - } { - String::from(parsed_id) - } else { - Err(IssueError::IdError)? - }; - - let (author, creation_time) = crate::git::git_log_oldest_author_timestamp(dir)?; - - Ok(Self { - id, - author, - creation_time, - done_time, - tags, - state: state, - dependencies, - assignee, - description, - comments, - dir: std::path::PathBuf::from(dir), - }) - } - - fn read_comments( - comments: &mut Vec, - dir: &std::path::Path, - ) -> Result<(), IssueError> { - for direntry in dir.read_dir()? { - if let Ok(direntry) = direntry { - let comment = crate::comment::Comment::new_from_dir(&direntry.path())?; - comments.push(comment); - } - } - comments.sort_by(|a, b| a.creation_time.cmp(&b.creation_time)); - Ok(()) - } - - fn read_dependencies(dir: &std::path::Path) -> Result>, IssueError> { - let mut dependencies: Option> = None; - for direntry in dir.read_dir()? { - if let Ok(direntry) = direntry { - match &mut dependencies { - Some(deps) => { - deps.push(direntry.file_name().into_string().unwrap()); - } - None => { - dependencies = Some(vec![direntry.file_name().into_string().unwrap()]); - } - } - } - } - if let Some(deps) = &mut dependencies { - deps.sort(); - } - Ok(dependencies) - } - - /// Add a new Comment to the Issue. Commits. - pub fn add_comment( - &mut self, - description: &Option, - ) -> Result { - let comment = crate::comment::Comment::new(self, description)?; - Ok(comment) - } - - /// Create a new Issue in an Issues database specified by a directory. - /// The new Issue will live in a new subdirectory, named by a unique - /// Issue identifier. - /// - /// If a description string is supplied, the new Issue's description - /// will be initialized from it with no user interaction. - /// - /// If no description is supplied, the user will be prompted to - /// input one into an editor. - /// - /// On success, the new Issue with its valid description is committed - /// to the Issues database. - pub fn new(dir: &std::path::Path, description: &Option) -> Result { - let mut issue_dir = std::path::PathBuf::from(dir); - let rnd: u128 = rand::random(); - let issue_id = format!("{:032x}", rnd); - issue_dir.push(&issue_id); - std::fs::create_dir(&issue_dir)?; - - let mut issue = Self { - id: String::from(&issue_id), - author: String::from(""), - creation_time: chrono::Local::now(), - done_time: None, - tags: Vec::::new(), - state: State::New, - dependencies: None, - assignee: None, - description: String::from(""), // FIXME: kind of bogus to use the empty string as None - comments: Vec::::new(), - dir: issue_dir.clone(), - }; - - match description { - Some(description) => { - if description.len() == 0 { - return Err(IssueError::EmptyDescription); - } - issue.description = String::from(description); - let description_filename = issue.description_filename(); - let mut description_file = std::fs::File::create(&description_filename)?; - write!(description_file, "{}", description)?; - } - None => issue.edit_description_file()?, - }; - - issue.commit(&format!("create new issue {}", issue_id))?; - - Ok(issue) - } - - /// Interactively edit the description of an existing Issue. - pub fn edit_description(&mut self) -> Result<(), IssueError> { - self.edit_description_file()?; - let description_filename = self.description_filename(); - self.commit(&format!( - "edit description of issue {}", - description_filename - .parent() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - ))?; - Ok(()) - } - - /// Return the Issue title (first line of the description). - pub fn title<'a>(&'a self) -> &'a str { - match self.description.find("\n") { - Some(index) => &self.description.as_str()[..index], - None => self.description.as_str(), - } - } - - /// Change the State of the Issue. If the new state is `Done`, - /// set the Issue `done_time`. Commits. - pub fn set_state(&mut self, new_state: State) -> Result<(), IssueError> { - let old_state = self.state.clone(); - let mut state_filename = std::path::PathBuf::from(&self.dir); - state_filename.push("state"); - let mut state_file = std::fs::File::create(&state_filename)?; - write!(state_file, "{}", new_state)?; - self.commit(&format!( - "change state of issue {}, {} -> {}", - self.dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - old_state, - new_state, - ))?; - if new_state == State::Done { - self.set_done_time(chrono::Local::now())?; - } - Ok(()) - } - - pub fn read_state(&mut self) -> Result<(), IssueError> { - let mut state_filename = std::path::PathBuf::from(&self.dir); - state_filename.push("state"); - let state_string = std::fs::read_to_string(state_filename)?; - self.state = State::from_str(state_string.trim())?; - Ok(()) - } - - /// Set the `done_time` of the Issue. Commits. - pub fn set_done_time( - &mut self, - done_time: chrono::DateTime, - ) -> Result<(), IssueError> { - let mut done_time_filename = std::path::PathBuf::from(&self.dir); - done_time_filename.push("done_time"); - let mut done_time_file = std::fs::File::create(&done_time_filename)?; - write!(done_time_file, "{}", done_time.to_rfc3339())?; - self.done_time = Some(done_time.clone()); - self.commit(&format!( - "set done-time of issue {} to {}", - self.dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - done_time, - ))?; - Ok(()) - } - - /// Set the Assignee of an Issue. - pub fn set_assignee(&mut self, new_assignee: &str) -> Result<(), IssueError> { - let old_assignee = match &self.assignee { - Some(assignee) => assignee.clone(), - None => String::from("None"), - }; - let mut assignee_filename = std::path::PathBuf::from(&self.dir); - assignee_filename.push("assignee"); - let mut assignee_file = std::fs::File::create(&assignee_filename)?; - write!(assignee_file, "{}", new_assignee)?; - self.commit(&format!( - "change assignee of issue {}, {} -> {}", - self.dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - old_assignee, - new_assignee, - ))?; - Ok(()) - } - - /// Add a new Tag to the Issue. Commits. - pub fn add_tag(&mut self, tag: &str) -> Result<(), IssueError> { - let tag_string = String::from(tag); - if self.tags.contains(&tag_string) { - return Ok(()); - } - self.tags.push(tag_string); - self.tags.sort(); - self.commit_tags(&format!( - "issue {} add tag {}", - self.dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - tag - ))?; - Ok(()) - } - - /// Remove a Tag from the Issue. Commits. - pub fn remove_tag(&mut self, tag: &str) -> Result<(), IssueError> { - let tag_string = String::from(tag); - let Some(index) = self.tags.iter().position(|x| x == &tag_string) else { - return Err(IssueError::TagNotFound(tag_string)); - }; - self.tags.remove(index); - self.commit_tags(&format!( - "issue {} remove tag {}", - self.dir - .file_name() - .ok_or(std::io::Error::from(std::io::ErrorKind::NotFound))? - .to_string_lossy(), - tag - ))?; - Ok(()) - } - - pub fn has_tag(&self, tag: &str) -> bool { - let tag_string = String::from(tag); - self.tags.iter().position(|x| x == &tag_string).is_some() - } - - pub fn has_any_tag(&self, tags: &std::collections::HashSet<&str>) -> bool { - for tag in tags.iter() { - if self.has_tag(tag) { - return true; - } - } - return false; - } - - pub fn add_dependency(&mut self, dep: IssueHandle) -> Result<(), IssueError> { - if self.id == dep { - Err(IssueError::DepSelf)?; - } - match &mut self.dependencies { - Some(v) => v.push(dep.clone()), - None => self.dependencies = Some(vec![dep.clone()]), - } - let mut dir = std::path::PathBuf::from(&self.dir); - dir.push("dependencies"); - if !dir.exists() { - std::fs::create_dir(&dir)?; - } - - dir.push(dep.clone()); - - if !dir.exists() { - std::fs::File::create(&dir)?; - self.commit(&format!("add dep {} to issue {}", dep, self.id))?; - } else { - Err(IssueError::DepExists)?; - } - Ok(()) - } - - pub fn remove_dependency(&mut self, dep: IssueHandle) -> Result<(), IssueError> { - match &mut self.dependencies { - Some(v) => { - if let Some(i) = v.iter().position(|d| d == &dep) { - v.remove(i); - } else { - Err(IssueError::DepNotFound)?; - } - } - None => Err(IssueError::DepNotFound)?, - } - self.commit(&format!("remove dep {} from issue {}", dep, self.id))?; - Ok(()) - } -} - -// This is the internal/private API of Issue. -impl Issue { - fn description_filename(&self) -> std::path::PathBuf { - let mut description_filename = std::path::PathBuf::from(&self.dir); - description_filename.push("description"); - description_filename - } - - /// Read the Issue's description file into the internal Issue representation. - fn read_description(&mut self) -> Result<(), IssueError> { - let description_filename = self.description_filename(); - self.description = std::fs::read_to_string(description_filename)?; - Ok(()) - } - - /// Opens the Issue's `description` file in an editor. Validates the - /// editor's exit code. Updates the Issue's internal description - /// from what the user saved in the file. - /// - /// Used by Issue::new() when no description is supplied, and also - /// used by `ent edit ISSUE`. - fn edit_description_file(&mut self) -> Result<(), IssueError> { - if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() { - return Err(IssueError::StdioIsNotTerminal); - } - - let description_filename = self.description_filename(); - let exists = description_filename.exists(); - let editor = match std::env::var("EDITOR") { - Ok(editor) => editor, - Err(std::env::VarError::NotPresent) => String::from("vi"), - Err(e) => return Err(e.into()), - }; - let result = std::process::Command::new(editor) - .arg(&description_filename.as_os_str()) - .spawn()? - .wait_with_output()?; - if !result.status.success() { - println!("stdout: {}", &String::from_utf8_lossy(&result.stdout)); - println!("stderr: {}", &String::from_utf8_lossy(&result.stderr)); - return Err(IssueError::EditorError); - } - if !description_filename.exists() || description_filename.metadata()?.len() == 0 { - // User saved an empty file, or exited without saving while - // editing a new description file. Both means they changed - // their mind and no longer want to edit the description. - if exists { - // File existed before the user emptied it, so restore - // the original. - crate::git::restore_file(&description_filename)?; - } - return Err(IssueError::EmptyDescription); - } - self.read_description()?; - Ok(()) - } - - fn commit_tags(&self, commit_message: &str) -> Result<(), IssueError> { - let mut tags_filename = self.dir.clone(); - tags_filename.push("tags"); - let mut tags_file = std::fs::File::create(&tags_filename)?; - for tag in &self.tags { - writeln!(tags_file, "{}", tag)?; - } - self.commit(commit_message)?; - Ok(()) - } - - fn commit(&self, commit_message: &str) -> Result<(), IssueError> { - crate::git::add(&self.dir)?; - if !crate::git::worktree_is_dirty(&self.dir.to_string_lossy())? { - return Ok(()); - } - crate::git::commit(&self.dir, commit_message)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn read_issue_0() { - let issue_dir = std::path::Path::new("test/0000/3943fc5c173fdf41c0a22251593cd476/"); - let issue = Issue::new_from_dir(issue_dir).unwrap(); - let expected = Issue { - id: String::from("3943fc5c173fdf41c0a22251593cd476"), - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:36:25-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::from([ - String::from("TAG2"), - String::from("i-am-also-a-tag"), - String::from("tag1"), - ]), - state: State::New, - dependencies: None, - assignee: None, - description: String::from( - "this is the title of my issue\n\nThis is the description of my issue.\nIt is multiple lines.\n* Arbitrary contents\n* But let's use markdown by convention\n", - ), - comments: Vec::::new(), - dir: std::path::PathBuf::from(issue_dir), - }; - assert_eq!(issue, expected); - } - - #[test] - fn read_issue_1() { - let issue_dir = std::path::Path::new("test/0000/7792b063eef6d33e7da5dc1856750c14/"); - let issue = Issue::new_from_dir(issue_dir).unwrap(); - let expected = Issue { - id: String::from("7792b063eef6d33e7da5dc1856750c14"), - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:37:07-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::new(), - state: State::InProgress, - dependencies: None, - assignee: Some(String::from("beep boop")), - description: String::from("minimal"), - comments: Vec::::new(), - dir: std::path::PathBuf::from(issue_dir), - }; - assert_eq!(issue, expected); - } -} diff --git a/src/issues.rs b/src/issues.rs deleted file mode 100644 index d3c57c0..0000000 --- a/src/issues.rs +++ /dev/null @@ -1,284 +0,0 @@ -#[cfg(feature = "log")] -use log::debug; - -// Just a placeholder for now, get rid of this if we don't need it. -#[derive(Debug, PartialEq, serde::Deserialize)] -pub struct Config {} - -#[derive(Debug, PartialEq)] -pub struct Issues { - pub issues: std::collections::HashMap, - pub config: Config, -} - -#[derive(Debug, thiserror::Error)] -pub enum ReadIssuesError { - #[error(transparent)] - StdIoError(#[from] std::io::Error), - #[error(transparent)] - IssueError(#[from] crate::issue::IssueError), - #[error("cannot handle filename")] - FilenameError(std::ffi::OsString), - #[error(transparent)] - TomlDeserializeError(#[from] toml::de::Error), -} - -impl Issues { - pub fn new() -> Self { - Self { - issues: std::collections::HashMap::new(), - config: Config {}, - } - } - - pub fn add_issue(&mut self, issue: crate::issue::Issue) { - self.issues.insert(issue.id.clone(), issue); - } - - pub fn get_issue(&self, issue_id: &str) -> Option<&crate::issue::Issue> { - self.issues.get(issue_id) - } - - pub fn get_mut_issue(&mut self, issue_id: &str) -> Option<&mut crate::issue::Issue> { - self.issues.get_mut(issue_id) - } - - fn parse_config(&mut self, config_path: &std::path::Path) -> Result<(), ReadIssuesError> { - let config_contents = std::fs::read_to_string(config_path)?; - let config: Config = toml::from_str(&config_contents)?; - self.config = config; - Ok(()) - } - - pub fn new_from_dir(dir: &std::path::Path) -> Result { - let mut issues = Self::new(); - - for direntry in dir.read_dir()? { - if let Ok(direntry) = direntry { - if direntry.metadata()?.is_dir() { - match crate::issue::Issue::new_from_dir(direntry.path().as_path()) { - Err(e) => { - eprintln!( - "failed to parse issue {}, skipping", - direntry.file_name().to_string_lossy() - ); - eprintln!("ignoring error: {:?}", e); - continue; - } - Ok(issue) => { - issues.add_issue(issue); - } - } - } else if direntry.file_name() == "config.toml" { - issues.parse_config(direntry.path().as_path())?; - } else { - #[cfg(feature = "log")] - debug!( - "ignoring unknown file in issues directory: {:?}", - direntry.file_name() - ); - } - } - } - return Ok(issues); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[test] - fn read_issues_0000() { - let issues_dir = std::path::Path::new("test/0000/"); - let issues = Issues::new_from_dir(issues_dir).unwrap(); - - let mut expected = Issues::new(); - - let uuid = String::from("7792b063eef6d33e7da5dc1856750c14"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - expected.add_issue(crate::issue::Issue { - id: uuid, - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:37:07-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::new(), - state: crate::issue::State::InProgress, - dependencies: None, - assignee: Some(String::from("beep boop")), - description: String::from("minimal"), - comments: Vec::::new(), - dir, - }); - - let uuid = String::from("3943fc5c173fdf41c0a22251593cd476"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - expected.add_issue( - crate::issue::Issue { - id: uuid, - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:36:25-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::from([ - String::from("TAG2"), - String::from("i-am-also-a-tag"), - String::from("tag1"), - ]), - state: crate::issue::State::New, - dependencies: None, - assignee: None, - description: String::from("this is the title of my issue\n\nThis is the description of my issue.\nIt is multiple lines.\n* Arbitrary contents\n* But let's use markdown by convention\n"), - comments: Vec::::new(), - dir, - } - ); - assert_eq!(issues, expected); - } - - #[test] - fn read_issues_0001() { - let issues_dir = std::path::Path::new("test/0001/"); - let issues = Issues::new_from_dir(issues_dir).unwrap(); - - let mut expected = Issues::new(); - - let uuid = String::from("3fa5bfd93317ad25772680071d5ac325"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - expected.add_issue(crate::issue::Issue { - id: uuid, - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:37:46-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: Some( - chrono::DateTime::parse_from_rfc3339("2025-07-15T15:15:15-06:00") - .unwrap() - .with_timezone(&chrono::Local), - ), - tags: Vec::::new(), - state: crate::issue::State::Done, - dependencies: None, - assignee: None, - description: String::from("oh yeah we got titles"), - comments: Vec::::new(), - dir, - }); - - let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - let mut comment_dir = dir.clone(); - let comment_uuid = String::from("9055dac36045fe36545bed7ae7b49347"); - comment_dir.push("comments"); - comment_dir.push(&comment_uuid); - let mut expected_comments = Vec::::new(); - expected_comments.push( - crate::comment::Comment { - uuid: comment_uuid, - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T10:08:38-06:00").unwrap().with_timezone(&chrono::Local), - description: String::from("This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe\n\nIt has multiple lines\n"), - dir: std::path::PathBuf::from(comment_dir), - } - ); - expected.add_issue( - crate::issue::Issue { - id: uuid, - author: String::from("Sebastian Kuzminsky "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T10:08:24-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::new(), - state: crate::issue::State::WontDo, - dependencies: None, - assignee: None, - description: String::from("issues out the wazoo\n\nLots of words\nthat don't say much\nbecause this is just\na test\n"), - comments: expected_comments, - dir, - }, - ); - assert_eq!(issues, expected); - } - - #[test] - fn read_issues_0002() { - let issues_dir = std::path::Path::new("test/0002/"); - let issues = Issues::new_from_dir(issues_dir).unwrap(); - - let mut expected = Issues::new(); - - let uuid = String::from("3fa5bfd93317ad25772680071d5ac325"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - expected.add_issue(crate::issue::Issue { - id: uuid, - author: String::from("sigil-03 "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:38:40-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::new(), - state: crate::issue::State::Done, - dependencies: None, - assignee: None, - description: String::from("oh yeah we got titles\n"), - comments: Vec::::new(), - dir, - }); - - let uuid = String::from("dd79c8cfb8beeacd0460429944b4ecbe"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - expected.add_issue( - crate::issue::Issue { - id: uuid, - author: String::from("sigil-03 "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:39:20-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::new(), - state: crate::issue::State::WontDo, - dependencies: None, - assignee: None, - description: String::from("issues out the wazoo\n\nLots of words\nthat don't say much\nbecause this is just\na test\n"), - comments: Vec::::new(), - dir, - }, - ); - - let uuid = String::from("a85f81fc5f14cb5d4851dd445dc9744c"); - let mut dir = std::path::PathBuf::from(issues_dir); - dir.push(&uuid); - expected.add_issue( - crate::issue::Issue { - id: uuid, - author: String::from("sigil-03 "), - creation_time: chrono::DateTime::parse_from_rfc3339("2025-07-24T08:39:02-06:00") - .unwrap() - .with_timezone(&chrono::Local), - done_time: None, - tags: Vec::::new(), - state: crate::issue::State::WontDo, - dependencies: Some(vec![ - crate::issue::IssueHandle::from("3fa5bfd93317ad25772680071d5ac325"), - crate::issue::IssueHandle::from("dd79c8cfb8beeacd0460429944b4ecbe"), - ]), - assignee: None, - description: String::from("issue with dependencies\n\na test has begun\nfor dependencies we seek\nintertwining life"), - comments: Vec::::new(), - dir, - }, - ); - assert_eq!(issues, expected); - } -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index b6245b9..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::str::FromStr; - -pub mod comment; -pub mod database; -pub mod git; -pub mod issue; -pub mod issues; - -use crate::issue::State; - -#[derive(Debug, thiserror::Error)] -pub enum ParseFilterError { - #[error("Failed to parse filter")] - ParseError, - #[error(transparent)] - IssueParseError(#[from] crate::issue::IssueError), - #[error(transparent)] - ChronoParseError(#[from] chrono::format::ParseError), -} - -// FIXME: It's easy to imagine a full dsl for filtering issues, for now -// i'm starting with obvious easy things. Chumsky looks appealing but -// more research is needed. -#[derive(Debug)] -pub struct Filter<'a> { - pub include_states: std::collections::HashSet, - pub include_assignees: std::collections::HashSet<&'a str>, - pub include_tags: std::collections::HashSet<&'a str>, - pub exclude_tags: std::collections::HashSet<&'a str>, - pub start_done_time: Option>, - pub end_done_time: Option>, -} - -impl<'a> Filter<'a> { - pub fn new() -> Filter<'a> { - Self { - include_states: std::collections::HashSet::::from([ - State::InProgress, - State::Blocked, - State::Backlog, - State::New, - ]), - include_assignees: std::collections::HashSet::<&'a str>::new(), - include_tags: std::collections::HashSet::<&'a str>::new(), - exclude_tags: std::collections::HashSet::<&'a str>::new(), - start_done_time: None, - end_done_time: None, - } - } - - pub fn parse(&mut self, filter_str: &'a str) -> Result<(), ParseFilterError> { - let tokens: Vec<&str> = filter_str.split("=").collect(); - if tokens.len() != 2 { - return Err(ParseFilterError::ParseError); - } - - match tokens[0] { - "state" => { - self.include_states.clear(); - for s in tokens[1].split(",") { - self.include_states - .insert(crate::issue::State::from_str(s)?); - } - } - - "assignee" => { - self.include_assignees.clear(); - for s in tokens[1].split(",") { - self.include_assignees.insert(s); - } - } - - "tag" => { - self.include_tags.clear(); - self.exclude_tags.clear(); - for s in tokens[1].split(",") { - if s.len() == 0 { - return Err(ParseFilterError::ParseError); - } - if s.chars().nth(0).unwrap() == '-' { - self.exclude_tags.insert(&s[1..]); - } else { - self.include_tags.insert(s); - } - } - } - - "done-time" => { - self.start_done_time = None; - self.end_done_time = None; - let times: Vec<&str> = tokens[1].split("..").collect(); - if times.len() > 2 { - return Err(ParseFilterError::ParseError); - } - if times[0].len() != 0 { - self.start_done_time = Some( - chrono::DateTime::parse_from_rfc3339(times[0])? - .with_timezone(&chrono::Local), - ); - } - if times[1].len() != 0 { - self.end_done_time = Some( - chrono::DateTime::parse_from_rfc3339(times[1])? - .with_timezone(&chrono::Local), - ); - } - } - - _ => { - println!("unknown filter string '{}'", filter_str); - return Err(ParseFilterError::ParseError); - } - } - - Ok(()) - } -} diff --git a/test/0000/3943fc5c173fdf41c0a22251593cd476/description b/test/0000/3943fc5c173fdf41c0a22251593cd476/description deleted file mode 100644 index e380829..0000000 --- a/test/0000/3943fc5c173fdf41c0a22251593cd476/description +++ /dev/null @@ -1,6 +0,0 @@ -this is the title of my issue - -This is the description of my issue. -It is multiple lines. -* Arbitrary contents -* But let's use markdown by convention diff --git a/test/0000/3943fc5c173fdf41c0a22251593cd476/tags b/test/0000/3943fc5c173fdf41c0a22251593cd476/tags deleted file mode 100644 index 04e82a6..0000000 --- a/test/0000/3943fc5c173fdf41c0a22251593cd476/tags +++ /dev/null @@ -1,3 +0,0 @@ -tag1 -TAG2 -i-am-also-a-tag diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c14/assignee b/test/0000/7792b063eef6d33e7da5dc1856750c14/assignee deleted file mode 100644 index fae06e3..0000000 --- a/test/0000/7792b063eef6d33e7da5dc1856750c14/assignee +++ /dev/null @@ -1 +0,0 @@ -beep boop diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c14/description b/test/0000/7792b063eef6d33e7da5dc1856750c14/description deleted file mode 100644 index 982085a..0000000 --- a/test/0000/7792b063eef6d33e7da5dc1856750c14/description +++ /dev/null @@ -1 +0,0 @@ -minimal \ No newline at end of file diff --git a/test/0000/7792b063eef6d33e7da5dc1856750c14/state b/test/0000/7792b063eef6d33e7da5dc1856750c14/state deleted file mode 100644 index 0737713..0000000 --- a/test/0000/7792b063eef6d33e7da5dc1856750c14/state +++ /dev/null @@ -1 +0,0 @@ -inprogress diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac325/description b/test/0001/3fa5bfd93317ad25772680071d5ac325/description deleted file mode 100644 index c73d593..0000000 --- a/test/0001/3fa5bfd93317ad25772680071d5ac325/description +++ /dev/null @@ -1 +0,0 @@ -oh yeah we got titles \ No newline at end of file diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac325/done_time b/test/0001/3fa5bfd93317ad25772680071d5ac325/done_time deleted file mode 100644 index d455c4d..0000000 --- a/test/0001/3fa5bfd93317ad25772680071d5ac325/done_time +++ /dev/null @@ -1 +0,0 @@ -2025-07-15T15:15:15-06:00 diff --git a/test/0001/3fa5bfd93317ad25772680071d5ac325/state b/test/0001/3fa5bfd93317ad25772680071d5ac325/state deleted file mode 100644 index 19f86f4..0000000 --- a/test/0001/3fa5bfd93317ad25772680071d5ac325/state +++ /dev/null @@ -1 +0,0 @@ -done diff --git a/test/0001/config.toml b/test/0001/config.toml deleted file mode 100644 index dfc15f2..0000000 --- a/test/0001/config.toml +++ /dev/null @@ -1 +0,0 @@ -states = [ "open", "closed" ] diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description deleted file mode 100644 index daa3d62..0000000 --- a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/comments/9055dac36045fe36545bed7ae7b49347/description +++ /dev/null @@ -1,3 +0,0 @@ -This is a comment on issue dd79c8cfb8beeacd0460429944b4ecbe - -It has multiple lines diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/description b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/description deleted file mode 100644 index a65ceb6..0000000 --- a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/description +++ /dev/null @@ -1,6 +0,0 @@ -issues out the wazoo - -Lots of words -that don't say much -because this is just -a test diff --git a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/state b/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/state deleted file mode 100644 index 7f19192..0000000 --- a/test/0001/dd79c8cfb8beeacd0460429944b4ecbe/state +++ /dev/null @@ -1 +0,0 @@ -wontdo diff --git a/test/0002/3fa5bfd93317ad25772680071d5ac325/description b/test/0002/3fa5bfd93317ad25772680071d5ac325/description deleted file mode 100644 index 18a1926..0000000 --- a/test/0002/3fa5bfd93317ad25772680071d5ac325/description +++ /dev/null @@ -1 +0,0 @@ -oh yeah we got titles diff --git a/test/0002/3fa5bfd93317ad25772680071d5ac325/state b/test/0002/3fa5bfd93317ad25772680071d5ac325/state deleted file mode 100644 index 19f86f4..0000000 --- a/test/0002/3fa5bfd93317ad25772680071d5ac325/state +++ /dev/null @@ -1 +0,0 @@ -done diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/description b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/description deleted file mode 100644 index 42e2ce3..0000000 --- a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/description +++ /dev/null @@ -1,5 +0,0 @@ -issue with dependencies - -a test has begun -for dependencies we seek -intertwining life \ No newline at end of file diff --git a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/state b/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/state deleted file mode 100644 index 7f19192..0000000 --- a/test/0002/a85f81fc5f14cb5d4851dd445dc9744c/state +++ /dev/null @@ -1 +0,0 @@ -wontdo diff --git a/test/0002/config.toml b/test/0002/config.toml deleted file mode 100644 index dfc15f2..0000000 --- a/test/0002/config.toml +++ /dev/null @@ -1 +0,0 @@ -states = [ "open", "closed" ] diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/description b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/description deleted file mode 100644 index a65ceb6..0000000 --- a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/description +++ /dev/null @@ -1,6 +0,0 @@ -issues out the wazoo - -Lots of words -that don't say much -because this is just -a test diff --git a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/state b/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/state deleted file mode 100644 index 7f19192..0000000 --- a/test/0002/dd79c8cfb8beeacd0460429944b4ecbe/state +++ /dev/null @@ -1 +0,0 @@ -wontdo diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index dd3cb2f..0000000 --- a/tools/README.md +++ /dev/null @@ -1,4 +0,0 @@ -This directory contains small helper scripts and tools that are peripheral -or tangent to the main entomologist tool. - -We make no guarantees about functionality or correctness. diff --git a/tools/done-last-week b/tools/done-last-week deleted file mode 100755 index da9ec94..0000000 --- a/tools/done-last-week +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -START=$(date --iso-8601=seconds --date='last monday - 1 week') -END=$(date --iso-8601=seconds --date='last monday') - -#echo START=${START} -#echo END=${END} - -ent list \ - state=done \ - done-time="${START}..${END}" diff --git a/tools/time-ent b/tools/time-ent deleted file mode 100755 index 366a02c..0000000 --- a/tools/time-ent +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# -# * Create a temporary ent issue database branch based on a specific -# commit in `entomologist-data`. -# -# * Perform some ent operations on this temporary branch and measure -# the runtime. -# -# * Clean up by deleteting the temporary branch. - -set -e -#set -x - - -# This is a commit in the `entomologist-data` branch that we're somewhat -# arbitrarily using here to time different `ent` operations. -TEST_COMMIT=a33f1165d77571d770f1a1021afe4c07360247f0 - -# This is the branch that we create from the above commit and test our -# `ent` operations on. We'll delete this branch when we're done with -# the tests. -TEST_BRANCH=$(mktemp --dry-run entomologist-data-XXXXXXXX) - - -function time_ent() { - echo timing: ent "$@" - time -p ent -b "${TEST_BRANCH}" "$@" - echo -} - - -git branch "${TEST_BRANCH}" "${TEST_COMMIT}" - -time_ent tag 7e2a3a59fb6b77403ff1035255367607 -time_ent tag 7e2a3a59fb6b77403ff1035255367607 new-tag - -time_ent assign 7e2a3a59fb6b77403ff1035255367607 -time_ent assign 7e2a3a59fb6b77403ff1035255367607 new-user - -time_ent done-time 7e2a3a59fb6b77403ff1035255367607 -time_ent done-time 7e2a3a59fb6b77403ff1035255367607 2025-04-01T01:23:45-06:00 - -git branch -D "${TEST_BRANCH}"