Compare commits

...

77 commits
v0.13.7 ... dev

Author SHA1 Message Date
18a537b18e build: add cargo machette to push-next workflow
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 6m49s
Rust / build (map[name:stable]) (push) Successful in 7m42s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 54s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-17 15:20:34 +01:00
ef6474ef9f test: also run CI tests against Rust nightly
All checks were successful
Rust / build (map[name:nightly]) (push) Successful in 6m23s
Rust / build (map[name:stable]) (push) Successful in 7m58s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 47s
2024-09-17 11:44:52 +01:00
dbf1a0db27 docs: add demo gif of tui
All checks were successful
Rust / build (push) Successful in 6m10s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m31s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-16 13:54:33 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
91c5973e31 chore: release
All checks were successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
ci/woodpecker/pull_request_closed/cron-docker-builder Pipeline was successful
ci/woodpecker/pull_request_closed/push-next Pipeline was successful
ci/woodpecker/pull_request_closed/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 3m44s
ci/woodpecker/tag/cron-docker-builder Pipeline was successful
ci/woodpecker/tag/push-next Pipeline was successful
ci/woodpecker/tag/tag-created Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
Rust / build (push) Successful in 6m16s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-09-14 14:24:04 +00:00
978205b823 feat(tui): add time and version in border
All checks were successful
Rust / build (push) Successful in 6m12s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m6s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-14 15:13:45 +01:00
8359d0d7ca refactor: Update TUI sooner when receiving CI status
All checks were successful
Rust / build (push) Successful in 6m6s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m49s
Looking to avoid getting stuck on 'Checking CI status', but this doesn't
appear to be where the problem is coming from.
2024-09-14 12:40:30 +01:00
93cf6f83df chore: add run and run-ui recipes to justfile
All checks were successful
Rust / build (push) Successful in 6m9s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m34s
2024-09-14 12:26:06 +01:00
681d85aac1 chore: remove manual crates.io publish recipe from justfile
All checks were successful
Rust / build (push) Successful in 6m9s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m50s
2024-09-14 12:22:29 +01:00
d4f16e6f5e feat: should fetch repo on startup when not cloning
All checks were successful
Rust / build (push) Successful in 6m8s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m5s
We already have a copy of the repo, so we don't clone, but we should
perform a `git fetch` to make sure it is up-to-date.
2024-09-14 12:19:24 +01:00
048111202a feat: Remove branches when fetching from remote
All checks were successful
Rust / build (push) Successful in 6m9s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m30s
2024-09-14 07:42:24 +01:00
3ea7f36c98 build(docker): Don't break when debian drops old packge versions
Some checks failed
Rust / build (push) Successful in 6m8s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
Release Please / Release-plz (push) Failing after 1h0m59s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Debian routinly drop older versions of packages from the repositories as
new versions replace them. Pinning the version causes the build to break
at seamingly random times when the pinned version gets dropped.
2024-09-14 07:38:13 +01:00
6c60e3fb7a refactor: reimplement git fetch using git
All checks were successful
Rust / build (push) Successful in 6m11s
Release Please / Release-plz (push) Successful in 1m38s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-13 18:55:21 +01:00
313d6d79c5 docs: mark tui as complete on roadmap
All checks were successful
Rust / build (push) Successful in 6m18s
Release Please / Release-plz (push) Successful in 1m6s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
2024-09-13 09:48:38 +01:00
189d579d33 docs: Add missing port mapping parameter for running in docker
All checks were successful
Rust / build (push) Successful in 6m11s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 56s
2024-09-13 08:59:38 +01:00
a77c6335a6 chore: ignore .local/ directory
All checks were successful
Rust / build (push) Successful in 6m23s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m27s
This directory is created when running git-next via docker with the --ui
option.
2024-09-13 08:53:04 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
82241de0dd chore: release
All checks were successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
ci/woodpecker/pull_request_closed/cron-docker-builder Pipeline was successful
ci/woodpecker/pull_request_closed/push-next Pipeline was successful
ci/woodpecker/pull_request_closed/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 3m53s
ci/woodpecker/tag/cron-docker-builder Pipeline was successful
ci/woodpecker/tag/push-next Pipeline was successful
ci/woodpecker/tag/tag-created Pipeline was successful
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
Rust / build (push) Successful in 6m11s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-09-12 19:46:24 +00:00
664e424d1a fix(tui): make tui work from docker image
All checks were successful
Rust / build (push) Successful in 6m13s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m26s
Add missing environment variable in Dockerfile and gave example command
to run via docker.

Closes kemitix/git-next#154
2024-09-12 19:50:29 +01:00
df6b96fbfd fix(tui): alerts, such as WIP aren't being reset
All checks were successful
Rust / build (push) Successful in 6m11s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m20s
2024-09-12 10:37:53 +01:00
566125f5c0 fix(test): tests requiring .git pass when not present
All checks were successful
Rust / build (push) Successful in 6m12s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m32s
These are tests that assume they are running in a locally checked out
git repository. If that isn't the case, e.g. when using jujutsu, then
the tests should not fail. They will continue to run as normal under
CI conditions as those do use a locally checked out git repository.
2024-09-12 10:37:46 +01:00
80af909ab0 build(push-next): use rust image v1.81.0
All checks were successful
Release Please / Release-plz (push) Successful in 1m18s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Rust / build (push) Successful in 10m7s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-08 12:07:18 +01:00
ecd460cdfb fix(tui): update ui when push next or main finishes
All checks were successful
Rust / build (push) Successful in 8m54s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m34s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
Removes the artificial pause while we wait for any CI to start before
checking the CI status.

Closes kemitix/git-next#160
2024-09-06 18:28:02 +01:00
Renovate Bot
35c2057f05 chore(deps): update docker.io/rust docker tag to v1.81.0
All checks were successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
ci/woodpecker/pull_request_closed/cron-docker-builder Pipeline was successful
ci/woodpecker/pull_request_closed/push-next Pipeline was successful
ci/woodpecker/pull_request_closed/tag-created Pipeline was successful
Rust / build (push) Successful in 7m38s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m0s
2024-09-06 07:31:55 +00:00
d2e2d00fe1 fix(tui): don't set background for normal repo alias
All checks were successful
Rust / build (push) Successful in 6m24s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 56s
This didn't look good when using a light coloured terminal.
2024-09-06 08:19:43 +01:00
e759e495fd feat: optionally specify max commits between dev and main
All checks were successful
Rust / build (push) Successful in 6m21s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m9s
The default is 25.

Closes kemitix/git-next#121
2024-09-06 08:10:10 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
3672fd5d45 chore: release
All checks were successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
ci/woodpecker/pull_request_closed/cron-docker-builder Pipeline was successful
ci/woodpecker/pull_request_closed/push-next Pipeline was successful
ci/woodpecker/pull_request_closed/tag-created Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
Release Please / Release-plz (push) Successful in 6m17s
ci/woodpecker/tag/push-next Pipeline was successful
ci/woodpecker/tag/cron-docker-builder Pipeline was successful
ci/woodpecker/tag/tag-created Pipeline was successful
Rust / build (push) Successful in 9m2s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-09-04 05:45:15 +00:00
1f0b5e867c fix(tui): alerts are cleared on next repo update
All checks were successful
Rust / build (push) Successful in 8m54s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m21s
Closes kemitix/git-next#151
2024-09-04 06:35:41 +01:00
8ca7aad3c3 docs: Expand docker docmentation
All checks were successful
Rust / build (push) Successful in 7m44s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m50s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-03 20:17:59 +01:00
d923e831f0 build(docker): enable passing arguments when running via docker
All checks were successful
Rust / build (push) Successful in 6m19s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m0s
2024-09-03 20:08:40 +01:00
5e0cf270dd fix: shutdown properly on error
All checks were successful
Rust / build (push) Successful in 9m7s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m2s
2024-09-03 20:08:12 +01:00
b4a4631a1d fix: shutdown properly on file parse error
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m24s
ci/woodpecker/push/push-next Pipeline was successful
Rust / build (push) Successful in 10m0s
Closes kemitix/git-next#152
2024-09-03 06:53:12 +01:00
181ec8eb0f build(woodpecker): build docker image on push to next
All checks were successful
Rust / build (push) Successful in 8m28s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 56s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-09-01 13:53:03 +01:00
47cbbad8e7 build(docker): update debian libssl3 dependency
All checks were successful
Rust / build (push) Successful in 6m2s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m55s
2024-09-01 13:52:47 +01:00
e793c18215 docs(release): add links
All checks were successful
Rust / build (push) Successful in 7m31s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m30s
2024-09-01 13:26:49 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
224b63deb1 chore: release
Some checks failed
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
ci/woodpecker/pull_request_closed/push-next Pipeline was successful
ci/woodpecker/pull_request_closed/cron-docker-builder Pipeline was successful
ci/woodpecker/pull_request_closed/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 3m17s
ci/woodpecker/tag/cron-docker-builder Pipeline was successful
ci/woodpecker/tag/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
ci/woodpecker/tag/tag-created Pipeline failed
Rust / build (push) Successful in 6m3s
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-09-01 12:20:59 +00:00
3c01a822fd feat: improved error display when startup fails
All checks were successful
Rust / build (push) Successful in 10m8s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m8s
2024-09-01 13:10:14 +01:00
4160b6d6ee fix: use configured branch names in user notification
All checks were successful
Rust / build (push) Successful in 7m19s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m41s
Remove near-duplicate to string implementations.
2024-09-01 08:38:08 +01:00
853b862f10 feat(tui): clean up alert display
All checks were successful
Rust / build (push) Successful in 7m14s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m1s
2024-09-01 07:35:58 +01:00
ca70c03e8b refactor: flatten nested blocks with early returns
All checks were successful
Rust / build (push) Successful in 5m52s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m4s
2024-09-01 07:18:05 +01:00
f475095f4a feat(tui): remove some borders to clean up appearance
All checks were successful
Rust / build (push) Successful in 10m3s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 53s
2024-09-01 06:57:16 +01:00
eae351d8a4 build(mac): make it clean when mac tunnel has closed
All checks were successful
Rust / build (push) Successful in 5m49s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 54s
2024-09-01 06:35:55 +01:00
c1564807f8 refactor: merge identical match branches
All checks were successful
Rust / build (push) Successful in 7m16s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m5s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-08-31 22:32:09 +01:00
b24005c3fe fix: remove unused imports
All checks were successful
Rust / build (push) Successful in 10m7s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m3s
2024-08-31 22:31:49 +01:00
22ce2d431a feat(tui): make progression of branches clearer
All checks were successful
Rust / build (push) Successful in 7m16s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m19s
Using the branch names configured for the repo, indicate that the
branches move towards dev.
2024-08-31 19:59:16 +01:00
be41842dae feat(tui): remove label from repo identity widget
All checks were successful
Rust / build (push) Successful in 10m11s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 2m1s
2024-08-31 19:55:33 +01:00
9720fd01fc feat(tui): hightlight repo alias in red when in alert
All checks were successful
Rust / build (push) Successful in 10m15s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m42s
2024-08-31 19:32:56 +01:00
8550adf79e fix(tui): remove logging from inside ui loop
All checks were successful
Rust / build (push) Successful in 10m6s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m7s
2024-08-31 19:26:20 +01:00
2b09872131 feat(tui): branch names look more like 'pills'
All checks were successful
Rust / build (push) Successful in 10m7s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m21s
Use round brackets
2024-08-31 19:20:50 +01:00
d2048d8a34 fix(tui): don't show HEAD in log
All checks were successful
Rust / build (push) Successful in 7m17s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m25s
2024-08-31 18:20:24 +01:00
02609fdc11 fix(tui): improve colour contrast on light background
All checks were successful
Rust / build (push) Successful in 7m14s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m53s
2024-08-31 18:19:05 +01:00
01f54d79ae feat(tui): highlight branchs in log
All checks were successful
Rust / build (push) Successful in 10m6s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m54s
2024-08-31 18:18:57 +01:00
1df982005e chore(tui): add regex dependency
All checks were successful
Rust / build (push) Successful in 10m6s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m16s
2024-08-31 14:17:28 +01:00
2abb36ad6c fix(tui): remove unused import
All checks were successful
Rust / build (push) Successful in 7m20s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m44s
2024-08-31 14:17:05 +01:00
576eaaf990 refactor(tui): introduce LogLine to wrap log formatting
All checks were successful
Rust / build (push) Successful in 10m0s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m58s
2024-08-31 13:33:45 +01:00
97b685363a refactor(tui): simplify repo identity widget
All checks were successful
Rust / build (push) Successful in 5m56s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m14s
Adds blue to repo alias
2024-08-31 13:23:18 +01:00
a2940ec753 refactor: rename method as peel
All checks were successful
Rust / build (push) Successful in 9m56s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m25s
Method on newtypes `unwrap` could be confused with the risky method of
the same name for Option and Result.
2024-08-31 11:18:09 +01:00
d5d313064a fix(alert): typo in email message
All checks were successful
Rust / build (push) Successful in 5m53s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 40s
2024-08-31 09:53:53 +01:00
f9e305afa4 feat(tui): hightlight status message in colour
All checks were successful
Rust / build (push) Successful in 5m54s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m12s
2024-08-31 09:53:53 +01:00
4555b3ae09 fix(repo): avoid blocking threads when pausing
All checks were successful
Rust / build (push) Successful in 7m13s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m16s
2024-08-31 09:53:53 +01:00
64da1d8a34 fix(test): give actix more time to process message
All checks were successful
Rust / build (push) Successful in 5m50s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m16s
2024-08-31 09:53:53 +01:00
a650996ecd fix(test): give actix more time to process message
All checks were successful
Rust / build (push) Successful in 10m4s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m1s
2024-08-31 09:31:27 +01:00
eca556f976 feat(tui): use moving heart emoji as liveness indicator
All checks were successful
Rust / build (push) Successful in 7m17s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 52s
The heart moves between two positions every second as long as the ui is
being updated.
2024-08-31 08:56:43 +01:00
a3dd82705f fix(test): give actix more time to process message
All checks were successful
Rust / build (push) Successful in 7m13s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m18s
2024-08-31 08:56:43 +01:00
7504ab5a2d fix(tui): improve reliability of status updates
All checks were successful
Rust / build (push) Successful in 7m16s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m25s
2024-08-31 07:03:58 +01:00
eb42745383 build: add start-mac-tunnel
All checks were successful
Rust / build (push) Successful in 10m0s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 52s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-08-30 09:12:57 +01:00
126d5d3ef5 fix: create git graph log to after doing a fetch
All checks were successful
Rust / build (push) Successful in 10m5s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 51s
2024-08-30 09:12:57 +01:00
4f6669548c feat(tui): add scrolling when overflow screen
All checks were successful
Rust / build (push) Successful in 10m2s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m31s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-08-29 09:40:16 +01:00
52bd9cc30b feat(tui): forge widgets only use required lines
All checks were successful
Rust / build (push) Successful in 10m32s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m30s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
Rather than filling all the space available, the ForgeWidget now only
uses as many lines as it needs to show its contents.
2024-08-28 22:25:31 +01:00
2959bdfad4 feat(tui): repo widgets only use required lines
All checks were successful
Rust / build (push) Successful in 10m58s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 50s
Rather than filling all the space available, the RepoWidget now only
uses as many lines as it needs to show its contents.
2024-08-28 09:14:02 +01:00
f85cbce4c6 refactor(tui): child widget can provide constraint to container
All checks were successful
Rust / build (push) Successful in 7m13s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m16s
2024-08-28 07:53:56 +01:00
4517fe62e4 feat(tui): move forge alias to left and add prefix
All checks were successful
Rust / build (push) Successful in 7m14s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m10s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-08-27 19:15:36 +01:00
c6bf287ed1 feat(tui): remove count of forges
All checks were successful
Rust / build (push) Successful in 10m29s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m27s
2024-08-27 19:15:21 +01:00
35e3676930 fix(tui): remove logging of tui updates
All checks were successful
Rust / build (push) Successful in 10m32s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 49s
2024-08-27 07:20:05 +01:00
95e9209e17 feat(tui): remove duplicate messages from repo body
All checks were successful
Rust / build (push) Successful in 7m12s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m22s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
The latest message is still displayed in the repo header
2024-08-26 08:39:33 +01:00
d1a685ae34 feat(tui): highlight user interventions in red
All checks were successful
Rust / build (push) Successful in 7m27s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 49s
2024-08-26 08:21:31 +01:00
e489fb36e9 refactor(tui): merge repo widgets into one
All checks were successful
Rust / build (push) Successful in 7m12s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 51s
2024-08-26 08:03:52 +01:00
09ff4c3a54 build(docker): enable all features in docker images
All checks were successful
Rust / build (push) Successful in 11m6s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 1m8s
Add support for the experimental TUI when using the docker image.
2024-08-26 06:51:37 +01:00
48a5ed7a3b docs: add notes on how to do a release
All checks were successful
Rust / build (push) Successful in 7m24s
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Release Please / Release-plz (push) Successful in 3m22s
ci/woodpecker/cron/cron-docker-builder Pipeline was successful
ci/woodpecker/cron/push-next Pipeline was successful
ci/woodpecker/cron/tag-created Pipeline was successful
2024-08-25 19:28:47 +01:00
89 changed files with 2291 additions and 994 deletions

View file

@ -22,13 +22,13 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run release-plz release-pr - name: Run release-plz release-pr
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2 uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with: with:
args: release-plz release-pr --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }} args: release-plz release-pr --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
env: env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Run release-plz release - name: Run release-plz release
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2 uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with: with:
args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }} args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
env: env:

View file

@ -13,26 +13,37 @@ jobs:
build: build:
runs-on: docker runs-on: docker
strategy:
matrix:
toolchain:
- name: stable
- name: nightly
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Format - name: Machete
uses: https://git.kemitix.net/kemitix/rust@v1.80.1-1 uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with: with:
args: cargo fmt --all -- --check args: ${{ matrix.toolchain.name }} cargo machete
- name: Format
uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with:
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
- name: Clippy - name: Clippy
uses: https://git.kemitix.net/kemitix/rust@v1.80.1-1 uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with: with:
args: cargo hack --feature-powerset clippy args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset clippy
- name: Build - name: Build
uses: https://git.kemitix.net/kemitix/rust@v1.80.1-1 uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with: with:
args: cargo hack --feature-powerset build args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset build
- name: Test - name: Test
uses: https://git.kemitix.net/kemitix/rust@v1.80.1-1 uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with: with:
args: cargo hack --feature-powerset test args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset test

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
# git-next ui logs
.local/
# ---> Rust # ---> Rust
# Generated by Cargo # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables

View file

@ -11,3 +11,16 @@ steps:
repository_token: "776a3b928b852472c2af727a360c85c00af64b9f" repository_token: "776a3b928b852472c2af727a360c85c00af64b9f"
prefix_regex: "(#|//) (TODO|FIXME): " prefix_regex: "(#|//) (TODO|FIXME): "
debug: false debug: false
docker-build:
when:
- event: push
branch: next
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
image: docker.io/woodpeckerci/plugin-docker-buildx:4.2.0
settings:
username: kemitix
repo: git.kemitix.net/kemitix/git-next
dockerfile: Dockerfile
auto_tag: false
dry-run: true # don't push to remote repo

View file

@ -2,6 +2,128 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## `git-next-core` - [0.13.11](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.10...git-next-core-v0.13.11) - 2024-09-14
### Added
- should fetch repo on startup when not cloning
- Remove branches when fetching from remote
### Other
- reimplement git fetch using git
## `git-next` - [0.13.11](https://git.kemitix.net/kemitix/git-next/compare/v0.13.10...v0.13.11) - 2024-09-14
### Added
- *(tui)* add time and version in border
- should fetch repo on startup when not cloning
- Remove branches when fetching from remote
### Other
- Update TUI sooner when receiving CI status
- reimplement git fetch using git
- mark tui as complete on roadmap
- Add missing port mapping parameter for running in docker
## `git-next-forge-github` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.9...git-next-forge-github-v0.13.10) - 2024-09-12
### Added
- optionally specify max commits between dev and main
## `git-next-forge-forgejo` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.9...git-next-forge-forgejo-v0.13.10) - 2024-09-12
### Added
- optionally specify max commits between dev and main
## `git-next-core` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.9...git-next-core-v0.13.10) - 2024-09-12
### Added
- optionally specify max commits between dev and main
## `git-next` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/v0.13.9...v0.13.10) - 2024-09-12
### Added
- optionally specify max commits between dev and main
### Fixed
- *(tui)* make tui work from docker image
- *(tui)* alerts, such as WIP aren't being reset
- *(test)* tests requiring .git pass when not present
- *(tui)* update ui when push next or main finishes
- *(tui)* don't set background for normal repo alias
## `git-next` - [0.13.9](https://git.kemitix.net/kemitix/git-next/compare/v0.13.8...v0.13.9) - 2024-09-04
### Fixed
- *(tui)* alerts are cleared on next repo update
- shutdown properly on error
- shutdown properly on file parse error
### Other
- Expand docker docmentation
## `git-next-forge-forgejo` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.7...git-next-forge-forgejo-v0.13.8) - 2024-09-01
### Other
- flatten nested blocks with early returns
- rename method as peel
## `git-next-core` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.7...git-next-core-v0.13.8) - 2024-09-01
### Fixed
- use configured branch names in user notification
- create git graph log to after doing a fetch
### Other
- flatten nested blocks with early returns
- rename method as peel
## `git-next` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/v0.13.7...v0.13.8) - 2024-09-01
### Added
- improved error display when startup fails
- *(tui)* clean up alert display
- *(tui)* remove some borders to clean up appearance
- *(tui)* make progression of branches clearer
- *(tui)* remove label from repo identity widget
- *(tui)* hightlight repo alias in red when in alert
- *(tui)* branch names look more like 'pills'
- *(tui)* highlight branchs in log
- *(tui)* hightlight status message in colour
- *(tui)* use moving heart emoji as liveness indicator
- *(tui)* add scrolling when overflow screen
- *(tui)* forge widgets only use required lines
- *(tui)* repo widgets only use required lines
- *(tui)* move forge alias to left and add prefix
- *(tui)* remove count of forges
- *(tui)* remove duplicate messages from repo body
- *(tui)* highlight user interventions in red
### Fixed
- use configured branch names in user notification
- remove unused imports
- *(tui)* remove logging from inside ui loop
- *(tui)* don't show HEAD in log
- *(tui)* improve colour contrast on light background
- *(tui)* remove unused import
- *(alert)* typo in email message
- *(repo)* avoid blocking threads when pausing
- *(test)* give actix more time to process message
- *(test)* give actix more time to process message
- *(test)* give actix more time to process message
- *(tui)* improve reliability of status updates
- create git graph log to after doing a fetch
- *(tui)* remove logging of tui updates
### Other
- flatten nested blocks with early returns
- merge identical match branches
- *(tui)* add regex dependency
- *(tui)* introduce LogLine to wrap log formatting
- *(tui)* simplify repo identity widget
- rename method as peel
- *(tui)* child widget can provide constraint to container
- *(tui)* merge repo widgets into one
## `git-next-core` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.6...git-next-core-v0.13.7) - 2024-08-25 ## `git-next-core` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.6...git-next-core-v0.13.7) - 2024-08-25
### Added ### Added

93
Cargo.lock generated
View file

@ -320,6 +320,28 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bon"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b71187ef9d11cfa48c023d574a00ec5e4850dcb145ef51619d99cc119486cb"
dependencies = [
"bon-macros",
]
[[package]]
name = "bon-macros"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f8407447d440da7b3de982f286d15e30e7646bef4ebca994eebebaa1690fd9c"
dependencies = [
"darling",
"ident_case",
"proc-macro2",
"quote",
"syn 2.0.76",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.10.0" version = "1.10.0"
@ -592,6 +614,41 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.76",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.76",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.0.1" version = "6.0.1"
@ -1035,13 +1092,15 @@ dependencies = [
[[package]] [[package]]
name = "git-next" name = "git-next"
version = "0.13.7" version = "0.13.11"
dependencies = [ dependencies = [
"actix", "actix",
"actix-rt", "actix-rt",
"anyhow", "anyhow",
"assert2", "assert2",
"bon",
"bytes", "bytes",
"chrono",
"clap", "clap",
"color-eyre", "color-eyre",
"derive-with", "derive-with",
@ -1060,6 +1119,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"rand", "rand",
"ratatui", "ratatui",
"regex",
"rstest", "rstest",
"secrecy", "secrecy",
"sendmail", "sendmail",
@ -1068,17 +1128,19 @@ dependencies = [
"test-log", "test-log",
"thiserror", "thiserror",
"time", "time",
"tokio",
"toml", "toml",
"tracing", "tracing",
"tracing-error", "tracing-error",
"tracing-subscriber", "tracing-subscriber",
"tui-scrollview",
"ulid", "ulid",
"warp", "warp",
] ]
[[package]] [[package]]
name = "git-next-core" name = "git-next-core"
version = "0.13.7" version = "0.13.11"
dependencies = [ dependencies = [
"actix", "actix",
"assert2", "assert2",
@ -1106,7 +1168,7 @@ dependencies = [
[[package]] [[package]]
name = "git-next-forge-forgejo" name = "git-next-forge-forgejo"
version = "0.13.7" version = "0.13.11"
dependencies = [ dependencies = [
"assert2", "assert2",
"async-trait", "async-trait",
@ -1122,7 +1184,7 @@ dependencies = [
[[package]] [[package]]
name = "git-next-forge-github" name = "git-next-forge-github"
version = "0.13.7" version = "0.13.11"
dependencies = [ dependencies = [
"assert2", "assert2",
"async-trait", "async-trait",
@ -2349,6 +2411,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -2385,6 +2453,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]] [[package]]
name = "inotify" name = "inotify"
version = "0.9.6" version = "0.9.6"
@ -4292,6 +4366,17 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tui-scrollview"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27a65189ac0c5f8af32660c453a1babae3ac7e72791b9dbeb1221073569f44ea"
dependencies = [
"indoc",
"ratatui",
"rstest",
]
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.21.0" version = "0.21.0"

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.13.7" version = "0.13.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -28,9 +28,12 @@ git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
# TUI # TUI
ratatui = "0.28" ratatui = "0.28"
directories = "5.0.1" directories = "5.0"
lazy_static = "1.5.0" lazy_static = "1.5"
color-eyre = "0.6.3" color-eyre = "0.6"
tui-scrollview = "0.4"
regex = "1.10"
chrono = "0.4"
# CLI parsing # CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] } clap = { version = "4.5", features = ["cargo", "derive"] }
@ -78,6 +81,7 @@ time = "0.3"
standardwebhooks = "1.0" standardwebhooks = "1.0"
# boilerplate # boilerplate
bon = "2.0"
derive_more = { version = "1.0.0-beta", features = [ derive_more = { version = "1.0.0-beta", features = [
"as_ref", "as_ref",
"constructor", "constructor",

View file

@ -12,20 +12,19 @@ FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --profile release --recipe-path recipe.json RUN cargo chef cook --profile release --recipe-path recipe.json
COPY . . COPY . .
RUN cargo build --release --bin git-next && \ RUN cargo build --release --bin git-next --all-features && \
strip target/release/git-next strip target/release/git-next
FROM docker.io/debian:stable-20240722-slim AS runtime FROM docker.io/debian:stable-20240904-slim AS runtime
WORKDIR /app WORKDIR /app
RUN apt-get update && \ RUN apt-get update && \
apt-get install --no-install-recommends -y \ apt-get satisfy -y "git (>=2.39), libssl3 (>=3.0.14), libdbus-1-dev (>=1.14.10), ca-certificates (>=20230311)" \
git=1:2.39.2-1.1 \
libssl3=3.0.13-1~deb12u1 \
libdbus-1-dev=1.14.10-1~deb12u1 \
ca-certificates=20230311 \
&& \ && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
USER 1000 USER 1000
COPY --from=builder /app/target/release/git-next /usr/local/bin COPY --from=builder /app/target/release/git-next /usr/local/bin
ENTRYPOINT [ "/usr/local/bin/git-next", "server", "start" ] ENV HOME=/app
ENTRYPOINT [ "/usr/local/bin/git-next" ]
CMD [ "server", "start" ]

View file

@ -1,4 +1,4 @@
FROM docker.io/rust:1.80.1-bookworm FROM docker.io/rust:1.81.0-bookworm
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y libdbus-1-dev && \ apt-get install -y libdbus-1-dev && \

View file

@ -6,4 +6,7 @@
development workflows where each commit must pass CI before being included in development workflows where each commit must pass CI before being included in
the main branch. the main branch.
![Demo](./demo.gif)
See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information. See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information.

18
RELEASE.md Normal file
View file

@ -0,0 +1,18 @@
# How to create a new Release
## TLDR
1. Merge PR `chore: release`
2. Wait for [`push-main` workflow](https://git.kemitix.net/kemitix/git-next/actions?workflow=push-main.yml) to complete
3. Replace [Release Notes](https://git.kemitix.net/kemitix/git-next/releases) body with details from [CHANGELOG](https://git.kemitix.net/kemitix/git-next/src/branch/main/CHANGELOG.md) (remove crates and duplicates)
4. Update thread: <https://mitra.kemitix.net/post/01907ef5-5bd9-b0b6-2b8a-e29762541d78>
## Detail
The workflow in `.forgejo/workflows/push-main.yaml` will create or update a PR whenever `main` branch is updated.
The create the new release, merge this PR. It will automatically create the Release on the `git-next` repo, and publish crates to <https://crates/io>.
The Release notes that are included with the create Release are currently incorrect, and will need to be manually updated from the `CHANGELOG.md`. Remove crate headers and any resulting duplicates.
Post an update to the Fediverse. See link above.

View file

@ -16,7 +16,7 @@ categories = { workspace = true }
default = ["forgejo", "github", "tui"] default = ["forgejo", "github", "tui"]
forgejo = ["git-next-forge-forgejo"] forgejo = ["git-next-forge-forgejo"]
github = ["git-next-forge-github"] github = ["git-next-forge-github"]
tui = ["ratatui", "directories", "lazy_static"] tui = ["ratatui", "directories", "lazy_static", "tui-scrollview", "regex", "chrono"]
[dependencies] [dependencies]
git-next-core = { workspace = true } git-next-core = { workspace = true }
@ -28,6 +28,9 @@ ratatui = { workspace = true, optional = true }
directories = { workspace = true, optional = true } directories = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true } lazy_static = { workspace = true, optional = true }
color-eyre = { workspace = true } color-eyre = { workspace = true }
tui-scrollview = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
chrono = { workspace = true, optional = true }
# CLI parsing # CLI parsing
clap = { workspace = true } clap = { workspace = true }
@ -49,8 +52,10 @@ toml = { workspace = true }
# Actors # Actors
actix = { workspace = true } actix = { workspace = true }
actix-rt = { workspace = true } actix-rt = { workspace = true }
tokio = { workspace = true }
# boilerplate # boilerplate
bon = { workspace = true }
derive_more = { workspace = true } derive_more = { workspace = true }
derive-with = { workspace = true } derive-with = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }

View file

@ -59,7 +59,7 @@ cargo install --path crates/cli
- [x] cli - [x] cli
- [x] server - [x] server
- [x] notifications - notify user when intervention required (e.g. to rebase) - [x] notifications - notify user when intervention required (e.g. to rebase)
- [ ] tui overview - [x] tui overview
- [ ] webui overview - [ ] webui overview
## Branch Names ## Branch Names
@ -198,13 +198,14 @@ forge_type = "GitHub"
hostname = "github.com" hostname = "github.com"
user = "username" user = "username"
token = "api-key" token = "api-key"
max_dev_commits = 25
``` ```
- **forge_type** - one of: `ForgeJo` or `GitHub` - **forge_type** - one of: `ForgeJo` or `GitHub`
- **hostname** - the hostname for the forge. - **hostname** - the hostname for the forge.
- **user** - the user to authenticate as - **user** - the user to authenticate as
- **token** - application token for the user. See below for the permissions - **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
required for on each forge. - **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
Generally, the `user` will need to be able to push to `main` and to _force-push_ Generally, the `user` will need to be able to push to `main` and to _force-push_
to `next`. to `next`.
@ -576,6 +577,58 @@ world = { repo = "user/world", branch = "master", main = "master", next = "upcom
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions. The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
## Docker
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
```shell
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
```
### Docker Compose
Here is an example `docker-compose.yml`:
```yaml
services:
server:
image: git.kemitix.net/kemitix/git-next:latest
container_name: git-next-server
restart: unless-stopped
environment:
RUST_LOG: "hyper=warn,info"
ports:
- 8080:8092
volumes:
- ./:/app/
```
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
`8092` and that you are using a reverse proxy to route traffic arriving at
`listen.url` to port `8080`.
### Docker Run
This will run with the `server start` options:
```shell
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
```
To perform `server init`:
```shell
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
```
To perform repo `init`:
```shell
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
```
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
## Contributing ## Contributing
Contributions to `git-next` are welcome! If you find a bug or have a feature Contributions to `git-next` are welcome! If you find a bug or have a feature

View file

@ -18,7 +18,9 @@ impl Handler<NotifyUser> for AlertsActor {
}; };
let net = self.net.clone(); let net = self.net.clone();
let shout = shout.clone(); let shout = shout.clone();
if let Some(user_notification) = self.history.sendable(msg.unwrap()) { let Some(user_notification) = self.history.sendable(msg.peel()) else {
return;
};
async move { async move {
if let Some(webhook_config) = shout.webhook() { if let Some(webhook_config) = shout.webhook() {
send_webhook(&user_notification, webhook_config, &net).await; send_webhook(&user_notification, webhook_config, &net).await;
@ -37,4 +39,3 @@ impl Handler<NotifyUser> for AlertsActor {
.wait(ctx); .wait(ctx);
} }
} }
}

View file

@ -7,6 +7,6 @@ impl Handler<UpdateShout> for AlertsActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result {
self.shout.replace(msg.unwrap()); self.shout.replace(msg.peel());
} }
} }

View file

@ -30,34 +30,8 @@ impl Actor for AlertsActor {
} }
fn short_message(user_notification: &UserNotification) -> String { fn short_message(user_notification: &UserNotification) -> String {
let tail = match user_notification { let (forge_alias, repo_alias) = user_notification.aliases();
UserNotification::CICheckFailed { format!("[git-next] {forge_alias}/{repo_alias}: {user_notification}")
forge_alias,
repo_alias,
commit,
log: _,
} => format!("{forge_alias}/{repo_alias}: CI Check Failed: {commit}"),
UserNotification::RepoConfigLoadFailure {
forge_alias,
repo_alias,
reason: _,
} => format!("{forge_alias}/{repo_alias}: Invalid Repo Config"),
UserNotification::WebhookRegistration {
forge_alias,
repo_alias,
reason: _,
} => format!("{forge_alias}/{repo_alias}: Failed Webhook Registration"),
UserNotification::DevNotBasedOnMain {
forge_alias,
repo_alias,
dev_branch: _,
main_branch: _,
dev_commit: _,
main_commit: _,
log: _,
} => format!("{forge_alias}/{repo_alias}: Dev not based on Main"),
};
format!("[git-next] {tail}")
} }
fn full_message(user_notification: &UserNotification) -> String { fn full_message(user_notification: &UserNotification) -> String {
@ -71,7 +45,7 @@ fn full_message(user_notification: &UserNotification) -> String {
let sha = commit.sha(); let sha = commit.sha();
let message = commit.message(); let message = commit.message();
[ [
"CI Checks had Failed".to_string(), "CI Checks have Failed".to_string(),
format!("Forge: {forge_alias}\nRepo : {repo_alias}"), format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
format!("Commit:\n - {sha}\n - {message}"), format!("Commit:\n - {sha}\n - {message}"),
"Log:".to_string(), "Log:".to_string(),

View file

@ -3,16 +3,17 @@ use actix::prelude::*;
use actix::Recipient; use actix::Recipient;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use notify::event::ModifyKind; use notify::{event::ModifyKind, Watcher};
use notify::Watcher; use tracing::{error, info};
use tracing::error;
use tracing::info;
use std::path::PathBuf; use std::{
use std::sync::atomic::AtomicBool; path::PathBuf,
use std::sync::atomic::Ordering; sync::{
use std::sync::Arc; atomic::{AtomicBool, Ordering},
use std::time::Duration; Arc,
},
time::Duration,
};
#[derive(Debug, Message)] #[derive(Debug, Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
@ -30,8 +31,7 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
let mut handler = notify::recommended_watcher(tx).context("file watcher")?; let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
handler handler
.watch(&path, notify::RecursiveMode::NonRecursive) .watch(&path, notify::RecursiveMode::NonRecursive)
.with_context(|| format!("Watch file: {path:?}"))?; .with_context(|| format!("Watching: {path:?}"))?;
info!("Watching: {:?}", path);
let thread_shutdown = shutdown.clone(); let thread_shutdown = shutdown.clone();
actix_rt::task::spawn_blocking(move || { actix_rt::task::spawn_blocking(move || {
loop { loop {
@ -43,7 +43,7 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
match result { match result {
Ok(event) => match event.kind { Ok(event) => match event.kind {
notify::EventKind::Modify(ModifyKind::Data(_)) => { notify::EventKind::Modify(ModifyKind::Data(_)) => {
tracing::info!("File modified"); info!("File modified");
recipient.do_send(FileUpdated); recipient.do_send(FileUpdated);
break; break;
} }

View file

@ -1,8 +1,5 @@
// //
use git_next_core::{ use git_next_core::git::{ForgeLike, RepoDetails};
git::{ForgeLike, RepoDetails},
ForgeType,
};
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
use git_next_forge_forgejo::ForgeJo; use git_next_forge_forgejo::ForgeJo;
@ -19,10 +16,14 @@ impl Forge {
pub fn create(repo_details: RepoDetails, net: Network) -> Box<dyn ForgeLike> { pub fn create(repo_details: RepoDetails, net: Network) -> Box<dyn ForgeLike> {
match repo_details.forge.forge_type() { match repo_details.forge.forge_type() {
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)), git_next_core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
#[cfg(feature = "github")] #[cfg(feature = "github")]
ForgeType::GitHub => Box::new(Github::new(repo_details, net)), git_next_core::ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
_ => unreachable!(), _ => {
drop(repo_details);
drop(net);
unreachable!();
}
} }
} }
} }

View file

@ -1,4 +1,5 @@
// //
#[cfg(any(feature = "forgejo", feature = "github"))]
use super::*; use super::*;
use git_next_core::{ use git_next_core::{
@ -11,7 +12,7 @@ use git_next_core::{
#[test] #[test]
fn test_forgejo_name() { fn test_forgejo_name() {
let net = Network::new_mock(); let net = Network::new_mock();
let repo_details = given_repo_details(ForgeType::ForgeJo); let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
let forge = Forge::create(repo_details, net); let forge = Forge::create(repo_details, net);
assert_eq!(forge.name(), "forgejo"); assert_eq!(forge.name(), "forgejo");
} }
@ -20,20 +21,17 @@ fn test_forgejo_name() {
#[test] #[test]
fn test_github_name() { fn test_github_name() {
let net = Network::new_mock(); let net = Network::new_mock();
let repo_details = given_repo_details(ForgeType::GitHub); let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
let forge = Forge::create(repo_details, net); let forge = Forge::create(repo_details, net);
assert_eq!(forge.name(), "github"); assert_eq!(forge.name(), "github");
} }
fn given_fs() -> kxio::fs::FileSystem { #[allow(dead_code)]
kxio::fs::temp().unwrap_or_else(|e| { fn given_repo_details(forge_type: git_next_core::ForgeType) -> RepoDetails {
let fs = kxio::fs::temp().unwrap_or_else(|e| {
println!("{e}"); println!("{e}");
panic!("fs") panic!("fs")
}) });
}
fn given_repo_details(forge_type: ForgeType) -> RepoDetails {
let fs = given_fs();
git::repo_details( git::repo_details(
1, 1,
git::Generation::default(), git::Generation::default(),

View file

@ -53,24 +53,25 @@ fn main() -> Result<()> {
let commands = Commands::parse(); let commands = Commands::parse();
match commands.command { match commands.command {
Command::Init => { Command::Init => init::run(&fs),
init::run(&fs)?;
}
Command::Server(server) => match server { Command::Server(server) => match server {
Server::Init => { Server::Init => server::init(&fs),
server::init(&fs)?;
}
#[cfg(not(feature = "tui"))] #[cfg(not(feature = "tui"))]
Server::Start {} => { Server::Start {} => server::start(
let sleep_duration = std::time::Duration::from_secs(10); false,
server::start(false, fs, net, repository_factory, sleep_duration)?; fs,
} net,
repository_factory,
std::time::Duration::from_secs(10),
),
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
Server::Start { ui } => { Server::Start { ui } => server::start(
let sleep_duration = std::time::Duration::from_secs(10); ui,
server::start(ui, fs, net, repository_factory, sleep_duration)?; fs,
} net,
repository_factory,
std::time::Duration::from_secs(10),
),
}, },
} }
Ok(())
} }

View file

@ -19,7 +19,7 @@ use tracing::{info, instrument, warn};
pub fn advance_next( pub fn advance_next(
commit: Option<Commit>, commit: Option<Commit>,
force: git_next_core::git::push::Force, force: git_next_core::git::push::Force,
repo_details: RepoDetails, repo_details: &RepoDetails,
repo_config: RepoConfig, repo_config: RepoConfig,
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken, message_token: MessageToken,
@ -29,7 +29,7 @@ pub fn advance_next(
info!("Advancing next to commit '{}'", commit); info!("Advancing next to commit '{}'", commit);
reset( reset(
open_repository, open_repository,
&repo_details, repo_details,
&repo_config.branches().next(), &repo_config.branches().next(),
&commit.into(), &commit.into(),
&force, &force,

View file

@ -1,7 +1,7 @@
// //
use actix::prelude::*; use actix::prelude::*;
use git_next_core::RepoConfigSource; use git_next_core::{git, RepoConfigSource};
use tracing::warn; use tracing::warn;
@ -29,24 +29,31 @@ impl Handler<AdvanceMain> for RepoActor {
let repo_details = self.repo_details.clone(); let repo_details = self.repo_details.clone();
let addr = ctx.address(); let addr = ctx.address();
let message_token = self.message_token; let message_token = self.message_token;
let commit = msg.unwrap(); let commit = msg.peel();
self.update_tui(RepoUpdate::AdvancingMain { self.update_tui(RepoUpdate::AdvancingMain {
commit: commit.clone(), commit: commit.clone(),
}); });
match advance_main(commit, &repo_details, &repo_config, &**open_repository) { if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
Err(err) => {
warn!("advance main: {err}"); warn!("advance main: {err}");
self.alert_tui(format!("advance main: {err}"));
} else {
self.update_tui(RepoUpdate::MainUpdated);
if let Some(open_repository) = &self.open_repository {
match open_repository.fetch() {
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
Err(err) => self.alert_tui(format!("fetching: {err}")),
} }
Ok(()) => match repo_config.source() { }
match repo_config.source() {
RepoConfigSource::Repo => { RepoConfigSource::Repo => {
do_send(&addr, LoadConfigFromRepo, self.log.as_ref()); do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
} }
RepoConfigSource::Server => { RepoConfigSource::Server => {
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref()); do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
} }
}, }
} }
} }
} }

View file

@ -1,7 +1,8 @@
// //
use actix::prelude::*; use actix::prelude::*;
use tracing::warn; use git_next_core::git;
use tracing::{warn, Instrument};
use crate::{ use crate::{
repo::{ repo::{
@ -28,31 +29,47 @@ impl Handler<AdvanceNext> for RepoActor {
next, next,
main, main,
dev_commit_history, dev_commit_history,
} = msg.unwrap(); } = msg.peel();
let repo_details = self.repo_details.clone(); let repo_details = self.repo_details.clone();
let repo_config = repo_config.clone(); let repo_config = repo_config.clone();
let addr = ctx.address(); let addr = ctx.address();
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history); let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
if let Some(commit) = &commit {
self.update_tui(RepoUpdate::AdvancingNext { self.update_tui(RepoUpdate::AdvancingNext {
commit: commit.clone(), commit: commit.clone(),
force: force.clone(), force: force.clone(),
}); });
};
match advance_next( match advance_next(
commit, commit,
force, force,
repo_details, &repo_details,
repo_config, repo_config,
&**open_repository, &**open_repository,
self.message_token, self.message_token,
) { ) {
Ok(message_token) => { Ok(message_token) => {
// pause to allow any CI checks to be started self.update_tui(RepoUpdate::NextUpdated);
std::thread::sleep(self.sleep_duration); match open_repository.fetch() {
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref()); Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
Err(err) => self.alert_tui(format!("fetching: {err}")),
}
// INFO: pause to allow any CI checks to be started
let sleep_duration = self.sleep_duration;
let log = self.log.clone();
async move {
actix_rt::time::sleep(sleep_duration).await;
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
Err(err) => {
warn!("advance next: {err}");
self.alert_tui(err.to_string());
} }
Err(err) => warn!("advance next: {err}"),
} }
} }
} }

View file

@ -20,7 +20,7 @@ impl Handler<CheckCIStatus> for RepoActor {
let addr = ctx.address(); let addr = ctx.address();
let forge = self.forge.duplicate(); let forge = self.forge.duplicate();
let next = msg.unwrap(); let next = msg.peel();
let log = self.log.clone(); let log = self.log.clone();
self.update_tui(RepoUpdate::CheckingCI); self.update_tui(RepoUpdate::CheckingCI);

View file

@ -2,11 +2,11 @@
use actix::prelude::*; use actix::prelude::*;
use git_next_core::git::{forge::commit::Status, graph, UserNotification}; use git_next_core::git::{forge::commit::Status, graph, UserNotification};
use tracing::debug; use tracing::{debug, Instrument};
use crate::{ use crate::{
repo::{ repo::{
delay_send, do_send, logger, do_send, logger,
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo}, messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
notify_user, RepoActor, notify_user, RepoActor,
}, },
@ -17,28 +17,33 @@ impl Handler<ReceiveCIStatus> for RepoActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
let log = self.log.clone(); logger(self.log.as_ref(), "start: ReceiveCIStatus");
logger(log.as_ref(), "start: ReceiveCIStatus"); let (next, status) = msg.peel();
self.update_tui(RepoUpdate::ReceiveCIStatus {
status: status.clone(),
});
debug!(?status, "");
let graph_log = graph::log(&self.repo_details);
self.update_tui_log(graph_log.clone());
let addr = ctx.address(); let addr = ctx.address();
let (next, status) = msg.unwrap();
let forge_alias = self.repo_details.forge.forge_alias().clone(); let forge_alias = self.repo_details.forge.forge_alias().clone();
let repo_alias = self.repo_details.repo_alias.clone(); let repo_alias = self.repo_details.repo_alias.clone();
let message_token = self.message_token; let message_token = self.message_token;
let sleep_duration = self.sleep_duration; let sleep_duration = self.sleep_duration;
let graph_log = graph::log(&self.repo_details);
self.update_tui_log(graph_log.clone());
self.update_tui(RepoUpdate::ReceiveCIStatus {
status: status.clone(),
});
debug!(?status, "");
match status { match status {
Status::Pass => { Status::Pass => {
do_send(&addr, AdvanceMain::new(next), self.log.as_ref()); do_send(&addr, AdvanceMain::new(next), self.log.as_ref());
} }
Status::Pending => { Status::Pending => {
std::thread::sleep(sleep_duration); let log = self.log.clone();
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref()); async move {
actix_rt::time::sleep(sleep_duration).await;
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
} }
Status::Fail => { Status::Fail => {
tracing::warn!("Checks have failed"); tracing::warn!("Checks have failed");
@ -51,14 +56,19 @@ impl Handler<ReceiveCIStatus> for RepoActor {
commit: next, commit: next,
log: graph_log, log: graph_log,
}, },
log.as_ref(),
);
delay_send(
&addr,
sleep_duration,
ValidateRepo::new(message_token),
self.log.as_ref(), self.log.as_ref(),
); );
let log = self.log.clone();
async move {
debug!("sleeping before retrying...");
logger(log.as_ref(), "before sleep");
actix_rt::time::sleep(sleep_duration).await;
logger(log.as_ref(), "after sleep");
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
} }
} }
} }

View file

@ -15,7 +15,7 @@ impl Handler<ReceiveRepoConfig> for RepoActor {
type Result = (); type Result = ();
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))] #[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
let repo_config = msg.unwrap(); let repo_config = msg.peel();
self.update_tui(RepoUpdate::ReceiveRepoConfig { self.update_tui(RepoUpdate::ReceiveRepoConfig {
repo_config: repo_config.clone(), repo_config: repo_config.clone(),
}); });

View file

@ -12,7 +12,9 @@ impl Handler<UnRegisterWebhook> for RepoActor {
type Result = (); type Result = ();
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
if let Some(webhook_id) = self.webhook_id.take() { let Some(webhook_id) = self.webhook_id.take() else {
return;
};
self.update_tui(RepoUpdate::UnregisteringWebhook); self.update_tui(RepoUpdate::UnregisteringWebhook);
let forge = self.forge.duplicate(); let forge = self.forge.duplicate();
debug!("unregistering webhook"); debug!("unregistering webhook");
@ -28,4 +30,3 @@ impl Handler<UnRegisterWebhook> for RepoActor {
debug!("unregistering webhook done"); debug!("unregistering webhook done");
} }
} }
}

View file

@ -1,7 +1,7 @@
// //
use actix::prelude::*; use actix::prelude::*;
use tracing::{debug, info, instrument, Instrument as _}; use tracing::{info, instrument, Instrument as _};
use crate::{ use crate::{
repo::{ repo::{
@ -13,8 +13,9 @@ use crate::{
}; };
use git_next_core::git::{ use git_next_core::git::{
self, push::Force,
validation::positions::{validate, Error, Positions}, validation::positions::{validate, Error, Positions},
UserNotification,
}; };
impl Handler<ValidateRepo> for RepoActor { impl Handler<ValidateRepo> for RepoActor {
@ -25,7 +26,7 @@ impl Handler<ValidateRepo> for RepoActor {
logger(self.log.as_ref(), "start: ValidateRepo"); logger(self.log.as_ref(), "start: ValidateRepo");
// Message Token - make sure we are only triggered for the latest/current token // Message Token - make sure we are only triggered for the latest/current token
match self.token_status(msg.unwrap()) { match self.token_status(msg.peel()) {
TokenStatus::Current => {} // do nothing TokenStatus::Current => {} // do nothing
TokenStatus::Expired => { TokenStatus::Expired => {
logger( logger(
@ -63,29 +64,28 @@ impl Handler<ValidateRepo> for RepoActor {
}; };
logger(self.log.as_ref(), "have repo config"); logger(self.log.as_ref(), "have repo config");
info!("collecting git graph log"); match validate(&**open_repository, &self.repo_details, &repo_config) {
let git_log = git::graph::log(&self.repo_details); Ok((
info!(?git_log, "collected git graph log"); Positions {
self.update_tui_log(git_log.clone());
info!("sent to ui git graph log");
match validate(
&**open_repository,
&self.repo_details,
&repo_config,
git_log,
) {
Ok(Positions {
main, main,
next, next,
dev, dev,
dev_commit_history, dev_commit_history,
next_is_valid, next_is_valid,
}) => { },
debug!(%main, %next, %dev, "positions"); git_log,
)) => {
info!(%main, %next, %dev, "positions");
self.update_tui_log(git_log);
if next_is_valid && next != main { if next_is_valid && next != main {
info!("Checking CI");
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref()); do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
} else if next != dev { } else if next != dev {
info!("Advance next");
self.update_tui(RepoUpdate::AdvancingNext {
commit: next.clone(),
force: Force::No,
});
do_send( do_send(
&ctx.address(), &ctx.address(),
AdvanceNext::new(AdvanceNextPayload { AdvanceNext::new(AdvanceNextPayload {
@ -96,18 +96,20 @@ impl Handler<ValidateRepo> for RepoActor {
self.log.as_ref(), self.log.as_ref(),
); );
} else { } else {
info!("do nothing");
self.update_tui(RepoUpdate::Okay { main, next, dev }); self.update_tui(RepoUpdate::Okay { main, next, dev });
} }
} }
Err(Error::Retryable(message)) => { Err(Error::Retryable(message)) => {
self.alert_tui(format!("[retryable: {message}]")); info!(?message, "Retryable");
self.alert_tui(format!("retryable: {message}"));
logger(self.log.as_ref(), message); logger(self.log.as_ref(), message);
let addr = ctx.address(); let addr = ctx.address();
let message_token = self.message_token; let message_token = self.message_token;
let sleep_duration = self.sleep_duration; let sleep_duration = self.sleep_duration;
let log = self.log.clone(); let log = self.log.clone();
async move { async move {
debug!("sleeping before retrying..."); info!("sleeping before retrying...");
logger(log.as_ref(), "before sleep"); logger(log.as_ref(), "before sleep");
actix_rt::time::sleep(sleep_duration).await; actix_rt::time::sleep(sleep_duration).await;
logger(log.as_ref(), "after sleep"); logger(log.as_ref(), "after sleep");
@ -118,7 +120,13 @@ impl Handler<ValidateRepo> for RepoActor {
.wait(ctx); .wait(ctx);
} }
Err(Error::UserIntervention(user_notification)) => { Err(Error::UserIntervention(user_notification)) => {
self.alert_tui(format!("[USER INTERVENTION: {user_notification}]")); info!(?user_notification, "User Intervention");
self.alert_tui(format!("USER INTERVENTION: {user_notification}"));
if let UserNotification::CICheckFailed { log, .. }
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
{
self.update_tui_log(log.clone());
}
notify_user( notify_user(
self.notify_user_recipient.as_ref(), self.notify_user_recipient.as_ref(),
user_notification, user_notification,
@ -126,7 +134,8 @@ impl Handler<ValidateRepo> for RepoActor {
); );
} }
Err(Error::NonRetryable(message)) => { Err(Error::NonRetryable(message)) => {
self.alert_tui(format!("[Error: {message}]")); info!(?message, "NonRetryable");
self.alert_tui(format!("Error: {message}"));
logger(self.log.as_ref(), message); logger(self.log.as_ref(), message);
} }
} }

View file

@ -3,14 +3,10 @@ use actix::prelude::*;
use crate::{ use crate::{
alerts::messages::NotifyUser, alerts::messages::NotifyUser,
server::{ server::{actor::messages::RepoUpdate, ServerActor},
actor::messages::{RepoUpdate, ServerUpdate},
ServerActor,
},
}; };
use derive_more::Deref; use derive_more::Deref;
use kxio::network::Network; use kxio::network::Network;
use std::time::Duration;
use tracing::{info, instrument, warn, Instrument}; use tracing::{info, instrument, warn, Instrument};
use git_next_core::{ use git_next_core::{
@ -103,8 +99,7 @@ impl RepoActor {
} }
fn update_tui_branches(&self) { fn update_tui_branches(&self) {
#[cfg(feature = "tui")] if cfg!(feature = "tui") {
{
use crate::server::actor::messages::RepoUpdate; use crate::server::actor::messages::RepoUpdate;
let Some(repo_config) = &self.repo_details.repo_config else { let Some(repo_config) = &self.repo_details.repo_config else {
return; return;
@ -114,34 +109,30 @@ impl RepoActor {
} }
} }
#[instrument] #[allow(unused_variables)]
fn update_tui_log(&self, log: git::graph::Log) { fn update_tui_log(&self, log: git::graph::Log) {
info!("ready to send log"); if cfg!(feature = "tui") {
#[cfg(feature = "tui")]
{
info!("sending...");
self.update_tui(RepoUpdate::Log { log }); self.update_tui(RepoUpdate::Log { log });
info!("sent");
} }
} }
#[allow(unused_variables)]
fn alert_tui(&self, alert: impl Into<String>) { fn alert_tui(&self, alert: impl Into<String>) {
#[cfg(feature = "tui")] if cfg!(feature = "tui") {
{
self.update_tui(RepoUpdate::Alert { self.update_tui(RepoUpdate::Alert {
alert: alert.into(), alert: alert.into(),
}); });
} }
} }
#[allow(unused_variables)]
fn update_tui(&self, repo_update: RepoUpdate) { fn update_tui(&self, repo_update: RepoUpdate) {
#[cfg(feature = "tui")] if cfg!(feature = "tui") {
{
let Some(server_addr) = &self.server_addr else { let Some(server_addr) = &self.server_addr else {
return; return;
}; };
let update = ServerUpdate::RepoUpdate { let update = crate::server::actor::messages::ServerUpdate::RepoUpdate {
forge_alias: self.repo_details.forge.forge_alias().clone(), forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(), repo_alias: self.repo_details.repo_alias.clone(),
repo_update, repo_update,
@ -152,7 +143,7 @@ impl RepoActor {
} }
impl Actor for RepoActor { impl Actor for RepoActor {
type Context = Context<Self>; type Context = Context<Self>;
#[tracing::instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))] #[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
fn stopping(&mut self, ctx: &mut Self::Context) -> Running { fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
tracing::debug!("stopping"); tracing::debug!("stopping");
info!("Checking webhook"); info!("Checking webhook");
@ -176,19 +167,6 @@ impl Actor for RepoActor {
} }
} }
pub fn delay_send<M>(addr: &Addr<RepoActor>, delay: Duration, msg: M, log: Option<&ActorLog>)
where
M: actix::Message + Send + 'static + std::fmt::Debug,
RepoActor: actix::Handler<M>,
<M as actix::Message>::Result: Send,
{
let log_message = format!("send-after-delay: {msg:?}");
tracing::debug!(log_message);
logger(log, log_message);
std::thread::sleep(delay);
do_send(addr, msg, log);
}
pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>) pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>)
where where
M: actix::Message + Send + 'static + std::fmt::Debug, M: actix::Message + Send + 'static + std::fmt::Debug,
@ -196,7 +174,7 @@ where
<M as actix::Message>::Result: Send, <M as actix::Message>::Result: Send,
{ {
let log_message = format!("send: {msg:?}"); let log_message = format!("send: {msg:?}");
tracing::debug!(log_message); info!(log_message);
logger(log, log_message); logger(log, log_message);
if cfg!(not(test)) { if cfg!(not(test)) {
// #[cfg(not(test))] // #[cfg(not(test))]

View file

@ -7,7 +7,7 @@ fn advance_next_sut(
next: &Commit, next: &Commit,
main: &Commit, main: &Commit,
dev_commit_history: &[Commit], dev_commit_history: &[Commit],
repo_details: RepoDetails, repo_details: &RepoDetails,
repo_config: RepoConfig, repo_config: RepoConfig,
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken, message_token: MessageToken,
@ -42,7 +42,7 @@ mod when_at_dev {
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
repo_details, &repo_details,
repo_config, repo_config,
&open_repository, &open_repository,
message_token, message_token,
@ -77,7 +77,7 @@ mod can_advance {
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
repo_details, &repo_details,
repo_config, repo_config,
&open_repository, &open_repository,
message_token, message_token,
@ -108,7 +108,7 @@ mod can_advance {
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
repo_details, &repo_details,
repo_config, repo_config,
&open_repository, &open_repository,
message_token, message_token,
@ -148,7 +148,7 @@ mod can_advance {
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
repo_details, &repo_details,
repo_config, repo_config,
&open_repository, &open_repository,
message_token, message_token,
@ -180,7 +180,7 @@ mod can_advance {
&next, &next,
main, main,
dev_commit_history, dev_commit_history,
repo_details, &repo_details,
repo_config, repo_config,
&open_repository, &open_repository,
message_token, message_token,

View file

@ -69,6 +69,22 @@ pub fn a_name() -> String {
generate(5) generate(5)
} }
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(0..100)
}
pub fn a_webhook_id() -> WebhookId { pub fn a_webhook_id() -> WebhookId {
WebhookId::new(a_name()) WebhookId::new(a_name())
} }
@ -89,6 +105,7 @@ pub fn a_forge_config() -> ForgeConfig {
a_name(), a_name(),
a_name(), a_name(),
a_name(), a_name(),
maybe_a_number(),
BTreeMap::default(), // no repos BTreeMap::default(), // no repos
) )
} }

View file

@ -25,6 +25,11 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
.times(1) .times(1)
.in_sequence(&mut seq) .in_sequence(&mut seq)
.return_once(|_, _, _, _| Ok(())); .return_once(|_, _, _, _| Ok(()));
open_repository
.expect_fetch()
.times(1)
.in_sequence(&mut seq)
.return_once(|| Ok(()));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
@ -70,6 +75,11 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
.times(1) .times(1)
.in_sequence(&mut seq) .in_sequence(&mut seq)
.return_once(|_, _, _, _| Ok(())); .return_once(|_, _, _, _| Ok(()));
open_repository
.expect_fetch()
.times(1)
.in_sequence(&mut seq)
.return_once(|| Ok(()));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
@ -83,10 +93,6 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: ValidateRepo")?;
assert!(l
.iter()
.any(|message| message.contains("send: ValidateRepo")));
})?;
Ok(()) Ok(())
} }

View file

@ -1,9 +1,11 @@
use std::time::Duration;
use crate::repo::messages::AdvanceNextPayload; use crate::repo::messages::AdvanceNextPayload;
// //
use super::*; use super::*;
#[actix::test] #[test_log::test(actix::test)]
async fn should_fetch_then_push_then_revalidate() -> TestResult { async fn should_fetch_then_push_then_revalidate() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
@ -26,6 +28,11 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
.times(1) .times(1)
.in_sequence(&mut seq) .in_sequence(&mut seq)
.return_once(|_, _, _, _| Ok(())); .return_once(|_, _, _, _| Ok(()));
open_repository
.expect_fetch()
.times(1)
.in_sequence(&mut seq)
.return_once(|| Ok(()));
//when //when
let (addr, log) = when::start_actor_with_open_repository( let (addr, log) = when::start_actor_with_open_repository(
@ -41,14 +48,11 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
}, },
)) ))
.await?; .await?;
actix_rt::time::sleep(Duration::from_millis(9)).await;
System::current().stop(); System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: ValidateRepo")?;
assert!(l
.iter()
.any(|message| message.contains("send: ValidateRepo")));
})?;
Ok(()) Ok(())
} }

View file

@ -43,6 +43,10 @@ async fn should_open() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs); let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details); given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
// factory opens a repository // factory opens a repository
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
@ -79,6 +83,10 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs); let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
let _repo_config = repo_details.repo_config.take().unwrap(); let _repo_config = repo_details.repo_config.take().unwrap();
@ -106,6 +114,10 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs); let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details); given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
@ -129,6 +141,10 @@ async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs); let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
given::has_remote_defaults( given::has_remote_defaults(
&mut open_repository, &mut open_repository,
@ -158,15 +174,10 @@ async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs); let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
given::has_remote_defaults( .expect_fetch()
&mut open_repository, .times(1)
HashMap::from([ .return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
(Direction::Push, repo_details.remote_url()),
(Direction::Fetch, None),
]),
);
let mut repository_factory = MockRepositoryFactory::new(); let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository); expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?; fs.dir_create(&repo_details.gitdir)?;

View file

@ -1,3 +1,5 @@
use std::time::Duration;
// //
use super::*; use super::*;
@ -49,15 +51,12 @@ async fn when_pending_should_recheck_ci_status() -> TestResult {
git::forge::commit::Status::Pending, git::forge::commit::Status::Pending,
))) )))
.await?; .await?;
actix_rt::time::sleep(Duration::from_millis(9)).await;
System::current().stop(); System::current().stop();
//then //then
tracing::debug!(?log, ""); tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| { log.require_message_containing("send: ValidateRepo")?;
assert!(l
.iter()
.any(|message| message.contains("send: ValidateRepo")));
})?;
Ok(()) Ok(())
} }
@ -79,11 +78,11 @@ async fn when_fail_should_recheck_after_delay() -> TestResult {
git::forge::commit::Status::Fail, git::forge::commit::Status::Fail,
))) )))
.await?; .await?;
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await; actix_rt::time::sleep(Duration::from_millis(9)).await;
System::current().stop(); System::current().stop();
//then //then
log.require_message_containing("send-after-delay: ValidateRepo")?; log.require_message_containing("send: ValidateRepo")?;
Ok(()) Ok(())
} }

View file

@ -3,4 +3,5 @@ mod receive_app_config;
mod receive_valid_app_config; mod receive_valid_app_config;
mod server_update; mod server_update;
mod shutdown; mod shutdown;
mod shutdown_trigger;
mod subscribe_updates; mod subscribe_updates;

View file

@ -25,7 +25,7 @@ impl Handler<ReceiveAppConfig> for ServerActor {
self.do_send( self.do_send(
ReceiveValidAppConfig::new(ValidAppConfig::new( ReceiveValidAppConfig::new(ValidAppConfig::new(
msg.unwrap(), msg.peel(),
socket_addr, socket_addr,
server_storage, server_storage,
)), )),

View file

@ -26,7 +26,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
app_config, app_config,
socket_address, socket_address,
storage: server_storage, storage: server_storage,
} = msg.unwrap(); } = msg.peel();
// shutdown any existing webhook actor // shutdown any existing webhook actor
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() { if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
webhook_actor_addr.do_send(ShutdownWebhook); webhook_actor_addr.do_send(ShutdownWebhook);

View file

@ -0,0 +1,12 @@
//
use actix::Handler;
use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
impl Handler<ShutdownTrigger> for ServerActor {
type Result = ();
fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result {
self.shutdown_trigger.replace(msg.peel());
}
}

View file

@ -5,6 +5,6 @@ impl actix::Handler<SubscribeToUpdates> for ServerActor {
type Result = (); type Result = ();
fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result {
self.subscribers.push(msg.unwrap()); self.subscribers.push(msg.peel());
} }
} }

View file

@ -70,7 +70,7 @@ pub enum RepoUpdate {
}, },
CheckingCI, CheckingCI,
AdvancingNext { AdvancingNext {
commit: Option<git::Commit>, commit: git::Commit,
force: git::push::Force, force: git::push::Force,
}, },
AdvancingMain { AdvancingMain {
@ -92,6 +92,8 @@ pub enum RepoUpdate {
}, },
RegisteredWebhook, RegisteredWebhook,
Opened, Opened,
NextUpdated,
MainUpdated,
} }
message!( message!(
@ -99,3 +101,13 @@ message!(
Recipient<ServerUpdate>, Recipient<ServerUpdate>,
"Subscribe to receive updates from the server" "Subscribe to receive updates from the server"
); );
/// Sends a channel to be used to shutdown the server
#[derive(Message, Constructor)]
#[rtype(result = "()")]
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
impl ShutdownTrigger {
pub fn peel(self) -> std::sync::mpsc::Sender<String> {
self.0
}
}

View file

@ -1,6 +1,6 @@
// //
use actix::prelude::*; use actix::prelude::*;
use messages::{ReceiveAppConfig, ServerUpdate}; use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
use tracing::error; use tracing::error;
#[cfg(test)] #[cfg(test)]
@ -58,6 +58,7 @@ pub struct ServerActor {
sleep_duration: std::time::Duration, sleep_duration: std::time::Duration,
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>, repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>,
subscribers: Vec<Recipient<ServerUpdate>>, subscribers: Vec<Recipient<ServerUpdate>>,
// testing // testing
@ -84,6 +85,7 @@ impl ServerActor {
net, net,
alerts, alerts,
repository_factory: repo, repository_factory: repo,
shutdown_trigger: None,
subscribers: Vec::default(), subscribers: Vec::default(),
sleep_duration, sleep_duration,
repo_actors: BTreeMap::new(), repo_actors: BTreeMap::new(),
@ -228,10 +230,15 @@ impl ServerActor {
} }
/// Attempts to gracefully shutdown the server before stopping the system. /// Attempts to gracefully shutdown the server before stopping the system.
fn abort(&self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) { fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
tracing::error!("Aborting: {}", message.into());
self.do_send(crate::server::actor::messages::Shutdown, ctx); self.do_send(crate::server::actor::messages::Shutdown, ctx);
System::current().stop_with_code(1); if let Some(t) = self.shutdown_trigger.take() {
let _ = t.send(message.into());
} else {
error!("{}", message.into());
self.do_send(Shutdown, ctx);
// System::current().stop_with_code(1);
}
} }
fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context) fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context)

View file

@ -6,6 +6,7 @@ mod tests;
use actix::prelude::*; use actix::prelude::*;
use actix_rt::signal; use actix_rt::signal;
use actor::messages::ShutdownTrigger;
use crate::{ use crate::{
alerts::{AlertsActor, History}, alerts::{AlertsActor, History},
@ -21,7 +22,11 @@ use color_eyre::{eyre::Context, Result};
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, network::Network};
use tracing::info; use tracing::info;
use std::{path::PathBuf, sync::atomic::Ordering, time::Duration}; use std::{
path::PathBuf,
sync::{atomic::Ordering, mpsc::channel, Arc, RwLock},
time::Duration,
};
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60); const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
@ -41,6 +46,7 @@ pub fn init(fs: &FileSystem) -> Result<()> {
Ok(()) Ok(())
} }
#[allow(clippy::too_many_lines)]
pub fn start( pub fn start(
ui: bool, ui: bool,
fs: FileSystem, fs: FileSystem,
@ -57,6 +63,10 @@ pub fn start(
init_logging(); init_logging();
} }
let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
let shutdown_message_holder_exec = shutdown_message_holder.clone();
let file_watcher_err_holder: Arc<RwLock<Option<anyhow::Error>>> = Arc::new(RwLock::new(None));
let file_watcher_err_holder_exec = file_watcher_err_holder.clone();
let execution = async move { let execution = async move {
info!("Starting Alert Dispatcher..."); info!("Starting Alert Dispatcher...");
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start(); let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
@ -66,35 +76,67 @@ pub fn start(
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start(); ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
info!("Starting File Watcher..."); info!("Starting File Watcher...");
#[allow(clippy::expect_used)] let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
let fw_shutdown = watch_file("git-next-server.toml".into(), server.clone().recipient()) let fw_shutdown = match watch_file {
.expect("file watcher"); Ok(fw_shutdown) => fw_shutdown,
Err(err) => {
// shutdown now
server.do_send(crate::server::actor::messages::Shutdown);
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
System::current().stop();
let _ = file_watcher_err_holder_exec
.write()
.map(|mut o| o.replace(err));
return;
}
};
let (tx_shutdown, rx_shutdown) = channel::<String>();
if ui { if ui {
#[cfg(feature = "tui")] #[cfg(feature = "tui")]
{ {
use crate::server::actor::messages::SubscribeToUpdates; use crate::server::actor::messages::SubscribeToUpdates;
use crate::tui; use crate::tui;
use std::sync::mpsc::channel;
let (tx_shutdown, rx_shutdown) = channel::<()>(); let tui_addr = tui::Tui::new(tx_shutdown.clone()).start();
let tui_addr = tui::Tui::new(tx_shutdown).start();
server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient())); server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient()));
server.do_send(ShutdownTrigger::new(tx_shutdown));
server.do_send(FileUpdated); // update file after ui subscription in place server.do_send(FileUpdated); // update file after ui subscription in place
loop { loop {
let _ = tui_addr.send(tui::Tick).await; let _ = tui_addr.send(tui::Tick).await;
if rx_shutdown.try_recv().is_ok() { if let Ok(message) = rx_shutdown.try_recv() {
let _ = shutdown_message_holder_exec
.write()
.map(|mut o| o.replace(message));
break; break;
} }
actix_rt::time::sleep(Duration::from_millis(16)).await; actix_rt::time::sleep(Duration::from_millis(16)).await;
} }
} }
} else { } else {
server.do_send(ShutdownTrigger::new(tx_shutdown.clone()));
server.do_send(FileUpdated); server.do_send(FileUpdated);
info!("Server running - Press Ctrl-C to stop..."); info!("Server running - Press Ctrl-C to stop...");
let _ = signal::ctrl_c().await; tokio::select! {
_r = signal::ctrl_c() => {
info!("Ctrl-C received, shutting down..."); info!("Ctrl-C received, shutting down...");
} }
_x = async move {
loop{
if let Ok(message) = rx_shutdown.try_recv() {
let _ = shutdown_message_holder_exec
.write()
.map(|mut o| o.replace(message));
break;
}
actix_rt::task::yield_now().await;
}
} => {
info!("signaled shutdown");
}
};
}
// shutdown // shutdown
fw_shutdown.store(true, Ordering::Relaxed); fw_shutdown.store(true, Ordering::Relaxed);
@ -106,6 +148,29 @@ pub fn start(
let system = System::new(); let system = System::new();
Arbiter::current().spawn(execution); Arbiter::current().spawn(execution);
system.run()?; system.run()?;
// check for error from server thread
#[allow(clippy::unwrap_used)]
if let Some(err) = &*shutdown_message_holder.read().unwrap() {
#[cfg(feature = "tui")]
if ui {
ratatui::restore();
}
if !err.is_empty() {
return Err(color_eyre::eyre::eyre!(format!("{err}")));
}
}
// check for error from file watcher thread
#[allow(clippy::unwrap_used)]
if let Some(err) = &*file_watcher_err_holder.read().unwrap() {
#[cfg(feature = "tui")]
if ui {
ratatui::restore();
}
return Err(color_eyre::eyre::eyre!(format!("{err}")));
}
Ok(()) Ok(())
} }

View file

@ -62,7 +62,10 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
.with_token(ApiToken::new(Secret::new(String::new()))) .with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string()); repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
let open_repository = git::repository::factory::real().open(&repo_details)?; let Ok(open_repository) = git::repository::factory::real().open(&repo_details) else {
// .git directory may not be present on dev environment
return Ok(());
};
let_assert!( let_assert!(
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push), Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
"Default Push Remote not found" "Default Push Remote not found"
@ -95,10 +98,10 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
.with_token(ApiToken::new(Secret::new(String::new()))) .with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
tracing::debug!("opening..."); tracing::debug!("opening...");
let_assert!( let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
Ok(repository) = git::repository::factory::real().open(&repo_details), // .git directory may not be present on dev environment
"open repository" return Ok(());
); };
tracing::debug!("open okay"); tracing::debug!("open okay");
tracing::info!(?repository, "FOO"); tracing::info!(?repository, "FOO");
tracing::info!(?repo_details, "BAR"); tracing::info!(?repo_details, "BAR");
@ -108,11 +111,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
} }
#[test] #[test]
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> { fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() {
let_assert!( let_assert!(
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io) Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
); );
eprintln!("cli_crate_dir: {cli_crate_dir:?}");
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent())); let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
eprintln!("root: {root:?}");
let mut repo_details = git::repo_details( let mut repo_details = git::repo_details(
1, 1,
git::Generation::default(), git::Generation::default(),
@ -126,14 +131,15 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
.with_user(User::new("git".to_string())) .with_user(User::new("git".to_string()))
.with_token(ApiToken::new(Secret::new(String::new()))) .with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
let repository = git::repository::factory::real().open(&repo_details)?; let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
// .git directory may not be present on dev environment
return;
};
let mut repo_details = repo_details.clone(); let mut repo_details = repo_details.clone();
repo_details.forge = repo_details repo_details.forge = repo_details
.forge .forge
.with_hostname(Hostname::new("code.kemitix.net")); .with_hostname(Hostname::new("code.kemitix.net"));
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details)); let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
Ok(())
} }
#[test] #[test]

View file

@ -1,121 +1,40 @@
# TUI Actor # Terminal UI
- Maintains it's own copy of data for holding state Currently the Terminal UI is an experimental feature, controlled by the feature flag `tui`.
- Is notified of update via actor messages from the Server and Repo Actors
- State is somewhat heirarchical
## State ## Build & Run
```rust The build `git-next` with the Terminal UI use: `cargo install git-next --features tui`
enum TuiState {
Initial,
Configured {
app_config: ValidAppConfig,
forges: BTreeMap<ForgeAlias, RepoState>,
},
}
enum RepoState {
Identified { repo_alias: RepoAlias },
Configured { repo_alias: RepoAlias, branches: RepoBranches },
Ready { repo_alias: RepoAlias, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log },
Alert { repo_alias: RepoAlias, message: String, branches: RepoBranches, main: Commit, next: Commit, dev: Commit, log: Log },
}
```
## State Transitions: To run `git-next` with the Terminal UI use: `git-next server start --ui`
### `TuiState` ### Docker
```mermaid If using the docker image you will need to create a directory to mount that contains the
stateDiagram-v2 `git-next-server.toml` file. Mount this directory as `/app`. In the example below we use
* --> Initial the current directory for this.
Initial --> Configured
```
- `message!(Configure, ValidAppConfig, "Initialise UI with valid config");` If you want to persist the clones of your monitored repos then point `storage.path` in
`git-next-server.toml` to the the directory `/app`, (e.g. `path = "/app/data"`).
### `RepoState` Map the port your webhook notifications are arriving on to the port specified in `listen.http.port`.
```mermaid `docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest server start --ui`
stateDiagram-v2
* --> Identified
Identified --> Configured
Identified --> Ready
Configured --> Ready
Ready --> Alert
Configured --> Alert
Identified --> Alert
Alert --> Ready
```
- `Identified` - from AppConfig where a repo alias is listed, but repo config needs to be loaded from `.git-next.toml` ## logs
- `Configured` - as `Identified` but either branches are identified in server config, OR the `.git-next.toml` file has been loaded
- `Ready` - as `Configured` but the branch positions have been validated and do not require user intervention
- `Alert` - as `Ready` but user intervention is required
## Widget When the Terminal UI is enabled via the `--ui` parameter, logs are written to the file:
Initial mock up of UI. Possibly add some borders and padding as it looks a little squached together. - `???` on Linux
- `~/Library/Application Support/net.kemitix.git-next/git-next.log` on MacOS
- `???` on Windows
``` ## Keys
+ gh
- jo
+ test (main/next/dev)
- tasyn (main/next/dev)
* 12ab32f (dev) added feature X
* bce43b1 (next) added feature Y
* f43e379 (main) added feature Z
- git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main
* 239cefd (main/next) fix bug A
* b4c290a (dev)
```
Adding a border around open forges: - `q` - Quit
- `j` - Down
``` - `k` - Up
+ gh - `f` - Page Down
- jo --------------------------------------------------------------------+ - `b` - Page Up
| + test (main/next/dev) | - `g` - Top/Home
| - tasyn (main/next/dev) | - `G` - Bottom/End
| * 12ab32f (dev) added feature X |
| * bce43b1 (next) added feature Y |
| * f43e379 (main) added feature Z |
| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main |
| * 239cefd (main/next) fix bug A |
| * b4c290a (dev) |
+------------------------------------------------------------------------+
```
Adding a border around open forges and repos (I like this one the best):
```
+ gh
- jo --------------------------------------------------------------------+
| + test (main/next/dev) |
| - tasyn (main/next/dev) ---------------------------------------------+ |
| | * 12ab32f (dev) added feature X | |
| | * bce43b1 (next) added feature Y | |
| | * f43e379 (main) added feature Z | |
| +--------------------------------------------------------------------+ |
| - git-next (main/next/dev) DEV NOT AHEAD OF MAIN - rebase onto main -+ |
| | * 239cefd (main/next) fix bug A | |
| | * b4c290a (dev) | |
| +--------------------------------------------------------------------+ |
+------------------------------------------------------------------------+
```
## Logging
- tui-logger to create an optional panel to show the normal server logs
## Branch Graph
- tui-nodes ?
## Scrolling
- tui-scrollview
## Tree View
- tui-tree-widget

View file

@ -1,11 +1,17 @@
// //
use actix::Handler; use actix::Handler;
use ratatui::style::Color;
use crate::{ use crate::{
server::actor::messages::{RepoUpdate, ServerUpdate}, server::actor::messages::{RepoUpdate, ServerUpdate},
tui::{actor::ServerState, Tui}, tui::{actor::ServerState, Tui},
}; };
static OKAY: Color = Color::Green;
static PREP: Color = Color::Gray;
static ACTING: Color = Color::LightBlue;
static WARN: Color = Color::Red;
impl Handler<ServerUpdate> for Tui { impl Handler<ServerUpdate> for Tui {
type Result = (); type Result = ();
@ -28,6 +34,7 @@ impl Handler<ServerUpdate> for Tui {
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else { let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
return; return;
}; };
repo_state.clear_alert();
match repo_update { match repo_update {
RepoUpdate::Branches { branches } => { RepoUpdate::Branches { branches } => {
repo_state.update_branches(branches); repo_state.update_branches(branches);
@ -35,51 +42,59 @@ impl Handler<ServerUpdate> for Tui {
RepoUpdate::Log { log } => { RepoUpdate::Log { log } => {
repo_state.update_log(log); repo_state.update_log(log);
} }
RepoUpdate::ValidateRepo => repo_state.update_message("polling..."), RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING),
RepoUpdate::Okay { main, next, dev } => { RepoUpdate::Okay { main, next, dev } => {
repo_state.clear_alert(); repo_state.clear_alert();
repo_state.update_message("okay"); repo_state.update_message("okay", OKAY);
*repo_state = repo_state.clone().ready(main, next, dev); *repo_state = repo_state.clone().ready(main, next, dev);
} }
RepoUpdate::Alert { alert } => { RepoUpdate::Alert { alert } => {
repo_state.alert(alert); repo_state.alert(alert);
} }
RepoUpdate::CheckingCI => { RepoUpdate::CheckingCI => {
repo_state.update_message("Checking CI status"); repo_state.update_message("Checking CI status", ACTING);
}
RepoUpdate::AdvancingNext { commit, force: _ } => {
repo_state
.update_message(format!("advancing next to {commit}"), ACTING);
}
RepoUpdate::NextUpdated => {
repo_state.update_message("next updated - pause while CI starts", OKAY);
} }
RepoUpdate::AdvancingNext {
commit: _,
force: _,
} => (),
RepoUpdate::AdvancingMain { commit } => { RepoUpdate::AdvancingMain { commit } => {
repo_state.update_message(format!("advancing main to {commit}")); repo_state
.update_message(format!("advancing main to {commit}"), ACTING);
}
RepoUpdate::MainUpdated => {
repo_state.update_message("main updated", OKAY);
} }
RepoUpdate::Opening => { RepoUpdate::Opening => {
repo_state.update_message("opening..."); repo_state.update_message("opening...", PREP);
} }
RepoUpdate::Opened => { RepoUpdate::Opened => {
repo_state.update_message("opened"); repo_state.update_message("opened", PREP);
} }
RepoUpdate::LoadingConfigFromRepo => { RepoUpdate::LoadingConfigFromRepo => {
repo_state.update_message("loading config from repo..."); repo_state.update_message("loading config from repo...", PREP);
} }
RepoUpdate::ReceiveCIStatus { status } => { RepoUpdate::ReceiveCIStatus { status } => {
repo_state.update_message(format!("ci status: {status:?}")); repo_state.update_message(format!("ci status: {status:?}"), WARN);
} }
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => { RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
repo_state.update_message("loaded config from repo"); repo_state.update_message("loaded config from repo", PREP);
} }
RepoUpdate::RegisteringWebhook => { RepoUpdate::RegisteringWebhook => {
repo_state.update_message("registering webhook..."); repo_state.update_message("registering webhook...", PREP);
} }
RepoUpdate::UnregisteringWebhook => { RepoUpdate::UnregisteringWebhook => {
repo_state.update_message("unregistering webhook..."); repo_state.update_message("unregistering webhook...", PREP);
} }
RepoUpdate::WebhookReceived { branch, push: _ } => { RepoUpdate::WebhookReceived { branch, push: _ } => {
repo_state.update_message(format!("webhook update: {branch:?}")); repo_state
.update_message(format!("webhook update: {branch:?}"), ACTING);
} }
RepoUpdate::RegisteredWebhook => { RepoUpdate::RegisteredWebhook => {
repo_state.update_message("registered webhook"); repo_state.update_message("registered webhook", PREP);
} }
} }
} }

View file

@ -3,6 +3,9 @@ mod handlers;
pub mod messages; pub mod messages;
mod model; mod model;
#[cfg(test)]
mod tests;
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use actix::{Actor, ActorContext as _, Context}; use actix::{Actor, ActorContext as _, Context};
@ -13,12 +16,14 @@ use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind}, crossterm::event::{self, KeyCode, KeyEventKind},
DefaultTerminal, DefaultTerminal,
}; };
use tui_scrollview::ScrollViewState;
#[derive(Debug)] #[derive(Debug)]
pub struct Tui { pub struct Tui {
terminal: Option<DefaultTerminal>, terminal: Option<DefaultTerminal>,
signal_shutdown: Sender<()>, signal_shutdown: Sender<String>,
pub state: State, pub state: State,
scroll_view_state: ScrollViewState,
} }
impl Actor for Tui { impl Actor for Tui {
type Context = Context<Self>; type Context = Context<Self>;
@ -31,23 +36,22 @@ impl Actor for Tui {
} }
} }
impl Tui { impl Tui {
pub fn new(signal_shutdown: Sender<()>) -> Self { pub fn new(signal_shutdown: Sender<String>) -> Self {
Self { Self {
terminal: None, terminal: None,
signal_shutdown, signal_shutdown,
state: State::initial(), state: State::initial(),
scroll_view_state: ScrollViewState::default(),
} }
} }
pub const fn state(&self) -> &State {
&self.state
}
fn draw(&mut self) -> std::io::Result<()> { fn draw(&mut self) -> std::io::Result<()> {
let t = self.terminal.take(); let t = self.terminal.take();
let scroll_view_state = &mut self.scroll_view_state;
let state = &self.state;
if let Some(mut terminal) = t { if let Some(mut terminal) = t {
terminal.draw(|frame| { terminal.draw(|frame| {
frame.render_widget(self.state(), frame.area()); frame.render_stateful_widget(state, frame.area(), scroll_view_state);
})?; })?;
self.terminal = Some(terminal); self.terminal = Some(terminal);
} else { } else {
@ -56,25 +60,38 @@ impl Tui {
Ok(()) Ok(())
} }
fn handle_input(&self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> { fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
if event::poll(std::time::Duration::from_millis(16))? { if event::poll(std::time::Duration::from_millis(16))? {
if let event::Event::Key(key) = event::read()? { let event::Event::Key(key) = event::read()? else {
if key.kind == KeyEventKind::Press { return Ok(());
};
if key.kind != KeyEventKind::Press {
return Ok(());
}
match key.code { match key.code {
KeyCode::Char('q') => { KeyCode::Char('q') => {
ctx.stop(); ctx.stop();
if let Err(err) = self.signal_shutdown.send(()) { if let Err(err) = self.signal_shutdown.send(String::new()) {
tracing::error!(?err, "Failed to signal shutdown"); tracing::error!(?err, "Failed to signal shutdown");
} }
} }
KeyCode::Esc => { KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
// KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
KeyCode::Char('f') | KeyCode::PageDown => {
self.scroll_view_state.scroll_page_down();
}
KeyCode::Char('b') | KeyCode::PageUp => {
self.scroll_view_state.scroll_page_up();
}
KeyCode::Char('g') | KeyCode::Home => {
self.scroll_view_state.scroll_to_top();
}
KeyCode::Char('G') | KeyCode::End => {
self.scroll_view_state.scroll_to_bottom();
} }
_ => (), _ => (),
} }
} }
}
}
Ok(()) Ok(())
} }
} }

View file

@ -2,10 +2,13 @@
use ratatui::{ use ratatui::{
layout::Alignment, layout::Alignment,
prelude::{Buffer, Rect}, prelude::{Buffer, Rect},
style::Stylize as _, style::{Color, Style, Stylize as _},
symbols::border, symbols::border,
text::Line, text::{Line, Span},
widgets::{block::Title, Block, Paragraph, Widget}, widgets::{
block::{Position, Title},
Block, Paragraph, StatefulWidget, Widget,
},
}; };
use git_next_core::{ use git_next_core::{
@ -13,6 +16,7 @@ use git_next_core::{
ForgeAlias, RepoAlias, RepoBranches, ForgeAlias, RepoAlias, RepoBranches,
}; };
use tracing::info; use tracing::info;
use tui_scrollview::ScrollViewState;
use std::{collections::BTreeMap, fmt::Display, time::Instant}; use std::{collections::BTreeMap, fmt::Display, time::Instant};
@ -39,6 +43,19 @@ impl State {
*tick += 1; *tick += 1;
} }
} }
fn beating_heart(&self) -> String {
if self.last_update.duration_since(self.started).as_secs() % 2 == 0 {
"💚 "
} else {
" 💚"
}
.to_string()
}
}
fn time() -> String {
chrono::Local::now().format("%H:%M").to_string()
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -117,11 +134,12 @@ impl From<ValidAppConfig> for ServerState {
repo_alias.clone(), repo_alias.clone(),
RepoState::Configured { RepoState::Configured {
repo_alias, repo_alias,
message: "configured".into(), message: RepoMessage::builder()
messages: Vec::new(), .text("configured".into())
.style(Style::default().fg(Color::LightGreen))
.build(),
alert: None, alert: None,
branches: rc.branches().clone(), branches: rc.branches().clone(),
histories: None,
log: git::graph::Log::default(), log: git::graph::Log::default(),
}, },
), ),
@ -129,8 +147,10 @@ impl From<ValidAppConfig> for ServerState {
repo_alias.clone(), repo_alias.clone(),
RepoState::Identified { RepoState::Identified {
repo_alias, repo_alias,
message: "identified".into(), message: RepoMessage::builder()
messages: Vec::new(), .text("identified".into())
.style(Style::default().fg(Color::Gray))
.build(),
alert: None, alert: None,
}, },
), ),
@ -175,30 +195,39 @@ pub struct ForgeState {
pub repos: BTreeMap<RepoAlias, RepoState>, pub repos: BTreeMap<RepoAlias, RepoState>,
} }
#[bon::builder]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoMessage {
text: String,
style: Style,
}
impl From<&RepoMessage> for Span<'_> {
fn from(value: &RepoMessage) -> Self {
Self::default()
.content(value.text.clone())
.style(value.style)
}
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoState { pub enum RepoState {
Identified { Identified {
repo_alias: RepoAlias, repo_alias: RepoAlias,
message: String, message: RepoMessage,
messages: Vec<String>,
alert: Option<String>, alert: Option<String>,
}, },
Configured { Configured {
repo_alias: RepoAlias, repo_alias: RepoAlias,
message: String, message: RepoMessage,
messages: Vec<String>,
alert: Option<String>, alert: Option<String>,
branches: RepoBranches, branches: RepoBranches,
histories: Option<git::commit::Histories>,
log: Log, log: Log,
}, },
Ready { Ready {
repo_alias: RepoAlias, repo_alias: RepoAlias,
message: String, message: RepoMessage,
messages: Vec<String>,
alert: Option<String>, alert: Option<String>,
branches: RepoBranches, branches: RepoBranches,
histories: Option<git::commit::Histories>,
view_state: ViewState, view_state: ViewState,
main: Commit, main: Commit,
next: Commit, next: Commit,
@ -239,21 +268,16 @@ impl RepoState {
} }
#[tracing::instrument] #[tracing::instrument]
pub fn update_message(&mut self, msg: impl Into<String> + std::fmt::Debug) { pub fn update_message(&mut self, msg: impl Into<String> + std::fmt::Debug, colour: Color) {
tracing::info!("new tui message");
let msg: String = msg.into();
match self { match self {
Self::Identified { Self::Identified { message, .. }
message, messages, .. | Self::Configured { message, .. }
} | Self::Ready { message, .. } => {
| Self::Configured { info!(?msg, "updating ui");
message, messages, .. *message = RepoMessage::builder()
} .text(msg.into())
| Self::Ready { .style(Style::default().fg(colour))
message, messages, .. .build();
} => {
messages.push(msg.clone());
*message = msg;
} }
} }
} }
@ -261,8 +285,11 @@ impl RepoState {
#[tracing::instrument] #[tracing::instrument]
pub fn clear_alert(&mut self) { pub fn clear_alert(&mut self) {
match self { match self {
Self::Identified { .. } | Self::Configured { .. } => (), Self::Identified { alert, .. }
Self::Ready { alert, .. } => *alert = None, | Self::Configured { alert, .. }
| Self::Ready { alert, .. } => {
*alert = None;
}
} }
} }
@ -270,7 +297,7 @@ impl RepoState {
pub fn alert(&mut self, msg: impl Into<String> + std::fmt::Debug) { pub fn alert(&mut self, msg: impl Into<String> + std::fmt::Debug) {
let msg: String = msg.into(); let msg: String = msg.into();
tracing::info!(%msg, "new tui alert"); tracing::info!(%msg, "new tui alert");
self.update_message("ALERT"); self.update_message("ALERT", Color::Red);
match self { match self {
Self::Identified { alert, .. } Self::Identified { alert, .. }
| Self::Configured { alert, .. } | Self::Configured { alert, .. }
@ -283,29 +310,23 @@ impl RepoState {
Self::Identified { Self::Identified {
repo_alias, repo_alias,
message, message,
messages,
alert, alert,
} => Self::Identified { } => Self::Identified {
repo_alias, repo_alias,
message, message,
messages,
alert, alert,
}, },
Self::Configured { Self::Configured {
repo_alias, repo_alias,
message, message,
messages,
alert, alert,
branches, branches,
histories,
log, log,
} => Self::Ready { } => Self::Ready {
repo_alias, repo_alias,
message, message,
messages,
alert, alert,
branches, branches,
histories,
view_state: ViewState::Expanded, view_state: ViewState::Expanded,
main, main,
next, next,
@ -315,20 +336,16 @@ impl RepoState {
Self::Ready { Self::Ready {
repo_alias, repo_alias,
message, message,
messages,
alert, alert,
branches, branches,
histories,
view_state, view_state,
log, log,
.. // drop existing main, next and dev to use parameters .. // drop existing main, next and dev to use parameters
} => Self::Ready { } => Self::Ready {
repo_alias, repo_alias,
message, message,
messages,
alert, alert,
branches, branches,
histories,
view_state, view_state,
main, main,
next, next,
@ -339,24 +356,30 @@ impl RepoState {
} }
} }
impl Widget for &State { impl StatefulWidget for &State {
fn render(self, area: Rect, buf: &mut Buffer) type State = ScrollViewState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
Self: Sized, Self: Sized,
{ {
let block = Block::bordered() let block = Block::bordered()
.title(Title::from(" Git-Next ".bold()).alignment(Alignment::Center)) .title(
Title::from(format!(" Git-Next v{} ", clap::crate_version!()).bold())
.alignment(Alignment::Center),
)
.title( .title(
Title::from(Line::from(vec![ Title::from(Line::from(vec![
" [q]uit ".into(), " [q]uit ".into(),
format!( self.beating_heart().into(),
"{}s ", " ".into(),
self.last_update.duration_since(self.started).as_secs()
)
.into(),
])) ]))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.position(ratatui::widgets::block::Position::Bottom), .position(Position::Bottom),
)
.title(
Title::from(format!(" {} ", time()))
.alignment(Alignment::Right)
.position(Position::Bottom),
) )
.border_set(border::THICK); .border_set(border::THICK);
let interior = block.inner(area); let interior = block.inner(area);
@ -366,7 +389,7 @@ impl Widget for &State {
.centered() .centered()
.render(interior, buf), .render(interior, buf),
ServerState::Configured { forges } => { ServerState::Configured { forges } => {
ConfiguredAppWidget { forges }.render(interior, buf); ConfiguredAppWidget { forges }.render(interior, buf, state);
} }
} }
} }

View file

@ -0,0 +1,99 @@
//
mod model {
mod repo_state {
use git_next_core::{git::graph::Log, RepoBranches};
use ratatui::style::Style;
use crate::{
repo::tests::given,
tui::actor::{RepoMessage, RepoState, ViewState},
};
type Alert = Option<String>;
fn identified_with_alert(alert: Alert) -> RepoState {
RepoState::Identified {
repo_alias: given::a_repo_alias(),
message: RepoMessage::builder()
.text(given::a_name())
.style(Style::default())
.build(),
alert,
}
}
fn configured_with_alert(alert: Alert) -> RepoState {
RepoState::Configured {
repo_alias: given::a_repo_alias(),
message: RepoMessage::builder()
.text(given::a_name())
.style(Style::default())
.build(),
alert,
branches: RepoBranches::new(String::new(), String::new(), String::new()),
log: Log::default(),
}
}
fn ready_with_alert(alert: Alert) -> RepoState {
RepoState::Ready {
repo_alias: given::a_repo_alias(),
message: RepoMessage::builder()
.text(given::a_name())
.style(Style::default())
.build(),
alert,
branches: RepoBranches::new(String::new(), String::new(), String::new()),
log: Log::default(),
view_state: ViewState::default(),
main: given::a_commit(),
next: given::a_commit(),
dev: given::a_commit(),
}
}
#[rstest::rstest]
#[case(identified_with_alert(None))]
#[case(configured_with_alert(None))]
#[case(ready_with_alert(None))]
fn none_alert_remains_none(#[case] mut repo_state: RepoState) {
// given
match &repo_state {
RepoState::Identified { alert, .. }
| RepoState::Configured { alert, .. }
| RepoState::Ready { alert, .. } => {
assert!(alert.is_none(), "should be none at start");
}
}
// when
repo_state.clear_alert();
// then
match &repo_state {
RepoState::Identified { alert, .. }
| RepoState::Configured { alert, .. }
| RepoState::Ready { alert, .. } => assert!(alert.is_none(), "should remain none"),
}
}
#[rstest::rstest]
#[case(identified_with_alert(Some(String::new())))]
#[case(configured_with_alert(Some(String::new())))]
#[case(ready_with_alert(Some(String::new())))]
fn some_alert_becomes_none(#[case] mut repo_state: RepoState) {
// given
match &repo_state {
RepoState::Identified { alert, .. }
| RepoState::Configured { alert, .. }
| RepoState::Ready { alert, .. } => {
assert!(alert.is_some(), "should be some at start");
}
}
// when
repo_state.clear_alert();
// then
match &repo_state {
RepoState::Identified { alert, .. }
| RepoState::Configured { alert, .. }
| RepoState::Ready { alert, .. } => assert!(alert.is_none(), "should become none"),
}
}
}
}

View file

@ -3,42 +3,59 @@ use std::collections::BTreeMap;
use git_next_core::ForgeAlias; use git_next_core::ForgeAlias;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect}, layout::{Direction, Layout, Rect, Size},
text::Line, widgets::StatefulWidget,
widgets::Widget,
}; };
use tui_scrollview::{ScrollView, ScrollViewState};
use crate::tui::actor::{ForgeState, ViewState}; use crate::tui::actor::ForgeState;
use super::forge::ForgeWidget; use super::{forge::ForgeWidget, HeightContraintLength};
pub struct ConfiguredAppWidget<'a> { pub struct ConfiguredAppWidget<'a> {
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>, pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
} }
impl<'a> Widget for ConfiguredAppWidget<'a> { impl<'a> HeightContraintLength for ConfiguredAppWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) fn height_constraint_length(&self) -> u16 {
self.children()
.iter()
.map(HeightContraintLength::height_constraint_length)
.sum::<u16>()
+ 2 // top + bottom borders
}
}
impl<'a> StatefulWidget for ConfiguredAppWidget<'a> {
type State = ScrollViewState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
Self: Sized, Self: Sized,
{ {
let layout_app = Layout::default() let height = self
.direction(Direction::Vertical) .children()
.constraints(vec![Constraint::Length(1), Constraint::Fill(1)]) .iter()
.split(area); .map(HeightContraintLength::height_constraint_length)
.sum::<u16>();
Line::from(format!("Forges: ({})", self.forges.keys().len())).render(layout_app[0], buf); let mut scroll = ScrollView::new(Size::new(area.width - 1, height));
let layout_forge_list = Layout::default() let layout_forge_list = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
self.forges self.children()
.iter() .iter()
.map(|(_alias, state)| match state.view_state { .map(HeightContraintLength::height_constraint_length),
ViewState::Collapsed => Constraint::Length(1),
ViewState::Expanded => Constraint::Fill(1),
}),
) )
.split(layout_app[1]); .split(scroll.area());
self.children()
.into_iter()
.enumerate()
.for_each(|(i, w)| scroll.render_widget(w, layout_forge_list[i]));
scroll.render(area, buf, state);
}
}
impl<'a> ConfiguredAppWidget<'a> {
fn children(&self) -> Vec<ForgeWidget<'a>> {
self.forges self.forges
.iter() .iter()
.map(|(forge_alias, state)| ForgeWidget { .map(|(forge_alias, state)| ForgeWidget {
@ -46,7 +63,6 @@ impl<'a> Widget for ConfiguredAppWidget<'a> {
repos: &state.repos, repos: &state.repos,
view_state: state.view_state, view_state: state.view_state,
}) })
.enumerate() .collect::<Vec<_>>()
.for_each(|(i, w)| w.render(layout_forge_list[i], buf));
} }
} }

View file

@ -2,9 +2,16 @@
use git_next_core::ForgeAlias; use git_next_core::ForgeAlias;
use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget}; use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
use crate::tui::components::HeightContraintLength;
pub struct CollapsedForgeWidget<'a> { pub struct CollapsedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias, pub forge_alias: &'a ForgeAlias,
} }
impl<'a> HeightContraintLength for CollapsedForgeWidget<'a> {
fn height_constraint_length(&self) -> u16 {
1
}
}
impl<'a> Widget for CollapsedForgeWidget<'a> { impl<'a> Widget for CollapsedForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) fn render(self, area: Rect, buf: &mut Buffer)
where where

View file

@ -4,37 +4,57 @@ use std::collections::BTreeMap;
use git_next_core::{ForgeAlias, RepoAlias}; use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Direction, Layout, Rect},
widgets::{block::Title, Block, Borders, Widget}, widgets::{block::Title, Block, Widget},
}; };
use crate::tui::{actor::RepoState, components::repo::RepoWidget}; use crate::tui::{
actor::RepoState,
components::{repo::RepoWidget, HeightContraintLength},
};
pub struct ExpandedForgeWidget<'a> { pub struct ExpandedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias, pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>, pub repos: &'a BTreeMap<RepoAlias, RepoState>,
} }
impl<'a> HeightContraintLength for ExpandedForgeWidget<'a> {
fn height_constraint_length(&self) -> u16 {
self.children()
.iter()
.map(HeightContraintLength::height_constraint_length)
.sum::<u16>()
+ 2 // top title + bottom padding
}
}
impl<'a> Widget for ExpandedForgeWidget<'a> { impl<'a> Widget for ExpandedForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) fn render(self, area: Rect, buf: &mut Buffer)
where where
Self: Sized, Self: Sized,
{ {
let block = Block::default() let block = Block::default().title(
.title(Title::from(self.forge_alias.to_string()).alignment(Alignment::Center)) Title::from(format!(" forge: {} ", self.forge_alias)).alignment(Alignment::Left),
.borders(Borders::ALL); );
let children = self.children();
let layout = Layout::default() let layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
self.repos children
.iter() .iter()
.map(|(_alias, _state)| Constraint::Fill(1)), .map(HeightContraintLength::height_constraint_length),
) )
.split(block.inner(area)); .split(block.inner(area));
block.render(area, buf); block.render(area, buf);
self.repos children
.values() .into_iter()
.map(|repo_state| RepoWidget { repo_state })
.enumerate() .enumerate()
.for_each(|(i, w)| w.render(layout[i], buf)); .for_each(|(i, w)| w.render(layout[i], buf));
} }
} }
impl<'a> ExpandedForgeWidget<'a> {
fn children(&self) -> Vec<RepoWidget<'a>> {
self.repos
.values()
.map(|repo_state| RepoWidget { repo_state })
.collect::<Vec<_>>()
}
}

View file

@ -4,27 +4,46 @@ mod expanded;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use collapsed::CollapsedForgeWidget;
use expanded::ExpandedForgeWidget;
use git_next_core::{ForgeAlias, RepoAlias}; use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use crate::tui::actor::{RepoState, ViewState}; use crate::tui::actor::{RepoState, ViewState};
use super::HeightContraintLength;
pub struct ForgeWidget<'a> { pub struct ForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias, pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>, pub repos: &'a BTreeMap<RepoAlias, RepoState>,
pub view_state: ViewState, pub view_state: ViewState,
} }
impl<'a> HeightContraintLength for ForgeWidget<'a> {
fn height_constraint_length(&self) -> u16 {
match self.view_state {
ViewState::Collapsed => CollapsedForgeWidget {
forge_alias: self.forge_alias,
}
.height_constraint_length(),
ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias,
repos: self.repos,
}
.height_constraint_length(),
}
}
}
impl<'a> Widget for ForgeWidget<'a> { impl<'a> Widget for ForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) fn render(self, area: Rect, buf: &mut Buffer)
where where
Self: Sized, Self: Sized,
{ {
match self.view_state { match self.view_state {
ViewState::Collapsed => collapsed::CollapsedForgeWidget { ViewState::Collapsed => CollapsedForgeWidget {
forge_alias: self.forge_alias, forge_alias: self.forge_alias,
} }
.render(area, buf), .render(area, buf),
ViewState::Expanded => expanded::ExpandedForgeWidget { ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias, forge_alias: self.forge_alias,
repos: self.repos, repos: self.repos,
} }

View file

@ -1,13 +1,22 @@
use git_next_core::git::graph::Log;
// //
use git_next_core::git::graph::Log;
use ratatui::{ use ratatui::{
text::{Line, Text}, style::{Color, Style},
text::{Line, Span, Text},
widgets::{Paragraph, Widget}, widgets::{Paragraph, Widget},
}; };
use regex::Regex;
use super::HeightContraintLength;
pub struct CommitLog<'a> { pub struct CommitLog<'a> {
pub log: &'a Log, pub log: &'a Log,
} }
impl<'a> HeightContraintLength for CommitLog<'a> {
fn height_constraint_length(&self) -> u16 {
u16::try_from(self.log.len()).unwrap_or(u16::MAX)
}
}
impl<'a> Widget for CommitLog<'a> { impl<'a> Widget for CommitLog<'a> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where where
@ -16,10 +25,104 @@ impl<'a> Widget for CommitLog<'a> {
Paragraph::new(Text::from( Paragraph::new(Text::from(
self.log self.log
.iter() .iter()
.map(ToString::to_string) .map(LogLine::new)
.map(Line::from) .map(Line::from)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)) ))
.render(area, buf); .render(area, buf);
} }
} }
struct LogLine {
raw: String,
}
impl LogLine {
fn new(raw: impl Into<String>) -> Self {
Self { raw: raw.into() }
}
}
lazy_static::lazy_static! {
static ref RE: Regex =
#[allow(clippy::unwrap_used)]
Regex::new(
r"^(?<pre>.*)\s(?<hash>[0-9a-f]{7})\s\((?<branches>.*?)\)\s(?<message>.*)",
).unwrap();
static ref BRANCHES: Regex =
#[allow(clippy::unwrap_used)]
Regex::new(
r"origin\/(?<branch>[^,]+)",
).unwrap();
}
impl From<LogLine> for Line<'_> {
fn from(value: LogLine) -> Self {
match RE.captures(&value.raw) {
Some(caps) => {
let pre = caps["pre"].to_owned();
let hash = caps["hash"].to_owned();
let message = caps["message"].to_owned();
let mut branches = BRANCHES
.captures_iter(&caps["branches"])
.map(|captures| captures["branch"].to_owned())
.filter(|branch| branch != "HEAD")
.collect::<Vec<_>>();
if branches.is_empty() {
// line without branches
Line::from(vec![
pre.into(),
" ".into(),
hash.into(),
" ".into(),
message.into(),
])
} else {
// line withbranches
let mut spans = vec![pre.into(), " ".into(), hash.into(), " ".into()];
branches.sort();
branches
.into_iter()
.map(|branch| format!("({branch})"))
.map(Span::from)
.map(|span| span.style(Style::default().fg(Color::White).bg(Color::Blue)))
.for_each(|span| spans.push(span));
spans.push(" ".into());
spans.push(message.into());
Line::from(spans)
}
}
None => {
// non-commit line
Line::from(value.raw.clone())
}
}
}
}
#[cfg(test)]
mod tests {
use tracing::info;
use super::RE;
#[test_log::test]
fn parse_log_line() -> Result<(), Box<dyn std::error::Error>> {
let line = "* 97b6853 (origin/next, origin/main, origin/dev, origin/HEAD) refactor(tui): simplify repo identity widget";
RE.captures(line).map_or_else(
|| Err("Failed to capture".into()),
|caps| {
info!(?caps, "");
assert_eq!(&caps["pre"], "*");
assert_eq!(&caps["hash"], "97b6853");
assert_eq!(
&caps["branches"],
"origin/next, origin/main, origin/dev, origin/HEAD"
);
assert_eq!(
&caps["message"],
"refactor(tui): simplify repo identity widget"
);
Ok(())
},
)
}
}

View file

@ -6,3 +6,7 @@ mod repo;
pub use configured_app::ConfiguredAppWidget; pub use configured_app::ConfiguredAppWidget;
pub use history::CommitLog; pub use history::CommitLog;
pub trait HeightContraintLength {
fn height_constraint_length(&self) -> u16;
}

View file

@ -1,43 +0,0 @@
//
use git_next_core::{git, RepoAlias, RepoBranches};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
widgets::{Block, Borders, Widget},
};
use crate::tui::components::CommitLog;
use super::{identity::Identity, messages::Messages};
pub struct ConfiguredRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a str,
pub messages: &'a Vec<String>,
pub alert: Option<&'a str>,
pub branches: &'a RepoBranches,
pub log: &'a git::graph::Log,
}
impl<'a> Widget for ConfiguredRepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Identity::new(
self.repo_alias,
self.alert,
self.message,
Some(self.branches),
))
.borders(Borders::ALL);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Fill(1), Constraint::Fill(1)])
.split(block.inner(area));
block.render(area, buf);
Messages::new(self.messages).render(layout[0], buf);
CommitLog { log: self.log }.render(layout[1], buf);
}
}

View file

@ -1,34 +0,0 @@
//
use git_next_core::RepoAlias;
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Block, Borders, Widget},
};
use super::{identity::Identity, messages::Messages};
pub struct IdentifiedRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a str,
pub messages: &'a Vec<String>,
pub alert: Option<&'a str>,
}
impl<'a> Widget for IdentifiedRepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Identity::new(
self.repo_alias,
self.alert,
self.message,
None,
))
.borders(Borders::ALL);
Messages::new(self.messages).render(block.inner(area), buf);
block.render(area, buf);
}
}

View file

@ -1,24 +1,27 @@
// use std::string::ToString;
use std::fmt::Display;
//
use git_next_core::{RepoAlias, RepoBranches}; use git_next_core::{RepoAlias, RepoBranches};
use ratatui::{ use ratatui::{
layout::Alignment, layout::Alignment,
text::Text, style::{Color, Style, Stylize as _},
widgets::{block::Title, Widget}, text::{Line, Span},
widgets::block::Title,
}; };
use crate::tui::actor::RepoMessage;
pub struct Identity<'a> { pub struct Identity<'a> {
pub repo_alias: &'a RepoAlias, pub repo_alias: &'a RepoAlias,
pub alert: Option<&'a str>, pub alert: Option<&'a str>,
pub message: &'a str, pub message: &'a RepoMessage,
pub repo_branches: Option<&'a RepoBranches>, pub repo_branches: Option<&'a RepoBranches>,
} }
impl<'a> Identity<'a> { impl<'a> Identity<'a> {
pub const fn new( pub const fn new(
repo_alias: &'a RepoAlias, repo_alias: &'a RepoAlias,
alert: Option<&'a str>, alert: Option<&'a str>,
message: &'a str, message: &'a RepoMessage,
repo_branches: Option<&'a RepoBranches>, repo_branches: Option<&'a RepoBranches>,
) -> Self { ) -> Self {
Self { Self {
@ -29,33 +32,56 @@ impl<'a> Identity<'a> {
} }
} }
} }
impl<'a> Widget for Identity<'a> { impl<'a> Identity<'a> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) fn spans(self) -> Vec<Span<'a>> {
where let alert = self
Self: Sized, .alert
{ .map(ToString::to_string)
Text::from(self.to_string()).render(area, buf); .map(|alert| alert.fg(Color::White).bg(Color::Red));
}
}
impl<'a> Display for Identity<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repo_alias = self.repo_alias;
let alert = self.alert.unwrap_or("");
let message = self.message; let message = self.message;
let text = self.repo_branches.map_or_else( let main = self
|| format!("{repo_alias} {alert} (_/_/_) [{message}]"), .repo_branches
|branches| { .map(RepoBranches::main)
let main = branches.main(); .map_or_else(|| "_".to_string(), |b| b.to_string());
let next = branches.next(); let next = self
let dev = branches.dev(); .repo_branches
format!("{repo_alias} {alert} ({main}/{next}/{dev}) [{message}]") .map(RepoBranches::next)
}, .map_or_else(|| "_".to_string(), |b| b.to_string());
let dev = self
.repo_branches
.map(RepoBranches::dev)
.map_or_else(|| "_".to_string(), |b| b.to_string());
let mut spans = vec![" ".into()];
match alert {
None => spans.push(
Span::from(self.repo_alias.to_string()).style(Style::default().fg(Color::Cyan)),
),
Some(alert) => {
spans.push(
Span::from(self.repo_alias.to_string())
.style(Style::default().fg(Color::White).bg(Color::Red)),
); );
f.write_str(text.as_str()) spans.push(" ".into());
spans.push(alert);
}
}
spans.push(" ".into());
spans.push(format!("({main} -> {next} -> {dev}) ").into());
spans.push(message.into());
spans.push(" ".into());
spans
} }
} }
impl<'a> From<Identity<'a>> for Title<'a> { impl<'a> From<Identity<'a>> for Title<'a> {
fn from(identity: Identity<'a>) -> Self { fn from(identity: Identity<'a>) -> Self {
Self::from(identity.to_string()).alignment(Alignment::Left) Self {
content: Line {
spans: identity.spans(),
style: Style::reset(),
alignment: None,
},
alignment: Some(Alignment::Left),
position: None,
}
} }
} }

View file

@ -1,26 +0,0 @@
//
use ratatui::{
text::{Line, Text},
widgets::Widget,
};
pub struct Messages<'a>(&'a Vec<String>);
impl<'a> Messages<'a> {
pub const fn new(messages: &'a Vec<String>) -> Self {
Self(messages)
}
}
impl<'a> Widget for Messages<'a> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
Text::from(
self.0
.iter()
.map(|m| Line::from(format!("- {m}")))
.collect::<Vec<_>>(),
)
.render(area, buf);
}
}

View file

@ -1,86 +1,114 @@
// //
mod configured;
mod identified;
mod identity; mod identity;
mod messages;
mod ready;
use std::string::String; use std::string::String;
use configured::ConfiguredRepoWidget; use git_next_core::{RepoAlias, RepoBranches};
use identified::IdentifiedRepoWidget;
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use ready::ReadyRepoWidget;
use crate::tui::actor::RepoState; use crate::{
git,
tui::{
actor::{RepoMessage, RepoState},
components::CommitLog,
},
};
use identity::Identity;
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Block, Borders, Widget},
};
use super::HeightContraintLength;
pub struct RepoWidget<'a> { pub struct RepoWidget<'a> {
pub repo_state: &'a RepoState, pub repo_state: &'a RepoState,
} }
impl<'a> HeightContraintLength for RepoWidget<'a> {
fn height_constraint_length(&self) -> u16 {
self.inner().height_constraint_length() + 2 // top + bottom borders
}
}
impl<'a> RepoWidget<'a> {
fn inner(&self) -> InnerRepoWidget {
match self.repo_state {
RepoState::Identified {
repo_alias,
message,
alert,
} => InnerRepoWidget {
repo_alias,
message,
alert: alert.as_ref().map(String::as_str),
branches: None,
log: None,
},
RepoState::Configured {
repo_alias,
message,
alert,
branches,
log,
}
| RepoState::Ready {
repo_alias,
message,
alert,
branches,
log,
..
} => InnerRepoWidget {
repo_alias,
message,
alert: alert.as_ref().map(String::as_str),
branches: Some(branches),
log: Some(log),
},
}
}
}
impl<'a> Widget for RepoWidget<'a> { impl<'a> Widget for RepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer) fn render(self, area: Rect, buf: &mut Buffer)
where where
Self: Sized, Self: Sized,
{ {
match self.repo_state { self.inner().render(area, buf);
RepoState::Identified {
repo_alias,
message,
messages,
alert,
} => {
IdentifiedRepoWidget {
repo_alias,
message,
messages,
alert: alert.as_ref().map(String::as_str),
} }
.render(area, buf);
} }
RepoState::Configured { struct InnerRepoWidget<'a> {
repo_alias, pub repo_alias: &'a RepoAlias,
message, pub message: &'a RepoMessage,
messages, pub alert: Option<&'a str>,
alert, pub branches: Option<&'a RepoBranches>,
branches, pub log: Option<&'a git::graph::Log>,
log,
..
} => ConfiguredRepoWidget {
repo_alias,
message,
messages,
alert: alert.as_ref().map(String::as_str),
branches,
log,
} }
.render(area, buf), impl<'a> HeightContraintLength for InnerRepoWidget<'a> {
fn height_constraint_length(&self) -> u16 {
RepoState::Ready { self.log
repo_alias, .map(|log| CommitLog { log })
message, .map_or(0, |w| w.height_constraint_length())
messages, }
alert, }
branches, impl<'a> Widget for InnerRepoWidget<'a> {
log, fn render(self, area: Rect, buf: &mut Buffer)
// view_state, where
// main, Self: Sized,
// next, {
// dev, let block = Block::default()
.. .title(Identity::new(
} => ReadyRepoWidget { self.repo_alias,
repo_alias, self.alert,
message, self.message,
messages, self.branches,
alert: alert.as_ref().map(String::as_str), ))
branches, .borders(Borders::TOP);
log, if let Some(log) = self.log {
// view_state, CommitLog { log }.render(block.inner(area), buf);
// main, }
// next, block.render(area, buf);
// dev,
}
.render(area, buf),
};
} }
} }

View file

@ -1,49 +0,0 @@
//
use git_next_core::{RepoAlias, RepoBranches};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
widgets::{Block, Borders, Widget},
};
use crate::{git, tui::components::CommitLog};
use super::{identity::Identity, messages::Messages};
pub struct ReadyRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a str,
pub messages: &'a Vec<String>,
pub alert: Option<&'a str>,
pub branches: &'a RepoBranches,
pub log: &'a git::graph::Log,
// pub view_state: &'a actor::ViewState,
// pub main: &'a Commit,
// pub next: &'a Commit,
// pub dev: &'a Commit,
}
impl<'a> Widget for ReadyRepoWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default()
.title(Identity::new(
self.repo_alias,
self.alert,
self.message,
Some(self.branches),
))
.borders(Borders::ALL);
let layout_repo = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Fill(1), Constraint::Fill(1)])
.split(block.inner(area));
block.render(area, buf);
Messages::new(self.messages).render(layout_repo[0], buf);
CommitLog { log: self.log }.render(layout_repo[1], buf);
}
}

View file

@ -0,0 +1,4 @@
//
use crate::newtype;
newtype!(CommitCount, u32, Default, "A number of commits");

View file

@ -11,6 +11,7 @@ pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
hostname(n), hostname(n),
user(n), user(n),
api_token(n), api_token(n),
None,
) )
} }

View file

@ -2,6 +2,8 @@ use std::collections::BTreeMap;
use crate::config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User}; use crate::config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User};
use super::CommitCount;
/// Defines a Forge to connect to /// Defines a Forge to connect to
/// Maps from `git-next-server.toml` at `forge.{forge}` /// Maps from `git-next-server.toml` at `forge.{forge}`
#[derive( #[derive(
@ -22,6 +24,7 @@ pub struct ForgeConfig {
hostname: String, hostname: String,
user: String, user: String,
token: String, token: String,
max_dev_commits: Option<u32>,
repos: BTreeMap<String, ServerRepoConfig>, repos: BTreeMap<String, ServerRepoConfig>,
} }
impl ForgeConfig { impl ForgeConfig {
@ -41,6 +44,10 @@ impl ForgeConfig {
ApiToken::new(self.token.clone().into()) ApiToken::new(self.token.clone().into())
} }
pub(crate) fn max_dev_commits(&self) -> Option<CommitCount> {
self.max_dev_commits.map(CommitCount::from)
}
pub fn repos(&self) -> impl Iterator<Item = (RepoAlias, &ServerRepoConfig)> { pub fn repos(&self) -> impl Iterator<Item = (RepoAlias, &ServerRepoConfig)> {
self.repos self.repos
.iter() .iter()

View file

@ -1,5 +1,7 @@
use crate::config::{ApiToken, ForgeAlias, ForgeConfig, ForgeType, Hostname, User}; use crate::config::{ApiToken, ForgeAlias, ForgeConfig, ForgeType, Hostname, User};
use super::CommitCount;
/// The derived information about a Forge, used to create interactions with it /// The derived information about a Forge, used to create interactions with it
#[derive(Clone, Default, Debug, derive_more::Constructor, derive_with::With)] #[derive(Clone, Default, Debug, derive_more::Constructor, derive_with::With)]
pub struct ForgeDetails { pub struct ForgeDetails {
@ -8,8 +10,7 @@ pub struct ForgeDetails {
hostname: Hostname, hostname: Hostname,
user: User, user: User,
token: ApiToken, token: ApiToken,
// API Token max_dev_commits: Option<CommitCount>,
// Private SSH Key Path
} }
impl ForgeDetails { impl ForgeDetails {
#[must_use] #[must_use]
@ -35,15 +36,21 @@ impl ForgeDetails {
pub const fn token(&self) -> &ApiToken { pub const fn token(&self) -> &ApiToken {
&self.token &self.token
} }
#[must_use]
pub const fn max_dev_commits(&self) -> Option<&CommitCount> {
self.max_dev_commits.as_ref()
}
} }
impl From<(&ForgeAlias, &ForgeConfig)> for ForgeDetails { impl From<(&ForgeAlias, &ForgeConfig)> for ForgeDetails {
fn from(forge: (&ForgeAlias, &ForgeConfig)) -> Self { fn from((forge_alias, forge_config): (&ForgeAlias, &ForgeConfig)) -> Self {
Self { Self {
forge_alias: forge.0.clone(), forge_alias: forge_alias.clone(),
forge_type: forge.1.forge_type(), forge_type: forge_config.forge_type(),
hostname: forge.1.hostname(), hostname: forge_config.hostname(),
user: forge.1.user(), user: forge_config.user(),
token: forge.1.token(), token: forge_config.token(),
max_dev_commits: forge_config.max_dev_commits(),
} }
} }
} }

View file

@ -1,6 +1,7 @@
// //
mod api_token; mod api_token;
mod branch_name; mod branch_name;
mod commit_count;
pub mod common; pub mod common;
mod forge_alias; mod forge_alias;
mod forge_config; mod forge_config;
@ -26,6 +27,7 @@ mod tests;
pub use api_token::ApiToken; pub use api_token::ApiToken;
pub use branch_name::BranchName; pub use branch_name::BranchName;
pub use commit_count::CommitCount;
pub use forge_alias::ForgeAlias; pub use forge_alias::ForgeAlias;
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub use forge_config::ForgeConfig; pub use forge_config::ForgeConfig;

View file

@ -149,6 +149,8 @@ mod repo_config {
} }
} }
mod forge_config { mod forge_config {
use given::maybe_a_number;
use super::*; use super::*;
#[test] #[test]
@ -166,7 +168,7 @@ mod forge_config {
let mut repos = BTreeMap::new(); let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone()); repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name.clone(), blue.clone()); repos.insert(blue_name.clone(), blue.clone());
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos); let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
let returned_repos = fc.repos().collect::<Vec<_>>(); let returned_repos = fc.repos().collect::<Vec<_>>();
@ -186,7 +188,7 @@ mod forge_config {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos); let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
assert_eq!(fc.forge_type(), ForgeType::MockForge); assert_eq!(fc.forge_type(), ForgeType::MockForge);
} }
@ -197,7 +199,14 @@ mod forge_config {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname.clone(), user, token, repos); let fc = ForgeConfig::new(
forge_type,
hostname.clone(),
user,
token,
maybe_a_number(),
repos,
);
assert_eq!(fc.hostname(), Hostname::new(hostname)); assert_eq!(fc.hostname(), Hostname::new(hostname));
} }
@ -208,7 +217,14 @@ mod forge_config {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user.clone(), token, repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user.clone(),
token,
maybe_a_number(),
repos,
);
assert_eq!(fc.user(), User::new(user)); assert_eq!(fc.user(), User::new(user));
} }
@ -219,7 +235,14 @@ mod forge_config {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user, token.clone(), repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user,
token.clone(),
maybe_a_number(),
repos,
);
assert_eq!(fc.token().expose_secret(), token.as_str()); assert_eq!(fc.token().expose_secret(), token.as_str());
} }
@ -237,7 +260,7 @@ mod forge_config {
let mut repos = BTreeMap::new(); let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone()); repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name, blue); repos.insert(blue_name, blue);
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos); let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
let returned_repo = fc.get_repo(red_name.as_str()); let returned_repo = fc.get_repo(red_name.as_str());
@ -255,8 +278,14 @@ mod forge_details {
let user = User::new(given::a_name()); let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into()); let token = ApiToken::new(given::a_name().into());
let forge_alias = ForgeAlias::new(given::a_name()); let forge_alias = ForgeAlias::new(given::a_name());
let forge_details = let forge_details = ForgeDetails::new(
ForgeDetails::new(forge_alias.clone(), forge_type, hostname, user, token); forge_alias.clone(),
forge_type,
hostname,
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.forge_alias(); let result = forge_details.forge_alias();
@ -269,7 +298,14 @@ mod forge_details {
let user = User::new(given::a_name()); let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into()); let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name()); let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(forge_name, forge_type, hostname, user, token); let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname,
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.forge_type(); let result = forge_details.forge_type();
@ -282,8 +318,14 @@ mod forge_details {
let user = User::new(given::a_name()); let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into()); let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name()); let forge_name = ForgeAlias::new(given::a_name());
let forge_details = let forge_details = ForgeDetails::new(
ForgeDetails::new(forge_name, forge_type, hostname.clone(), user, token); forge_name,
forge_type,
hostname.clone(),
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.hostname(); let result = forge_details.hostname();
@ -296,8 +338,14 @@ mod forge_details {
let user = User::new(given::a_name()); let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into()); let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name()); let forge_name = ForgeAlias::new(given::a_name());
let forge_details = let forge_details = ForgeDetails::new(
ForgeDetails::new(forge_name, forge_type, hostname, user.clone(), token); forge_name,
forge_type,
hostname,
user.clone(),
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.user(); let result = forge_details.user();
@ -310,8 +358,14 @@ mod forge_details {
let user = User::new(given::a_name()); let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into()); let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name()); let forge_name = ForgeAlias::new(given::a_name());
let forge_details = let forge_details = ForgeDetails::new(
ForgeDetails::new(forge_name, forge_type, hostname, user, token.clone()); forge_name,
forge_type,
hostname,
user,
token.clone(),
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.token(); let result = forge_details.token();
@ -325,7 +379,14 @@ mod forge_details {
let user = User::new(given::a_name()); let user = User::new(given::a_name());
let token = ApiToken::new(given::a_name().into()); let token = ApiToken::new(given::a_name().into());
let forge_name = ForgeAlias::new(given::a_name()); let forge_name = ForgeAlias::new(given::a_name());
let forge_details = ForgeDetails::new(forge_name, forge_type, hostname, user, token); let forge_details = ForgeDetails::new(
forge_name,
forge_type,
hostname,
user,
token,
given::maybe_a_number().map(CommitCount::from),
);
let result = forge_details.with_hostname(other_hostname.clone()); let result = forge_details.with_hostname(other_hostname.clone());
@ -340,12 +401,14 @@ mod forge_details {
let user = User::new(user_value.clone()); let user = User::new(user_value.clone());
let token_value = given::a_name(); let token_value = given::a_name();
let token = ApiToken::new(token_value.clone().into()); let token = ApiToken::new(token_value.clone().into());
let max_dev_commits = given::maybe_a_number();
let forge_alias = ForgeAlias::new(given::a_name()); let forge_alias = ForgeAlias::new(given::a_name());
let forge_config = ForgeConfig::new( let forge_config = ForgeConfig::new(
forge_type, forge_type,
hostname_value, hostname_value,
user_value, user_value,
token_value, token_value,
max_dev_commits,
BTreeMap::new(), BTreeMap::new(),
); );
@ -355,6 +418,12 @@ mod forge_details {
assert_eq!(forge_details.hostname(), &hostname); assert_eq!(forge_details.hostname(), &hostname);
assert_eq!(forge_details.user(), &user); assert_eq!(forge_details.user(), &user);
assert_eq!(forge_details.token().expose_secret(), token.expose_secret()); assert_eq!(forge_details.token().expose_secret(), token.expose_secret());
assert_eq!(
forge_details
.max_dev_commits()
.map(|commit_count| commit_count.clone().peel()),
max_dev_commits
);
} }
} }
mod forge_name { mod forge_name {
@ -492,6 +561,12 @@ mod server {
let forge_hostname = forge_default.hostname(); let forge_hostname = forge_default.hostname();
let forge_user = forge_default.user(); let forge_user = forge_default.user();
let forge_token = forge_default.token().expose_secret().to_string(); let forge_token = forge_default.token().expose_secret().to_string();
let optional_max_dev_commits = forge_default
.max_dev_commits()
.map(CommitCount::peel)
.map_or_else(String::new, |max_dev_commits| {
format!("max_dev_commits = {max_dev_commits}")
});
let mut repos: Vec<String> = vec![]; let mut repos: Vec<String> = vec![];
for (repo_alias, server_repo_config) in forge_default.repos() { for (repo_alias, server_repo_config) in forge_default.repos() {
let repo_path = server_repo_config.repo(); let repo_path = server_repo_config.repo();
@ -542,6 +617,7 @@ forge_type = "{forge_type}"
hostname = "{forge_hostname}" hostname = "{forge_hostname}"
user = "{forge_user}" user = "{forge_user}"
token = "{forge_token}" token = "{forge_token}"
{optional_max_dev_commits}
[forge.{forge_alias}.repos] [forge.{forge_alias}.repos]
{repos} {repos}
@ -726,6 +802,23 @@ mod given {
} }
generate(5) generate(5)
} }
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(0..100)
}
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn an_app_config() -> AppConfig { pub fn an_app_config() -> AppConfig {
AppConfig::new( AppConfig::new(
a_listen(), a_listen(),
@ -785,6 +878,7 @@ mod given {
a_name(), // hostname a_name(), // hostname
a_name(), // user a_name(), // user
a_name(), // token a_name(), // token
maybe_a_number(),
some_server_repo_configs(), some_server_repo_configs(),
) )
} }

View file

@ -3,6 +3,9 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("io")]
Io(#[from] std::io::Error),
#[error("unable to open repo: {0}")] #[error("unable to open repo: {0}")]
UnableToOpenRepo(String), UnableToOpenRepo(String),

View file

@ -49,6 +49,7 @@ impl RepoDetails {
forge_config.hostname(), forge_config.hostname(),
forge_config.user(), forge_config.user(),
forge_config.token(), forge_config.token(),
forge_config.max_dev_commits(),
), ),
} }
} }
@ -94,12 +95,7 @@ impl RepoDetails {
|> GitDir::pathbuf |> GitDir::pathbuf
|> gix::ThreadSafeRepository::open |> gix::ThreadSafeRepository::open
}?; }?;
let repo = pike! { let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo)), self.forge.clone());
gix_repo
|> RwLock::new
|> Arc::new
|> RealOpenRepository::new
};
Ok(repo) Ok(repo)
} }

View file

@ -66,7 +66,10 @@ impl RepositoryFactory for RealRepositoryFactory {
)? )?
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
tracing::info!("created"); tracing::info!("created");
let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo.into()))); let repo = RealOpenRepository::new(
Arc::new(RwLock::new(gix_repo.into())),
repo_details.forge.clone(),
);
Ok(Box::new(repo)) Ok(Box::new(repo))
} }

View file

@ -1,6 +1,7 @@
// //
use crate::{ use crate::{
git::{ git::{
self,
repository::{ repository::{
open::{OpenRepository, OpenRepositoryLike}, open::{OpenRepository, OpenRepositoryLike},
test::TestRepository, test::TestRepository,
@ -31,8 +32,11 @@ pub enum Repository {
} }
#[cfg(test)] #[cfg(test)]
pub(crate) const fn test(fs: kxio::fs::FileSystem) -> TestRepository { pub(crate) const fn test(
TestRepository::new(fs, vec![], vec![]) fs: kxio::fs::FileSystem,
forge_details: crate::ForgeDetails,
) -> TestRepository {
TestRepository::new(fs, vec![], vec![], forge_details)
} }
/// Opens a repository, cloning if necessary /// Opens a repository, cloning if necessary
@ -44,7 +48,9 @@ pub fn open(
) -> Result<Box<dyn OpenRepositoryLike>> { ) -> Result<Box<dyn OpenRepositoryLike>> {
let open_repository = if repo_details.gitdir.exists() { let open_repository = if repo_details.gitdir.exists() {
info!("Local copy found - opening..."); info!("Local copy found - opening...");
repository_factory.open(repo_details)? let repo = repository_factory.open(repo_details)?;
repo.fetch()?;
repo
} else { } else {
info!("Local copy not found - cloning..."); info!("Local copy not found - cloning...");
repository_factory.git_clone(repo_details)? repository_factory.git_clone(repo_details)?
@ -114,6 +120,9 @@ pub enum Error {
#[error("git clone: {0}")] #[error("git clone: {0}")]
Clone(String), Clone(String),
#[error("git fetch: {0}")]
FetchError(#[from] git::fetch::Error),
#[error("open: {0}")] #[error("open: {0}")]
Open(String), Open(String),

View file

@ -37,10 +37,11 @@ pub enum OpenRepository {
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
pub fn real(gix_repo: gix::Repository) -> OpenRepository { pub fn real(gix_repo: gix::Repository, forge_details: crate::ForgeDetails) -> OpenRepository {
OpenRepository::Real(oreal::RealOpenRepository::new(Arc::new(RwLock::new( OpenRepository::Real(oreal::RealOpenRepository::new(
gix_repo.into(), Arc::new(RwLock::new(gix_repo.into())),
)))) forge_details,
))
} }
#[cfg(not(tarpaulin_include))] // don't test mocks #[cfg(not(tarpaulin_include))] // don't test mocks
@ -49,8 +50,15 @@ pub(crate) fn test(
fs: &kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
on_fetch: Vec<otest::OnFetch>, on_fetch: Vec<otest::OnFetch>,
on_push: Vec<otest::OnPush>, on_push: Vec<otest::OnPush>,
forge_details: crate::ForgeDetails,
) -> OpenRepository { ) -> OpenRepository {
OpenRepository::Test(TestOpenRepository::new(gitdir, fs, on_fetch, on_push)) OpenRepository::Test(TestOpenRepository::new(
gitdir,
fs,
on_fetch,
on_push,
forge_details,
))
} }
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]

View file

@ -1,7 +1,7 @@
// //
use crate::{ use crate::{
git::{self, repository::OpenRepositoryLike}, git::{self, repository::OpenRepositoryLike},
BranchName, Hostname, RemoteUrl, RepoPath, BranchName, ForgeDetails, Hostname, RemoteUrl, RepoPath,
}; };
use derive_more::Constructor; use derive_more::Constructor;
@ -16,11 +16,14 @@ use std::{
}; };
#[derive(Clone, Debug, Constructor)] #[derive(Clone, Debug, Constructor)]
pub struct RealOpenRepository(Arc<RwLock<gix::ThreadSafeRepository>>); pub struct RealOpenRepository {
inner: Arc<RwLock<gix::ThreadSafeRepository>>,
forge_details: ForgeDetails,
}
impl super::OpenRepositoryLike for RealOpenRepository { impl super::OpenRepositoryLike for RealOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>> { fn remote_branches(&self) -> git::push::Result<Vec<BranchName>> {
let refs = self let refs = self
.0 .inner
.read() .read()
.map_err(|_| git::push::Error::Lock) .map_err(|_| git::push::Error::Lock)
.and_then(|repo| { .and_then(|repo| {
@ -44,7 +47,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
#[tracing::instrument] #[tracing::instrument]
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<RemoteUrl> { fn find_default_remote(&self, direction: git::repository::Direction) -> Option<RemoteUrl> {
let Ok(repository) = self.0.read() else { let Ok(repository) = self.inner.read() else {
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure #[cfg(not(tarpaulin_include))] // don't test mutex lock failure
tracing::debug!("no repository"); tracing::debug!("no repository");
return None; return None;
@ -64,34 +67,32 @@ impl super::OpenRepositoryLike for RealOpenRepository {
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // would require writing to external service #[cfg(not(tarpaulin_include))] // would require writing to external service
fn fetch(&self) -> Result<(), git::fetch::Error> { fn fetch(&self) -> Result<(), git::fetch::Error> {
use std::sync::atomic::AtomicBool; if self
.find_default_remote(git::repository::Direction::Fetch)
let Ok(repository) = self.0.read() else { .is_none()
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure {
return Err(git::fetch::Error::Lock);
};
let thread_local = repository.to_thread_local();
let Some(Ok(remote)) =
thread_local.find_default_remote(git::repository::Direction::Fetch.into())
else {
#[cfg(not(tarpaulin_include))] // test is on local repo - should always have remotes
return Err(git::fetch::Error::NoFetchRemoteFound); return Err(git::fetch::Error::NoFetchRemoteFound);
}; }
remote info!("Fetching");
.connect(gix::remote::Direction::Fetch) gix::command::prepare("/usr/bin/git fetch --prune")
.map_err(|gix| git::fetch::Error::Connect(gix.to_string()))? .with_context(gix::diff::command::Context {
.prepare_fetch( git_dir: Some(
gix::progress::Discard, self.inner
gix::remote::ref_map::Options::default(), .read()
) .map_err(|_| git::fetch::Error::Lock)
.map_err(|gix| git::fetch::Error::Prepare(gix.to_string()))? .map(|r| r.git_dir().to_path_buf())?,
.receive(gix::progress::Discard, &AtomicBool::default()) ),
.map_err(|gix| git::fetch::Error::Receive(gix.to_string()))?; ..Default::default()
})
.with_shell_allow_argument_splitting()
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?
.wait()?;
info!("Fetch okay"); info!("Fetch okay");
Ok(()) Ok(())
} }
// TODO: (#72) reimplement using `gix`
#[cfg(not(tarpaulin_include))] // would require writing to external service #[cfg(not(tarpaulin_include))] // would require writing to external service
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
fn push( fn push(
@ -117,7 +118,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
) )
.into(); .into();
let git_dir = self let git_dir = self
.0 .inner
.read() .read()
.map_err(|_| git::push::Error::Lock) .map_err(|_| git::push::Error::Lock)
.map(|r| r.git_dir().to_path_buf())?; .map(|r| r.git_dir().to_path_buf())?;
@ -140,8 +141,14 @@ impl super::OpenRepositoryLike for RealOpenRepository {
branch_name: &BranchName, branch_name: &BranchName,
find_commits: &[git::Commit], find_commits: &[git::Commit],
) -> Result<Vec<git::Commit>, git::commit::log::Error> { ) -> Result<Vec<git::Commit>, git::commit::log::Error> {
let limit = if find_commits.is_empty() { 1 } else { 25 }; let limit: usize = if find_commits.is_empty() {
self.0 1
} else {
self.forge_details
.max_dev_commits()
.map_or(25, |commit_count| commit_count.clone().peel() as usize)
};
self.inner
.read() .read()
.map_err(|_| git::commit::log::Error::Lock) .map_err(|_| git::commit::log::Error::Lock)
.map(|repo| { .map(|repo| {
@ -196,7 +203,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
#[tracing::instrument(skip_all, fields(%branch_name, ?file_name))] #[tracing::instrument(skip_all, fields(%branch_name, ?file_name))]
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String> { fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String> {
self.0 self.inner
.read() .read()
.map_err(|_| git::file::Error::Lock) .map_err(|_| git::file::Error::Lock)
.and_then(|repo| { .and_then(|repo| {

View file

@ -4,7 +4,7 @@ use crate::{
self, self,
repository::open::{OpenRepositoryLike, RealOpenRepository}, repository::open::{OpenRepositoryLike, RealOpenRepository},
}, },
BranchName, GitDir, RemoteUrl, RepoBranches, BranchName, ForgeDetails, GitDir, RemoteUrl, RepoBranches,
}; };
use derive_more::Constructor; use derive_more::Constructor;
@ -155,6 +155,7 @@ impl TestOpenRepository {
fs: &kxio::fs::FileSystem, fs: &kxio::fs::FileSystem,
on_fetch: Vec<OnFetch>, on_fetch: Vec<OnFetch>,
on_push: Vec<OnPush>, on_push: Vec<OnPush>,
forge_details: ForgeDetails,
) -> Self { ) -> Self {
let pathbuf = fs.base().join(gitdir.to_path_buf()); let pathbuf = fs.base().join(gitdir.to_path_buf());
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
@ -165,7 +166,7 @@ impl TestOpenRepository {
fetch_counter: Arc::new(RwLock::new(0)), fetch_counter: Arc::new(RwLock::new(0)),
on_push, on_push,
push_counter: Arc::new(RwLock::new(0)), push_counter: Arc::new(RwLock::new(0)),
real: RealOpenRepository::new(Arc::new(RwLock::new(gix.into()))), real: RealOpenRepository::new(Arc::new(RwLock::new(gix.into())), forge_details),
} }
} }

View file

@ -1,3 +1,5 @@
use crate::CommitCount;
// //
use super::*; use super::*;
@ -6,7 +8,8 @@ use super::*;
fn should_return_single_item_in_commit_log_when_not_searching() -> TestResult { fn should_return_single_item_in_commit_log_when_not_searching() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp()); let_assert!(Ok(fs) = kxio::fs::temp());
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
let repo_config = &given::a_repo_config(); let repo_config = &given::a_repo_config();
let branches = repo_config.branches(); let branches = repo_config.branches();
@ -23,7 +26,10 @@ fn should_return_capacity_25_in_commit_log_when_searching_for_garbage() -> TestR
let_assert!(Ok(fs) = kxio::fs::temp()); let_assert!(Ok(fs) = kxio::fs::temp());
let branch_name = given::a_branch_name(); let branch_name = given::a_branch_name();
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(25)));
let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
assert!(**max_dev_commits >= 25);
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
for _ in [0; 25] { for _ in [0; 25] {
then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?; then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?;
@ -39,7 +45,10 @@ fn should_return_5_in_commit_log_when_searching_for_5th_item() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp(), "create temp directory"); let_assert!(Ok(fs) = kxio::fs::temp(), "create temp directory");
let branch_name = given::a_branch_name(); let branch_name = given::a_branch_name();
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(10)));
let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
assert!(**max_dev_commits > 5);
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!( let_assert!(
Ok(open_repository) = test_repository.open(&gitdir), Ok(open_repository) = test_repository.open(&gitdir),
"open repository" "open repository"

View file

@ -14,7 +14,14 @@ fn should_return_repos() {
let mut repos = BTreeMap::new(); let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone()); repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name.clone(), blue.clone()); repos.insert(blue_name.clone(), blue.clone());
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user,
token,
given::maybe_a_number(),
repos,
);
let returned_repos = fc.repos().collect::<Vec<_>>(); let returned_repos = fc.repos().collect::<Vec<_>>();
@ -35,7 +42,14 @@ fn should_return_forge_type() {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user,
token,
given::maybe_a_number(),
repos,
);
assert_eq!(fc.forge_type(), ForgeType::MockForge); assert_eq!(fc.forge_type(), ForgeType::MockForge);
} }
@ -47,7 +61,14 @@ fn should_return_hostname() {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname.clone(), user, token, repos); let fc = ForgeConfig::new(
forge_type,
hostname.clone(),
user,
token,
given::maybe_a_number(),
repos,
);
assert_eq!(fc.hostname(), Hostname::new(hostname)); assert_eq!(fc.hostname(), Hostname::new(hostname));
} }
@ -59,7 +80,14 @@ fn should_return_user() {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user.clone(), token, repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user.clone(),
token,
given::maybe_a_number(),
repos,
);
assert_eq!(fc.user(), User::new(user)); assert_eq!(fc.user(), User::new(user));
} }
@ -71,7 +99,14 @@ fn should_return_token() {
let user = given::a_name(); let user = given::a_name();
let token = given::a_name(); let token = given::a_name();
let repos = BTreeMap::new(); let repos = BTreeMap::new();
let fc = ForgeConfig::new(forge_type, hostname, user, token.clone(), repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user,
token.clone(),
given::maybe_a_number(),
repos,
);
assert_eq!(fc.token().expose_secret(), token.as_str()); assert_eq!(fc.token().expose_secret(), token.as_str());
} }
@ -90,7 +125,14 @@ fn should_return_repo() {
let mut repos = BTreeMap::new(); let mut repos = BTreeMap::new();
repos.insert(red_name.clone(), red.clone()); repos.insert(red_name.clone(), red.clone());
repos.insert(blue_name, blue); repos.insert(blue_name, blue);
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos); let fc = ForgeConfig::new(
forge_type,
hostname,
user,
token,
given::maybe_a_number(),
repos,
);
let returned_repo = fc.get_repo(red_name.as_str()); let returned_repo = fc.get_repo(red_name.as_str());

View file

@ -9,8 +9,9 @@ fn should_return_file() -> TestResult {
let file_name = given::a_pathbuf(); let file_name = given::a_pathbuf();
let contents = given::a_name(); let contents = given::a_name();
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let forge_details = given::forge_details();
let test_repository = git::repository::test(fs.clone()); let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
then::commit_named_file_to_branch( then::commit_named_file_to_branch(
&file_name, &file_name,
@ -33,7 +34,8 @@ fn should_return_file() -> TestResult {
fn should_error_on_missing_file() -> TestResult { fn should_error_on_missing_file() -> TestResult {
let_assert!(Ok(fs) = kxio::fs::temp()); let_assert!(Ok(fs) = kxio::fs::temp());
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let test_repository = git::repository::test(fs.clone(), forge_details);
let_assert!(Ok(open_repository) = test_repository.open(&gitdir)); let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
let repo_config = &given::a_repo_config(); let repo_config = &given::a_repo_config();
let branches = repo_config.branches(); let branches = repo_config.branches();

View file

@ -13,7 +13,7 @@ use crate::{
}, },
RepoDetails, RepoDetails,
}, },
GitDir, ForgeDetails, GitDir,
}; };
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
@ -22,6 +22,7 @@ pub struct TestRepository {
fs: kxio::fs::FileSystem, fs: kxio::fs::FileSystem,
on_fetch: Vec<git::repository::open::otest::OnFetch>, on_fetch: Vec<git::repository::open::otest::OnFetch>,
on_push: Vec<git::repository::open::otest::OnPush>, on_push: Vec<git::repository::open::otest::OnPush>,
forge_details: ForgeDetails,
} }
impl TestRepository { impl TestRepository {
pub fn on_fetch(&mut self, on_fetch: OnFetch) { pub fn on_fetch(&mut self, on_fetch: OnFetch) {
@ -39,6 +40,7 @@ impl RepositoryLike for TestRepository {
&self.fs, &self.fs,
self.on_fetch.clone(), self.on_fetch.clone(),
self.on_push.clone(), self.on_push.clone(),
self.forge_details.clone(),
)) ))
} }

View file

@ -172,6 +172,7 @@ mod repo_details {
"host".to_string(), "host".to_string(),
"user".to_string(), "user".to_string(),
"token".to_string(), "token".to_string(),
given::maybe_a_number(), // max dev commits
BTreeMap::new(), BTreeMap::new(),
), ),
GitDir::new(PathBuf::default().join("foo"), StoragePathType::Internal), GitDir::new(PathBuf::default().join("foo"), StoragePathType::Internal),
@ -184,6 +185,8 @@ mod repo_details {
} }
} }
pub mod given { pub mod given {
use crate::ForgeDetails;
use super::*; use super::*;
pub fn repo_branches() -> RepoBranches { pub fn repo_branches() -> RepoBranches {
@ -219,6 +222,22 @@ pub mod given {
generate(5) generate(5)
} }
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(5..100)
}
pub fn a_branch_name() -> BranchName { pub fn a_branch_name() -> BranchName {
BranchName::new(a_name()) BranchName::new(a_name())
} }
@ -235,14 +254,19 @@ pub mod given {
format!("hostname-{}", a_name()), format!("hostname-{}", a_name()),
format!("user-{}", a_name()), format!("user-{}", a_name()),
format!("token-{}", a_name()), format!("token-{}", a_name()),
given::maybe_a_number(), // max dev commits
BTreeMap::default(), // no repos BTreeMap::default(), // no repos
) )
} }
pub fn forge_details() -> ForgeDetails {
(&a_forge_alias(), &a_forge_config()).into()
}
pub fn a_server_repo_config() -> ServerRepoConfig { pub fn a_server_repo_config() -> ServerRepoConfig {
let main = a_branch_name().unwrap(); let main = a_branch_name().peel();
let next = a_branch_name().unwrap(); let next = a_branch_name().peel();
let dev = a_branch_name().unwrap(); let dev = a_branch_name().peel();
ServerRepoConfig::new( ServerRepoConfig::new(
format!("{}/{}", a_name(), a_name()), format!("{}/{}", a_name(), a_name()),
main.clone(), main.clone(),

View file

@ -116,6 +116,32 @@ impl UserNotification {
}), }),
} }
} }
#[must_use]
pub fn aliases(&self) -> (ForgeAlias, RepoAlias) {
match self {
Self::CICheckFailed {
forge_alias,
repo_alias,
..
}
| Self::RepoConfigLoadFailure {
forge_alias,
repo_alias,
..
}
| Self::WebhookRegistration {
forge_alias,
repo_alias,
..
}
| Self::DevNotBasedOnMain {
forge_alias,
repo_alias,
..
} => (forge_alias.clone(), repo_alias.clone()),
}
}
} }
impl Display for UserNotification { impl Display for UserNotification {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -127,7 +153,11 @@ impl Display for UserNotification {
Self::WebhookRegistration { reason, .. } => { Self::WebhookRegistration { reason, .. } => {
format!("Failed to register webhook: {reason}") format!("Failed to register webhook: {reason}")
} }
Self::DevNotBasedOnMain { .. } => "Dev not based on main".to_string(), Self::DevNotBasedOnMain {
dev_branch,
main_branch,
..
} => format!("{dev_branch} not based on {main_branch}"),
}; };
write!(f, "{message}") write!(f, "{message}")
} }

View file

@ -1,10 +1,9 @@
use tracing::{debug, instrument};
// //
use crate::{ use crate::{
git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification}, git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
BranchName, RepoConfig, BranchName, RepoConfig,
}; };
use tracing::{debug, instrument};
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;
@ -29,13 +28,14 @@ pub fn validate(
open_repository: &dyn OpenRepositoryLike, open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails, repo_details: &git::RepoDetails,
repo_config: &RepoConfig, repo_config: &RepoConfig,
log: git::graph::Log, ) -> Result<(Positions, git::graph::Log)> {
) -> Result<Positions> {
let main_branch = repo_config.branches().main(); let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next(); let next_branch = repo_config.branches().next();
let dev_branch = repo_config.branches().dev(); let dev_branch = repo_config.branches().dev();
// Collect Commit Histories for `main`, `next` and `dev` branches // Collect Commit Histories for `main`, `next` and `dev` branches
open_repository.fetch()?; open_repository.fetch()?;
let git_log = git::graph::log(repo_details);
let commit_histories = get_commit_histories(open_repository, repo_config)?; let commit_histories = get_commit_histories(open_repository, repo_config)?;
// branch tips // branch tips
let main = commit_histories let main = commit_histories
@ -64,7 +64,7 @@ pub fn validate(
main_branch, main_branch,
dev_commit: dev, dev_commit: dev,
main_commit: main, main_commit: main,
log, log: git_log,
}, },
)); ));
} }
@ -80,23 +80,38 @@ pub fn validate(
&main, &main,
) { ) {
tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",); tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",);
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch); return Err(reset_next_to_main(
open_repository,
repo_details,
&main,
&next,
&next_branch,
));
} }
// verify that next is an ancestor of dev, else reset it back to main if dev not ahead of main // verify that next is an ancestor of dev, else reset it back to main if dev not ahead of main
if is_not_based_on(&commit_histories.dev, &next) if is_not_based_on(&commit_histories.dev, &next)
&& commit_histories.main.first() == commit_histories.dev.first() && commit_histories.main.first() == commit_histories.dev.first()
{ {
tracing::info!("Next is not an ancestor of dev - resetting next to main"); tracing::info!("Next is not an ancestor of dev - resetting next to main");
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch); return Err(reset_next_to_main(
open_repository,
repo_details,
&main,
&next,
&next_branch,
));
} }
let next_is_valid = is_based_on(&commit_histories.dev, &next); let next_is_valid = is_based_on(&commit_histories.dev, &next);
Ok(git::validation::positions::Positions { Ok((
git::validation::positions::Positions {
main, main,
next, next,
dev, dev,
dev_commit_history: commit_histories.dev, dev_commit_history: commit_histories.dev,
next_is_valid, next_is_valid,
}) },
git_log,
))
} }
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
@ -106,22 +121,19 @@ fn reset_next_to_main(
main: &git::Commit, main: &git::Commit,
next: &git::Commit, next: &git::Commit,
next_branch: &BranchName, next_branch: &BranchName,
) -> Result<Positions> { ) -> Error {
git::push::reset( match git::push::reset(
open_repository, open_repository,
repo_details, repo_details,
next_branch, next_branch,
&main.clone().into(), &main.clone().into(),
&git::push::Force::From(next.clone().into()), &git::push::Force::From(next.clone().into()),
) ) {
.map_err(|err| { Ok(()) => Error::Retryable(format!("Branch {next_branch} has been reset")),
Error::NonRetryable(format!( Err(err) => Error::NonRetryable(format!(
"Failed to reset branch '{next_branch}' to commit '{next}': {err}" "Failed to reset branch '{next_branch}' to commit '{next}': {err}"
)) )),
})?; }
Err(Error::Retryable(format!(
"Branch {next_branch} has been reset"
)))
} }
fn is_not_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool { fn is_not_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {

View file

@ -75,12 +75,7 @@ mod positions {
); );
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
let result = validate( let result = validate(&*repository, &repo_details, &repo_config);
&*repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
let_assert!(Err(err) = result, "validate"); let_assert!(Err(err) = result, "validate");
@ -120,12 +115,7 @@ mod positions {
"open repo" "open repo"
); );
let result = validate( let result = validate(&*open_repository, &repo_details, &repo_config);
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -164,12 +154,7 @@ mod positions {
"open repo" "open repo"
); );
let result = validate( let result = validate(&*open_repository, &repo_details, &repo_config);
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -208,12 +193,7 @@ mod positions {
"open repo" "open repo"
); );
let result = validate( let result = validate(&*open_repository, &repo_details, &repo_config);
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default(),
);
println!("{result:?}"); println!("{result:?}");
assert!(matches!( assert!(matches!(
@ -227,7 +207,8 @@ mod positions {
//given //given
let fs = given::a_filesystem(); let fs = given::a_filesystem();
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let mut test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
@ -260,12 +241,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate( Err(err) = validate(&*open_repository, &repo_details, &repo_config),
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -282,7 +258,8 @@ mod positions {
//given //given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let mut test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
@ -350,12 +327,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate( Err(err) = validate(&*open_repository, &repo_details, &repo_config),
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -373,7 +345,8 @@ mod positions {
//given //given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let mut test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
@ -424,12 +397,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate( Err(err) = validate(&*open_repository, &repo_details, &repo_config),
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -451,7 +419,8 @@ mod positions {
//given //given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let mut test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
@ -516,12 +485,7 @@ mod positions {
//when //when
let_assert!( let_assert!(
Err(err) = validate( Err(err) = validate(&*open_repository, &repo_details, &repo_config),
&*open_repository,
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -541,7 +505,8 @@ mod positions {
//given //given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let mut test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
@ -607,12 +572,8 @@ mod positions {
//when //when
let_assert!( let_assert!(
Ok(positions) = validate( Ok((positions, _git_log)) =
&*open_repository, validate(&*open_repository, &repo_details, &repo_config),
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );
@ -642,7 +603,8 @@ mod positions {
//given //given
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs"); let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal); let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
let mut test_repository = git::repository::test(fs.clone()); let forge_details = given::forge_details();
let mut test_repository = git::repository::test(fs.clone(), forge_details);
let repo_config = given::a_repo_config(); let repo_config = given::a_repo_config();
test_repository.on_fetch(OnFetch::new( test_repository.on_fetch(OnFetch::new(
repo_config.branches().clone(), repo_config.branches().clone(),
@ -671,12 +633,8 @@ mod positions {
//when //when
let_assert!( let_assert!(
Ok(positions) = validate( Ok((positions, _git_log)) =
&*open_repository, validate(&*open_repository, &repo_details, &repo_config),
&repo_details,
&repo_config,
git::graph::Log::default()
),
"validate" "validate"
); );

View file

@ -39,13 +39,13 @@ macro_rules! newtype {
} }
#[allow(clippy::missing_const_for_fn)] #[allow(clippy::missing_const_for_fn)]
#[must_use] #[must_use]
pub fn unwrap(self) -> $type { pub fn peel(self) -> $type {
self.0 self.0
} }
} }
impl From<$name> for $type { impl From<$name> for $type {
fn from(value: $name) -> $type { fn from(value: $name) -> $type {
value.unwrap() value.peel()
} }
} }
}; };

View file

@ -691,6 +691,22 @@ mod forgejo {
generate(5) generate(5)
} }
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(0..100)
}
pub fn a_webhook_id() -> WebhookId { pub fn a_webhook_id() -> WebhookId {
WebhookId::new(a_name()) WebhookId::new(a_name())
} }
@ -711,14 +727,15 @@ mod forgejo {
a_name(), a_name(),
a_name(), a_name(),
a_name(), a_name(),
maybe_a_number(),
BTreeMap::default(), // no repos BTreeMap::default(), // no repos
) )
} }
pub fn a_server_repo_config() -> ServerRepoConfig { pub fn a_server_repo_config() -> ServerRepoConfig {
let main = a_branch_name().unwrap(); let main = a_branch_name().peel();
let next = a_branch_name().unwrap(); let next = a_branch_name().peel();
let dev = a_branch_name().unwrap(); let dev = a_branch_name().peel();
ServerRepoConfig::new( ServerRepoConfig::new(
format!("{}/{}", a_name(), a_name()), format!("{}/{}", a_name(), a_name()),
main.clone(), main.clone(),

View file

@ -24,12 +24,13 @@ pub async fn unregister(
None, None,
network::NetRequestLogging::None, network::NetRequestLogging::None,
); );
let result = net.delete(request).await; match net.delete(request).await {
if let Err(e) = result { Err(e) => {
tracing::warn!("Failed to unregister webhook"); tracing::warn!("Failed to unregister webhook");
return Err(git::forge::webhook::Error::FailedToUnregister( Err(git::forge::webhook::Error::FailedToUnregister(
e.to_string(), e.to_string(),
)); ))
}
_ => Ok(()),
} }
Ok(())
} }

View file

@ -600,6 +600,7 @@ mod github {
a_name(), a_name(),
a_name(), a_name(),
a_name(), a_name(),
maybe_a_number(),
BTreeMap::default(), BTreeMap::default(),
), ),
GitDir::new(PathBuf::default(), StoragePathType::External), GitDir::new(PathBuf::default(), StoragePathType::External),
@ -641,6 +642,22 @@ mod github {
generate(5) generate(5)
} }
pub fn maybe_a_number() -> Option<u32> {
use rand::Rng;
let mut rng = rand::thread_rng();
if Rng::gen_ratio(&mut rng, 1, 2) {
Some(a_number())
} else {
None
}
}
pub fn a_number() -> u32 {
use rand::Rng;
let mut rng = rand::thread_rng();
rng.gen_range(0..100)
}
pub fn a_webhook_id() -> WebhookId { pub fn a_webhook_id() -> WebhookId {
WebhookId::new(given::a_name()) WebhookId::new(given::a_name())
} }

447
demo.cast Normal file
View file

@ -0,0 +1,447 @@
{"version": 2, "width": 80, "height": 24, "timestamp": 1726490135, "env": {"SHELL": "/opt/homebrew/bin/bash", "TERM": "xterm-256color"}}
[0.029531, "o", "\u001b[?1049h"]
[0.030008, "o", "\u001b[1;1H┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[1m Git-Next v0.13.11 \u001b[22m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\u001b[2;1H┃\u001b[2;36HLoading...1\u001b[2;80H┃\u001b[3;1H┃\u001b[3;80H┃\u001b[4;1H┃\u001b[4;80H┃\u001b[5;1H┃\u001b[5;80H┃\u001b[6;1H┃\u001b[6;80H┃\u001b[7;1H┃\u001b[7;80H┃\u001b[8;1H┃\u001b[8;80H┃\u001b[9;1H┃\u001b[9;80H┃\u001b[10;1H┃\u001b[10;80H┃\u001b[11;1H┃\u001b[11;80H┃\u001b[12;1H┃\u001b[12;80H┃\u001b[13;1H┃\u001b[13;80H┃\u001b[14;1H┃\u001b[14;80H┃\u001b[15;1H┃\u001b[15;80H┃\u001b[16;1H┃\u001b[16;80H┃\u001b[17;1H┃\u001b[17;80H┃\u001b[18;1H┃\u001b[18;80H┃\u001b[19;1H┃\u001b[19;80H┃\u001b[20;1H┃\u001b[20;80H┃\u001b[21;1H┃\u001b[21;80H┃\u001b[22;1H┃\u001b[22;80H┃\u001b[23;1H┃\u001b[23;80H┃\u001b[24;1H┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[24;36H[q]uit\u001b[24;43H💚\u001b[24;47H━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[24;74H13:35\u001b[24;80H┛\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.552043, "o", "\u001b[2;3Hforge:\u001b[2;10Hjo\u001b[2;36H \u001b[3;3H\u001b[38;5;6;49mgit-next\u001b[3;12H\u001b[39;49m(main\u001b[3;18H->\u001b[3;21Hnext\u001b[3;26H->\u001b[3;29Hdev)\u001b[3;34H\u001b[38;5;7;49mregistering webhook...\u001b[3;57H\u001b[39;49m──────────────────────\u001b[5;3H\u001b[38;5;6;49mkxio\u001b[5;8H\u001b[39;49m(main\u001b[5;14H->\u001b[5;17Hnext\u001b[5;22H->\u001b[5;25Hdev)\u001b[5;30H\u001b[38;5;7;49mregistering webhook...\u001b[5;53H\u001b[39;49m──────────────────────────\u001b[7;3H\u001b[38;5;6;49mpodal\u001b[7;9H\u001b[39;49m(main\u001b[7;15H->\u001b[7;18Hnext\u001b[7;23H->\u001b[7;26Hdev)\u001b[7;31H\u001b[38;5;7;49mregistering webhook...\u001b[7;54H\u001b[39;49m─────────────────────────\u001b[9;3H\u001b[38;5;6;49mrefile-m4b\u001b[9;14H\u001b[39;49m(main\u001b[9;20H->\u001b[9;23Hnext\u001b[9;28H->\u001b[9;31Hdev)\u001b[9;36H\u001b[38;5;7;49mregistering webhook...\u001b[9;59H\u001b[39;49m────────────────────\u001b[11;3H\u001b[38;5;6;49mrust-action\u001b[11;15H\u001b[39;49m(main\u001b[11;21H->\u001b[11;24Hnext\u001b[11;29H->\u001b[11;32Hdev)\u001b[11;37H\u001b[38;5;7;49mregistering webhook...\u001b[11;60H"]
[2.552123, "o", "\u001b[39;49m───────────────────\u001b[13;3H\u001b[38;5;6;49mskip\u001b[13;8H\u001b[39;49m(main\u001b[13;14H->\u001b[13;17Hnext\u001b[13;22H->\u001b[13;25Hdev)\u001b[13;30H\u001b[38;5;7;49mregistering webhook...\u001b[13;53H\u001b[39;49m──────────────────────────\u001b[15;3H\u001b[38;5;6;49mtasyn\u001b[15;9H\u001b[39;49m(main\u001b[15;15H->\u001b[15;18Hnext\u001b[15;23H->\u001b[15;26Hdev)\u001b[15;31H\u001b[38;5;7;49mregistering webhook...\u001b[15;54H\u001b[39;49m─────────────────────────\u001b[17;3H\u001b[38;5;6;49mtest\u001b[17;8H\u001b[39;49m(main\u001b[17;14H->\u001b[17;17Hnext\u001b[17;22H->\u001b[17;25Hdev)\u001b[17;30H\u001b[38;5;7;49mregistering webhook...\u001b[17;53H\u001b[39;49m──────────────────────────\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.614517, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.649175, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.684709, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.71967, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.75525, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.789211, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[2.826615, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[3.673281, "o", "\u001b[7;31H\u001b[38;5;2;49mokay\u001b[39;49m ──────────────────\u001b[8;2H*\u001b[8;4H249b943\u001b[8;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[8;30H\u001b[39;49mfix(deps):\u001b[8;41Hupdate\u001b[8;48Hrust\u001b[8;53Hcrate\u001b[8;59Hscraper\u001b[8;67Hto\u001b[8;70H0.20\u001b[9;3H \u001b[9;14H \u001b[9;20H \u001b[9;23H \u001b[9;28H \u001b[9;31H \u001b[9;36H \u001b[9;59H \u001b[10;3H\u001b[38;5;6;49mrefile-m4b\u001b[10;14H\u001b[39;49m(main\u001b[10;20H->\u001b[10;23Hnext\u001b[10;28H->\u001b[10;31Hdev)\u001b[10;36H\u001b[38;5;7;49mregistering webhook...\u001b[10;59H\u001b[39;49m────────────────────\u001b[11;3H \u001b[11;15H \u001b[11;21H \u001b[11;24H \u001b[11;29H \u001b[11;32H \u001b[11;37H \u001b[11;60H \u001b[12;3H\u001b[38;5;6;49mrust-action\u001b[12;15H\u001b[39;49m(main\u001b[12;21H->\u001b[12;24Hnext\u001b[12;29H->\u001b[12;32Hdev)\u001b[12;37H\u001b[38;5;7;49mregistering webhook...\u001b[12;60H\u001b[39;49m───────────────────\u001b[13;3H \u001b[13;8H \u001b[13;14H \u001b[13;17H \u001b[13;22H \u001b[13;25H \u001b[13;30H \u001b[13;53"]
[3.673319, "o", "H \u001b[14;3H\u001b[38;5;6;49mskip\u001b[14;8H\u001b[39;49m(main\u001b[14;14H->\u001b[14;17Hnext\u001b[14;22H->\u001b[14;25Hdev)\u001b[14;30H\u001b[38;5;2;49mokay\u001b[14;35H\u001b[39;49m────────────────────────────────────────────\u001b[15;2H* fc7fbca \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/rust docker tag to \u001b[17;4H\u001b[38;5;6;49ma\u001b[17;6Hyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;7;49mregistering webh\u001b[17;48Hok\u001b[17;52H.\u001b[39;49m \u001b[19;3H\u001b[38;5;6;49mtest\u001b[19;8H\u001b[39;49m(main\u001b[19;14H->\u001b[19;17Hnext\u001b[19;22H->\u001b[19;25Hdev)\u001b[19;30H\u001b[38;5;7;49mregistering webhook...\u001b[19;53H\u001b[39;49m──────────────────────────\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.567297, "o", "\u001b[2;79H▲\u001b[3;34H\u001b[38;5;2;49mokay\u001b[39;49m ──────────────────\u001b[3;79H█\u001b[4;2H*\u001b[4;4H91c5973\u001b[4;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[4;30H\u001b[39;49mchore:\u001b[4;37Hrelease\u001b[4;79H█\u001b[5;3H \u001b[5;8H \u001b[5;14H \u001b[5;17H \u001b[5;22H \u001b[5;25H \u001b[5;30H \u001b[5;53H █\u001b[6;3H\u001b[38;5;6;49mkxio\u001b[6;8H\u001b[39;49m(main\u001b[6;14H->\u001b[6;17Hnext\u001b[6;22H->\u001b[6;25Hdev)\u001b[6;30H\u001b[38;5;7;49mregistering webhook...\u001b[6;53H\u001b[39;49m──────────────────────────█\u001b[7;3H \u001b[7;9H \u001b[7;15H \u001b[7;18H \u001b[7;23H \u001b[7;26H \u001b[7;31H \u001b[7;36H █\u001b[8;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────█\u001b[9;2H*\u001b[9;4H249b943\u001b[9;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[9;30H\u001b[39;49mfix(deps):\u001b[9;41Hupdate\u001b[9;48Hrust\u001b[9;53Hcrate\u001b[9;59Hs"]
[4.567379, "o", "craper\u001b[9;67Hto\u001b[9;70H0.20\u001b[9;79H█\u001b[10;3H \u001b[10;14H \u001b[10;20H \u001b[10;23H \u001b[10;28H \u001b[10;31H \u001b[10;36H \u001b[10;59H █\u001b[11;3H\u001b[38;5;6;49mrefile-m4b\u001b[11;14H\u001b[39;49m(main\u001b[11;20H->\u001b[11;23Hnext\u001b[11;28H->\u001b[11;31Hdev)\u001b[11;36H\u001b[38;5;7;49mregistering webhook...\u001b[11;59H\u001b[39;49m────────────────────█\u001b[12;3H \u001b[12;15H \u001b[12;21H \u001b[12;24H \u001b[12;29H \u001b[12;32H \u001b[12;37H \u001b[12;60H █\u001b[13;3H\u001b[38;5;6;49mrust-action\u001b[13;15H\u001b[39;49m(main\u001b[13;21H->\u001b[13;24Hnext\u001b[13;29H->\u001b[13;32Hdev)\u001b[13;37H\u001b[38;5;2;49mokay\u001b[13;42H\u001b[39;49m─────────────────────────────────────║\u001b[14;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[14;30H\u001b[39;49mfeat: ensure toolchains are up-to-date ║\u001b[15;2H \u001b[15;4H \u001b[15;12H \u001b[15;30H \u001b[15;43H \u001b[15;50H \u001b[15;65H \u001b[15;72H \u001b[15;76"]
[4.567437, "o", "H \u001b[15;79H║\u001b[16;3H\u001b[38;5;6;49mskip\u001b[16;8H\u001b[39;49m(main\u001b[16;14H->\u001b[16;17Hnext\u001b[16;22H->\u001b[16;25Hdev)\u001b[16;30H\u001b[38;5;2;49mokay\u001b[16;35H\u001b[39;49m────────────────────────────────────────────║\u001b[17;2H* fc7fbca \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/rust docker tag to ║\u001b[18;79H║\u001b[19;4H\u001b[38;5;6;49ma\u001b[19;6Hyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────\u001b[19;79H║\u001b[20;2H*\u001b[20;4H24fe845\u001b[20;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[20;30H\u001b[39;49mchore(deps):\u001b[20;43Hupdate\u001b[20;50Hdocker.io/postgres:16.4-alpin║\u001b[21;79H║\u001b[22;3H\u001b[38;5;6;49mtest\u001b[22;8H\u001b[39;49m(main\u001b[22;14H->\u001b[22;17Hnext\u001b[22;22H->\u001b[22;25Hdev)\u001b[22;30H\u001b[38;5;7;49mregistering webhook...\u001b[22;53H\u001b[39;49m──────────────────────────║\u001b[23;79H▼\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.603024, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[4.636433, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.373579, "o", "\u001b[11;36H\u001b[38;5;2;49mokay\u001b[39;49m ──────────────────\u001b[12;2H*\u001b[12;4H7fba5fb\u001b[12;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[12;30H\u001b[39;49mSupport\u001b[12;38H--version,\u001b[12;49H--help\u001b[12;56Hand\u001b[12;60H--directory\u001b[12;72Hcli\u001b[12;76Harg\u001b[13;3H \u001b[13;15H \u001b[13;21H \u001b[13;24H \u001b[13;29H \u001b[13;32H \u001b[13;37H \u001b[13;42H \u001b[14;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[15;2H*\u001b[15;4Hde4785c\u001b[15;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[15;30H\u001b[39;49mfeat:\u001b[15;36Hensure\u001b[15;43Htoolchains\u001b[15;54Hare\u001b[15;58Hup-to-date\u001b[16;3H \u001b[16;8H \u001b[16;14H \u001b[16;17H \u001b[16;22H \u001b[16;25H \u001b[16;30H \u001b[16;35H \u001b[17;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[17;11Hin -> next -> dev)\u001b[17;30H\u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────"]
[5.373661, "o", "───────────────────────\u001b[18;2H*\u001b[18;4Hfc7fbca\u001b[18;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[18;30H\u001b[39;49mchore(deps):\u001b[18;43Hupdate\u001b[18;50Hdocker.io/rust\u001b[18;65Hdocker\u001b[18;72Htag\u001b[18;76Hto\u001b[19;3H \u001b[19;9H \u001b[19;15H \u001b[19;18H \u001b[19;23H \u001b[19;26H \u001b[19;31H \u001b[19;36H \u001b[20;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[21;2H*\u001b[21;4H24fe845\u001b[21;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[21;30H\u001b[39;49mchore(deps):\u001b[21;43Hupdate\u001b[21;50Hdocker.io/postgres:16.4-alpin\u001b[22;3H \u001b[22;8H \u001b[22;14H \u001b[22;17H \u001b[22;22H \u001b[22;25H \u001b[22;30H \u001b[22;53H \u001b[23;3H\u001b[38;5;6;49mtest\u001b[23;8H\u001b[39;49m(main\u001b[23;14H->\u001b[23;17Hnext\u001b[23;22H->\u001b[23;25Hdev)\u001b[23;30H\u001b[38;5;2;49mokay\u001b[23;35H\u001b[39;49m─────────────────"]
[5.373769, "o", "───────────────────────────\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.667499, "o", "\u001b[6;3H\u001b[38;5;15;48;5;1mkxio\u001b[6;8Hcommit is a Work-in-progress\u001b[39;49m (main -> next -> dev) \u001b[38;5;1;49mALERT\u001b[39;49m \u001b[7;2H*\u001b[7;4H805d0b3\u001b[7;12H\u001b[38;5;15;48;5;4m(dev)\u001b[7;18H\u001b[39;49mWIP:\u001b[7;23Hdocs(fs):\u001b[7;33Hadd\u001b[7;37Hsome\u001b[7;42Hdocumentation\u001b[8;2H* 9943a0f \u001b[38;5;15;48;5;4m(main)(next)\u001b[39;49m chore(deps): update docker.io/rust docker tag to v1.81\u001b[9;2H \u001b[9;4H \u001b[9;12H \u001b[9;30H \u001b[9;41H \u001b[9;48H \u001b[9;53H \u001b[9;59H \u001b[9;67H \u001b[9;70H \u001b[10;3H\u001b[38;5;6;49mpodal\u001b[10;9H\u001b[39;49m(main\u001b[10;15H->\u001b[10;18Hnext\u001b[10;23H->\u001b[10;26Hdev)\u001b[10;31H\u001b[38;5;2;49mokay\u001b[10;36H\u001b[39;49m───────────────────────────────────────────\u001b[11;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps):\u001b[11;41Hupdate rust crate scraper to 0.20 \u001b[12;2H \u001b[12;4H \u001b[12;12H \u001b[12;30H \u001b[12;38H \u001b[12;49H \u001b[12;56H \u001b[12;60H \u001b[12;72H \u001b[12;76H ║\u001b[13;3H\u001b[38;5;6;49mr"]
[5.667549, "o", "efile-m4b\u001b[13;14H\u001b[39;49m(main\u001b[13;20H->\u001b[13;23Hnext\u001b[13;28H->\u001b[13;31Hdev)\u001b[13;36H\u001b[38;5;2;49mokay\u001b[13;41H\u001b[39;49m──────────────────────────────────────\u001b[14;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[15;2H \u001b[15;4H \u001b[15;12H \u001b[15;30H \u001b[15;36H \u001b[15;43H \u001b[15;54H \u001b[15;58H \u001b[16;3H\u001b[38;5;6;49mrust-action\u001b[16;15H\u001b[39;49m(main\u001b[16;21H->\u001b[16;24Hnext\u001b[16;29H->\u001b[16;32Hdev)\u001b[16;37H\u001b[38;5;2;49mokay\u001b[16;42H\u001b[39;49m─────────────────────────────────────\u001b[17;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[17;30H\u001b[39;49mfeat: ensure toolchains are up-to-date \u001b[18;2H \u001b[18;4H \u001b[18;12H \u001b[18;30H \u001b[18;43H \u001b[18;50H \u001b[18;65H \u001b[18;72H \u001b[18;76H \u001b[19;3H\u001b[38;5;6;49mskip\u001b[19;8H\u001b[39;49m(main\u001b[19;14H->\u001b[19;17Hnext\u001b"]
[5.667635, "o", "[19;22H->\u001b[19;25Hdev)\u001b[19;30H\u001b[38;5;2;49mokay\u001b[19;35H\u001b[39;49m────────────────────────────────────────────\u001b[20;2H* fc7fbca \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/rust docker tag to \u001b[21;2H \u001b[21;4H \u001b[21;12H \u001b[21;30H \u001b[21;43H \u001b[21;50H \u001b[22;3H\u001b[38;5;6;49mtasyn\u001b[22;9H\u001b[39;49m(main\u001b[22;15H->\u001b[22;18Hnext\u001b[22;23H->\u001b[22;26Hdev)\u001b[22;31H\u001b[38;5;2;49mokay\u001b[22;36H\u001b[39;49m───────────────────────────────────────────\u001b[23;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[23;30H\u001b[39;49mchore(deps): update docker.io/postgres:16.4-alpin\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.703032, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.738998, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.774205, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.808972, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.844592, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.881476, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.91946, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.956562, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[5.993522, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.031996, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.069336, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.109029, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.1469, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.184289, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.220107, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.256197, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.293358, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.331134, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.366742, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.402568, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.439465, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.478813, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.517268, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.560687, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.594301, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.629263, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.669428, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.706761, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.742165, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.78203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.822172, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.859887, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.897026, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.938296, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[6.975571, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.01065, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.047644, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.08457, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.121364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.157513, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.195418, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.228658, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.264878, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.305593, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.3437, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.381122, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.418853, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.454425, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.492184, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.540907, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.578132, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.614231, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.65196, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.69186, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.731624, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.767836, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.804511, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.840577, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.877375, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.913764, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.952312, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[7.989869, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.031084, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.068302, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.104549, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.140569, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.177935, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.21429, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.251383, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.289527, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.329826, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.367669, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.404116, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.441232, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.479153, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.516467, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.554111, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.590533, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.631784, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.672414, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.708258, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.745135, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.78123, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.82166, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.861283, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.898503, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.934001, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[8.971135, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.008281, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.045177, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.082668, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.119739, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.156767, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.194149, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.23081, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.2488, "o", "\u001b[2;3H\u001b[38;5;6;49mgit-next\u001b[39;49m (main\u001b[2;18H->\u001b[2;21Hnext\u001b[2;26H->\u001b[2;29Hdev)\u001b[2;34H\u001b[38;5;2;49mokay\u001b[2;39H\u001b[39;49m────────────────────────────────────────\u001b[3;2H* 91c5973\u001b[3;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore: release \u001b[4;2H \u001b[4;4H \u001b[4;12H \u001b[4;30H \u001b[4;37H \u001b[5;3H\u001b[38;5;15;48;5;1mkxio\u001b[5;8Hcommit is a Work-in-progress\u001b[5;37H\u001b[39;49m(main\u001b[5;43H->\u001b[5;46Hnext\u001b[5;51H->\u001b[5;54Hdev)\u001b[5;59H\u001b[38;5;1;49mALERT\u001b[5;65H\u001b[39;49m──────────────\u001b[6;2H* 805d0b3 \u001b[38;5;15;48;5;4m(dev)\u001b[39;49m WIP: docs(fs): add\u001b[6;37Hsome docum\u001b[6;48Hn\u001b[6;50Hation \u001b[6;59H \u001b[6;65H \u001b[7;4H9943a0f\u001b[7;13H\u001b[38;5;15;48;5;4mmain)(next)\u001b[39;49m \u001b[7;26Hhore(deps): update docker.\u001b[7;54H/rust\u001b[7;60Hdocker\u001b[7;67Htag\u001b[7;71Hto\u001b[7;74Hv1.81\u001b[8;2H \u001b[8;4H \u001b[8;12H \u001b[8;25H \u001b[8;38H \u001b[8;45H \u001b[8;60H \u001b[8;67H "]
[9.248957, "o", "\u001b[8;71H \u001b[8;74H \u001b[9;3H\u001b[38;5;6;49mpodal\u001b[9;9H\u001b[39;49m(main\u001b[9;15H->\u001b[9;18Hnext\u001b[9;23H->\u001b[9;26Hdev)\u001b[9;31H\u001b[38;5;2;49mokay\u001b[9;36H\u001b[39;49m───────────────────────────────────────────\u001b[10;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[11;2H \u001b[11;4H \u001b[11;12H \u001b[11;30H \u001b[11;41H \u001b[11;48H \u001b[11;53H \u001b[11;59H \u001b[11;67H \u001b[11;70H \u001b[12;3H\u001b[38;5;6;49mrefile-m4b\u001b[12;14H\u001b[39;49m(main\u001b[12;20H->\u001b[12;23Hnext\u001b[12;28H->\u001b[12;31Hdev)\u001b[12;36H\u001b[38;5;2;49mokay\u001b[12;41H\u001b[39;49m──────────────────────────────────────█\u001b[13;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[14;2H \u001b[14;4H \u001b[14;12H \u001b[14;30H \u001b[14;38H \u001b[14;49H \u001b[14;56H \u001b[14;60H \u001b[14;72H \u001b[14;76H "]
[9.249109, "o", " \u001b[15;3H\u001b[38;5;6;49mrust-action\u001b[15;15H\u001b[39;49m(main\u001b[15;21H->\u001b[15;24Hnext\u001b[15;29H->\u001b[15;32Hdev)\u001b[15;37H\u001b[38;5;2;49mokay\u001b[15;42H\u001b[39;49m─────────────────────────────────────\u001b[16;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[17;2H \u001b[17;4H \u001b[17;12H \u001b[17;30H \u001b[17;36H \u001b[17;43H \u001b[17;54H \u001b[17;58H \u001b[18;3H\u001b[38;5;6;49mskip\u001b[18;8H\u001b[39;49m(main\u001b[18;14H->\u001b[18;17Hnext\u001b[18;22H->\u001b[18;25Hdev)\u001b[18;30H\u001b[38;5;2;49mokay\u001b[18;35H\u001b[39;49m────────────────────────────────────────────\u001b[19;2H* fc7fbc\u001b[19;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[19;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[20;2H \u001b[20;4H \u001b[20;12H \u001b[20;30H \u001b[20;43H \u001b[20;50H \u001b[20;65H \u001b[20;72H \u001b[20;76H \u001b[21;3H\u001b[38;5;6;49mtasyn\u001b["]
[9.249125, "o", "21;9H\u001b[39;49m(main\u001b[21;15H->\u001b[21;18Hnext\u001b[21;23H->\u001b[21;26Hdev)\u001b[21;31H\u001b[38;5;2;49mokay\u001b[21;36H\u001b[39;49m───────────────────────────────────────────\u001b[22;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[23;2H \u001b[23;4H \u001b[23;12H \u001b[23;30H \u001b[23;43H \u001b[23;50H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.269683, "o", "\u001b[2;2H* 91c5973\u001b[2;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore: release \u001b[3;2H \u001b[3;4H \u001b[3;12H \u001b[3;30H \u001b[3;37H \u001b[3;79H║\u001b[4;3H\u001b[38;5;15;48;5;1mkxio\u001b[4;8Hcommit is a Work-in-progress\u001b[4;37H\u001b[39;49m(main\u001b[4;43H->\u001b[4;46Hnext\u001b[4;51H->\u001b[4;54Hdev)\u001b[4;59H\u001b[38;5;1;49mALERT\u001b[4;65H\u001b[39;49m──────────────\u001b[5;2H* 805d0b3 \u001b[38;5;15;48;5;4m(dev)\u001b[39;49m WIP: docs(fs): add\u001b[5;37Hsome docum\u001b[5;48Hn\u001b[5;50Hation \u001b[5;59H \u001b[5;65H \u001b[6;4H9943a0f\u001b[6;13H\u001b[38;5;15;48;5;4mmain)(next)\u001b[39;49m \u001b[6;26Hhore(deps): update docker.\u001b[6;54H/rust\u001b[6;60Hdocker\u001b[6;67Htag\u001b[6;71Hto\u001b[6;74Hv1.81\u001b[7;2H \u001b[7;4H \u001b[7;12H \u001b[7;25H \u001b[7;38H \u001b[7;45H \u001b[7;60H \u001b[7;67H \u001b[7;71H \u001b[7;74H \u001b[8;3H\u001b[38;5;6;49mpodal\u001b[8;9H\u001b[39;49m(main\u001b[8;15H->\u001b[8;18Hnext\u001b[8;23H->\u001b[8;26Hdev)\u001b[8;31H\u001b[38;5;2;49mokay\u001b[8;36H\u001b[39;49m─────────────────────────────"]
[9.271446, "o", "──────────────\u001b[9;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[10;2H \u001b[10;4H \u001b[10;12H \u001b[10;30H \u001b[10;41H \u001b[10;48H \u001b[10;53H \u001b[10;59H \u001b[10;67H \u001b[10;70H \u001b[11;3H\u001b[38;5;6;49mrefile-m4b\u001b[11;14H\u001b[39;49m(main\u001b[11;20H->\u001b[11;23Hnext\u001b[11;28H->\u001b[11;31Hdev)\u001b[11;36H\u001b[38;5;2;49mokay\u001b[11;41H\u001b[39;49m──────────────────────────────────────\u001b[12;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[13;2H \u001b[13;4H \u001b[13;12H \u001b[13;30H \u001b[13;38H \u001b[13;49H \u001b[13;56H \u001b[13;60H \u001b[13;72H \u001b[13;76H \u001b[14;3H\u001b[38;5;6;49mrust-action\u001b[14;15H\u001b[39;49m(main\u001b[14;21H->\u001b[14;24Hnext\u001b[14;29H->\u001b[14;32Hdev)\u001b[14;37H\u001b[38;5;2;49mokay\u001b[14;42H\u001b[39;49m───────────────────────────────"]
[9.271582, "o", "──────\u001b[15;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[16;2H \u001b[16;4H \u001b[16;12H \u001b[16;30H \u001b[16;36H \u001b[16;43H \u001b[16;54H \u001b[16;58H \u001b[17;3H\u001b[38;5;6;49mskip\u001b[17;8H\u001b[39;49m(main\u001b[17;14H->\u001b[17;17Hnext\u001b[17;22H->\u001b[17;25Hdev)\u001b[17;30H\u001b[38;5;2;49mokay\u001b[17;35H\u001b[39;49m────────────────────────────────────────────\u001b[18;2H* fc7fbc\u001b[18;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[18;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[19;2H \u001b[19;4H \u001b[19;12H \u001b[19;30H \u001b[19;43H \u001b[19;50H \u001b[19;65H \u001b[19;72H \u001b[19;76H \u001b[20;3H\u001b[38;5;6;49mtasyn\u001b[20;9H\u001b[39;49m(main\u001b[20;15H->\u001b[20;18Hnext\u001b[20;23H->\u001b[20;26Hdev)\u001b[20;31H\u001b[38;5;2;49mokay\u001b[20;36H\u001b[39;49m──────────────────────────────────────────"]
[9.271711, "o", "─\u001b[21;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[22;2H \u001b[22;4H \u001b[22;12H \u001b[22;30H \u001b[22;43H \u001b[22;50H \u001b[23;3H\u001b[38;5;6;49mtest\u001b[23;8H\u001b[39;49m(main\u001b[23;14H->\u001b[23;17Hnext\u001b[23;22H->\u001b[23;25Hdev)\u001b[23;30H\u001b[38;5;2;49mokay\u001b[23;35H\u001b[39;49m────────────────────────────────────────────\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.291758, "o", "\u001b[2;2H \u001b[2;4H \u001b[2;12H \u001b[2;30H \u001b[2;37H \u001b[3;3H\u001b[38;5;15;48;5;1mkxio\u001b[3;8Hcommit is a Work-in-progress\u001b[3;37H\u001b[39;49m(main\u001b[3;43H->\u001b[3;46Hnext\u001b[3;51H->\u001b[3;54Hdev)\u001b[3;59H\u001b[38;5;1;49mALERT\u001b[3;65H\u001b[39;49m──────────────\u001b[4;2H* 805d0b3 \u001b[38;5;15;48;5;4m(dev)\u001b[39;49m WIP: docs(fs): add\u001b[4;37Hsome docum\u001b[4;48Hn\u001b[4;50Hation \u001b[4;59H \u001b[4;65H \u001b[5;4H9943a0f\u001b[5;13H\u001b[38;5;15;48;5;4mmain)(next)\u001b[39;49m \u001b[5;26Hhore(deps): update docker.\u001b[5;54H/rust\u001b[5;60Hdocker\u001b[5;67Htag\u001b[5;71Hto\u001b[5;74Hv1.81\u001b[6;2H \u001b[6;4H \u001b[6;12H \u001b[6;25H \u001b[6;38H \u001b[6;45H \u001b[6;60H \u001b[6;67H \u001b[6;71H \u001b[6;74H \u001b[7;3H\u001b[38;5;6;49mpodal\u001b[7;9H\u001b[39;49m(main\u001b[7;15H->\u001b[7;18Hnext\u001b[7;23H->\u001b[7;26Hdev)\u001b[7;31H\u001b[38;5;2;49mokay\u001b[7;36H\u001b[39;49m───────────────────────────────────────────\u001b[8;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust "]
[9.292005, "o", "crate scraper to 0.20 \u001b[9;2H \u001b[9;4H \u001b[9;12H \u001b[9;30H \u001b[9;41H \u001b[9;48H \u001b[9;53H \u001b[9;59H \u001b[9;67H \u001b[9;70H \u001b[10;3H\u001b[38;5;6;49mrefile-m4b\u001b[10;14H\u001b[39;49m(main\u001b[10;20H->\u001b[10;23Hnext\u001b[10;28H->\u001b[10;31Hdev)\u001b[10;36H\u001b[38;5;2;49mokay\u001b[10;41H\u001b[39;49m──────────────────────────────────────\u001b[11;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[12;2H \u001b[12;4H \u001b[12;12H \u001b[12;30H \u001b[12;38H \u001b[12;49H \u001b[12;56H \u001b[12;60H \u001b[12;72H \u001b[12;76H \u001b[13;3H\u001b[38;5;6;49mrust-action\u001b[13;15H\u001b[39;49m(main\u001b[13;21H->\u001b[13;24Hnext\u001b[13;29H->\u001b[13;32Hdev)\u001b[13;37H\u001b[38;5;2;49mokay\u001b[13;42H\u001b[39;49m─────────────────────────────────────\u001b[14;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[15;2H \u001b"]
[9.292207, "o", "[15;4H \u001b[15;12H \u001b[15;30H \u001b[15;36H \u001b[15;43H \u001b[15;54H \u001b[15;58H \u001b[16;3H\u001b[38;5;6;49mskip\u001b[16;8H\u001b[39;49m(main\u001b[16;14H->\u001b[16;17Hnext\u001b[16;22H->\u001b[16;25Hdev)\u001b[16;30H\u001b[38;5;2;49mokay\u001b[16;35H\u001b[39;49m────────────────────────────────────────────\u001b[17;2H* fc7fbc\u001b[17;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[17;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[18;2H \u001b[18;4H \u001b[18;12H \u001b[18;30H \u001b[18;43H \u001b[18;50H \u001b[18;65H \u001b[18;72H \u001b[18;76H \u001b[19;3H\u001b[38;5;6;49mtasyn\u001b[19;9H\u001b[39;49m(main\u001b[19;15H->\u001b[19;18Hnext\u001b[19;23H->\u001b[19;26Hdev)\u001b[19;31H\u001b[38;5;2;49mokay\u001b[19;36H\u001b[39;49m───────────────────────────────────────────\u001b[20;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[21;2H \u001b[21;4H \u001b[21"]
[9.292245, "o", ";12H \u001b[21;30H \u001b[21;43H \u001b[21;50H \u001b[22;3H\u001b[38;5;6;49mtest\u001b[22;8H\u001b[39;49m(main\u001b[22;14H->\u001b[22;17Hnext\u001b[22;22H->\u001b[22;25Hdev)\u001b[22;30H\u001b[38;5;2;49mokay\u001b[22;35H\u001b[39;49m────────────────────────────────────────────\u001b[23;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[23;30H\u001b[39;49mchore: update 6 \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.314279, "o", "\u001b[2;3H\u001b[38;5;15;48;5;1mkxio\u001b[2;8Hcommit is a Work-in-progress\u001b[2;37H\u001b[39;49m(main\u001b[2;43H->\u001b[2;46Hnext\u001b[2;51H->\u001b[2;54Hdev)\u001b[2;59H\u001b[38;5;1;49mALERT\u001b[2;65H\u001b[39;49m──────────────\u001b[3;2H* 805d0b3 \u001b[38;5;15;48;5;4m(dev)\u001b[39;49m WIP: docs(fs): add\u001b[3;37Hsome docum\u001b[3;48Hn\u001b[3;50Hation \u001b[3;59H \u001b[3;65H \u001b[4;4H9943a0f\u001b[4;13H\u001b[38;5;15;48;5;4mmain)(next)\u001b[39;49m \u001b[4;26Hhore(deps): update docker.\u001b[4;54H/rust\u001b[4;60Hdocker\u001b[4;67Htag\u001b[4;71Hto\u001b[4;74Hv1.81║\u001b[5;2H \u001b[5;4H \u001b[5;12H \u001b[5;25H \u001b[5;38H \u001b[5;45H \u001b[5;60H \u001b[5;67H \u001b[5;71H \u001b[5;74H \u001b[6;3H\u001b[38;5;6;49mpodal\u001b[6;9H\u001b[39;49m(main\u001b[6;15H->\u001b[6;18Hnext\u001b[6;23H->\u001b[6;26Hdev)\u001b[6;31H\u001b[38;5;2;49mokay\u001b[6;36H\u001b[39;49m───────────────────────────────────────────\u001b[7;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[8;2H \u001b[8;4H \u001b[8;12H "]
[9.314894, "o", " \u001b[8;30H \u001b[8;41H \u001b[8;48H \u001b[8;53H \u001b[8;59H \u001b[8;67H \u001b[8;70H \u001b[9;3H\u001b[38;5;6;49mrefile-m4b\u001b[9;14H\u001b[39;49m(main\u001b[9;20H->\u001b[9;23Hnext\u001b[9;28H->\u001b[9;31Hdev)\u001b[9;36H\u001b[38;5;2;49mokay\u001b[9;41H\u001b[39;49m──────────────────────────────────────\u001b[10;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[11;2H \u001b[11;4H \u001b[11;12H \u001b[11;30H \u001b[11;38H \u001b[11;49H \u001b[11;56H \u001b[11;60H \u001b[11;72H \u001b[11;76H \u001b[12;3H\u001b[38;5;6;49mrust-action\u001b[12;15H\u001b[39;49m(main\u001b[12;21H->\u001b[12;24Hnext\u001b[12;29H->\u001b[12;32Hdev)\u001b[12;37H\u001b[38;5;2;49mokay\u001b[12;42H\u001b[39;49m─────────────────────────────────────\u001b[13;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date █\u001b[14;2H \u001b[14;4H \u001b[14;12H \u001b[14;30H \u001b[14;36H \u001b[14;43H"]
[9.315054, "o", " \u001b[14;54H \u001b[14;58H \u001b[15;3H\u001b[38;5;6;49mskip\u001b[15;8H\u001b[39;49m(main\u001b[15;14H->\u001b[15;17Hnext\u001b[15;22H->\u001b[15;25Hdev)\u001b[15;30H\u001b[38;5;2;49mokay\u001b[15;35H\u001b[39;49m────────────────────────────────────────────\u001b[16;2H* fc7fbc\u001b[16;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[16;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[17;2H \u001b[17;4H \u001b[17;12H \u001b[17;30H \u001b[17;43H \u001b[17;50H \u001b[17;65H \u001b[17;72H \u001b[17;76H \u001b[18;3H\u001b[38;5;6;49mtasyn\u001b[18;9H\u001b[39;49m(main\u001b[18;15H->\u001b[18;18Hnext\u001b[18;23H->\u001b[18;26Hdev)\u001b[18;31H\u001b[38;5;2;49mokay\u001b[18;36H\u001b[39;49m───────────────────────────────────────────\u001b[19;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[20;2H \u001b[20;4H \u001b[20;12H \u001b[20;30H \u001b[20;43H \u001b[20;50H "]
[9.315195, "o", " \u001b[21;3H\u001b[38;5;6;49mtest\u001b[21;8H\u001b[39;49m(main\u001b[21;14H->\u001b[21;17Hnext\u001b[21;22H->\u001b[21;25Hdev)\u001b[21;30H\u001b[38;5;2;49mokay\u001b[21;35H\u001b[39;49m────────────────────────────────────────────\u001b[22;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[22;30H\u001b[39;49mchore: update 6 \u001b[23;2H \u001b[23;4H \u001b[23;12H \u001b[23;30H \u001b[23;37H \u001b[23;44H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.334731, "o", "\u001b[2;2H* 805d0b3 \u001b[38;5;15;48;5;4m(dev)\u001b[39;49m WIP: docs(fs): add\u001b[2;37Hsome docum\u001b[2;48Hn\u001b[2;50Hation \u001b[2;59H \u001b[2;65H \u001b[3;4H9943a0f\u001b[3;13H\u001b[38;5;15;48;5;4mmain)(next)\u001b[39;49m \u001b[3;26Hhore(deps): update docker.\u001b[3;54H/rust\u001b[3;60Hdocker\u001b[3;67Htag\u001b[3;71Hto\u001b[3;74Hv1.81\u001b[4;2H \u001b[4;4H \u001b[4;12H \u001b[4;25H \u001b[4;38H \u001b[4;45H \u001b[4;60H \u001b[4;67H \u001b[4;71H \u001b[4;74H \u001b[5;3H\u001b[38;5;6;49mpodal\u001b[5;9H\u001b[39;49m(main\u001b[5;15H->\u001b[5;18Hnext\u001b[5;23H->\u001b[5;26Hdev)\u001b[5;31H\u001b[38;5;2;49mokay\u001b[5;36H\u001b[39;49m───────────────────────────────────────────\u001b[6;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[7;2H \u001b[7;4H \u001b[7;12H \u001b[7;30H \u001b[7;41H \u001b[7;48H \u001b[7;53H \u001b[7;59H \u001b[7;67H \u001b[7;70H \u001b[8;3H\u001b[38;5;6;49mrefile-m4b\u001b[8;14H\u001b[39;49m(main\u001b[8;20H->\u001b[8;23Hnext\u001b[8;28H->\u001b[8;31Hdev)\u001b[8;36H\u001b[38;5;2;49mokay\u001b[8;4"]
[9.334863, "o", "1H\u001b[39;49m──────────────────────────────────────\u001b[9;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[10;2H \u001b[10;4H \u001b[10;12H \u001b[10;30H \u001b[10;38H \u001b[10;49H \u001b[10;56H \u001b[10;60H \u001b[10;72H \u001b[10;76H \u001b[11;3H\u001b[38;5;6;49mrust-action\u001b[11;15H\u001b[39;49m(main\u001b[11;21H->\u001b[11;24Hnext\u001b[11;29H->\u001b[11;32Hdev)\u001b[11;37H\u001b[38;5;2;49mokay\u001b[11;42H\u001b[39;49m─────────────────────────────────────\u001b[12;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[13;2H \u001b[13;4H \u001b[13;12H \u001b[13;30H \u001b[13;36H \u001b[13;43H \u001b[13;54H \u001b[13;58H \u001b[14;3H\u001b[38;5;6;49mskip\u001b[14;8H\u001b[39;49m(main\u001b[14;14H->\u001b[14;17Hnext\u001b[14;22H->\u001b[14;25Hdev)\u001b[14;30H\u001b[38;5;2;49mokay\u001b[14;35H\u001b[39;49m──────────────"]
[9.334916, "o", "──────────────────────────────\u001b[15;2H* fc7fbc\u001b[15;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[15;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[16;2H \u001b[16;4H \u001b[16;12H \u001b[16;30H \u001b[16;43H \u001b[16;50H \u001b[16;65H \u001b[16;72H \u001b[16;76H \u001b[17;3H\u001b[38;5;6;49mtasyn\u001b[17;9H\u001b[39;49m(main\u001b[17;15H->\u001b[17;18Hnext\u001b[17;23H->\u001b[17;26Hdev)\u001b[17;31H\u001b[38;5;2;49mokay\u001b[17;36H\u001b[39;49m───────────────────────────────────────────\u001b[18;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[19;2H \u001b[19;4H \u001b[19;12H \u001b[19;30H \u001b[19;43H \u001b[19;50H \u001b[20;3H\u001b[38;5;6;49mtest\u001b[20;8H\u001b[39;49m(main\u001b[20;14H->\u001b[20;17Hnext\u001b[20;22H->\u001b[20;25Hdev)\u001b[20;30H\u001b[38;5;2;49mokay\u001b[20;35H\u001b[39;49m─────────────────────"]
[9.335083, "o", "───────────────────────\u001b[21;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[21;30H\u001b[39;49mchore: update 6 \u001b[22;2H \u001b[22;4H \u001b[22;12H \u001b[22;30H \u001b[22;37H \u001b[22;44H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.355977, "o", "\u001b[2;4H9943a0f\u001b[2;13H\u001b[38;5;15;48;5;4mmain)(next)\u001b[39;49m \u001b[2;26Hhore(deps): update docker.\u001b[2;54H/rust\u001b[2;60Hdocker\u001b[2;67Htag\u001b[2;71Hto\u001b[2;74Hv1.81\u001b[3;2H \u001b[3;4H \u001b[3;12H \u001b[3;25H \u001b[3;38H \u001b[3;45H \u001b[3;60H \u001b[3;67H \u001b[3;71H \u001b[3;74H \u001b[4;3H\u001b[38;5;6;49mpodal\u001b[4;9H\u001b[39;49m(main\u001b[4;15H->\u001b[4;18Hnext\u001b[4;23H->\u001b[4;26Hdev)\u001b[4;31H\u001b[38;5;2;49mokay\u001b[4;36H\u001b[39;49m───────────────────────────────────────────\u001b[5;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 ║\u001b[6;2H \u001b[6;4H \u001b[6;12H \u001b[6;30H \u001b[6;41H \u001b[6;48H \u001b[6;53H \u001b[6;59H \u001b[6;67H \u001b[6;70H \u001b[7;3H\u001b[38;5;6;49mrefile-m4b\u001b[7;14H\u001b[39;49m(main\u001b[7;20H->\u001b[7;23Hnext\u001b[7;28H->\u001b[7;31Hdev)\u001b[7;36H\u001b[38;5;2;49mokay\u001b[7;41H\u001b[39;49m──────────────────────────────────────\u001b[8;2H* 7fb"]
[9.357512, "o", "a5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[9;2H \u001b[9;4H \u001b[9;12H \u001b[9;30H \u001b[9;38H \u001b[9;49H \u001b[9;56H \u001b[9;60H \u001b[9;72H \u001b[9;76H \u001b[10;3H\u001b[38;5;6;49mrust-action\u001b[10;15H\u001b[39;49m(main\u001b[10;21H->\u001b[10;24Hnext\u001b[10;29H->\u001b[10;32Hdev)\u001b[10;37H\u001b[38;5;2;49mokay\u001b[10;42H\u001b[39;49m─────────────────────────────────────\u001b[11;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[12;2H \u001b[12;4H \u001b[12;12H \u001b[12;30H \u001b[12;36H \u001b[12;43H \u001b[12;54H \u001b[12;58H \u001b[13;3H\u001b[38;5;6;49mskip\u001b[13;8H\u001b[39;49m(main\u001b[13;14H->\u001b[13;17Hnext\u001b[13;22H->\u001b[13;25Hdev)\u001b[13;30H\u001b[38;5;2;49mokay\u001b[13;35H\u001b[39;49m────────────────────────────────────────────\u001b[14;2H* fc7fbc\u001b[14;11H \u001b[38;5;15;48;5;4m(dev)(main)(nex"]
[9.358399, "o", "t)\u001b[14;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to █\u001b[15;2H \u001b[15;4H \u001b[15;12H \u001b[15;30H \u001b[15;43H \u001b[15;50H \u001b[15;65H \u001b[15;72H \u001b[15;76H \u001b[16;3H\u001b[38;5;6;49mtasyn\u001b[16;9H\u001b[39;49m(main\u001b[16;15H->\u001b[16;18Hnext\u001b[16;23H->\u001b[16;26Hdev)\u001b[16;31H\u001b[38;5;2;49mokay\u001b[16;36H\u001b[39;49m───────────────────────────────────────────\u001b[17;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[18;2H \u001b[18;4H \u001b[18;12H \u001b[18;30H \u001b[18;43H \u001b[18;50H \u001b[19;3H\u001b[38;5;6;49mtest\u001b[19;8H\u001b[39;49m(main\u001b[19;14H->\u001b[19;17Hnext\u001b[19;22H->\u001b[19;25Hdev)\u001b[19;30H\u001b[38;5;2;49mokay\u001b[19;35H\u001b[39;49m────────────────────────────────────────────\u001b[20;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[20;30H\u001b[39;49mchore:"]
[9.358584, "o", " update 6 \u001b[21;2H \u001b[21;4H \u001b[21;12H \u001b[21;30H \u001b[21;37H \u001b[21;44H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.392711, "o", "\u001b[2;2H \u001b[2;4H \u001b[2;12H \u001b[2;25H \u001b[2;38H \u001b[2;45H \u001b[2;60H \u001b[2;67H \u001b[2;71H \u001b[2;74H \u001b[3;3H\u001b[38;5;6;49mpodal\u001b[3;9H\u001b[39;49m(main\u001b[3;15H->\u001b[3;18Hnext\u001b[3;23H->\u001b[3;26Hdev)\u001b[3;31H\u001b[38;5;2;49mokay\u001b[3;36H\u001b[39;49m───────────────────────────────────────────\u001b[4;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[5;2H \u001b[5;4H \u001b[5;12H \u001b[5;30H \u001b[5;41H \u001b[5;48H \u001b[5;53H \u001b[5;59H \u001b[5;67H \u001b[5;70H \u001b[6;3H\u001b[38;5;6;49mrefile-m4b\u001b[6;14H\u001b[39;49m(main\u001b[6;20H->\u001b[6;23Hnext\u001b[6;28H->\u001b[6;31Hdev)\u001b[6;36H\u001b[38;5;2;49mokay\u001b[6;41H\u001b[39;49m──────────────────────────────────────\u001b[7;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[8;2H \u001b[8;4H \u001b[8;12H \u001b[8;30H "]
[9.392818, "o", " \u001b[8;38H \u001b[8;49H \u001b[8;56H \u001b[8;60H \u001b[8;72H \u001b[8;76H \u001b[9;3H\u001b[38;5;6;49mrust-action\u001b[9;15H\u001b[39;49m(main\u001b[9;21H->\u001b[9;24Hnext\u001b[9;29H->\u001b[9;32Hdev)\u001b[9;37H\u001b[38;5;2;49mokay\u001b[9;42H\u001b[39;49m─────────────────────────────────────\u001b[10;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[11;2H \u001b[11;4H \u001b[11;12H \u001b[11;30H \u001b[11;36H \u001b[11;43H \u001b[11;54H \u001b[11;58H \u001b[12;3H\u001b[38;5;6;49mskip\u001b[12;8H\u001b[39;49m(main\u001b[12;14H->\u001b[12;17Hnext\u001b[12;22H->\u001b[12;25Hdev)\u001b[12;30H\u001b[38;5;2;49mokay\u001b[12;35H\u001b[39;49m────────────────────────────────────────────\u001b[13;2H* fc7fbc\u001b[13;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[13;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[14;2H \u001b[14;4H \u001b[14;12H \u001b[14;30H \u001b[14;43H \u001b[14;50H "]
[9.392892, "o", " \u001b[14;65H \u001b[14;72H \u001b[14;76H \u001b[15;3H\u001b[38;5;6;49mtasyn\u001b[15;9H\u001b[39;49m(main\u001b[15;15H->\u001b[15;18Hnext\u001b[15;23H->\u001b[15;26Hdev)\u001b[15;31H\u001b[38;5;2;49mokay\u001b[15;36H\u001b[39;49m───────────────────────────────────────────\u001b[16;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin\u001b[17;2H \u001b[17;4H \u001b[17;12H \u001b[17;30H \u001b[17;43H \u001b[17;50H \u001b[18;3H\u001b[38;5;6;49mtest\u001b[18;8H\u001b[39;49m(main\u001b[18;14H->\u001b[18;17Hnext\u001b[18;22H->\u001b[18;25Hdev)\u001b[18;30H\u001b[38;5;2;49mokay\u001b[18;35H\u001b[39;49m────────────────────────────────────────────\u001b[19;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[19;30H\u001b[39;49mchore: update 6 \u001b[20;2H \u001b[20;4H \u001b[20;12H \u001b[20;30H \u001b[20;37H \u001b[20;44H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.413689, "o", "\u001b[2;3H\u001b[38;5;6;49mpodal\u001b[2;9H\u001b[39;49m(main\u001b[2;15H->\u001b[2;18Hnext\u001b[2;23H->\u001b[2;26Hdev)\u001b[2;31H\u001b[38;5;2;49mokay\u001b[2;36H\u001b[39;49m───────────────────────────────────────────\u001b[3;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[4;2H \u001b[4;4H \u001b[4;12H \u001b[4;30H \u001b[4;41H \u001b[4;48H \u001b[4;53H \u001b[4;59H \u001b[4;67H \u001b[4;70H \u001b[5;3H\u001b[38;5;6;49mrefile-m4b\u001b[5;14H\u001b[39;49m(main\u001b[5;20H->\u001b[5;23Hnext\u001b[5;28H->\u001b[5;31Hdev)\u001b[5;36H\u001b[38;5;2;49mokay\u001b[5;41H\u001b[39;49m──────────────────────────────────────\u001b[6;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[7;2H \u001b[7;4H \u001b[7;12H \u001b[7;30H \u001b[7;38H \u001b[7;49H \u001b[7;56H \u001b[7;60H \u001b[7;72H \u001b[7;76H \u001b[8;3H\u001b[38;5;6;49mrust-action\u001b[8;15H\u001b[39;49m(main\u001b[8"]
[9.413775, "o", ";21H->\u001b[8;24Hnext\u001b[8;29H->\u001b[8;32Hdev)\u001b[8;37H\u001b[38;5;2;49mokay\u001b[8;42H\u001b[39;49m─────────────────────────────────────\u001b[9;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m feat: ensure toolchains are up-to-date \u001b[10;2H \u001b[10;4H \u001b[10;12H \u001b[10;30H \u001b[10;36H \u001b[10;43H \u001b[10;54H \u001b[10;58H \u001b[11;3H\u001b[38;5;6;49mskip\u001b[11;8H\u001b[39;49m(main\u001b[11;14H->\u001b[11;17Hnext\u001b[11;22H->\u001b[11;25Hdev)\u001b[11;30H\u001b[38;5;2;49mokay\u001b[11;35H\u001b[39;49m────────────────────────────────────────────\u001b[12;2H* fc7fbc\u001b[12;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[12;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[13;2H \u001b[13;4H \u001b[13;12H \u001b[13;30H \u001b[13;43H \u001b[13;50H \u001b[13;65H \u001b[13;72H \u001b[13;76H \u001b[14;3H\u001b[38;5;6;49mtasyn\u001b[14;9H\u001b[39;49m(main\u001b[14;15H->\u001b[14;18Hnext\u001b[14;23H->\u001b[14;26Hdev)\u001b"]
[9.413866, "o", "[14;31H\u001b[38;5;2;49mokay\u001b[14;36H\u001b[39;49m───────────────────────────────────────────\u001b[15;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker.io/postgres:16.4-alpin█\u001b[16;2H \u001b[16;4H \u001b[16;12H \u001b[16;30H \u001b[16;43H \u001b[16;50H \u001b[17;3H\u001b[38;5;6;49mtest\u001b[17;8H\u001b[39;49m(main\u001b[17;14H->\u001b[17;17Hnext\u001b[17;22H->\u001b[17;25Hdev)\u001b[17;30H\u001b[38;5;2;49mokay\u001b[17;35H\u001b[39;49m────────────────────────────────────────────\u001b[18;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[18;30H\u001b[39;49mchore: update 6 \u001b[19;2H \u001b[19;4H \u001b[19;12H \u001b[19;30H \u001b[19;37H \u001b[19;44H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.434833, "o", "\u001b[2;2H* 249b943 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m fix(deps): update rust crate scraper to 0.20 \u001b[3;2H \u001b[3;4H \u001b[3;12H \u001b[3;30H \u001b[3;41H \u001b[3;48H \u001b[3;53H \u001b[3;59H \u001b[3;67H \u001b[3;70H \u001b[4;3H\u001b[38;5;6;49mrefile-m4b\u001b[4;14H\u001b[39;49m(main\u001b[4;20H->\u001b[4;23Hnext\u001b[4;28H->\u001b[4;31Hdev)\u001b[4;36H\u001b[38;5;2;49mokay\u001b[4;41H\u001b[39;49m──────────────────────────────────────\u001b[5;2H* 7fba5fb \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m Support --version, --help and --directory cli arg\u001b[6;2H \u001b[6;4H \u001b[6;12H \u001b[6;30H \u001b[6;38H \u001b[6;49H \u001b[6;56H \u001b[6;60H \u001b[6;72H \u001b[6;76H ║\u001b[7;3H\u001b[38;5;6;49mrust-action\u001b[7;15H\u001b[39;49m(main\u001b[7;21H->\u001b[7;24Hnext\u001b[7;29H->\u001b[7;32Hdev)\u001b[7;37H\u001b[38;5;2;49mokay\u001b[7;42H\u001b[39;49m─────────────────────────────────────\u001b[8;2H* de4785c \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m f"]
[9.435142, "o", "eat: ensure toolchains are up-to-date \u001b[9;2H \u001b[9;4H \u001b[9;12H \u001b[9;30H \u001b[9;36H \u001b[9;43H \u001b[9;54H \u001b[9;58H \u001b[10;3H\u001b[38;5;6;49mskip\u001b[10;8H\u001b[39;49m(main\u001b[10;14H->\u001b[10;17Hnext\u001b[10;22H->\u001b[10;25Hdev)\u001b[10;30H\u001b[38;5;2;49mokay\u001b[10;35H\u001b[39;49m────────────────────────────────────────────\u001b[11;2H* fc7fbc\u001b[11;11H \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[11;30H\u001b[39;49mchore(deps): update docker.io/rust docker tag to \u001b[12;2H \u001b[12;4H \u001b[12;12H \u001b[12;30H \u001b[12;43H \u001b[12;50H \u001b[12;65H \u001b[12;72H \u001b[12;76H \u001b[13;3H\u001b[38;5;6;49mtasyn\u001b[13;9H\u001b[39;49m(main\u001b[13;15H->\u001b[13;18Hnext\u001b[13;23H->\u001b[13;26Hdev)\u001b[13;31H\u001b[38;5;2;49mokay\u001b[13;36H\u001b[39;49m───────────────────────────────────────────\u001b[14;2H* 24fe845 \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[39;49m chore(deps): update docker"]
[9.435248, "o", ".io/postgres:16.4-alpin\u001b[15;2H \u001b[15;4H \u001b[15;12H \u001b[15;30H \u001b[15;43H \u001b[15;50H \u001b[16;3H\u001b[38;5;6;49mtest\u001b[16;8H\u001b[39;49m(main\u001b[16;14H->\u001b[16;17Hnext\u001b[16;22H->\u001b[16;25Hdev)\u001b[16;30H\u001b[38;5;2;49mokay\u001b[16;35H\u001b[39;49m────────────────────────────────────────────\u001b[17;2H* b7340df \u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[17;30H\u001b[39;49mchore: update 6 \u001b[18;2H \u001b[18;4H \u001b[18;12H \u001b[18;30H \u001b[18;37H \u001b[18;44H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.472802, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.510706, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.547102, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.586478, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.621846, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.656939, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.694522, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.730343, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.768279, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.804285, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.844258, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.881454, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.920047, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.957121, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[9.995542, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.03548, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.072844, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.110417, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.146604, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.183643, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.223757, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.259221, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.298185, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.335671, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.377657, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.413226, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.449543, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.485979, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.530581, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.568398, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.588452, "o", "\u001b[2;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[3;2H*\u001b[3;4H249b943\u001b[3;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[3;30H\u001b[39;49mfix(deps):\u001b[3;41Hupdate\u001b[3;48Hrust\u001b[3;53Hcrate\u001b[3;59Hscraper\u001b[3;67Hto\u001b[3;70H0.20\u001b[4;3H \u001b[4;14H \u001b[4;20H \u001b[4;23H \u001b[4;28H \u001b[4;31H \u001b[4;36H \u001b[4;41H \u001b[5;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[6;2H*\u001b[6;4H7fba5fb\u001b[6;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[6;30H\u001b[39;49mSupport\u001b[6;38H--version,\u001b[6;49H--help\u001b[6;56Hand\u001b[6;60H--directory\u001b[6;72Hcli\u001b[6;76Harg█\u001b[7;3H \u001b[7;15H \u001b[7;21H \u001b[7;24H \u001b[7;29H \u001b[7;32H \u001b[7;37H \u001b[7;42H \u001b[8;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> "]
[10.588626, "o", "next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[9;2H*\u001b[9;4Hde4785c\u001b[9;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[9;30H\u001b[39;49mfeat:\u001b[9;36Hensure\u001b[9;43Htoolchains\u001b[9;54Hare\u001b[9;58Hup-to-date\u001b[10;3H \u001b[10;8H \u001b[10;14H \u001b[10;17H \u001b[10;22H \u001b[10;25H \u001b[10;30H \u001b[10;35H \u001b[11;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[11;11Hin -> next -> dev)\u001b[11;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[12;2H*\u001b[12;4Hfc7fbca\u001b[12;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[12;30H\u001b[39;49mchore(deps):\u001b[12;43Hupdate\u001b[12;50Hdocker.io/rust\u001b[12;65Hdocker\u001b[12;72Htag\u001b[12;76Hto\u001b[13;3H \u001b[13;9H \u001b[13;15H \u001b[13;18H \u001b[13;23H \u001b[13;26H \u001b[13;31H \u001b[13;36H \u001b[14;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─"]
[10.588685, "o", "──────────────────────────────────────────\u001b[15;2H*\u001b[15;4H24fe845\u001b[15;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[15;30H\u001b[39;49mchore(deps):\u001b[15;43Hupdate\u001b[15;50Hdocker.io/postgres:16.4-alpin\u001b[16;3H \u001b[16;8H \u001b[16;14H \u001b[16;17H \u001b[16;22H \u001b[16;25H \u001b[16;30H \u001b[16;35H \u001b[17;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[17;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[18;2H*\u001b[18;4Hb7340df\u001b[18;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[18;30H\u001b[39;49mchore:\u001b[18;37Hupdate\u001b[18;44H6\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.607079, "o", "\u001b[2;3H \u001b[2;9H \u001b[2;15H \u001b[2;18H \u001b[2;23H \u001b[2;26H \u001b[2;31H \u001b[2;36H \u001b[3;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[4;2H*\u001b[4;4H249b943\u001b[4;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[4;30H\u001b[39;49mfix(deps):\u001b[4;41Hupdate\u001b[4;48Hrust\u001b[4;53Hcrate\u001b[4;59Hscraper\u001b[4;67Hto\u001b[4;70H0.20\u001b[5;3H \u001b[5;14H \u001b[5;20H \u001b[5;23H \u001b[5;28H \u001b[5;31H \u001b[5;36H \u001b[5;41H \u001b[6;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[7;2H*\u001b[7;4H7fba5fb\u001b[7;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[7;30H\u001b[39;49mSupport\u001b[7;38H--version,\u001b[7;49H--help\u001b[7;56Hand\u001b[7;60H--directory\u001b[7;72Hcli\u001b[7;76Harg\u001b[8;3H \u001b[8;15H \u001b[8;21H \u001b[8;24H \u001b[8"]
[10.607191, "o", ";29H \u001b[8;32H \u001b[8;37H \u001b[8;42H \u001b[9;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[10;2H*\u001b[10;4Hde4785c\u001b[10;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[10;30H\u001b[39;49mfeat:\u001b[10;36Hensure\u001b[10;43Htoolchains\u001b[10;54Hare\u001b[10;58Hup-to-date\u001b[11;3H \u001b[11;8H \u001b[11;14H \u001b[11;17H \u001b[11;22H \u001b[11;25H \u001b[11;30H \u001b[11;35H \u001b[12;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[12;11Hin -> next -> dev)\u001b[12;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[13;2H*\u001b[13;4Hfc7fbca\u001b[13;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[13;30H\u001b[39;49mchore(deps):\u001b[13;43Hupdate\u001b[13;50Hdocker.io/rust\u001b[13;65Hdocker\u001b[13;72Htag\u001b[13;76Hto\u001b[14;3H \u001b[14;9H \u001b[14;15H \u001b[14;18H \u001b[14;23H \u001b[14;26H \u001b[14;31H \u001b[14;36H "]
[10.607383, "o", " \u001b[15;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────║\u001b[16;2H*\u001b[16;4H24fe845\u001b[16;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[16;30H\u001b[39;49mchore(deps):\u001b[16;43Hupdate\u001b[16;50Hdocker.io/postgres:16.4-alpin\u001b[17;3H \u001b[17;8H \u001b[17;14H \u001b[17;17H \u001b[17;22H \u001b[17;25H \u001b[17;30H \u001b[17;35H \u001b[18;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[18;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[19;2H*\u001b[19;4Hb7340df\u001b[19;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[19;30H\u001b[39;49mchore:\u001b[19;37Hupdate\u001b[19;44H6\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.625804, "o", "\u001b[2;2H*\u001b[2;4H9943a0f\u001b[2;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[2;25H\u001b[39;49mchore(deps):\u001b[2;38Hupdate\u001b[2;45Hdocker.io/rust\u001b[2;60Hdocker\u001b[2;67Htag\u001b[2;71Hto\u001b[2;74Hv1.81\u001b[3;3H \u001b[3;9H \u001b[3;15H \u001b[3;18H \u001b[3;23H \u001b[3;26H \u001b[3;31H \u001b[3;36H \u001b[4;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[5;2H*\u001b[5;4H249b943\u001b[5;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[5;30H\u001b[39;49mfix(deps):\u001b[5;41Hupdate\u001b[5;48Hrust\u001b[5;53Hcrate\u001b[5;59Hscraper\u001b[5;67Hto\u001b[5;70H0.20\u001b[6;3H \u001b[6;14H \u001b[6;20H \u001b[6;23H \u001b[6;28H \u001b[6;31H \u001b[6;36H \u001b[6;41H \u001b[7;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[8;2H*\u001b[8;4H7fba5fb\u001b[8;12H\u001b[38;5;15;48;5;4m(dev)(ma"]
[10.625948, "o", "in)(next)\u001b[8;30H\u001b[39;49mSupport\u001b[8;38H--version,\u001b[8;49H--help\u001b[8;56Hand\u001b[8;60H--directory\u001b[8;72Hcli\u001b[8;76Harg\u001b[9;3H \u001b[9;15H \u001b[9;21H \u001b[9;24H \u001b[9;29H \u001b[9;32H \u001b[9;37H \u001b[9;42H \u001b[10;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[11;2H*\u001b[11;4Hde4785c\u001b[11;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[11;30H\u001b[39;49mfeat:\u001b[11;36Hensure\u001b[11;43Htoolchains\u001b[11;54Hare\u001b[11;58Hup-to-date\u001b[12;3H \u001b[12;8H \u001b[12;14H \u001b[12;17H \u001b[12;22H \u001b[12;25H \u001b[12;30H \u001b[12;35H \u001b[13;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[13;11Hin -> next -> dev)\u001b[13;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[14;2H*\u001b[14;4Hfc7fbca\u001b[14;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[14;30H\u001b[39;49mchore(deps"]
[10.626011, "o", "):\u001b[14;43Hupdate\u001b[14;50Hdocker.io/rust\u001b[14;65Hdocker\u001b[14;72Htag\u001b[14;76Hto\u001b[15;3H \u001b[15;9H \u001b[15;15H \u001b[15;18H \u001b[15;23H \u001b[15;26H \u001b[15;31H \u001b[15;36H \u001b[16;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[17;2H*\u001b[17;4H24fe845\u001b[17;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[17;30H\u001b[39;49mchore(deps):\u001b[17;43Hupdate\u001b[17;50Hdocker.io/postgres:16.4-alpin\u001b[18;3H \u001b[18;8H \u001b[18;14H \u001b[18;17H \u001b[18;22H \u001b[18;25H \u001b[18;30H \u001b[18;35H \u001b[19;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[19;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[20;2H*\u001b[20;4Hb7340df\u001b[20;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[20;30H\u001b[39;49mchore:\u001b[20;37Hupdate\u001b[20;44H6\u001b[39m\u001b[4"]
[10.626024, "o", "9m\u001b[59m\u001b[0m\u001b[?25l"]
[10.644844, "o", "\u001b[2;4H805d0b3\u001b[2;13H\u001b[38;5;15;48;5;4mdev)\u001b[39;49m WIP: do\u001b[2;26Hs(fs): add some documentat\u001b[2;54Hn \u001b[2;60H \u001b[2;67H \u001b[2;71H \u001b[2;74H \u001b[3;2H*\u001b[3;4H9943a0f\u001b[3;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[3;25H\u001b[39;49mchore(deps):\u001b[3;38Hupdate\u001b[3;45Hdocker.io/rust\u001b[3;60Hdocker\u001b[3;67Htag\u001b[3;71Hto\u001b[3;74Hv1.81\u001b[4;3H \u001b[4;9H \u001b[4;15H \u001b[4;18H \u001b[4;23H \u001b[4;26H \u001b[4;31H \u001b[4;36H \u001b[5;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────█\u001b[6;2H*\u001b[6;4H249b943\u001b[6;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[6;30H\u001b[39;49mfix(deps):\u001b[6;41Hupdate\u001b[6;48Hrust\u001b[6;53Hcrate\u001b[6;59Hscraper\u001b[6;67Hto\u001b[6;70H0.20\u001b[7;3H \u001b[7;14H \u001b[7;20H \u001b[7;23H \u001b[7;28H \u001b[7;31H \u001b[7;36H \u001b[7;41H \u001b[8;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────"]
[10.644992, "o", "─────────────────────────────────\u001b[9;2H*\u001b[9;4H7fba5fb\u001b[9;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[9;30H\u001b[39;49mSupport\u001b[9;38H--version,\u001b[9;49H--help\u001b[9;56Hand\u001b[9;60H--directory\u001b[9;72Hcli\u001b[9;76Harg\u001b[10;3H \u001b[10;15H \u001b[10;21H \u001b[10;24H \u001b[10;29H \u001b[10;32H \u001b[10;37H \u001b[10;42H \u001b[11;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[12;2H*\u001b[12;4Hde4785c\u001b[12;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[12;30H\u001b[39;49mfeat:\u001b[12;36Hensure\u001b[12;43Htoolchains\u001b[12;54Hare\u001b[12;58Hup-to-date\u001b[13;3H \u001b[13;8H \u001b[13;14H \u001b[13;17H \u001b[13;22H \u001b[13;25H \u001b[13;30H \u001b[13;35H \u001b[14;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[14;11Hin -> next -> dev)\u001b[14;30H\u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────"]
[10.645137, "o", "───────────────────────║\u001b[15;2H*\u001b[15;4Hfc7fbca\u001b[15;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[15;30H\u001b[39;49mchore(deps):\u001b[15;43Hupdate\u001b[15;50Hdocker.io/rust\u001b[15;65Hdocker\u001b[15;72Htag\u001b[15;76Hto\u001b[16;3H \u001b[16;9H \u001b[16;15H \u001b[16;18H \u001b[16;23H \u001b[16;26H \u001b[16;31H \u001b[16;36H \u001b[17;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[18;2H*\u001b[18;4H24fe845\u001b[18;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[18;30H\u001b[39;49mchore(deps):\u001b[18;43Hupdate\u001b[18;50Hdocker.io/postgres:16.4-alpin\u001b[19;3H \u001b[19;8H \u001b[19;14H \u001b[19;17H \u001b[19;22H \u001b[19;25H \u001b[19;30H \u001b[19;35H \u001b[20;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[20;30H\u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────"]
[10.645277, "o", "───────────────\u001b[21;2H*\u001b[21;4Hb7340df\u001b[21;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[21;30H\u001b[39;49mchore:\u001b[21;37Hupdate\u001b[21;44H6\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.663505, "o", "\u001b[2;2H \u001b[38;5;15;48;5;1mkxio\u001b[39;49m \u001b[38;5;15;48;5;1mcommit is a Work-in-progress\u001b[2;37H\u001b[39;49m(main -> n\u001b[2;48Hx\u001b[2;50H -> dev)\u001b[2;59H\u001b[38;5;1;49mALERT\u001b[2;65H\u001b[39;49m──────────────\u001b[3;4H805d0b3\u001b[3;13H\u001b[38;5;15;48;5;4mdev)\u001b[39;49m WIP: do\u001b[3;26Hs(fs): add some documentat\u001b[3;54Hn \u001b[3;60H \u001b[3;67H \u001b[3;71H \u001b[3;74H \u001b[4;2H*\u001b[4;4H9943a0f\u001b[4;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[4;25H\u001b[39;49mchore(deps):\u001b[4;38Hupdate\u001b[4;45Hdocker.io/rust\u001b[4;60Hdocker\u001b[4;67Htag\u001b[4;71Hto\u001b[4;74Hv1.81\u001b[5;3H \u001b[5;9H \u001b[5;15H \u001b[5;18H \u001b[5;23H \u001b[5;26H \u001b[5;31H \u001b[5;36H \u001b[6;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[7;2H*\u001b[7;4H249b943\u001b[7;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[7;30H\u001b[39;49mfix(deps):\u001b[7;41Hupdate\u001b[7;48Hrust\u001b[7;53Hcrate\u001b[7;59Hscraper\u001b[7;67Hto\u001b[7;70H0.20\u001b[8;3H \u001b["]
[10.663582, "o", "8;14H \u001b[8;20H \u001b[8;23H \u001b[8;28H \u001b[8;31H \u001b[8;36H \u001b[8;41H \u001b[9;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[10;2H*\u001b[10;4H7fba5fb\u001b[10;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[10;30H\u001b[39;49mSupport\u001b[10;38H--version,\u001b[10;49H--help\u001b[10;56Hand\u001b[10;60H--directory\u001b[10;72Hcli\u001b[10;76Harg\u001b[11;3H \u001b[11;15H \u001b[11;21H \u001b[11;24H \u001b[11;29H \u001b[11;32H \u001b[11;37H \u001b[11;42H \u001b[12;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[13;2H*\u001b[13;4Hde4785c\u001b[13;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[13;30H\u001b[39;49mfeat:\u001b[13;36Hensure\u001b[13;43Htoolchains\u001b[13;54Hare\u001b[13;58Hup-to-date\u001b[14;3H \u001b[14;8H \u001b[14;14H \u001b[14;17H \u001b[14;22H \u001b[14;25H \u001b[14;"]
[10.663631, "o", "30H \u001b[14;35H \u001b[15;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[15;11Hin -> next -> dev)\u001b[15;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[16;2H*\u001b[16;4Hfc7fbca\u001b[16;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[16;30H\u001b[39;49mchore(deps):\u001b[16;43Hupdate\u001b[16;50Hdocker.io/rust\u001b[16;65Hdocker\u001b[16;72Htag\u001b[16;76Hto\u001b[17;3H \u001b[17;9H \u001b[17;15H \u001b[17;18H \u001b[17;23H \u001b[17;26H \u001b[17;31H \u001b[17;36H \u001b[18;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[19;2H*\u001b[19;4H24fe845\u001b[19;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[19;30H\u001b[39;49mchore(deps):\u001b[19;43Hupdate\u001b[19;50Hdocker.io/postgres:16.4-alpin\u001b[20;3H \u001b[20;8H \u001b[20;14H \u001b[20;17H \u001b[20;22H \u001b[20;25H \u001b[20;30H \u001b[20;35H "]
[10.663766, "o", " \u001b[21;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[21;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[22;2H*\u001b[22;4Hb7340df\u001b[22;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[22;30H\u001b[39;49mchore:\u001b[22;37Hupdate\u001b[22;44H6\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.682703, "o", "\u001b[2;3H \u001b[2;8H \u001b[2;37H \u001b[2;43H \u001b[2;46H \u001b[2;51H \u001b[2;54H \u001b[2;59H \u001b[2;65H \u001b[3;2H \u001b[38;5;15;48;5;1mkxio\u001b[39;49m \u001b[38;5;15;48;5;1mcommit is a Work-in-progress\u001b[3;37H\u001b[39;49m(main -> n\u001b[3;48Hx\u001b[3;50H -> dev)\u001b[3;59H\u001b[38;5;1;49mALERT\u001b[3;65H\u001b[39;49m──────────────\u001b[4;4H805d0b3\u001b[4;13H\u001b[38;5;15;48;5;4mdev)\u001b[39;49m WIP: do\u001b[4;26Hs(fs): add some documentat\u001b[4;54Hn \u001b[4;60H \u001b[4;67H \u001b[4;71H \u001b[4;74H █\u001b[5;2H*\u001b[5;4H9943a0f\u001b[5;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[5;25H\u001b[39;49mchore(deps):\u001b[5;38Hupdate\u001b[5;45Hdocker.io/rust\u001b[5;60Hdocker\u001b[5;67Htag\u001b[5;71Hto\u001b[5;74Hv1.81\u001b[6;3H \u001b[6;9H \u001b[6;15H \u001b[6;18H \u001b[6;23H \u001b[6;26H \u001b[6;31H \u001b[6;36H \u001b[7;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[8;2H*\u001b[8;4H249b943\u001b[8;12H\u001b[38;5;15;48;5;"]
[10.682851, "o", "4m(dev)(main)(next)\u001b[8;30H\u001b[39;49mfix(deps):\u001b[8;41Hupdate\u001b[8;48Hrust\u001b[8;53Hcrate\u001b[8;59Hscraper\u001b[8;67Hto\u001b[8;70H0.20\u001b[9;3H \u001b[9;14H \u001b[9;20H \u001b[9;23H \u001b[9;28H \u001b[9;31H \u001b[9;36H \u001b[9;41H \u001b[10;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[11;2H*\u001b[11;4H7fba5fb\u001b[11;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[11;30H\u001b[39;49mSupport\u001b[11;38H--version,\u001b[11;49H--help\u001b[11;56Hand\u001b[11;60H--directory\u001b[11;72Hcli\u001b[11;76Harg\u001b[12;3H \u001b[12;15H \u001b[12;21H \u001b[12;24H \u001b[12;29H \u001b[12;32H \u001b[12;37H \u001b[12;42H \u001b[13;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────║\u001b[14;2H*\u001b[14;4Hde4785c\u001b[14;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[14;30H\u001b[39;49m"]
[10.683246, "o", "feat:\u001b[14;36Hensure\u001b[14;43Htoolchains\u001b[14;54Hare\u001b[14;58Hup-to-date\u001b[15;3H \u001b[15;8H \u001b[15;14H \u001b[15;17H \u001b[15;22H \u001b[15;25H \u001b[15;30H \u001b[15;35H \u001b[16;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[16;11Hin -> next -> dev)\u001b[16;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[17;2H*\u001b[17;4Hfc7fbca\u001b[17;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[17;30H\u001b[39;49mchore(deps):\u001b[17;43Hupdate\u001b[17;50Hdocker.io/rust\u001b[17;65Hdocker\u001b[17;72Htag\u001b[17;76Hto\u001b[18;3H \u001b[18;9H \u001b[18;15H \u001b[18;18H \u001b[18;23H \u001b[18;26H \u001b[18;31H \u001b[18;36H \u001b[19;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[20;2H*\u001b[20;4H24fe845\u001b[20;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[20;30H\u001b[39;49mchore(deps):\u001b[20;"]
[10.683342, "o", "43Hupdate\u001b[20;50Hdocker.io/postgres:16.4-alpin\u001b[21;3H \u001b[21;8H \u001b[21;14H \u001b[21;17H \u001b[21;22H \u001b[21;25H \u001b[21;30H \u001b[21;35H \u001b[22;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[22;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[23;2H*\u001b[23;4Hb7340df\u001b[23;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[23;30H\u001b[39;49mchore:\u001b[23;37Hupdate\u001b[23;44H6\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.702671, "o", "\u001b[2;2H*\u001b[2;4H91c5973\u001b[2;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[2;30H\u001b[39;49mchore:\u001b[2;37Hrelease\u001b[3;3H \u001b[3;8H \u001b[3;37H \u001b[3;43H \u001b[3;46H \u001b[3;51H \u001b[3;54H \u001b[3;59H \u001b[3;65H \u001b[4;2H \u001b[38;5;15;48;5;1mkxio\u001b[39;49m \u001b[38;5;15;48;5;1mcommit is a Work-in-progress\u001b[4;37H\u001b[39;49m(main -> n\u001b[4;48Hx\u001b[4;50H -> dev)\u001b[4;59H\u001b[38;5;1;49mALERT\u001b[4;65H\u001b[39;49m──────────────\u001b[5;4H805d0b3\u001b[5;13H\u001b[38;5;15;48;5;4mdev)\u001b[39;49m WIP: do\u001b[5;26Hs(fs): add some documentat\u001b[5;54Hn \u001b[5;60H \u001b[5;67H \u001b[5;71H \u001b[5;74H \u001b[6;2H*\u001b[6;4H9943a0f\u001b[6;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[6;25H\u001b[39;49mchore(deps):\u001b[6;38Hupdate\u001b[6;45Hdocker.io/rust\u001b[6;60Hdocker\u001b[6;67Htag\u001b[6;71Hto\u001b[6;74Hv1.81\u001b[7;3H \u001b[7;9H \u001b[7;15H \u001b[7;18H \u001b[7;23H \u001b[7;26H \u001b[7;31H \u001b[7;36H \u001b[8;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────"]
[10.703019, "o", "─────────────────\u001b[9;2H*\u001b[9;4H249b943\u001b[9;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[9;30H\u001b[39;49mfix(deps):\u001b[9;41Hupdate\u001b[9;48Hrust\u001b[9;53Hcrate\u001b[9;59Hscraper\u001b[9;67Hto\u001b[9;70H0.20\u001b[10;3H \u001b[10;14H \u001b[10;20H \u001b[10;23H \u001b[10;28H \u001b[10;31H \u001b[10;36H \u001b[10;41H \u001b[11;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[12;2H*\u001b[12;4H7fba5fb\u001b[12;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[12;30H\u001b[39;49mSupport\u001b[12;38H--version,\u001b[12;49H--help\u001b[12;56Hand\u001b[12;60H--directory\u001b[12;72Hcli\u001b[12;76Harg\u001b[13;3H \u001b[13;15H \u001b[13;21H \u001b[13;24H \u001b[13;29H \u001b[13;32H \u001b[13;37H \u001b[13;42H \u001b[14;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────"]
[10.703551, "o", "──────\u001b[15;2H*\u001b[15;4Hde4785c\u001b[15;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[15;30H\u001b[39;49mfeat:\u001b[15;36Hensure\u001b[15;43Htoolchains\u001b[15;54Hare\u001b[15;58Hup-to-date\u001b[16;3H \u001b[16;8H \u001b[16;14H \u001b[16;17H \u001b[16;22H \u001b[16;25H \u001b[16;30H \u001b[16;35H \u001b[17;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[17;11Hin -> next -> dev)\u001b[17;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[18;2H*\u001b[18;4Hfc7fbca\u001b[18;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[18;30H\u001b[39;49mchore(deps):\u001b[18;43Hupdate\u001b[18;50Hdocker.io/rust\u001b[18;65Hdocker\u001b[18;72Htag\u001b[18;76Hto\u001b[19;3H \u001b[19;9H \u001b[19;15H \u001b[19;18H \u001b[19;23H \u001b[19;26H \u001b[19;31H \u001b[19;36H \u001b[20;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────────"]
[10.703743, "o", "─\u001b[21;2H*\u001b[21;4H24fe845\u001b[21;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[21;30H\u001b[39;49mchore(deps):\u001b[21;43Hupdate\u001b[21;50Hdocker.io/postgres:16.4-alpin\u001b[22;3H \u001b[22;8H \u001b[22;14H \u001b[22;17H \u001b[22;22H \u001b[22;25H \u001b[22;30H \u001b[22;35H \u001b[23;2H \u001b[38;5;6;49mtest\u001b[39;49m (main -> next -> dev)\u001b[23;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.72275, "o", "\u001b[2;2H \u001b[38;5;6;49mgit-next\u001b[2;12H\u001b[39;49m(main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────\u001b[3;2H*\u001b[3;4H91c5973\u001b[3;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[3;30H\u001b[39;49mchore:\u001b[3;37Hrelease\u001b[3;79H█\u001b[4;3H \u001b[4;8H \u001b[4;37H \u001b[4;43H \u001b[4;46H \u001b[4;51H \u001b[4;54H \u001b[4;59H \u001b[4;65H \u001b[5;2H \u001b[38;5;15;48;5;1mkxio\u001b[39;49m \u001b[38;5;15;48;5;1mcommit is a Work-in-progress\u001b[5;37H\u001b[39;49m(main -> n\u001b[5;48Hx\u001b[5;50H -> dev)\u001b[5;59H\u001b[38;5;1;49mALERT\u001b[5;65H\u001b[39;49m──────────────\u001b[6;4H805d0b3\u001b[6;13H\u001b[38;5;15;48;5;4mdev)\u001b[39;49m WIP: do\u001b[6;26Hs(fs): add some documentat\u001b[6;54Hn \u001b[6;60H \u001b[6;67H \u001b[6;71H \u001b[6;74H \u001b[7;2H*\u001b[7;4H9943a0f\u001b[7;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[7;25H\u001b[39;49mchore(deps):\u001b[7;38Hupdate\u001b[7;45Hdocker.io/rust\u001b[7;60Hdocker\u001b[7;67Htag\u001b[7;71Hto\u001b[7;74Hv1.81\u001b[8;3H \u001b[8;9H \u001b[8;15H \u001b[8;18H \u001b[8;23H \u001b[8;26H "]
[10.7228, "o", "\u001b[8;31H \u001b[8;36H \u001b[9;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[10;2H*\u001b[10;4H249b943\u001b[10;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[10;30H\u001b[39;49mfix(deps):\u001b[10;41Hupdate\u001b[10;48Hrust\u001b[10;53Hcrate\u001b[10;59Hscraper\u001b[10;67Hto\u001b[10;70H0.20\u001b[11;3H \u001b[11;14H \u001b[11;20H \u001b[11;23H \u001b[11;28H \u001b[11;31H \u001b[11;36H \u001b[11;41H \u001b[12;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[13;2H*\u001b[13;4H7fba5fb\u001b[13;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[13;30H\u001b[39;49mSupport\u001b[13;38H--version,\u001b[13;49H--help\u001b[13;56Hand\u001b[13;60H--directory\u001b[13;72Hcli\u001b[13;76Harg\u001b[14;3H \u001b[14;15H \u001b[14;21H \u001b[14;24H \u001b[14;29H \u001b[14;32H \u001b[14;37H "]
[10.723231, "o", " \u001b[14;42H \u001b[15;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[16;2H*\u001b[16;4Hde4785c\u001b[16;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[16;30H\u001b[39;49mfeat:\u001b[16;36Hensure\u001b[16;43Htoolchains\u001b[16;54Hare\u001b[16;58Hup-to-date\u001b[17;3H \u001b[17;8H \u001b[17;14H \u001b[17;17H \u001b[17;22H \u001b[17;25H \u001b[17;30H \u001b[17;35H \u001b[18;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[18;11Hin -> next -> dev)\u001b[18;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[19;2H*\u001b[19;4Hfc7fbca\u001b[19;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[19;30H\u001b[39;49mchore(deps):\u001b[19;43Hupdate\u001b[19;50Hdocker.io/rust\u001b[19;65Hdocker\u001b[19;72Htag\u001b[19;76Hto\u001b[20;3H \u001b[20;9H \u001b[20;15H \u001b[20;18H \u001b[20;23H \u001b[20;26H \u001b[20;31H \u001b[20;36H "]
[10.723349, "o", " \u001b[21;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[22;2H*\u001b[22;4H24fe845\u001b[22;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[22;30H\u001b[39;49mchore(deps):\u001b[22;43Hupdate\u001b[22;50Hdocker.io/postgres:16.4-alpin\u001b[23;3H \u001b[23;8H \u001b[23;14H \u001b[23;17H \u001b[23;22H \u001b[23;25H \u001b[23;30H \u001b[23;35H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.742361, "o", "\u001b[2;3Hforge: jo \u001b[2;18H \u001b[2;21H \u001b[2;26H \u001b[2;29H \u001b[2;34H \u001b[2;39H \u001b[3;2H \u001b[38;5;6;49mgit-next\u001b[3;12H\u001b[39;49m(main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────\u001b[4;2H*\u001b[4;4H91c5973\u001b[4;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[4;30H\u001b[39;49mchore:\u001b[4;37Hrelease\u001b[5;3H \u001b[5;8H \u001b[5;37H \u001b[5;43H \u001b[5;46H \u001b[5;51H \u001b[5;54H \u001b[5;59H \u001b[5;65H \u001b[6;2H \u001b[38;5;15;48;5;1mkxio\u001b[39;49m \u001b[38;5;15;48;5;1mcommit is a Work-in-progress\u001b[6;37H\u001b[39;49m(main -> n\u001b[6;48Hx\u001b[6;50H -> dev)\u001b[6;59H\u001b[38;5;1;49mALERT\u001b[6;65H\u001b[39;49m──────────────\u001b[7;4H805d0b3\u001b[7;13H\u001b[38;5;15;48;5;4mdev)\u001b[39;49m WIP: do\u001b[7;26Hs(fs): add some documentat\u001b[7;54Hn \u001b[7;60H \u001b[7;67H \u001b[7;71H \u001b[7;74H \u001b[8;2H*\u001b[8;4H9943a0f\u001b[8;12H\u001b[38;5;15;48;5;4m(main)(next)\u001b[8;25H\u001b[39;49mchore(deps):\u001b[8;38Hupdate\u001b[8;45Hdocker.io/ru"]
[10.742451, "o", "st\u001b[8;60Hdocker\u001b[8;67Htag\u001b[8;71Hto\u001b[8;74Hv1.81\u001b[9;3H \u001b[9;9H \u001b[9;15H \u001b[9;18H \u001b[9;23H \u001b[9;26H \u001b[9;31H \u001b[9;36H \u001b[10;2H \u001b[38;5;6;49mpodal\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[11;2H*\u001b[11;4H249b943\u001b[11;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[11;30H\u001b[39;49mfix(deps):\u001b[11;41Hupdate\u001b[11;48Hrust\u001b[11;53Hcrate\u001b[11;59Hscraper\u001b[11;67Hto\u001b[11;70H0.20\u001b[12;3H \u001b[12;14H \u001b[12;20H \u001b[12;23H \u001b[12;28H \u001b[12;31H \u001b[12;36H \u001b[12;41H ║\u001b[13;2H \u001b[38;5;6;49mrefile-m4b\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ──────────────────────────────────────\u001b[14;2H*\u001b[14;4H7fba5fb\u001b[14;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[14;30H\u001b[39;49mSupport\u001b[14;38H--version,\u001b[14;49H--help\u001b[14;56Hand\u001b[14;60H--dire"]
[10.742513, "o", "ctory\u001b[14;72Hcli\u001b[14;76Harg\u001b[15;3H \u001b[15;15H \u001b[15;21H \u001b[15;24H \u001b[15;29H \u001b[15;32H \u001b[15;37H \u001b[15;42H \u001b[16;2H \u001b[38;5;6;49mrust-action\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ─────────────────────────────────────\u001b[17;2H*\u001b[17;4Hde4785c\u001b[17;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[17;30H\u001b[39;49mfeat:\u001b[17;36Hensure\u001b[17;43Htoolchains\u001b[17;54Hare\u001b[17;58Hup-to-date\u001b[18;3H \u001b[18;8H \u001b[18;14H \u001b[18;17H \u001b[18;22H \u001b[18;25H \u001b[18;30H \u001b[18;35H \u001b[19;2H \u001b[38;5;6;49mskip\u001b[39;49m (m\u001b[19;11Hin -> next -> dev)\u001b[19;30H\u001b[38;5;2;49mokay\u001b[39;49m ────────────────────────────────────────────\u001b[20;2H*\u001b[20;4Hfc7fbca\u001b[20;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[20;30H\u001b[39;49mchore(deps):\u001b[20;43Hupdate\u001b[20;50Hdocker.io/rust\u001b[20;65Hdocker\u001b[20;72Htag\u001b[20;76Hto\u001b"]
[10.742578, "o", "[21;3H \u001b[21;9H \u001b[21;15H \u001b[21;18H \u001b[21;23H \u001b[21;26H \u001b[21;31H \u001b[21;36H \u001b[22;2H \u001b[38;5;6;49mtasyn\u001b[39;49m (main -> next -> dev) \u001b[38;5;2;49mokay\u001b[39;49m ───────────────────────────────────────────\u001b[23;2H*\u001b[23;4H24fe845\u001b[23;12H\u001b[38;5;15;48;5;4m(dev)(main)(next)\u001b[23;30H\u001b[39;49mchore(deps):\u001b[23;43Hupdate\u001b[23;50Hdocker.io/postgres:16.4-alpin\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.761454, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.782349, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.801877, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.822526, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.842672, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.861559, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.879728, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.898859, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.917931, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.946604, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.966104, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[10.985743, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.023523, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.061011, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.100465, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.138144, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.174063, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.211364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.24819, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.285142, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.322272, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.358341, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.39547, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.431719, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.469256, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.511171, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.546092, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.583528, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.618694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.657656, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.693252, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.731247, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.769971, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.804203, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.841036, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.877137, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.914426, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.954717, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[11.990908, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.028533, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.066414, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.107049, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.142204, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.179509, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.216573, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.253728, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.289769, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.326863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.363905, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.40106, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.43865, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.475818, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.512054, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.550834, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.589195, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.626474, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.665855, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.702211, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.737993, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.774967, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.815764, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.852596, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.892064, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.92746, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[12.964137, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.002874, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.03968, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.076753, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.113219, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.151584, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.188483, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.229106, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.264818, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.301622, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.338226, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.375524, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.411574, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.447307, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.483603, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.520106, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.560836, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.598547, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.635115, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.672651, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.712643, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.7507, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.786601, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.82323, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.858725, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.8978, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.934514, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[13.972287, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.008834, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.046299, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.083399, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.120763, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.155627, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.193875, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.231887, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.267898, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.302399, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.343537, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.380356, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.417261, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.453534, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.49263, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.530636, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.566577, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.606367, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.641865, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.679072, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.71686, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.760187, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.795183, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.83164, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.868972, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.905974, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.94637, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[14.983598, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.019429, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.056081, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.093109, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.130406, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.172122, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.209038, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.244863, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.282047, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.316998, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.360829, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.39752, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.435025, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.476643, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.518269, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.551214, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.588339, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.626227, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.668075, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.704194, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.740791, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.778154, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.819102, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.855694, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.891253, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.927393, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[15.963477, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.000617, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.036768, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.074615, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.109168, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.147181, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.184001, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.221019, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.259995, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.298455, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.335449, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.374772, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.412157, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.45174, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.487307, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.525937, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.564883, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.602005, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.638765, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.674415, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.712126, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.746806, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.783087, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.821257, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.859364, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.897079, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.934126, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[16.970609, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.007439, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.043377, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.081879, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.119148, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.156208, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.192544, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.229623, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.268441, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.304077, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.341245, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.377117, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.417434, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.456131, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.492909, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.530736, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.568137, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.608431, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.646151, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.683754, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.718481, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.75638, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.792562, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.829402, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.866012, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.903936, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.943019, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[17.984106, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.021665, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.059774, "o", "\u001b[24;43H💚\u001b[24;45H \u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.098821, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.135715, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.17206, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.208703, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.245904, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.283908, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.31957, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.355896, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.393339, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.429976, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.466756, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.506115, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.545975, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.583191, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.621038, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.656638, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.701071, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.739152, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.776111, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.81384, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.850448, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.888985, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.925942, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[18.962416, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.000353, "o", "\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.039442, "o", "\u001b[24;43H 💚\u001b[39m\u001b[49m\u001b[59m\u001b[0m\u001b[?25l"]
[19.042413, "o", "\u001b[?25h"]
[19.042469, "o", "\u001b[?1049l"]
[19.110591, "o", "\u001b[?1049l"]

BIN
demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

View file

@ -21,6 +21,10 @@ start-ngrok:
ngrok http --domain=${NGROK_DOMAIN} 8080 ngrok http --domain=${NGROK_DOMAIN} 8080
fi fi
start-mac-tunnel:
ssh dagon -R7777:localhost:8888
@echo "Mac Tunnel has closed"
coverage-update: coverage-update:
cargo tarpaulin --lib --out html cargo tarpaulin --lib --out html
echo "Now:\n\topen tarpaulin-report.html" echo "Now:\n\topen tarpaulin-report.html"
@ -43,24 +47,17 @@ grcov-coverage:
find . -name '*.profraw' -exec rm "{}" \; find . -name '*.profraw' -exec rm "{}" \;
echo "Now:\n\topen target/debug/coverage/index.html" echo "Now:\n\topen target/debug/coverage/index.html"
publish version: docker-build-builder:
#!/usr/bin/bash -e docker build -t git.kemitix.net/kemitix/git-next-builder:2024.08.04 -f Dockerfile.builder .
echo "Publishing git-next v{{version}} to crates.io..."
if [ -z $(git status --short) ]; then docker-build: docker-build-builder
echo "Worktree is clean - proceeding" docker build -t git.kemitix.net/kemitix/git-next:latest .
else
echo "Worktree is Dirty - aborting" ; exit docker-run: docker-build
fi docker run -it -p "7777:8888" -v .:/app/ git.kemitix.net/kemitix/git-next:latest server start --ui
git checkout v{{version}}
ORDER=$(cargo publish-workspace --target-version {{version}} --crate-prefix git-next --show-order 2>/dev/null | cut -d\ -f2-) run *args:
echo "Publishing crates in order: ${ORDER}" cargo run -- {{ args }}
# INFO: Why not use publish-workspace to publish? It doesn't support when crates-io registry is replaced
for P in ${ORDER} run-ui:
do just run server start --ui
echo "Publishing ${P}..."
cargo publish --registry crates-io -p $P
echo "Done: ${P}"
echo "======================================"
done
echo "All crates published"
git checkout dev