Compare commits

...

100 commits

Author SHA1 Message Date
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
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
76ae37a9a5 chore: release
All checks were successful
ci/woodpecker/pr/push-next Pipeline was successful
ci/woodpecker/pr/cron-docker-builder Pipeline was successful
ci/woodpecker/pr/tag-created Pipeline was successful
Rust / build (pull_request) Successful in 7m5s
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 3m16s
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
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 10m31s
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-08-25 15:14:41 +00:00
5d9915bdbd feat(tui): (experimental) show repo state, messages and git log
All checks were successful
Rust / build (push) Successful in 14m16s
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 1m3s
2024-08-25 15:59:42 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
f504b62ff6 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 1m2s
Rust / build (push) Successful in 5m47s
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/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
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-08-23 07:39:32 +00:00
47dbc1a8a4 chore(deps): update rust crate gix to 0.66
All checks were successful
Rust / build (push) Successful in 6m5s
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 44s
This required manually bumping a locked transitive dependency:

> cargo update -p winnow@0.6.7

Ref: https://github.com/Byron/gitoxide/issues/1538
2024-08-23 08:14:20 +01:00
7a4f9a45a6 fix(github): register webhook with valid callback url
All checks were successful
Rust / build (push) Successful in 8m58s
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 1m33s
2024-08-22 19:55:32 +01:00
622e144986 feat(tui): (experimental) tui option
All checks were successful
Rust / build (push) Successful in 6m27s
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
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
When the 'tui' feature is enabled, then server start accepts an optional
--ui parameter. When specified a ratatui ui will display, showing
liveness and a ping update when a valid config is loaded.
2024-08-12 10:01:35 +01:00
0632225752 build: test all feature combinations
All checks were successful
Rust / build (push) Successful in 5m22s
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 1m13s
2024-08-12 10:01:32 +01:00
08d2377404 fix: file_watcher runs on own thread
All checks were successful
Rust / build (push) Successful in 2m15s
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 43s
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
Closes kemitix/git-next#142
2024-08-11 13:55:38 +01:00
e34c6e0ef6 fix: Revert "fix: release-plz generated PR changelog"
All checks were successful
Rust / build (push) Successful in 1m23s
Release Please / Release-plz (push) Successful in 15s
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
This reverts commit f5a3524cb9.
2024-08-10 18:38:14 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
6c5e1c1a80 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 2m56s
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/cron-docker-builder Pipeline was successful
ci/woodpecker/tag/push-next Pipeline was successful
ci/woodpecker/tag/tag-created Pipeline was successful
Rust / build (push) Successful in 1m21s
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-08-10 17:23:24 +00:00
ac069551d8 chore: simplify just validate-dev-branch task
All checks were successful
Rust / build (push) Successful in 1m17s
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 41s
2024-08-10 18:21:33 +01:00
f5a3524cb9 fix: release-plz generated PR changelog
All checks were successful
Rust / build (push) Successful in 1m24s
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 31s
2024-08-10 18:13:47 +01:00
f0daac76b4 feat: make forge and repo alias more prominent in email
All checks were successful
Rust / build (push) Successful in 1m17s
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 42s
Closes kemitix/git-next#141
2024-08-10 18:12:00 +01:00
Renovate Bot
bbbb010762 chore(deps): update docker.io/rust docker tag to v1.80.1
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 2m9s
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 2m25s
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-08 20:46:07 +00:00
60d05c8b3b fix: invalid config section typo in README
All checks were successful
Rust / build (push) Successful in 1m36s
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
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-08 09:44:54 +01:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
ad916cb845 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
Rust / build (pull_request) Successful in 2m30s
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 4m49s
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
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful
Rust / build (push) Successful in 2m44s
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-08-08 07:41:52 +00:00
ef24cb583c feat: add short git log graph to notifications
All checks were successful
Rust / build (push) Successful in 2m7s
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
Closes kemitix/git-next#133
2024-08-08 08:39:01 +01:00
8c19680056 refactor: macros use a more common syntax
Some checks failed
Rust / build (push) Successful in 1m15s
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) Failing after 10m22s
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
Parameters were separated by ':', but are now separated by ','.
2024-08-06 20:06:39 +01:00
ad358ad7c2 refactor: cleanup pedantic clippy in forge-github crate
All checks were successful
Rust / build (push) Successful in 2m17s
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 37s
2024-08-06 16:21:25 +01:00
067296ffab refactor: cleanup pedantic clippy in forge-forgejo crate
Some checks are pending
Rust / build (push) Successful in 1m21s
Release Please / Release-plz (push) Waiting to run
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-08-06 16:15:56 +01:00
6acefda5d3 refactor: cleanup pedantic clippy in core crate
All checks were successful
Rust / build (push) Successful in 1m19s
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-08-06 16:07:25 +01:00
24251f0c9c refactor: cleanup pedantic clippy in cli crate
All checks were successful
Rust / build (push) Successful in 1m43s
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 44s
2024-08-06 07:10:14 +01:00
281c07c849 fix: remove dependcy on clang & mold
All checks were successful
Rust / build (push) Successful in 1m32s
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
This was only added to try and improve compile times.

Re-measuring the difference after months of work and refactoring, the
gain from the additional requirements was marginal (39.8s -> 37.5s).

So, to simplify the requirement, clang and mold have been removed.

Closes: kemitix/git-next#131
2024-08-04 20:41:38 +01:00
9a1756bf6c build(forgejo): remove publish-to-crates-io step
All checks were successful
Rust / build (push) Successful in 1m22s
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 45s
This is now handled by release-plz
2024-08-04 19:24:27 +01:00
34019b5c4a build(woodpecker): remove publish-to-forgejo step
Some checks failed
Rust / build (push) Successful in 2m9s
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) Failing after 56s
This is now handled by release-plz
2024-08-04 19:24:03 +01:00
166 changed files with 5631 additions and 1766 deletions

View file

@ -1,11 +1,4 @@
# ./cargo/config.toml
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang-16"
rustflags = [
"-C",
"link-arg=--ld-path=/usr/bin/mold",
]
[profile.dev]
debug = 0
strip = "debuginfo"

View file

@ -1,25 +0,0 @@
name: Publish to crates.io
on:
release:
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
with:
args: login "$CARGO_REGISTRY_TOKEN"
- name: Publish
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
with:
args: publish --registry crates-io --no-verify

View file

@ -18,21 +18,21 @@ jobs:
uses: actions/checkout@v4
- name: Format
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
uses: https://git.kemitix.net/kemitix/rust@v1.81.0
with:
args: cargo fmt --all -- --check
- name: Clippy
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
uses: https://git.kemitix.net/kemitix/rust@v1.81.0
with:
args: cargo clippy
args: cargo hack --feature-powerset clippy
- name: Build
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
uses: https://git.kemitix.net/kemitix/rust@v1.81.0
with:
args: cargo build
args: cargo hack --feature-powerset build
- name: Test
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
uses: https://git.kemitix.net/kemitix/rust@v1.81.0
with:
args: cargo test
args: cargo hack --feature-powerset test

3
.gitignore vendored
View file

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

View file

@ -11,3 +11,16 @@ steps:
repository_token: "776a3b928b852472c2af727a360c85c00af64b9f"
prefix_regex: "(#|//) (TODO|FIXME): "
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

@ -1,17 +1,4 @@
steps:
publish-to-forgejo:
when:
- event: tag
ref: refs/tags/v*
# INFO: https://woodpecker-ci.org/plugins/Gitea%20Release
image: docker.io/woodpeckerci/plugin-gitea-release:0.3.2
settings:
base_url: https://git.kemitix.net
api_key:
from_secret: FORGEJO_RELEASE_PLUGIN
target: main
prerelease: true
docker-build:
when:
- event: tag

View file

@ -2,6 +2,199 @@
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
### Added
- *(tui)* (experimental) show repo state, messages and git log
## `git-next` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/v0.13.6...v0.13.7) - 2024-08-25
### Added
- *(tui)* (experimental) show repo state, messages and git log
## `git-next-forge-github` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.5...git-next-forge-github-v0.13.6) - 2024-08-23
### Fixed
- *(github)* register webhook with valid callback url
## `git-next-core` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.5...git-next-core-v0.13.6) - 2024-08-23
### Added
- *(tui)* (experimental) tui option
## `git-next` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/v0.13.5...v0.13.6) - 2024-08-23
### Added
- *(tui)* (experimental) tui option
### Fixed
- file_watcher runs on own thread
### Other
- test all feature combinations
## `git-next` - [0.13.5](https://git.kemitix.net/kemitix/git-next/compare/git-next-v0.13.4...git-next-v0.13.5) - 2024-08-10
### Added
- make forge and repo alias more prominent in email
### Fixed
- invalid config section typo in README
## `git-next-forge-github` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.3...git-next-forge-github-v0.13.4) - 2024-08-08
### Other
- cleanup pedantic clippy in forge-github crate
## `git-next-forge-forgejo` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.3...git-next-forge-forgejo-v0.13.4) - 2024-08-08
### Other
- cleanup pedantic clippy in forge-forgejo crate
## `git-next-core` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.3...git-next-core-v0.13.4) - 2024-08-08
### Added
- add short git log graph to notifications
### Other
- macros use a more common syntax
- cleanup pedantic clippy in core crate
## `git-next` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/v0.13.3...v0.13.4) - 2024-08-08
### Added
- add short git log graph to notifications
### Fixed
- remove dependcy on clang & mold
### Other
- macros use a more common syntax
- cleanup pedantic clippy in core crate
- cleanup pedantic clippy in cli crate
## `git-next-core` - [0.13.3](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.2...git-next-core-v0.13.3) - 2024-08-04
### Fixed

1705
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/*"]
[workspace.package]
version = "0.13.3"
version = "0.13.11"
edition = "2021"
license = "MIT"
@ -15,23 +15,33 @@ documentation = "https://git.kemitix.net/kemitix/git-next/src/branch/main/README
keywords = ["git", "cli", "server", "tool"]
categories = ["development-tools"]
[workspace.lints.clippy]
nursery = { level = "warn", priority = -1 }
# pedantic = "warn"
unwrap_used = "warn"
expect_used = "warn"
# [workspace.lints.clippy]
# pedantic = { level = "warn", priority = -1 }
# nursery = { level = "warn", priority = -1 }
# unwrap_used = "warn"
# expect_used = "warn"
[workspace.dependencies]
git-next-core = { path = "crates/core", version = "0.13" }
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.13" }
git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
# TUI
ratatui = "0.28"
directories = "5.0"
lazy_static = "1.5"
color-eyre = "0.6"
tui-scrollview = "0.4"
regex = "1.10"
chrono = "0.4"
# CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] }
# logging
tracing = "0.1"
tracing-subscriber = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-error = "0.2.0"
# base64 decoding
base64 = "0.22"
@ -42,8 +52,7 @@ sha2 = "0.10"
hex = "0.4"
# git
# gix = "0.62"
gix = { version = "0.64", features = [
gix = { version = "0.66", features = [
"dirwalk",
"blocking-http-transport-reqwest-rust-tls",
] }
@ -72,6 +81,7 @@ time = "0.3"
standardwebhooks = "1.0"
# boilerplate
bon = "2.0"
derive_more = { version = "1.0.0-beta", features = [
"as_ref",
"constructor",
@ -84,6 +94,9 @@ anyhow = "1.0"
thiserror = "1.0"
pike = "0.1"
# iters
take-until = "0.2"
# file watcher
notify = "6.1"
@ -105,3 +118,4 @@ pretty_assertions = "1.4"
rand = "0.8"
mockall = "0.13"
test-log = "0.2"
rstest = { version = "0.22", features = ["async-timeout"] }

View file

@ -12,20 +12,19 @@ FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --profile release --recipe-path recipe.json
COPY . .
RUN cargo build --release --bin git-next && \
RUN cargo build --release --bin git-next --all-features && \
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
RUN apt-get update && \
apt-get install --no-install-recommends -y \
git=1:2.39.2-1.1 \
libssl3=3.0.13-1~deb12u1 \
libdbus-1-dev=1.14.10-1~deb12u1 \
ca-certificates=20230311 \
apt-get satisfy -y "git (>=2.39), libssl3 (>=3.0.14), libdbus-1-dev (>=1.14.10), ca-certificates (>=20230311)" \
&& \
rm -rf /var/lib/apt/lists/*
USER 1000
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,7 +1,7 @@
FROM docker.io/rust:1.80.0-bookworm
FROM docker.io/rust:1.81.0-bookworm
RUN apt-get update && \
apt-get install -y clang-16 mold libdbus-1-dev && \
apt-get install -y libdbus-1-dev && \
curl -L https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz -o cargo-binstall.tgz && \
tar -xzf cargo-binstall.tgz && \
rm cargo-binstall.tgz && \
@ -15,8 +15,6 @@ RUN cargo chef --version
RUN rustfmt --version
RUN cargo fmt --version
RUN cargo clippy --version
RUN mold --version
RUN clang-16 --version
RUN cargo --version
RUN rustc --version
RUN rustup --version

View file

@ -6,4 +6,7 @@
development workflows where each commit must pass CI before being included in
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.

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

@ -12,15 +12,26 @@ keywords = { workspace = true }
categories = { workspace = true }
[features]
default = ["forgejo", "github"]
# default = ["forgejo", "github"]
default = ["forgejo", "github", "tui"]
forgejo = ["git-next-forge-forgejo"]
github = ["git-next-forge-github"]
tui = ["ratatui", "directories", "lazy_static", "tui-scrollview", "regex", "chrono"]
[dependencies]
git-next-core = { workspace = true }
git-next-forge-forgejo = { workspace = true, optional = true }
git-next-forge-github = { workspace = true, optional = true }
# TUI
ratatui = { workspace = true, optional = true }
directories = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true }
color-eyre = { workspace = true }
tui-scrollview = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
chrono = { workspace = true, optional = true }
# CLI parsing
clap = { workspace = true }
@ -30,6 +41,7 @@ kxio = { workspace = true }
# logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-error.workspace = true
# Conventional Commit check
git-conventional = { workspace = true }
@ -40,8 +52,10 @@ toml = { workspace = true }
# Actors
actix = { workspace = true }
actix-rt = { workspace = true }
tokio = { workspace = true }
# boilerplate
bon = { workspace = true }
derive_more = { workspace = true }
derive-with = { workspace = true }
anyhow = { workspace = true }
@ -73,10 +87,11 @@ test-log = { workspace = true }
rand = { workspace = true }
pretty_assertions = { workspace = true }
mockall = { workspace = true }
rstest = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
# pedantic = "warn"
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"

View file

@ -32,13 +32,6 @@ See [Behaviour](#behaviour) to learn how we do this.
- libdbus-1-dev (ubuntu/debian)
- dbus-devel (fedora)
### x86_64-unknown-linux-gnu
Additionally for this platform, to improved compilation times:
- clang-16
- mold
See `.cargo/config.toml` for how they are configured.
## Installation
@ -66,7 +59,7 @@ cargo install --path crates/cli
- [x] cli
- [x] server
- [x] notifications - notify user when intervention required (e.g. to rebase)
- [ ] tui overview
- [x] tui overview
- [ ] webui overview
## Branch Names
@ -127,7 +120,7 @@ forge is running on.
The server should be able to notify the user when manual intervention is required.
```toml
[shoult]
[shout]
desktop = true
[shout.webhook]
@ -205,13 +198,14 @@ forge_type = "GitHub"
hostname = "github.com"
user = "username"
token = "api-key"
max_dev_commits = 25
```
- **forge_type** - one of: `ForgeJo` or `GitHub`
- **hostname** - the hostname for the forge.
- **user** - the user to authenticate as
- **token** - application token for the user. See below for the permissions
required for on each forge.
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for 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_
to `next`.
@ -304,7 +298,13 @@ Sample payload:
"main": "main"
},
"forge_alias": "jo",
"repo_alias": "kxio"
"repo_alias": "kxio",
"log": [
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
"|/",
"* 8c19680 refactor: macros use a more common syntax"
]
},
"timestamp": "1721760933",
"type": "branch.dev.not-on-main"
@ -325,11 +325,16 @@ Sample payload:
{
"data": {
"commit": {
"sha": "98abef1af6825f9770d725a681e5cfc09d7fd4f2",
"message": "feat: add foo to bar template"
"sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
"message": "feat: add log graph to notifications (1 of 2)"
},
"forge_alias": "jo",
"repo_alias": "kxio"
"repo_alias": "kxio",
"log": [
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
]
},
"timestamp": "1721760933",
"type": "cicheck.failed"
@ -572,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.
## 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
Contributions to `git-next` are welcome! If you find a bug or have a feature

View file

@ -23,11 +23,11 @@ pub(super) fn send_email(user_notification: &UserNotification, email_config: &Em
};
match email_config.smtp() {
Some(smtp) => send_email_smtp(email_message, smtp),
None => send_email_sendmail(email_message),
None => send_email_sendmail(&email_message),
}
}
fn send_email_sendmail(email_message: EmailMessage) {
fn send_email_sendmail(email_message: &EmailMessage) {
use sendmail::email;
match email::send(
&email_message.from,
@ -35,7 +35,7 @@ fn send_email_sendmail(email_message: EmailMessage) {
&email_message.subject,
&email_message.body,
) {
Ok(_) => tracing::info!("Email sent successfully!"),
Ok(()) => tracing::info!("Email sent successfully!"),
Err(e) => tracing::warn!("Could not send email: {:?}", e),
}
}
@ -62,7 +62,7 @@ fn do_send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) -> Result<
.map(|response| {
response
.message()
.map(|s| s.to_string())
.map(ToString::to_string)
.collect::<Vec<_>>()
})
.map(|response| {

View file

@ -18,23 +18,24 @@ impl Handler<NotifyUser> for AlertsActor {
};
let net = self.net.clone();
let shout = shout.clone();
if let Some(user_notification) = self.history.sendable(msg.unwrap()) {
async move {
if let Some(webhook_config) = shout.webhook() {
send_webhook(&user_notification, webhook_config, &net).await;
}
if let Some(email_config) = shout.email() {
send_email(&user_notification, email_config);
}
if let Some(desktop) = shout.desktop() {
if desktop {
send_desktop_notification(&user_notification);
}
let Some(user_notification) = self.history.sendable(msg.peel()) else {
return;
};
async move {
if let Some(webhook_config) = shout.webhook() {
send_webhook(&user_notification, webhook_config, &net).await;
}
if let Some(email_config) = shout.email() {
send_email(&user_notification, email_config);
}
if let Some(desktop) = shout.desktop() {
if desktop {
send_desktop_notification(&user_notification);
}
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
}

View file

@ -7,6 +7,6 @@ impl Handler<UpdateShout> for AlertsActor {
type 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

@ -16,7 +16,7 @@ pub struct History {
/// Maps a user notification to when it was last seen.
///
/// The user notification will not be sent until after max_age_seconds from last seen.
/// The user notification will not be sent until after `max_age_seconds` from last seen.
///
/// Each time we see a given user notification, the last seen time will be updated.
items: HashMap<UserNotification, Instant>,
@ -44,7 +44,7 @@ impl History {
pub fn prune(&mut self, now: &Instant) {
if let Some(threshold) = now.checked_sub(self.max_age_seconds) {
self.items.retain(|_, last_seen| *last_seen > threshold)
self.items.retain(|_, last_seen| *last_seen > threshold);
};
}
}

View file

@ -1,6 +1,10 @@
//
use git_next_core::{git::UserNotification, message, server::Shout};
message!(UpdateShout: Shout: "Updated Shout configuration");
message!(UpdateShout, Shout, "Updated Shout configuration");
message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook");
message!(
NotifyUser,
UserNotification,
"Request to send the message payload to the notification webhook"
);

View file

@ -17,6 +17,7 @@ mod webhook;
#[cfg(test)]
mod tests;
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Constructor)]
pub struct AlertsActor {
shout: Option<Shout>, // config for sending alerts to users
@ -29,32 +30,8 @@ impl Actor for AlertsActor {
}
fn short_message(user_notification: &UserNotification) -> String {
let tail = match user_notification {
UserNotification::CICheckFailed {
forge_alias,
repo_alias,
commit,
} => format!("CI Check Failed: {forge_alias}/{repo_alias}: {commit}"),
UserNotification::RepoConfigLoadFailure {
forge_alias,
repo_alias,
reason: _,
} => format!("Invalid Repo Config: {forge_alias}/{repo_alias}"),
UserNotification::WebhookRegistration {
forge_alias,
repo_alias,
reason: _,
} => format!("Failed Webhook Registration: {forge_alias}/{repo_alias}"),
UserNotification::DevNotBasedOnMain {
forge_alias,
repo_alias,
dev_branch: _,
main_branch: _,
dev_commit: _,
main_commit: _,
} => format!("Dev not based on Main: {forge_alias}/{repo_alias}"),
};
format!("[git-next] {tail}")
let (forge_alias, repo_alias) = user_notification.aliases();
format!("[git-next] {forge_alias}/{repo_alias}: {user_notification}")
}
fn full_message(user_notification: &UserNotification) -> String {
@ -63,13 +40,16 @@ fn full_message(user_notification: &UserNotification) -> String {
forge_alias,
repo_alias,
commit,
log,
} => {
let sha = commit.sha();
let message = commit.message();
[
"CI Checks had Failed".to_string(),
"CI Checks have Failed".to_string(),
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
format!("Commit:\n - {sha}\n - {message}"),
"Log:".to_string(),
log.join("\n"),
]
.join("\n\n")
}
@ -98,21 +78,16 @@ fn full_message(user_notification: &UserNotification) -> String {
repo_alias,
dev_branch,
main_branch,
dev_commit,
main_commit,
} => {
let dev_sha = dev_commit.sha();
let dev_message = dev_commit.message();
let main_sha = main_commit.sha();
let main_message = main_commit.message();
[
format!("The branch '{dev_branch}' is not based on the branch '{main_branch}'."),
format!("TODO: Rebase '{dev_branch}' onto '{main_branch}'."),
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
format!("{dev_branch}:\n - {dev_sha}\n - {dev_message}"),
format!("{main_branch}:\n - {main_sha}\n - {main_message}"),
]
.join("\n\n")
}
dev_commit: _,
main_commit: _,
log,
} => [
format!("The branch '{dev_branch}' is not based on the branch '{main_branch}'."),
format!("TODO: Rebase '{dev_branch}' onto '{main_branch}'."),
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
"Log:".to_string(),
log.join("\n"),
]
.join("\n\n"),
}
}

View file

@ -46,7 +46,7 @@ fn when_history_has_expired_then_message_is_passed() {
#[test]
fn when_history_has_unexpired_then_message_is_blocked() {
let dur = Duration::from_millis(1);
let dur = Duration::from_secs(1);
let mut history = History::new(dur);
let user_notification = UserNotification::RepoConfigLoadFailure {

View file

@ -1,5 +1,6 @@
//
use git_next_core::{git::UserNotification, server::OutboundWebhook};
use kxio::network::{NetRequest, NetUrl, RequestBody, ResponseType};
use secrecy::ExposeSecret as _;
use standardwebhooks::Webhook;
@ -35,7 +36,7 @@ async fn do_send_webhook(
.expect("signature");
tracing::info!(?signature, "");
let url = webhook_config.url();
use kxio::network::{NetRequest, NetUrl, RequestBody, ResponseType};
let net_url = NetUrl::new(url.to_string());
let request = NetRequest::post(net_url)
.body(RequestBody::Json(payload))

View file

@ -3,13 +3,17 @@ use actix::prelude::*;
use actix::Recipient;
use anyhow::{Context, Result};
use notify::event::ModifyKind;
use notify::Watcher;
use tracing::error;
use tracing::info;
use notify::{event::ModifyKind, Watcher};
use tracing::{error, info};
use std::path::PathBuf;
use std::time::Duration;
use std::{
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
time::Duration,
};
#[derive(Debug, Message)]
#[rtype(result = "()")]
@ -20,21 +24,26 @@ pub enum Error {
#[error("io")]
Io(#[from] std::io::Error),
}
pub async fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<()> {
pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Arc<AtomicBool>> {
let (tx, rx) = std::sync::mpsc::channel();
let shutdown = Arc::new(AtomicBool::default());
let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
handler
.watch(&path, notify::RecursiveMode::NonRecursive)
.with_context(|| format!("Watch file: {path:?}"))?;
info!("Watching: {:?}", path);
async move {
.with_context(|| format!("Watching: {path:?}"))?;
let thread_shutdown = shutdown.clone();
actix_rt::task::spawn_blocking(move || {
loop {
if thread_shutdown.load(Ordering::Relaxed) {
drop(handler);
break;
}
for result in rx.try_iter() {
match result {
Ok(event) => match event.kind {
notify::EventKind::Modify(ModifyKind::Data(_)) => {
tracing::info!("File modified");
info!("File modified");
recipient.do_send(FileUpdated);
break;
}
@ -50,9 +59,8 @@ pub async fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Res
}
}
}
actix_rt::time::sleep(Duration::from_millis(1000)).await;
std::thread::sleep(Duration::from_millis(1000));
}
}
.await;
Ok(())
});
Ok(shutdown)
}

View file

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

View file

@ -1,4 +1,5 @@
//
#[cfg(any(feature = "forgejo", feature = "github"))]
use super::*;
use git_next_core::{
@ -7,31 +8,30 @@ use git_next_core::{
GitDir, RepoConfigSource, StoragePathType,
};
#[cfg(feature = "forgejo")]
#[test]
fn test_forgejo_name() {
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);
assert_eq!(forge.name(), "forgejo");
}
#[cfg(feature = "github")]
#[test]
fn test_github_name() {
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);
assert_eq!(forge.name(), "github");
}
fn given_fs() -> kxio::fs::FileSystem {
kxio::fs::temp().unwrap_or_else(|e| {
#[allow(dead_code)]
fn given_repo_details(forge_type: git_next_core::ForgeType) -> RepoDetails {
let fs = kxio::fs::temp().unwrap_or_else(|e| {
println!("{e}");
panic!("fs")
})
}
fn given_repo_details(forge_type: ForgeType) -> RepoDetails {
let fs = given_fs();
});
git::repo_details(
1,
git::Generation::default(),

View file

@ -1,8 +1,8 @@
//
use anyhow::{Context, Result};
use color_eyre::{eyre::Context, Result};
use kxio::fs::FileSystem;
pub fn run(fs: FileSystem) -> Result<()> {
pub fn run(fs: &FileSystem) -> Result<()> {
let pathbuf = fs.base().join(".git-next.toml");
if fs
.path_exists(&pathbuf)

View file

@ -1,21 +1,26 @@
//
#![allow(clippy::module_name_repetitions)]
mod alerts;
mod file_watcher;
mod forge;
mod init;
mod repo;
mod server;
mod webhook;
#[cfg(feature = "tui")]
mod tui;
#[cfg(test)]
mod tests;
mod webhook;
use git_next_core::git;
use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use color_eyre::Result;
use kxio::{fs, network::Network};
#[derive(Parser, Debug)]
@ -33,7 +38,12 @@ enum Command {
#[derive(Parser, Debug)]
enum Server {
Init,
Start,
Start {
/// Display a UI (experimental)
#[cfg(feature = "tui")]
#[arg(long, required = false)]
ui: bool,
},
}
fn main() -> Result<()> {
@ -43,18 +53,25 @@ fn main() -> Result<()> {
let commands = Commands::parse();
match commands.command {
Command::Init => {
init::run(fs)?;
}
Command::Init => init::run(&fs),
Command::Server(server) => match server {
Server::Init => {
server::init(fs)?;
}
Server::Start => {
let sleep_duration = std::time::Duration::from_secs(10);
server::start(fs, net, repository_factory, sleep_duration)?;
}
Server::Init => server::init(&fs),
#[cfg(not(feature = "tui"))]
Server::Start {} => server::start(
false,
fs,
net,
repository_factory,
std::time::Duration::from_secs(10),
),
#[cfg(feature = "tui")]
Server::Start { ui } => server::start(
ui,
fs,
net,
repository_factory,
std::time::Duration::from_secs(10),
),
},
}
Ok(())
}

View file

@ -17,21 +17,19 @@ use tracing::{info, instrument, warn};
// advance next to the next commit towards the head of the dev branch
#[instrument(fields(next), skip_all)]
pub fn advance_next(
next: &Commit,
main: &Commit,
dev_commit_history: &[Commit],
repo_details: RepoDetails,
commit: Option<Commit>,
force: git_next_core::git::push::Force,
repo_details: &RepoDetails,
repo_config: RepoConfig,
open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken,
) -> Result<MessageToken> {
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
let commit = commit.ok_or_else(|| Error::NextAtDev)?;
validate_commit_message(commit.message())?;
info!("Advancing next to commit '{}'", commit);
reset(
open_repository,
&repo_details,
repo_details,
&repo_config.branches().next(),
&commit.into(),
&force,
@ -66,7 +64,7 @@ pub fn find_next_commit_on_dev(
) -> (Option<Commit>, Force) {
let mut next_commit: Option<&Commit> = None;
let mut force = Force::No;
for commit in dev_commit_history.iter() {
for commit in dev_commit_history {
if commit == next {
break;
};

View file

@ -1,15 +1,18 @@
//
use actix::prelude::*;
use git_next_core::RepoConfigSource;
use git_next_core::{git, RepoConfigSource};
use tracing::warn;
use crate::repo::{
branch::advance_main,
do_send,
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
RepoActor,
use crate::{
repo::{
branch::advance_main,
do_send,
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<AdvanceMain> for RepoActor {
@ -26,24 +29,31 @@ impl Handler<AdvanceMain> for RepoActor {
let repo_details = self.repo_details.clone();
let addr = ctx.address();
let message_token = self.message_token;
let commit = msg.peel();
match advance_main(
msg.unwrap(),
&repo_details,
&repo_config,
&**open_repository,
) {
Err(err) => {
warn!("advance main: {err}");
self.update_tui(RepoUpdate::AdvancingMain {
commit: commit.clone(),
});
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
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 => {
do_send(addr, LoadConfigFromRepo, self.log.as_ref());
do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
}
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,13 +1,17 @@
//
use actix::prelude::*;
use tracing::warn;
use git_next_core::git;
use tracing::{warn, Instrument};
use crate::repo::{
branch::advance_next,
do_send,
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
RepoActor,
use crate::{
repo::{
branch::{advance_next, find_next_commit_on_dev},
do_send,
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<AdvanceNext> for RepoActor {
@ -20,30 +24,52 @@ impl Handler<AdvanceNext> for RepoActor {
let Some(open_repository) = &self.open_repository else {
return;
};
let AdvanceNextPayload {
next,
main,
dev_commit_history,
} = msg.unwrap();
} = msg.peel();
let repo_details = self.repo_details.clone();
let repo_config = repo_config.clone();
let addr = ctx.address();
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
if let Some(commit) = &commit {
self.update_tui(RepoUpdate::AdvancingNext {
commit: commit.clone(),
force: force.clone(),
});
};
match advance_next(
&next,
&main,
&dev_commit_history,
repo_details,
commit,
force,
&repo_details,
repo_config,
&**open_repository,
self.message_token,
) {
Ok(message_token) => {
// pause to allow any CI checks to be started
std::thread::sleep(self.sleep_duration);
do_send(addr, ValidateRepo::new(message_token), self.log.as_ref());
self.update_tui(RepoUpdate::NextUpdated);
match open_repository.fetch() {
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

@ -3,10 +3,13 @@ use actix::prelude::*;
use tracing::{debug, Instrument as _};
use crate::repo::{
do_send,
messages::{CheckCIStatus, ReceiveCIStatus},
RepoActor,
use crate::{
repo::{
do_send,
messages::{CheckCIStatus, ReceiveCIStatus},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<CheckCIStatus> for RepoActor {
@ -14,16 +17,18 @@ impl Handler<CheckCIStatus> for RepoActor {
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
let addr = ctx.address();
let forge = self.forge.duplicate();
let next = msg.unwrap();
let next = msg.peel();
let log = self.log.clone();
self.update_tui(RepoUpdate::CheckingCI);
// get the status - pass, fail, pending (all others map to fail, e.g. error)
async move {
let status = forge.commit_status(&next).await;
debug!("got status: {status:?}");
do_send(addr, ReceiveCIStatus::new((next, status)), log.as_ref());
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
}
.in_current_span()
.into_actor(self)

View file

@ -4,10 +4,13 @@ use actix::prelude::*;
use git_next_core::git;
use tracing::{debug, instrument, warn};
use crate::repo::{
do_send, logger,
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
RepoActor,
use crate::{
repo::{
do_send, logger,
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<CloneRepo> for RepoActor {
@ -15,21 +18,24 @@ impl Handler<CloneRepo> for RepoActor {
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
logger(self.log.as_ref(), "Handler: CloneRepo: start");
self.update_tui(RepoUpdate::Opening);
debug!("Handler: CloneRepo: start");
match git::repository::open(&*self.repository_factory, &self.repo_details) {
Ok(repository) => {
logger(self.log.as_ref(), "open okay");
debug!("open okay");
self.update_tui(RepoUpdate::Opened);
self.open_repository.replace(repository);
if self.repo_details.repo_config.is_none() {
do_send(ctx.address(), LoadConfigFromRepo, self.log.as_ref());
do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref());
} else {
do_send(ctx.address(), RegisterWebhook::new(), self.log.as_ref());
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
}
}
Err(err) => {
logger(self.log.as_ref(), "open failed");
warn!("Could not open repo: {err:?}")
warn!("Could not open repo: {err:?}");
self.alert_tui(err.to_string());
}
}
debug!("Handler: CloneRepo: finish");

View file

@ -5,10 +5,13 @@ use git_next_core::git::UserNotification;
use tracing::{debug, instrument, Instrument as _};
use crate::repo::{
do_send, load,
messages::{LoadConfigFromRepo, ReceiveRepoConfig},
notify_user, RepoActor,
use crate::{
repo::{
do_send, load,
messages::{LoadConfigFromRepo, ReceiveRepoConfig},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<LoadConfigFromRepo> for RepoActor {
@ -16,6 +19,7 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
debug!("Handler: LoadConfigFromRepo: start");
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
let Some(open_repository) = &self.open_repository else {
return;
};
@ -28,7 +32,9 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
let log = self.log.clone();
async move {
match load::config_from_repository(repo_details, &*open_repository).await {
Ok(repo_config) => do_send(addr, ReceiveRepoConfig::new(repo_config), log.as_ref()),
Ok(repo_config) => {
do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref());
}
Err(err) => notify_user(
notify_user_recipient.as_ref(),
UserNotification::RepoConfigLoadFailure {

View file

@ -1,54 +1,74 @@
//
use actix::prelude::*;
use git_next_core::git::{forge::commit::Status, UserNotification};
use tracing::debug;
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
use tracing::{debug, Instrument};
use crate::repo::{
delay_send, do_send, logger,
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
notify_user, RepoActor,
use crate::{
repo::{
do_send, logger,
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<ReceiveCIStatus> for RepoActor {
type Result = ();
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
let log = self.log.clone();
logger(log.as_ref(), "start: ReceiveCIStatus");
logger(self.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 (next, status) = msg.unwrap();
let forge_alias = self.repo_details.forge.forge_alias().clone();
let repo_alias = self.repo_details.repo_alias.clone();
let message_token = self.message_token;
let sleep_duration = self.sleep_duration;
debug!(?status, "");
match status {
Status::Pass => {
do_send(addr, AdvanceMain::new(next), self.log.as_ref());
do_send(&addr, AdvanceMain::new(next), self.log.as_ref());
}
Status::Pending => {
std::thread::sleep(sleep_duration);
do_send(addr, ValidateRepo::new(message_token), self.log.as_ref());
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);
}
Status::Fail => {
tracing::warn!("Checks have failed");
notify_user(
self.notify_user_recipient.as_ref(),
UserNotification::CICheckFailed {
forge_alias,
repo_alias,
commit: next,
log: graph_log,
},
log.as_ref(),
);
delay_send(
addr,
sleep_duration,
ValidateRepo::new(message_token),
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

@ -2,19 +2,25 @@
use actix::prelude::*;
use tracing::instrument;
use crate::repo::{
do_send,
messages::{ReceiveRepoConfig, RegisterWebhook},
RepoActor,
use crate::{
repo::{
do_send,
messages::{ReceiveRepoConfig, RegisterWebhook},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<ReceiveRepoConfig> for RepoActor {
type Result = ();
#[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 {
let repo_config = msg.unwrap();
let repo_config = msg.peel();
self.update_tui(RepoUpdate::ReceiveRepoConfig {
repo_config: repo_config.clone(),
});
self.repo_details.repo_config.replace(repo_config);
do_send(ctx.address(), RegisterWebhook::new(), self.log.as_ref());
self.update_tui_branches();
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
}
}

View file

@ -1,12 +1,15 @@
//
use actix::prelude::*;
use tracing::{debug, Instrument as _};
use tracing::{debug, error, Instrument as _};
use crate::repo::{
do_send,
messages::{RegisterWebhook, WebhookRegistered},
notify_user, RepoActor,
use crate::{
repo::{
do_send,
messages::{RegisterWebhook, WebhookRegistered},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
};
use git_next_core::git::UserNotification;
@ -25,18 +28,20 @@ impl Handler<RegisterWebhook> for RepoActor {
let addr = ctx.address();
let notify_user_recipient = self.notify_user_recipient.clone();
let log = self.log.clone();
self.update_tui(RepoUpdate::RegisteringWebhook);
debug!("registering webhook");
async move {
match forge.register_webhook(&repo_listen_url).await {
Ok(registered_webhook) => {
debug!(?registered_webhook, "");
debug!(?registered_webhook, "webhook registered");
do_send(
addr,
&addr,
WebhookRegistered::from(registered_webhook),
log.as_ref(),
);
}
Err(err) => {
error!(?err, "failed to register webhook");
notify_user(
notify_user_recipient.as_ref(),
UserNotification::WebhookRegistration {
@ -52,6 +57,8 @@ impl Handler<RegisterWebhook> for RepoActor {
.in_current_span()
.into_actor(self)
.wait(ctx);
} else {
self.alert_tui("already have a webhook id - cant register webhook");
}
}
}

View file

@ -3,25 +3,30 @@ use actix::prelude::*;
use tracing::{debug, warn, Instrument as _};
use crate::repo::{messages::UnRegisterWebhook, RepoActor};
use crate::{
repo::{messages::UnRegisterWebhook, RepoActor},
server::actor::messages::RepoUpdate,
};
impl Handler<UnRegisterWebhook> for RepoActor {
type Result = ();
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
if let Some(webhook_id) = self.webhook_id.take() {
let forge = self.forge.duplicate();
debug!("unregistering webhook");
async move {
match forge.unregister_webhook(&webhook_id).await {
Ok(_) => debug!("unregistered webhook"),
Err(err) => warn!(?err, "unregistering webhook"),
}
let Some(webhook_id) = self.webhook_id.take() else {
return;
};
self.update_tui(RepoUpdate::UnregisteringWebhook);
let forge = self.forge.duplicate();
debug!("unregistering webhook");
async move {
match forge.unregister_webhook(&webhook_id).await {
Ok(()) => debug!("unregistered webhook"),
Err(err) => warn!(?err, "unregistering webhook"),
}
.in_current_span()
.into_actor(self)
.wait(ctx);
debug!("unregistering webhook done");
}
.in_current_span()
.into_actor(self)
.wait(ctx);
debug!("unregistering webhook done");
}
}

View file

@ -1,26 +1,32 @@
//
use actix::prelude::*;
use derive_more::Deref as _;
use tracing::{debug, instrument, Instrument as _};
use tracing::{info, instrument, Instrument as _};
use crate::repo::{
do_send, logger,
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
notify_user, RepoActor,
use crate::{
repo::{
do_send, logger,
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
notify_user, RepoActor,
},
server::actor::messages::RepoUpdate,
};
use git_next_core::git::validation::positions::{validate_positions, Error, Positions};
use git_next_core::git::{
push::Force,
validation::positions::{validate, Error, Positions},
UserNotification,
};
impl Handler<ValidateRepo> for RepoActor {
type Result = ();
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %msg.deref()))]
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))]
fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
logger(self.log.as_ref(), "start: ValidateRepo");
// 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::Expired => {
logger(
@ -42,66 +48,94 @@ impl Handler<ValidateRepo> for RepoActor {
format!("accepted token: {}", self.message_token),
);
self.update_tui(RepoUpdate::ValidateRepo);
// Repository positions
let Some(ref open_repository) = self.open_repository else {
logger(self.log.as_ref(), "no open repository");
self.alert_tui("repo not open");
return;
};
logger(self.log.as_ref(), "have open repository");
let Some(repo_config) = self.repo_details.repo_config.clone() else {
logger(self.log.as_ref(), "no repo config");
self.alert_tui("no repo config");
return;
};
logger(self.log.as_ref(), "have repo config");
match validate_positions(&**open_repository, &self.repo_details, repo_config) {
Ok(Positions {
main,
next,
dev,
dev_commit_history,
next_is_valid,
}) => {
debug!(%main, %next, %dev, "positions");
match validate(&**open_repository, &self.repo_details, &repo_config) {
Ok((
Positions {
main,
next,
dev,
dev_commit_history,
next_is_valid,
},
git_log,
)) => {
info!(%main, %next, %dev, "positions");
self.update_tui_log(git_log);
if next_is_valid && next != main {
do_send(ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
info!("Checking CI");
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
} else if next != dev {
info!("Advance next");
self.update_tui(RepoUpdate::AdvancingNext {
commit: next.clone(),
force: Force::No,
});
do_send(
ctx.address(),
&ctx.address(),
AdvanceNext::new(AdvanceNextPayload {
next,
main,
dev_commit_history,
}),
self.log.as_ref(),
)
);
} else {
// do nothing
info!("do nothing");
self.update_tui(RepoUpdate::Okay { main, next, dev });
}
}
Err(Error::Retryable(message)) => {
info!(?message, "Retryable");
self.alert_tui(format!("retryable: {message}"));
logger(self.log.as_ref(), message);
let addr = ctx.address();
let message_token = self.message_token;
let sleep_duration = self.sleep_duration;
let log = self.log.clone();
async move {
debug!("sleeping before retrying...");
info!("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());
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
}
.in_current_span()
.into_actor(self)
.wait(ctx);
}
Err(Error::UserIntervention(user_notification)) => notify_user(
self.notify_user_recipient.as_ref(),
user_notification,
self.log.as_ref(),
),
Err(Error::UserIntervention(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(
self.notify_user_recipient.as_ref(),
user_notification,
self.log.as_ref(),
);
}
Err(Error::NonRetryable(message)) => {
info!(?message, "NonRetryable");
self.alert_tui(format!("Error: {message}"));
logger(self.log.as_ref(), message);
}
}

View file

@ -3,10 +3,13 @@ use actix::prelude::*;
use tracing::{info, instrument, warn};
use crate::repo::{
do_send, logger,
messages::{ValidateRepo, WebhookNotification},
RepoActor, RepoActorLog,
use crate::{
repo::{
do_send, logger,
messages::{ValidateRepo, WebhookNotification},
ActorLog, RepoActor,
},
server::actor::messages::RepoUpdate,
};
use git_next_core::{
@ -52,9 +55,13 @@ impl Handler<WebhookNotification> for RepoActor {
return;
}
Some(Branch::Main) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Main,
push: push.clone(),
});
if handle_push(
push,
config.branches().main(),
&config.branches().main(),
&mut self.last_main_commit,
self.log.as_ref(),
)
@ -64,9 +71,13 @@ impl Handler<WebhookNotification> for RepoActor {
};
}
Some(Branch::Next) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Next,
push: push.clone(),
});
if handle_push(
push,
config.branches().next(),
&config.branches().next(),
&mut self.last_next_commit,
self.log.as_ref(),
)
@ -76,9 +87,13 @@ impl Handler<WebhookNotification> for RepoActor {
};
}
Some(Branch::Dev) => {
self.update_tui(RepoUpdate::WebhookReceived {
branch: Branch::Dev,
push: push.clone(),
});
if handle_push(
push,
config.branches().dev(),
&config.branches().dev(),
&mut self.last_dev_commit,
self.log.as_ref(),
)
@ -95,7 +110,7 @@ impl Handler<WebhookNotification> for RepoActor {
"New commit"
);
do_send(
ctx.address(),
&ctx.address(),
ValidateRepo::new(message_token),
self.log.as_ref(),
);
@ -106,7 +121,7 @@ fn validate_notification(
msg: &WebhookNotification,
webhook_auth: Option<&WebhookAuth>,
forge: &dyn ForgeLike,
log: Option<&RepoActorLog>,
log: Option<&ActorLog>,
) -> Result<(), ()> {
let Some(expected_authorization) = webhook_auth else {
logger(log, "server has no auth token");
@ -131,11 +146,11 @@ fn validate_notification(
fn handle_push(
push: Push,
branch: BranchName,
branch: &BranchName,
last_commit: &mut Option<Commit>,
log: Option<&RepoActorLog>,
log: Option<&ActorLog>,
) -> Result<(), ()> {
logger(log, "message is for dev branch");
logger(log, format!("message is for {branch} branch"));
let commit = Commit::from(push);
if last_commit.as_ref() == Some(&commit) {
logger(log, format!("not a new commit on {branch}"));

View file

@ -2,20 +2,24 @@
use actix::prelude::*;
use tracing::instrument;
use crate::repo::{
do_send,
messages::{ValidateRepo, WebhookRegistered},
RepoActor,
use crate::{
repo::{
do_send,
messages::{ValidateRepo, WebhookRegistered},
RepoActor,
},
server::actor::messages::RepoUpdate,
};
impl Handler<WebhookRegistered> for RepoActor {
type Result = ();
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result {
self.update_tui(RepoUpdate::RegisteredWebhook);
self.webhook_id.replace(msg.webhook_id().clone());
self.webhook_auth.replace(msg.webhook_auth().clone());
do_send(
ctx.address(),
&ctx.address(),
ValidateRepo::new(self.message_token),
self.log.as_ref(),
);

View file

@ -6,22 +6,37 @@ use git_next_core::{
message, newtype, ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId,
};
message!(LoadConfigFromRepo: "Request to load the `git-next.toml` from the git repo.");
message!(CloneRepo: "Request to clone (or open) the git repo.");
message!(ReceiveRepoConfig: RepoConfig: r#"Notification that the `git-next.toml` file has been loaded from the repo and parsed.
message!(
LoadConfigFromRepo,
"Request to load the `git-next.toml` from the git repo."
);
message!(CloneRepo, "Request to clone (or open) the git repo.");
message!(
ReceiveRepoConfig,
RepoConfig,
r#"Notification that the `git-next.toml` file has been loaded from the repo and parsed.
Contains the parsed contents of the `git-next.toml` file."#);
message!(ValidateRepo: MessageToken: r#"Request that the state of the branches in the git repo be assessed and generate any followup actions.
Contains the parsed contents of the `git-next.toml` file."#
);
message!(
ValidateRepo,
MessageToken,
r#"Request that the state of the branches in the git repo be assessed and generate any followup actions.
This is the main function of `git-next` where decisions are made on what branches need to be updated and when.
Contains a [MessageToken] to reduce duplicate messages being sent. Only messages with the latest [MessageToken] are handled,
all others are dropped."#);
all others are dropped."#
);
message!(WebhookRegistered: (WebhookId, WebhookAuth): r#"Notification that a webhook has been registered with a forge.
message!(
WebhookRegistered,
(WebhookId, WebhookAuth),
r#"Notification that a webhook has been registered with a forge.
Contains a tuple of the ID for the webhook returned from the forge, and the unique authorisation token that
incoming messages from the forge must provide."#);
incoming messages from the forge must provide."#
);
impl WebhookRegistered {
pub const fn webhook_id(&self) -> &WebhookId {
&self.0 .0
@ -38,28 +53,51 @@ impl From<RegisteredWebhook> for WebhookRegistered {
}
}
message!(UnRegisterWebhook: "Request that the webhook be removed from the forge, so they will stop notifying us.");
message!(
UnRegisterWebhook,
"Request that the webhook be removed from the forge, so they will stop notifying us."
);
newtype!(MessageToken: u32, Copy, Default, Display, PartialOrd, Ord: r#"An incremental token used to identify the current set of messages.
newtype!(
MessageToken,
u32,
Copy,
Default,
Display,
PartialOrd,
Ord,
r#"An incremental token used to identify the current set of messages.
Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is
received, marking that message the latest, and causing any previous messages still being processed to be dropped when
they next send a [ValidateRepo] message."#);
they next send a [ValidateRepo] message."#
);
impl MessageToken {
pub const fn next(&self) -> Self {
pub const fn next(self) -> Self {
Self(self.0 + 1)
}
}
message!(RegisterWebhook: "Requests that a Webhook be registered with the forge.");
message!(CheckCIStatus: Commit: r#"Requests that the CI status for the commit be checked.
message!(
RegisterWebhook,
"Requests that a Webhook be registered with the forge."
);
message!(
CheckCIStatus,
Commit,
r#"Requests that the CI status for the commit be checked.
Once the CI Status has been received it will be sent via a [ReceiveCIStatus] message.
Contains the commit from the tip of the `next` branch."#); // next commit
message!(ReceiveCIStatus: (Commit, Status): r#"Notification of the status of the CI checks for the commit.
Contains the commit from the tip of the `next` branch."#
); // next commit
message!(
ReceiveCIStatus,
(Commit, Status),
r#"Notification of the status of the CI checks for the commit.
Contains a tuple of the commit that was checked (the tip of the `next` branch) and the status."#); // commit and it's status
Contains a tuple of the commit that was checked (the tip of the `next` branch) and the status."#
); // commit and it's status
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AdvanceNextPayload {
@ -67,7 +105,23 @@ pub struct AdvanceNextPayload {
pub main: Commit,
pub dev_commit_history: Vec<Commit>,
}
message!(AdvanceNext: AdvanceNextPayload: "Request to advance the `next` branch on to the next commit on the `dev branch."); // next commit and the dev commit history
message!(AdvanceMain: Commit: "Request to advance the `main` branch on to same commit as the `next` branch."); // next commit
message!(WebhookNotification: ForgeNotification: "Notification of a webhook message from the forge.");
message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook");
message!(
AdvanceNext,
AdvanceNextPayload,
"Request to advance the `next` branch on to the next commit on the `dev branch."
); // next commit and the dev commit history
message!(
AdvanceMain,
Commit,
"Request to advance the `main` branch on to same commit as the `next` branch."
); // next commit
message!(
WebhookNotification,
ForgeNotification,
"Notification of a webhook message from the forge."
);
message!(
NotifyUser,
UserNotification,
"Request to send the message payload to the notification webhook"
);

View file

@ -1,11 +1,13 @@
//
use actix::prelude::*;
use crate::alerts::messages::NotifyUser;
use crate::{
alerts::messages::NotifyUser,
server::{actor::messages::RepoUpdate, ServerActor},
};
use derive_more::Deref;
use kxio::network::Network;
use std::time::Duration;
use tracing::{info, warn, Instrument};
use tracing::{info, instrument, warn, Instrument};
use git_next_core::{
git::{
@ -27,8 +29,8 @@ mod notifications;
pub mod tests;
#[derive(Clone, Debug, Default)]
pub struct RepoActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
impl Deref for RepoActorLog {
pub struct ActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
impl Deref for ActorLog {
type Target = std::sync::Arc<std::sync::RwLock<Vec<String>>>;
fn deref(&self) -> &Self::Target {
@ -38,7 +40,8 @@ impl Deref for RepoActorLog {
/// An actor that represents a Git Repository.
///
/// When this actor is started it is sent the [CloneRepo] message.
/// When this actor is started it is sent the `CloneRepo` message.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug, derive_more::Display, derive_with::With)]
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
pub struct RepoActor {
@ -56,8 +59,9 @@ pub struct RepoActor {
open_repository: Option<Box<dyn OpenRepositoryLike>>,
net: Network,
forge: Box<dyn git::ForgeLike>,
log: Option<RepoActorLog>,
log: Option<ActorLog>,
notify_user_recipient: Option<Recipient<NotifyUser>>,
server_addr: Option<Addr<ServerActor>>,
}
impl RepoActor {
#[allow(clippy::too_many_arguments)]
@ -70,6 +74,7 @@ impl RepoActor {
repository_factory: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration,
notify_user_recipient: Option<Recipient<NotifyUser>>,
server_addr: Option<Addr<ServerActor>>,
) -> Self {
let message_token = messages::MessageToken::default();
Self {
@ -89,12 +94,56 @@ impl RepoActor {
sleep_duration,
log: None,
notify_user_recipient,
server_addr,
}
}
fn update_tui_branches(&self) {
if cfg!(feature = "tui") {
use crate::server::actor::messages::RepoUpdate;
let Some(repo_config) = &self.repo_details.repo_config else {
return;
};
let branches = repo_config.branches().clone();
self.update_tui(RepoUpdate::Branches { branches });
}
}
#[allow(unused_variables)]
fn update_tui_log(&self, log: git::graph::Log) {
if cfg!(feature = "tui") {
self.update_tui(RepoUpdate::Log { log });
}
}
#[allow(unused_variables)]
fn alert_tui(&self, alert: impl Into<String>) {
if cfg!(feature = "tui") {
self.update_tui(RepoUpdate::Alert {
alert: alert.into(),
});
}
}
#[allow(unused_variables)]
fn update_tui(&self, repo_update: RepoUpdate) {
if cfg!(feature = "tui") {
let Some(server_addr) = &self.server_addr else {
return;
};
let update = crate::server::actor::messages::ServerUpdate::RepoUpdate {
forge_alias: self.repo_details.forge.forge_alias().clone(),
repo_alias: self.repo_details.repo_alias.clone(),
repo_update,
};
server_addr.do_send(update);
}
}
}
impl Actor for RepoActor {
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 {
tracing::debug!("stopping");
info!("Checking webhook");
@ -118,33 +167,22 @@ impl Actor for RepoActor {
}
}
pub fn delay_send<M>(addr: Addr<RepoActor>, delay: Duration, msg: M, log: Option<&RepoActorLog>)
pub fn do_send<M>(addr: &Addr<RepoActor>, 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);
let log_message = format!("send: {msg:?}");
info!(log_message);
logger(log, log_message);
std::thread::sleep(delay);
do_send(addr, msg, log)
if cfg!(not(test)) {
// #[cfg(not(test))]
addr.do_send(msg);
}
}
pub fn do_send<M>(_addr: Addr<RepoActor>, msg: M, log: Option<&RepoActorLog>)
where
M: actix::Message + Send + 'static + std::fmt::Debug,
RepoActor: actix::Handler<M>,
<M as actix::Message>::Result: Send,
{
let log_message = format!("send: {:?}", msg);
tracing::debug!(log_message);
logger(log, log_message);
#[cfg(not(test))]
_addr.do_send(msg)
}
pub fn logger(log: Option<&RepoActorLog>, message: impl Into<String>) {
pub fn logger(log: Option<&ActorLog>, message: impl Into<String>) {
if let Some(log) = log {
let message: String = message.into();
tracing::debug!(message);
@ -154,10 +192,10 @@ pub fn logger(log: Option<&RepoActorLog>, message: impl Into<String>) {
pub fn notify_user(
recipient: Option<&Recipient<NotifyUser>>,
user_notification: UserNotification,
log: Option<&RepoActorLog>,
log: Option<&ActorLog>,
) {
let msg = NotifyUser::from(user_notification);
let log_message = format!("send: {:?}", msg);
let log_message = format!("send: {msg:?}");
tracing::debug!(log_message);
logger(log, log_message);
if let Some(recipient) = &recipient {

View file

@ -1,5 +1,4 @@
//
use derive_more::Deref as _;
use crate::repo::messages::NotifyUser;
@ -10,11 +9,12 @@ use serde_json::json;
impl NotifyUser {
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
let timestamp = timestamp.unix_timestamp().to_string();
match self.deref() {
match &**self {
UserNotification::CICheckFailed {
forge_alias,
repo_alias,
commit,
log,
} => json!({
"type": "cicheck.failed",
"timestamp": timestamp,
@ -24,7 +24,8 @@ impl NotifyUser {
"commit": {
"sha": commit.sha(),
"message": commit.message()
}
},
"log": **log
}
}),
UserNotification::RepoConfigLoadFailure {
@ -60,6 +61,7 @@ impl NotifyUser {
main_branch,
dev_commit,
main_commit,
log,
} => json!({
"type": "branch.dev.not-on-main",
"timestamp": timestamp,
@ -79,7 +81,8 @@ impl NotifyUser {
"sha": main_commit.sha(),
"message": main_commit.message()
}
}
},
"log": **log
}
}),
}

View file

@ -1,11 +1,34 @@
//
use crate::repo::branch::find_next_commit_on_dev;
use super::*;
fn advance_next_sut(
next: &Commit,
main: &Commit,
dev_commit_history: &[Commit],
repo_details: &RepoDetails,
repo_config: RepoConfig,
open_repository: &dyn OpenRepositoryLike,
message_token: MessageToken,
) -> branch::Result<MessageToken> {
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
branch::advance_next(
commit,
force,
repo_details,
repo_config,
open_repository,
message_token,
)
}
mod when_at_dev {
// next and dev branches are the same
use super::*;
#[test]
fn should_not_push() -> TestResult {
fn should_not_push() {
let next = given::a_commit();
let main = &next;
let dev_commit_history = &[next.clone()];
@ -15,11 +38,11 @@ mod when_at_dev {
// no on_push defined - so any call to push will cause an error
let message_token = given::a_message_token();
let_assert!(
Err(err) = branch::advance_next(
Err(err) = advance_next_sut(
&next,
main,
dev_commit_history,
repo_details,
&repo_details,
repo_config,
&open_repository,
message_token,
@ -27,7 +50,6 @@ mod when_at_dev {
);
tracing::debug!("Got: {err}");
assert!(matches!(err, branch::Error::NextAtDev));
Ok(())
}
}
@ -40,7 +62,7 @@ mod can_advance {
use super::*;
#[test]
fn should_not_push() -> TestResult {
fn should_not_push() {
let next = given::a_commit();
let main = &next;
let dev = given::a_commit_with_message("wip: test: message".to_string());
@ -51,11 +73,11 @@ mod can_advance {
// no on_push defined - so any call to push will cause an error
let message_token = given::a_message_token();
let_assert!(
Err(err) = branch::advance_next(
Err(err) = advance_next_sut(
&next,
main,
dev_commit_history,
repo_details,
&repo_details,
repo_config,
&open_repository,
message_token,
@ -63,7 +85,6 @@ mod can_advance {
);
tracing::debug!("Got: {err}");
assert!(matches!(err, branch::Error::IsWorkInProgress));
Ok(())
}
}
@ -72,7 +93,7 @@ mod can_advance {
use super::*;
#[test]
fn should_not_push_and_error() -> TestResult {
fn should_not_push_and_error() {
let next = given::a_commit();
let main = &next;
let dev = given::a_commit();
@ -83,11 +104,11 @@ mod can_advance {
// no on_push defined - so any call to push will cause an error
let message_token = given::a_message_token();
let_assert!(
Err(err) = branch::advance_next(
Err(err) = advance_next_sut(
&next,
main,
dev_commit_history,
repo_details,
&repo_details,
repo_config,
&open_repository,
message_token,
@ -99,7 +120,6 @@ mod can_advance {
branch::Error::InvalidCommitMessage{reason}
if reason == "Missing type in the commit summary, expected `type: description`"
));
Ok(())
}
}
@ -112,7 +132,7 @@ mod can_advance {
use super::*;
#[test]
fn should_error() -> TestResult {
fn should_error() {
let next = given::a_commit();
let main = &next;
let dev = given::a_commit_with_message("test: message".to_string());
@ -124,11 +144,11 @@ mod can_advance {
expect::push(&mut open_repository, Err(git::push::Error::Lock));
let message_token = given::a_message_token();
let_assert!(
Err(err) = branch::advance_next(
Err(err) = advance_next_sut(
&next,
main,
dev_commit_history,
repo_details,
&repo_details,
repo_config,
&open_repository,
message_token,
@ -136,7 +156,6 @@ mod can_advance {
);
tracing::debug!("Got: {err:?}");
assert!(matches!(err, branch::Error::Push(git::push::Error::Lock)));
Ok(())
}
}
@ -145,7 +164,7 @@ mod can_advance {
use super::*;
#[test]
fn should_ok() -> TestResult {
fn should_ok() {
let next = given::a_commit();
let main = &next;
let dev = given::a_commit_with_message("test: message".to_string());
@ -157,11 +176,11 @@ mod can_advance {
expect::push_ok(&mut open_repository);
let message_token = given::a_message_token();
let_assert!(
Ok(mt) = branch::advance_next(
Ok(mt) = advance_next_sut(
&next,
main,
dev_commit_history,
repo_details,
&repo_details,
repo_config,
&open_repository,
message_token,
@ -169,7 +188,6 @@ mod can_advance {
);
tracing::debug!("Got: {mt:?}");
assert_eq!(mt, message_token);
Ok(())
}
}
}

View file

@ -15,7 +15,7 @@ pub fn fetch(open_repository: &mut MockOpenRepositoryLike, result: Result<(), fe
}
pub fn push_ok(open_repository: &mut MockOpenRepositoryLike) {
expect::push(open_repository, Ok(()))
expect::push(open_repository, Ok(()));
}
pub fn push(

View file

@ -20,12 +20,12 @@ pub fn has_remote_defaults(
open_repository: &mut MockOpenRepositoryLike,
remotes: HashMap<Direction, Option<RemoteUrl>>,
) {
remotes.into_iter().for_each(|(direction, remote)| {
for (direction, remote) in remotes {
open_repository
.expect_find_default_remote()
.with(eq(direction))
.return_once(|_| remote);
});
}
}
pub fn a_webhook_auth() -> WebhookAuth {
@ -69,6 +69,22 @@ pub fn a_name() -> String {
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 {
WebhookId::new(a_name())
}
@ -89,7 +105,8 @@ pub fn a_forge_config() -> ForgeConfig {
a_name(),
a_name(),
a_name(),
Default::default(), // no repos
maybe_a_number(),
BTreeMap::default(), // no repos
)
}
@ -170,6 +187,7 @@ pub fn a_message_token() -> MessageToken {
MessageToken::default()
}
#[allow(clippy::unnecessary_box_returns)]
pub fn a_forge() -> Box<MockForgeLike> {
Box::new(MockForgeLike::new())
}
@ -179,10 +197,10 @@ pub fn a_repo_actor(
repository_factory: Box<dyn RepositoryFactory>,
forge: Box<dyn ForgeLike>,
net: kxio::network::Network,
) -> (RepoActor, RepoActorLog) {
) -> (RepoActor, ActorLog) {
let listen_url = given::a_listen_url();
let generation = Generation::default();
let log = RepoActorLog::default();
let log = ActorLog::default();
let actors_log = log.clone();
(
RepoActor::new(
@ -194,6 +212,7 @@ pub fn a_repo_actor(
repository_factory,
std::time::Duration::from_nanos(1),
None,
None,
)
.with_log(actors_log),
log,

View file

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

View file

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

View file

@ -31,7 +31,7 @@ async fn should_passthrough_to_receive_ci_status() -> TestResult {
log.read().map_err(|e| e.to_string()).map(|l| {
assert!(l
.iter()
.any(|message| message.contains("send: ReceiveCIStatus")))
.any(|message| message.contains("send: ReceiveCIStatus")));
})?;
Ok(())
}

View file

@ -43,6 +43,10 @@ async fn should_open() -> TestResult {
//given
let fs = given::a_filesystem();
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);
// factory opens a repository
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
let fs = given::a_filesystem();
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)]
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
let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
#[allow(clippy::unwrap_used)]
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
let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Ok(()));
given::has_remote_defaults(
&mut open_repository,
@ -158,15 +174,10 @@ async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
//given
let fs = given::a_filesystem();
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
given::has_remote_defaults(
&mut open_repository,
HashMap::from([
(Direction::Push, repo_details.remote_url()),
(Direction::Fetch, None),
]),
);
open_repository
.expect_fetch()
.times(1)
.return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
let mut repository_factory = MockRepositoryFactory::new();
expect::open_repository(&mut repository_factory, open_repository);
fs.dir_create(&repo_details.gitdir)?;

View file

@ -32,7 +32,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
Ok(())
}
#[actix::test]
#[test_log::test(actix::test)]
async fn should_register_webhook() -> TestResult {
//given
let fs = given::a_filesystem();
@ -54,10 +54,6 @@ async fn should_register_webhook() -> TestResult {
//then
tracing::debug!(?log, "");
log.read().map_err(|e| e.to_string()).map(|l| {
assert!(l
.iter()
.any(|message| message.contains("send: RegisterWebhook")))
})?;
log.require_message_containing("send: RegisterWebhook")?;
Ok(())
}

View file

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

View file

@ -31,7 +31,7 @@ async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
log.read().map_err(|e| e.to_string()).map(|l| {
assert!(l
.iter()
.any(|message| message.contains("send: WebhookRegistered")))
.any(|message| message.contains("send: WebhookRegistered")));
})?;
Ok(())
}

View file

@ -9,7 +9,7 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let (actor, log) = given::a_repo_actor(
@ -43,7 +43,7 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let (actor, log) = given::a_repo_actor(
@ -77,7 +77,7 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let mut forge = given::a_forge();
@ -115,7 +115,7 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let mut forge = given::a_forge();
@ -157,7 +157,7 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let mut forge = given::a_forge();
@ -200,7 +200,7 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let commit = given::a_commit();
@ -248,7 +248,7 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let commit = given::a_commit();
@ -297,7 +297,7 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let commit = given::a_commit();
@ -346,7 +346,7 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let commit = given::a_commit();
@ -395,7 +395,7 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let push_commit = given::a_commit();
@ -443,7 +443,7 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let push_commit = given::a_commit();
@ -491,7 +491,7 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
let forge_alias = given::a_forge_alias();
let repo_alias = given::a_repo_alias();
let headers = BTreeMap::new();
let body = Body::new("".to_string());
let body = Body::new(String::new());
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
let repository_factory = MockRepositoryFactory::new();
let push_commit = given::a_commit();

View file

@ -5,7 +5,7 @@ use crate::{
git,
repo::{
messages::{CloneRepo, MessageToken},
RepoActor, RepoActorLog,
ActorLog, RepoActor,
},
};
@ -45,7 +45,7 @@ mod handlers;
mod load;
mod when;
impl RepoActorLog {
impl ActorLog {
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult {
if self.find_in_messages(needle.as_ref())? {
error!(?self, "");
@ -78,7 +78,7 @@ impl RepoActorLog {
}
}
message!(ExamineActor => RepoActorView: "Request a view of the current state of the [RepoActor].");
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
impl Handler<ExamineActor> for RepoActor {
type Result = RepoActorView;

View file

@ -5,7 +5,7 @@ pub fn start_actor(
repository_factory: MockRepositoryFactory,
repo_details: RepoDetails,
forge: Box<dyn ForgeLike>,
) -> (actix::Addr<RepoActor>, RepoActorLog) {
) -> (actix::Addr<RepoActor>, ActorLog) {
let (actor, log) = given::a_repo_actor(
repo_details,
Box::new(repository_factory),
@ -19,7 +19,7 @@ pub fn start_actor_with_open_repository(
open_repository: Box<dyn OpenRepositoryLike>,
repo_details: RepoDetails,
forge: Box<dyn ForgeLike>,
) -> (actix::Addr<RepoActor>, RepoActorLog) {
) -> (actix::Addr<RepoActor>, ActorLog) {
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into());
let actor = actor.with_open_repository(Some(open_repository));
(actor.start(), log)

View file

@ -1,20 +1,20 @@
//
use actix::prelude::*;
use git_next_core::server::ServerConfig;
use git_next_core::server::AppConfig;
use crate::{
file_watcher::FileUpdated,
server::actor::{messages::ReceiveServerConfig, ServerActor},
server::actor::{messages::ReceiveAppConfig, ServerActor},
};
impl Handler<FileUpdated> for ServerActor {
type Result = ();
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result {
match ServerConfig::load(&self.fs) {
Ok(server_config) => self.do_send(ReceiveServerConfig::new(server_config), ctx),
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {}", err)),
match AppConfig::load(&self.fs) {
Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx),
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")),
};
}
}

View file

@ -1,4 +1,7 @@
mod file_updated;
mod receive_server_config;
mod receive_valid_server_config;
mod receive_app_config;
mod receive_valid_app_config;
mod server_update;
mod shutdown;
mod shutdown_trigger;
mod subscribe_updates;

View file

@ -1,15 +1,15 @@
use actix::prelude::*;
use crate::server::actor::{
messages::{ReceiveServerConfig, ReceiveValidServerConfig, ValidServerConfig},
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
ServerActor,
};
impl Handler<ReceiveServerConfig> for ServerActor {
impl Handler<ReceiveAppConfig> for ServerActor {
type Result = ();
#[allow(clippy::cognitive_complexity)]
fn handle(&mut self, msg: ReceiveServerConfig, ctx: &mut Self::Context) -> Self::Result {
fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result {
tracing::info!("recieved server config");
let Ok(socket_addr) = msg.listen_socket_addr() else {
return self.abort(ctx, "Unable to parse http.addr");
@ -24,8 +24,8 @@ impl Handler<ReceiveServerConfig> for ServerActor {
}
self.do_send(
ReceiveValidServerConfig::new(ValidServerConfig::new(
msg.unwrap(),
ReceiveValidAppConfig::new(ValidAppConfig::new(
msg.peel(),
socket_addr,
server_storage,
)),

View file

@ -0,0 +1,97 @@
//
use actix::prelude::*;
use git_next_core::{ForgeAlias, RepoAlias};
use tracing::info;
use crate::{
alerts::messages::UpdateShout,
repo::{messages::CloneRepo, RepoActor},
server::actor::{
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
ServerActor,
},
webhook::{
messages::ShutdownWebhook,
router::{AddWebhookRecipient, WebhookRouterActor},
WebhookActor,
},
};
impl Handler<ReceiveValidAppConfig> for ServerActor {
type Result = ();
fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result {
let ValidAppConfig {
app_config,
socket_address,
storage: server_storage,
} = msg.peel();
// shutdown any existing webhook actor
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
webhook_actor_addr.do_send(ShutdownWebhook);
}
self.generation.inc();
// Webhook Server
info!("Starting Webhook Server...");
let webhook_router = WebhookRouterActor::default().start();
let listen_url = app_config.listen().url();
let notify_user_recipient = self.alerts.clone().recipient();
let server_addr = Some(ctx.address());
// Forge Actors
for (forge_alias, forge_config) in app_config.forges() {
let repo_actors = self
.create_forge_repos(
forge_config,
forge_alias.clone(),
&server_storage,
listen_url,
&notify_user_recipient,
server_addr.clone(),
)
.into_iter()
.map(start_repo_actor)
.collect::<Vec<_>>();
repo_actors
.iter()
.map(|(repo_alias, addr)| {
AddWebhookRecipient::new(
forge_alias.clone(),
repo_alias.clone(),
addr.clone().recipient(),
)
})
.for_each(|msg| webhook_router.do_send(msg));
for (repo_alias, addr) in repo_actors {
self.repo_actors
.insert((forge_alias.clone(), repo_alias), addr);
}
}
let webhook_actor_addr =
WebhookActor::new(socket_address, webhook_router.recipient()).start();
self.webhook_actor_addr.replace(webhook_actor_addr);
let shout = app_config.shout().clone();
self.app_config.replace(app_config.clone());
self.do_send(
ServerUpdate::AppConfigLoaded {
app_config: ValidAppConfig {
app_config,
socket_address,
storage: server_storage,
},
},
ctx,
);
self.alerts.do_send(UpdateShout::new(shout));
}
}
fn start_repo_actor(actor: (ForgeAlias, RepoAlias, RepoActor)) -> (RepoAlias, Addr<RepoActor>) {
let (forge_name, repo_alias, actor) = actor;
let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias);
let _guard = span.enter();
let addr = actor.start();
addr.do_send(CloneRepo);
tracing::info!("Started");
(repo_alias, addr)
}

View file

@ -1,73 +0,0 @@
//
use actix::prelude::*;
use tracing::info;
use crate::{
alerts::messages::UpdateShout,
server::actor::{
messages::{ReceiveValidServerConfig, ValidServerConfig},
ServerActor,
},
webhook::{
messages::ShutdownWebhook,
router::{AddWebhookRecipient, WebhookRouter},
WebhookActor,
},
};
impl Handler<ReceiveValidServerConfig> for ServerActor {
type Result = ();
fn handle(&mut self, msg: ReceiveValidServerConfig, _ctx: &mut Self::Context) -> Self::Result {
let ValidServerConfig {
server_config,
socket_address,
server_storage,
} = msg.unwrap();
// shutdown any existing webhook actor
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
webhook_actor_addr.do_send(ShutdownWebhook);
}
self.generation.inc();
// Webhook Server
info!("Starting Webhook Server...");
let webhook_router = WebhookRouter::default().start();
let listen_url = server_config.listen().url();
let alerts = self.alerts.clone();
// Forge Actors
for (forge_alias, forge_config) in server_config.forges() {
let repo_actors = self
.create_forge_repos(
forge_config,
forge_alias.clone(),
&server_storage,
listen_url,
alerts.clone().recipient(),
)
.into_iter()
.map(|a| self.start_actor(a))
.collect::<Vec<_>>();
repo_actors
.iter()
.map(|(repo_alias, addr)| {
AddWebhookRecipient::new(
forge_alias.clone(),
repo_alias.clone(),
addr.clone().recipient(),
)
})
.for_each(|msg| webhook_router.do_send(msg));
repo_actors.into_iter().for_each(|(repo_alias, addr)| {
self.repo_actors
.insert((forge_alias.clone(), repo_alias), addr);
});
}
let webhook_actor_addr =
WebhookActor::new(socket_address, webhook_router.recipient()).start();
self.webhook_actor_addr.replace(webhook_actor_addr);
let shout = server_config.shout().clone();
self.server_config.replace(server_config);
self.alerts.do_send(UpdateShout::new(shout));
}
}

View file

@ -0,0 +1,14 @@
use actix::Handler;
//
use crate::server::{actor::messages::ServerUpdate, ServerActor};
impl Handler<ServerUpdate> for ServerActor {
type Result = ();
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
self.subscribers.iter().for_each(move |subscriber| {
subscriber.do_send(msg.clone());
});
}
}

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

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

View file

@ -1,27 +1,113 @@
//-
//
use actix::{Message, Recipient};
use derive_more::Constructor;
use git_next_core::{
git::{self, forge::commit::Status, graph::Log, Commit},
message,
server::{ServerConfig, ServerStorage},
server::{AppConfig, Storage},
webhook::{push::Branch, Push},
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
};
use std::net::SocketAddr;
// receive server config
message!(ReceiveServerConfig: ServerConfig: "Notification of newly loaded server configuration.
message!(
ReceiveAppConfig,
AppConfig,
"Notification of newly loaded server configuration.
This message will prompt the `git-next` server to stop and restart all repo-actors.
Contains the new server configuration.");
Contains the new server configuration."
);
// receive valid server config
#[derive(Clone, Debug, PartialEq, Eq, Constructor)]
pub struct ValidServerConfig {
pub server_config: ServerConfig,
pub struct ValidAppConfig {
pub app_config: AppConfig,
pub socket_address: SocketAddr,
pub server_storage: ServerStorage,
pub storage: Storage,
}
message!(ReceiveValidServerConfig: ValidServerConfig: "Notification of validated server configuration.");
message!(
ReceiveValidAppConfig,
ValidAppConfig,
"Notification of validated server configuration."
);
message!(Shutdown: "Notification to shutdown the server actor");
message!(Shutdown, "Notification to shutdown the server actor");
#[derive(Clone, Debug, PartialEq, Eq, Message)]
#[rtype(result = "()")]
pub enum ServerUpdate {
/// List of all configured forges and aliases
AppConfigLoaded { app_config: ValidAppConfig },
RepoUpdate {
forge_alias: ForgeAlias,
repo_alias: RepoAlias,
repo_update: RepoUpdate,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoUpdate {
Branches {
branches: RepoBranches,
},
Log {
log: Log,
},
ValidateRepo,
Okay {
main: Commit,
next: Commit,
dev: Commit,
},
Alert {
alert: String,
},
CheckingCI,
AdvancingNext {
commit: git::Commit,
force: git::push::Force,
},
AdvancingMain {
commit: git::Commit,
},
Opening,
LoadingConfigFromRepo,
ReceiveCIStatus {
status: Status,
},
ReceiveRepoConfig {
repo_config: RepoConfig,
},
RegisteringWebhook,
UnregisteringWebhook,
WebhookReceived {
branch: Branch,
push: Push,
},
RegisteredWebhook,
Opened,
NextUpdated,
MainUpdated,
}
message!(
SubscribeToUpdates,
Recipient<ServerUpdate>,
"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 messages::ReceiveServerConfig;
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
use tracing::error;
#[cfg(test)]
@ -10,16 +10,13 @@ mod handlers;
pub mod messages;
use crate::{
alerts::messages::NotifyUser,
alerts::AlertsActor,
forge::Forge,
repo::{messages::CloneRepo, RepoActor},
alerts::messages::NotifyUser, alerts::AlertsActor, forge::Forge, repo::RepoActor,
webhook::WebhookActor,
};
use git_next_core::{
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
server::{self, ListenUrl, ServerConfig, ServerStorage},
server::{self, AppConfig, ListenUrl, Storage},
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
};
@ -47,10 +44,11 @@ pub enum Error {
}
type Result<T> = core::result::Result<T, Error>;
#[allow(clippy::module_name_repetitions)]
#[derive(derive_with::With)]
#[with(message_log)]
pub struct ServerActor {
server_config: Option<ServerConfig>,
app_config: Option<AppConfig>,
generation: Generation,
webhook_actor_addr: Option<Addr<WebhookActor>>,
fs: FileSystem,
@ -60,6 +58,9 @@ pub struct ServerActor {
sleep_duration: std::time::Duration,
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>,
subscribers: Vec<Recipient<ServerUpdate>>,
// testing
message_log: Option<Arc<RwLock<Vec<String>>>>,
}
@ -77,13 +78,15 @@ impl ServerActor {
) -> Self {
let generation = Generation::default();
Self {
server_config: None,
app_config: None,
generation,
webhook_actor_addr: None,
fs,
net,
alerts,
repository_factory: repo,
shutdown_trigger: None,
subscribers: Vec::default(),
sleep_duration,
repo_actors: BTreeMap::new(),
message_log: None,
@ -91,10 +94,10 @@ impl ServerActor {
}
fn create_forge_data_directories(
&self,
server_config: &ServerConfig,
app_config: &AppConfig,
server_dir: &std::path::Path,
) -> Result<()> {
for (forge_name, _forge_config) in server_config.forges() {
for (forge_name, _forge_config) in app_config.forges() {
let forge_dir: PathBuf = (&forge_name).into();
let path = server_dir.join(&forge_dir);
if self.fs.path_exists(&path)? {
@ -114,9 +117,10 @@ impl ServerActor {
&self,
forge_config: &ForgeConfig,
forge_name: ForgeAlias,
server_storage: &ServerStorage,
server_storage: &Storage,
listen_url: &ListenUrl,
notify_user_recipient: Recipient<NotifyUser>,
notify_user_recipient: &Recipient<NotifyUser>,
server_addr: Option<Addr<Self>>,
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
let span =
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
@ -124,8 +128,13 @@ impl ServerActor {
let _guard = span.enter();
tracing::info!("Creating Forge");
let mut repos = vec![];
let creator =
self.create_actor(forge_name, forge_config.clone(), server_storage, listen_url);
let creator = self.create_actor(
forge_name,
forge_config.clone(),
server_storage,
listen_url,
server_addr,
);
for (repo_alias, server_repo_config) in forge_config.repos() {
let forge_repo = creator((
repo_alias,
@ -145,8 +154,9 @@ impl ServerActor {
&self,
forge_name: ForgeAlias,
forge_config: ForgeConfig,
server_storage: &ServerStorage,
server_storage: &Storage,
listen_url: &ListenUrl,
server_addr: Option<Addr<Self>>,
) -> impl Fn(
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
) -> (ForgeAlias, RepoAlias, RepoActor) {
@ -193,26 +203,14 @@ impl ServerActor {
repository_factory.duplicate(),
sleep_duration,
Some(notify_user_recipient),
server_addr.clone(),
);
(forge_name.clone(), repo_alias, actor)
}
}
fn start_actor(
&self,
actor: (ForgeAlias, RepoAlias, RepoActor),
) -> (RepoAlias, Addr<RepoActor>) {
let (forge_name, repo_alias, actor) = actor;
let span = tracing::info_span!("start_actor", forge = %forge_name, repo = %repo_alias);
let _guard = span.enter();
let addr = actor.start();
addr.do_send(CloneRepo);
tracing::info!("Started");
(repo_alias, addr)
}
fn server_storage(&self, server_config: &ReceiveServerConfig) -> Option<ServerStorage> {
let server_storage = server_config.storage().clone();
fn server_storage(&self, app_config: &ReceiveAppConfig) -> Option<Storage> {
let server_storage = app_config.storage().clone();
let dir = server_storage.path();
if !dir.exists() {
if let Err(err) = self.fs.dir_create(dir) {
@ -224,7 +222,7 @@ impl ServerActor {
error!(?dir, "Failed to confirm server storage");
return None;
};
if let Err(err) = self.create_forge_data_directories(server_config, &canon) {
if let Err(err) = self.create_forge_data_directories(app_config, &canon) {
error!(?err, "Failure creating forge storage");
return None;
}
@ -232,25 +230,31 @@ impl ServerActor {
}
/// Attempts to gracefully shutdown the server before stopping the system.
fn abort(&self, ctx: &mut <Self as actix::Actor>::Context, message: impl Into<String>) {
tracing::error!("Aborting: {}", message.into());
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
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: &mut <Self as actix::Actor>::Context)
fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context)
where
M: actix::Message + Send + 'static + std::fmt::Debug,
Self: actix::Handler<M>,
<M as actix::Message>::Result: Send,
{
if let Some(message_log) = &self.message_log {
let log_message = format!("send: {:?}", msg);
let log_message = format!("send: {msg:?}");
if let Ok(mut log) = message_log.write() {
log.push(log_message);
}
}
#[cfg(not(test))]
_ctx.address().do_send(msg);
if cfg!(not(test)) {
ctx.address().do_send(msg);
}
}
}

View file

@ -1,3 +1,3 @@
mod receive_server_config;
mod receive_app_config;
mod given;

View file

@ -1,10 +1,10 @@
//
use actix::prelude::*;
use crate::server::actor::{tests::given, ReceiveServerConfig, ServerActor};
use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor};
use git_next_core::{
git,
server::{Http, Listen, ListenUrl, ServerConfig, ServerStorage, Shout},
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
};
use std::{
@ -31,7 +31,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
ListenUrl::new("http://localhost/".to_string()), // with trailing slash
);
let shout = Shout::default();
let server_storage = ServerStorage::new((fs.base()).to_path_buf());
let server_storage = Storage::new((fs.base()).to_path_buf());
let repos = BTreeMap::default();
// debugging
@ -39,14 +39,12 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
let server = server.with_message_log(Some(message_log.clone()));
//when
server
.start()
.do_send(ReceiveServerConfig::new(ServerConfig::new(
listen,
shout,
server_storage,
repos,
)));
server.start().do_send(ReceiveAppConfig::new(AppConfig::new(
listen,
shout,
server_storage,
repos,
)));
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
//then

View file

@ -1,27 +1,36 @@
//
mod actor;
pub mod actor;
#[cfg(test)]
mod tests;
use actix::prelude::*;
use actix_rt::signal;
use actor::messages::ShutdownTrigger;
use crate::{
alerts::{AlertsActor, History},
file_watcher::{watch_file, FileUpdated},
};
use actor::ServerActor;
#[allow(clippy::module_name_repetitions)]
pub use actor::ServerActor;
use git_next_core::git::RepositoryFactory;
use anyhow::{Context, Result};
use color_eyre::{eyre::Context, Result};
use kxio::{fs::FileSystem, network::Network};
use tracing::info;
use std::{path::PathBuf, 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);
pub fn init(fs: FileSystem) -> Result<()> {
pub fn init(fs: &FileSystem) -> Result<()> {
let file_name = "git-next-server.toml";
let pathbuf = PathBuf::from(file_name);
if fs
@ -37,45 +46,131 @@ pub fn init(fs: FileSystem) -> Result<()> {
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn start(
ui: bool,
fs: FileSystem,
net: Network,
repo: Box<dyn RepositoryFactory>,
sleep_duration: std::time::Duration,
) -> Result<()> {
init_logging();
if ui {
#[cfg(feature = "tui")]
{
crate::tui::logging::initialize_logging()?;
}
} else {
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 {
info!("Starting Alert Dispatcher...");
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
info!("Starting Server...");
let server = ServerActor::new(
fs.clone(),
net.clone(),
alerts_addr.clone(),
repo,
sleep_duration,
)
.start();
server.do_send(FileUpdated);
let server =
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
info!("Starting File Watcher...");
#[allow(clippy::expect_used)]
watch_file("git-next-server.toml".into(), server.clone().recipient())
.await
.expect("file watcher");
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
let fw_shutdown = match watch_file {
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;
}
};
info!("Server running - Press Ctrl-C to stop...");
let _ = actix_rt::signal::ctrl_c().await;
info!("Ctrl-C received, shutting down...");
let (tx_shutdown, rx_shutdown) = channel::<String>();
if ui {
#[cfg(feature = "tui")]
{
use crate::server::actor::messages::SubscribeToUpdates;
use crate::tui;
let tui_addr = tui::Tui::new(tx_shutdown.clone()).start();
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
loop {
let _ = tui_addr.send(tui::Tick).await;
if let Ok(message) = rx_shutdown.try_recv() {
let _ = shutdown_message_holder_exec
.write()
.map(|mut o| o.replace(message));
break;
}
actix_rt::time::sleep(Duration::from_millis(16)).await;
}
}
} else {
server.do_send(ShutdownTrigger::new(tx_shutdown.clone()));
server.do_send(FileUpdated);
info!("Server running - Press Ctrl-C to stop...");
tokio::select! {
_r = signal::ctrl_c() => {
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
fw_shutdown.store(true, Ordering::Relaxed);
server.do_send(crate::server::actor::messages::Shutdown);
actix_rt::time::sleep(std::time::Duration::from_millis(200)).await;
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
System::current().stop();
};
let system = System::new();
Arbiter::current().spawn(execution);
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(())
}

View file

@ -37,7 +37,7 @@ fn gitdir_should_display_as_pathbuf() {
//given
let gitdir = GitDir::new("foo/dir".into(), StoragePathType::External);
//when
let result = format!("{}", gitdir);
let result = format!("{gitdir}");
//then
assert_eq!(result, "foo/dir");
}
@ -59,10 +59,13 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
repo_details.forge = repo_details
.forge
.with_user(User::new("git".to_string()))
.with_token(ApiToken::new(Secret::new("".to_string())))
.with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net"));
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!(
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
"Default Push Remote not found"
@ -92,13 +95,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
repo_details.forge = repo_details
.forge
.with_user(User::new("git".to_string()))
.with_token(ApiToken::new(Secret::new("".to_string())))
.with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net"));
tracing::debug!("opening...");
let_assert!(
Ok(repository) = git::repository::factory::real().open(&repo_details),
"open repository"
);
let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
// .git directory may not be present on dev environment
return Ok(());
};
tracing::debug!("open okay");
tracing::info!(?repository, "FOO");
tracing::info!(?repo_details, "BAR");
@ -108,11 +111,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
}
#[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!(
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()));
eprintln!("root: {root:?}");
let mut repo_details = git::repo_details(
1,
git::Generation::default(),
@ -124,16 +129,17 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
repo_details.forge = repo_details
.forge
.with_user(User::new("git".to_string()))
.with_token(ApiToken::new(Secret::new("".to_string())))
.with_token(ApiToken::new(Secret::new(String::new())))
.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();
repo_details.forge = repo_details
.forge
.with_hostname(Hostname::new("code.kemitix.net"));
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
Ok(())
}
#[test]

View file

@ -8,7 +8,7 @@ mod init {
let file = fs.base().join(".git-next.toml");
fs.file_write(&file, "contents")?;
crate::init::run(fs.clone())?;
crate::init::run(&fs)?;
assert_eq!(
fs.file_read_to_string(&file)?,
@ -23,7 +23,7 @@ mod init {
fn should_create_default_file_if_not_exists() -> TestResult {
let fs = kxio::fs::temp()?;
crate::init::run(fs.clone())?;
crate::init::run(&fs)?;
let file = fs.base().join(".git-next.toml");
@ -38,3 +38,44 @@ mod init {
Ok(())
}
}
mod file_watcher {
use std::{sync::atomic::Ordering, time::Duration};
use actix::{Actor, Context, Handler};
use rstest::*;
use crate::file_watcher::{self, FileUpdated};
use super::TestResult;
#[rstest]
#[actix::test]
#[timeout(Duration::from_millis(80))]
async fn should_not_block_calling_thread() -> TestResult {
let fs = kxio::fs::temp()?;
let path = fs.base().join("file");
fs.file_write(&path, "foo")?;
let listener = Listener;
let l_addr = listener.start();
let recipient = l_addr.recipient();
let fw_shutdown = file_watcher::watch_file(path, recipient)?;
std::thread::sleep(Duration::from_millis(10));
fw_shutdown.store(true, Ordering::Relaxed);
Ok(()) // was not blocked
}
struct Listener;
impl Actor for Listener {
type Context = Context<Self>;
}
impl Handler<FileUpdated> for Listener {
type Result = ();
fn handle(&mut self, _msg: FileUpdated, _ctx: &mut Self::Context) -> Self::Result {
// todo!()
}
}
}

View file

@ -0,0 +1,40 @@
# Terminal UI
Currently the Terminal UI is an experimental feature, controlled by the feature flag `tui`.
## Build & Run
The build `git-next` with the Terminal UI use: `cargo install git-next --features tui`
To run `git-next` with the Terminal UI use: `git-next server start --ui`
### Docker
If using the docker image you will need to create a directory to mount that contains the
`git-next-server.toml` file. Mount this directory as `/app`. In the example below we use
the current directory for this.
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"`).
Map the port your webhook notifications are arriving on to the port specified in `listen.http.port`.
`docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest server start --ui`
## logs
When the Terminal UI is enabled via the `--ui` parameter, logs are written to the file:
- `???` on Linux
- `~/Library/Application Support/net.kemitix.git-next/git-next.log` on MacOS
- `???` on Windows
## Keys
- `q` - Quit
- `j` - Down
- `k` - Up
- `f` - Page Down
- `b` - Page Up
- `g` - Top/Home
- `G` - Bottom/End

View file

@ -0,0 +1,3 @@
//
mod server_update;
mod tick;

View file

@ -0,0 +1,104 @@
//
use actix::Handler;
use ratatui::style::Color;
use crate::{
server::actor::messages::{RepoUpdate, ServerUpdate},
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 {
type Result = ();
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
self.state.tap();
match msg {
ServerUpdate::AppConfigLoaded { app_config } => {
self.state.mode = ServerState::from(app_config);
}
ServerUpdate::RepoUpdate {
forge_alias,
repo_alias,
repo_update,
} => {
if let ServerState::Configured { forges } = &mut self.state.mode {
let Some(forge_state) = forges.get_mut(&forge_alias) else {
return;
};
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
return;
};
repo_state.clear_alert();
match repo_update {
RepoUpdate::Branches { branches } => {
repo_state.update_branches(branches);
}
RepoUpdate::Log { log } => {
repo_state.update_log(log);
}
RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING),
RepoUpdate::Okay { main, next, dev } => {
repo_state.clear_alert();
repo_state.update_message("okay", OKAY);
*repo_state = repo_state.clone().ready(main, next, dev);
}
RepoUpdate::Alert { alert } => {
repo_state.alert(alert);
}
RepoUpdate::CheckingCI => {
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::AdvancingMain { commit } => {
repo_state
.update_message(format!("advancing main to {commit}"), ACTING);
}
RepoUpdate::MainUpdated => {
repo_state.update_message("main updated", OKAY);
}
RepoUpdate::Opening => {
repo_state.update_message("opening...", PREP);
}
RepoUpdate::Opened => {
repo_state.update_message("opened", PREP);
}
RepoUpdate::LoadingConfigFromRepo => {
repo_state.update_message("loading config from repo...", PREP);
}
RepoUpdate::ReceiveCIStatus { status } => {
repo_state.update_message(format!("ci status: {status:?}"), WARN);
}
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
repo_state.update_message("loaded config from repo", PREP);
}
RepoUpdate::RegisteringWebhook => {
repo_state.update_message("registering webhook...", PREP);
}
RepoUpdate::UnregisteringWebhook => {
repo_state.update_message("unregistering webhook...", PREP);
}
RepoUpdate::WebhookReceived { branch, push: _ } => {
repo_state
.update_message(format!("webhook update: {branch:?}"), ACTING);
}
RepoUpdate::RegisteredWebhook => {
repo_state.update_message("registered webhook", PREP);
}
}
}
}
}
}
}

View file

@ -0,0 +1,15 @@
//
use actix::Handler;
use crate::tui::actor::{messages::Tick, Tui};
impl Handler<Tick> for Tui {
type Result = std::io::Result<()>;
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
self.state.tap();
self.draw()?;
self.handle_input(ctx)?;
Ok(())
}
}

View file

@ -0,0 +1,4 @@
//
use git_next_core::message;
message!(Tick => std::io::Result<()>, "Update the TUI");

View file

@ -0,0 +1,97 @@
//
mod handlers;
pub mod messages;
mod model;
#[cfg(test)]
mod tests;
use std::sync::mpsc::Sender;
use actix::{Actor, ActorContext as _, Context};
pub use model::*;
use ratatui::{
crossterm::event::{self, KeyCode, KeyEventKind},
DefaultTerminal,
};
use tui_scrollview::ScrollViewState;
#[derive(Debug)]
pub struct Tui {
terminal: Option<DefaultTerminal>,
signal_shutdown: Sender<String>,
pub state: State,
scroll_view_state: ScrollViewState,
}
impl Actor for Tui {
type Context = Context<Self>;
fn started(&mut self, _ctx: &mut Self::Context) {
self.terminal.replace(ratatui::init());
}
fn stopped(&mut self, _ctx: &mut Self::Context) {
self.terminal.take();
ratatui::restore();
}
}
impl Tui {
pub fn new(signal_shutdown: Sender<String>) -> Self {
Self {
terminal: None,
signal_shutdown,
state: State::initial(),
scroll_view_state: ScrollViewState::default(),
}
}
fn draw(&mut self) -> std::io::Result<()> {
let t = self.terminal.take();
let scroll_view_state = &mut self.scroll_view_state;
let state = &self.state;
if let Some(mut terminal) = t {
terminal.draw(|frame| {
frame.render_stateful_widget(state, frame.area(), scroll_view_state);
})?;
self.terminal = Some(terminal);
} else {
eprintln!("No terminal setup");
}
Ok(())
}
fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
if event::poll(std::time::Duration::from_millis(16))? {
let event::Event::Key(key) = event::read()? else {
return Ok(());
};
if key.kind != KeyEventKind::Press {
return Ok(());
}
match key.code {
KeyCode::Char('q') => {
ctx.stop();
if let Err(err) = self.signal_shutdown.send(String::new()) {
tracing::error!(?err, "Failed to signal shutdown");
}
}
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(())
}
}

View file

@ -0,0 +1,396 @@
//
use ratatui::{
layout::Alignment,
prelude::{Buffer, Rect},
style::{Color, Style, Stylize as _},
symbols::border,
text::{Line, Span},
widgets::{
block::{Position, Title},
Block, Paragraph, StatefulWidget, Widget,
},
};
use git_next_core::{
git::{self, graph::Log, Commit},
ForgeAlias, RepoAlias, RepoBranches,
};
use tracing::info;
use tui_scrollview::ScrollViewState;
use std::{collections::BTreeMap, fmt::Display, time::Instant};
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct State {
last_update: Instant,
started: Instant,
pub mode: ServerState,
}
impl State {
pub fn initial() -> Self {
Self {
last_update: Instant::now(),
started: Instant::now(),
mode: ServerState::Initial { tick: 0 },
}
}
pub fn tap(&mut self) {
self.last_update = Instant::now();
if let ServerState::Initial { tick } = &mut self.mode {
*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)]
pub enum ServerState {
/// UI has started but has no information on the state of the server
Initial { tick: usize }, // NOTE: for use with throbber-widgets-tui ?
/// The application configuration has been loaded, individual forges and repos have their own
/// states
Configured {
forges: BTreeMap<ForgeAlias, ForgeState>,
},
}
impl ServerState {
pub fn update_branches(
&mut self,
forge_alias: &ForgeAlias,
repo_alias: &RepoAlias,
branches: RepoBranches,
) {
if let Self::Configured { forges } = self {
let Some(forge_state) = forges.get_mut(forge_alias) else {
return;
};
let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else {
return;
};
match repo_state {
RepoState::Configured {
branches: state_branches,
..
}
| RepoState::Ready {
branches: state_branches,
..
} => *state_branches = branches,
RepoState::Identified { .. } => (),
}
}
}
pub fn update_log(&mut self, forge_alias: &ForgeAlias, repo_alias: &RepoAlias, log: Log) {
if let Self::Configured { forges } = self {
let Some(forge_state) = forges.get_mut(forge_alias) else {
return;
};
let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else {
return;
};
match repo_state {
RepoState::Ready { log: state_log, .. } => *state_log = log,
RepoState::Identified { .. } | RepoState::Configured { .. } => (),
}
}
}
}
impl From<ValidAppConfig> for ServerState {
fn from(app_config: ValidAppConfig) -> Self {
Self::Configured {
forges: app_config
.app_config
.forges()
.map(|(forge_alias, config)| {
(
forge_alias,
config
.repos()
.map(|(repo_alias, server_repo_config)| {
(repo_alias, server_repo_config.repo_config())
})
.map(
|(repo_alias, option_repo_config)| match option_repo_config {
Some(rc) => (
repo_alias.clone(),
RepoState::Configured {
repo_alias,
message: RepoMessage::builder()
.text("configured".into())
.style(Style::default().fg(Color::LightGreen))
.build(),
alert: None,
branches: rc.branches().clone(),
log: git::graph::Log::default(),
},
),
None => (
repo_alias.clone(),
RepoState::Identified {
repo_alias,
message: RepoMessage::builder()
.text("identified".into())
.style(Style::default().fg(Color::Gray))
.build(),
alert: None,
},
),
},
)
.collect::<Vec<_>>(),
)
})
.map(|(forge_alias, vec_repo_alias_state)| {
let forge_state: ForgeState = ForgeState {
alias: forge_alias.clone(),
view_state: ViewState::default(),
repos: vec_repo_alias_state.into_iter().collect::<BTreeMap<_, _>>(),
};
(forge_alias, forge_state)
})
.collect::<BTreeMap<_, _>>(),
}
}
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum ViewState {
Collapsed,
#[default]
Expanded,
}
impl Display for ViewState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let view_state = match self {
Self::Collapsed => "+",
Self::Expanded => "-",
};
write!(f, "{view_state}")
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ForgeState {
pub alias: ForgeAlias,
pub view_state: ViewState,
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)]
pub enum RepoState {
Identified {
repo_alias: RepoAlias,
message: RepoMessage,
alert: Option<String>,
},
Configured {
repo_alias: RepoAlias,
message: RepoMessage,
alert: Option<String>,
branches: RepoBranches,
log: Log,
},
Ready {
repo_alias: RepoAlias,
message: RepoMessage,
alert: Option<String>,
branches: RepoBranches,
view_state: ViewState,
main: Commit,
next: Commit,
dev: Commit,
log: Log,
},
}
impl RepoState {
#[tracing::instrument]
pub fn update_branches(&mut self, branches: RepoBranches) {
match self {
Self::Configured {
branches: state_branches,
..
}
| Self::Ready {
branches: state_branches,
..
} => {
*state_branches = branches;
}
Self::Identified { .. } => (),
}
}
#[tracing::instrument]
pub fn update_log(&mut self, log: Log) {
match self {
Self::Configured { log: state_log, .. } | Self::Ready { log: state_log, .. } => {
*state_log = log;
}
Self::Identified { .. } => {
info!("git graph log ignored by ui");
}
}
}
#[tracing::instrument]
pub fn update_message(&mut self, msg: impl Into<String> + std::fmt::Debug, colour: Color) {
match self {
Self::Identified { message, .. }
| Self::Configured { message, .. }
| Self::Ready { message, .. } => {
info!(?msg, "updating ui");
*message = RepoMessage::builder()
.text(msg.into())
.style(Style::default().fg(colour))
.build();
}
}
}
#[tracing::instrument]
pub fn clear_alert(&mut self) {
match self {
Self::Identified { alert, .. }
| Self::Configured { alert, .. }
| Self::Ready { alert, .. } => {
*alert = None;
}
}
}
#[tracing::instrument]
pub fn alert(&mut self, msg: impl Into<String> + std::fmt::Debug) {
let msg: String = msg.into();
tracing::info!(%msg, "new tui alert");
self.update_message("ALERT", Color::Red);
match self {
Self::Identified { alert, .. }
| Self::Configured { alert, .. }
| Self::Ready { alert, .. } => *alert = Some(msg),
}
}
pub fn ready(self, main: Commit, next: Commit, dev: Commit) -> Self {
match self {
Self::Identified {
repo_alias,
message,
alert,
} => Self::Identified {
repo_alias,
message,
alert,
},
Self::Configured {
repo_alias,
message,
alert,
branches,
log,
} => Self::Ready {
repo_alias,
message,
alert,
branches,
view_state: ViewState::Expanded,
main,
next,
dev,
log,
},
Self::Ready {
repo_alias,
message,
alert,
branches,
view_state,
log,
.. // drop existing main, next and dev to use parameters
} => Self::Ready {
repo_alias,
message,
alert,
branches,
view_state,
main,
next,
dev,
log,
},
}
}
}
impl StatefulWidget for &State {
type State = ScrollViewState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where
Self: Sized,
{
let block = Block::bordered()
.title(
Title::from(format!(" Git-Next v{} ", clap::crate_version!()).bold())
.alignment(Alignment::Center),
)
.title(
Title::from(Line::from(vec![
" [q]uit ".into(),
self.beating_heart().into(),
" ".into(),
]))
.alignment(Alignment::Center)
.position(Position::Bottom),
)
.title(
Title::from(format!(" {} ", time()))
.alignment(Alignment::Right)
.position(Position::Bottom),
)
.border_set(border::THICK);
let interior = block.inner(area);
block.render(area, buf);
match &self.mode {
ServerState::Initial { tick } => Paragraph::new(format!("Loading...{tick}"))
.centered()
.render(interior, buf),
ServerState::Configured { forges } => {
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

@ -0,0 +1,68 @@
use std::collections::BTreeMap;
use git_next_core::ForgeAlias;
use ratatui::{
buffer::Buffer,
layout::{Direction, Layout, Rect, Size},
widgets::StatefulWidget,
};
use tui_scrollview::{ScrollView, ScrollViewState};
use crate::tui::actor::ForgeState;
use super::{forge::ForgeWidget, HeightContraintLength};
pub struct ConfiguredAppWidget<'a> {
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
}
impl<'a> HeightContraintLength for ConfiguredAppWidget<'a> {
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
Self: Sized,
{
let height = self
.children()
.iter()
.map(HeightContraintLength::height_constraint_length)
.sum::<u16>();
let mut scroll = ScrollView::new(Size::new(area.width - 1, height));
let layout_forge_list = Layout::default()
.direction(Direction::Vertical)
.constraints(
self.children()
.iter()
.map(HeightContraintLength::height_constraint_length),
)
.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
.iter()
.map(|(forge_alias, state)| ForgeWidget {
forge_alias,
repos: &state.repos,
view_state: state.view_state,
})
.collect::<Vec<_>>()
}
}

View file

@ -0,0 +1,22 @@
//
use git_next_core::ForgeAlias;
use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
use crate::tui::components::HeightContraintLength;
pub struct CollapsedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
}
impl<'a> HeightContraintLength for CollapsedForgeWidget<'a> {
fn height_constraint_length(&self) -> u16 {
1
}
}
impl<'a> Widget for CollapsedForgeWidget<'a> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
Text::from(format!("- {}", self.forge_alias)).render(area, buf);
}
}

View file

@ -0,0 +1,60 @@
//
use std::collections::BTreeMap;
use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Direction, Layout, Rect},
widgets::{block::Title, Block, Widget},
};
use crate::tui::{
actor::RepoState,
components::{repo::RepoWidget, HeightContraintLength},
};
pub struct ExpandedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
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> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let block = Block::default().title(
Title::from(format!(" forge: {} ", self.forge_alias)).alignment(Alignment::Left),
);
let children = self.children();
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
children
.iter()
.map(HeightContraintLength::height_constraint_length),
)
.split(block.inner(area));
block.render(area, buf);
children
.into_iter()
.enumerate()
.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

@ -0,0 +1,53 @@
//
mod collapsed;
mod expanded;
use std::collections::BTreeMap;
use collapsed::CollapsedForgeWidget;
use expanded::ExpandedForgeWidget;
use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use crate::tui::actor::{RepoState, ViewState};
use super::HeightContraintLength;
pub struct ForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
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> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
match self.view_state {
ViewState::Collapsed => CollapsedForgeWidget {
forge_alias: self.forge_alias,
}
.render(area, buf),
ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias,
repos: self.repos,
}
.render(area, buf),
}
}
}

View file

@ -0,0 +1,128 @@
//
use git_next_core::git::graph::Log;
use ratatui::{
style::{Color, Style},
text::{Line, Span, Text},
widgets::{Paragraph, Widget},
};
use regex::Regex;
use super::HeightContraintLength;
pub struct CommitLog<'a> {
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> {
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
where
Self: Sized,
{
Paragraph::new(Text::from(
self.log
.iter()
.map(LogLine::new)
.map(Line::from)
.collect::<Vec<_>>(),
))
.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

@ -0,0 +1,12 @@
//
mod configured_app;
mod forge;
mod history;
mod repo;
pub use configured_app::ConfiguredAppWidget;
pub use history::CommitLog;
pub trait HeightContraintLength {
fn height_constraint_length(&self) -> u16;
}

View file

@ -0,0 +1,87 @@
use std::string::ToString;
//
use git_next_core::{RepoAlias, RepoBranches};
use ratatui::{
layout::Alignment,
style::{Color, Style, Stylize as _},
text::{Line, Span},
widgets::block::Title,
};
use crate::tui::actor::RepoMessage;
pub struct Identity<'a> {
pub repo_alias: &'a RepoAlias,
pub alert: Option<&'a str>,
pub message: &'a RepoMessage,
pub repo_branches: Option<&'a RepoBranches>,
}
impl<'a> Identity<'a> {
pub const fn new(
repo_alias: &'a RepoAlias,
alert: Option<&'a str>,
message: &'a RepoMessage,
repo_branches: Option<&'a RepoBranches>,
) -> Self {
Self {
repo_alias,
alert,
message,
repo_branches,
}
}
}
impl<'a> Identity<'a> {
fn spans(self) -> Vec<Span<'a>> {
let alert = self
.alert
.map(ToString::to_string)
.map(|alert| alert.fg(Color::White).bg(Color::Red));
let message = self.message;
let main = self
.repo_branches
.map(RepoBranches::main)
.map_or_else(|| "_".to_string(), |b| b.to_string());
let next = self
.repo_branches
.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)),
);
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> {
fn from(identity: Identity<'a>) -> Self {
Self {
content: Line {
spans: identity.spans(),
style: Style::reset(),
alignment: None,
},
alignment: Some(Alignment::Left),
position: None,
}
}
}

View file

@ -0,0 +1,114 @@
//
mod identity;
use std::string::String;
use git_next_core::{RepoAlias, RepoBranches};
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 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> {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
self.inner().render(area, buf);
}
}
struct InnerRepoWidget<'a> {
pub repo_alias: &'a RepoAlias,
pub message: &'a RepoMessage,
pub alert: Option<&'a str>,
pub branches: Option<&'a RepoBranches>,
pub log: Option<&'a git::graph::Log>,
}
impl<'a> HeightContraintLength for InnerRepoWidget<'a> {
fn height_constraint_length(&self) -> u16 {
self.log
.map(|log| CommitLog { log })
.map_or(0, |w| w.height_constraint_length())
}
}
impl<'a> Widget for InnerRepoWidget<'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,
self.branches,
))
.borders(Borders::TOP);
if let Some(log) = self.log {
CommitLog { log }.render(block.inner(area), buf);
}
block.render(area, buf);
}
}

View file

@ -0,0 +1,86 @@
//
use std::path::PathBuf;
use color_eyre::eyre::Result;
use directories::ProjectDirs;
use lazy_static::lazy_static;
use tracing_error::ErrorLayer;
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer};
lazy_static! {
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase();
pub static ref DATA_FOLDER: Option<PathBuf> =
std::env::var(format!("{}_DATA", PROJECT_NAME.clone()))
.ok()
.map(PathBuf::from);
pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
}
fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("net", "kemitix", env!("CARGO_PKG_NAME"))
}
pub fn get_data_dir() -> PathBuf {
let directory = DATA_FOLDER.clone().map_or_else(
|| {
project_directory().map_or_else(
|| PathBuf::from(".").join(".data"),
|proj_dirs| proj_dirs.data_local_dir().to_path_buf(),
)
},
|data_folder| data_folder,
);
directory
}
pub fn initialize_logging() -> Result<()> {
let directory = get_data_dir();
std::fs::create_dir_all(directory.clone())?;
let log_path = directory.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;
std::env::set_var(
"RUST_LOG",
std::env::var("RUST_LOG")
.or_else(|_| std::env::var(LOG_ENV.clone()))
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
);
let file_subscriber = tracing_subscriber::fmt::layer()
.with_file(true)
.with_line_number(true)
.with_writer(log_file)
.with_target(false)
.with_ansi(false)
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
tracing_subscriber::registry()
.with(file_subscriber)
.with(ErrorLayer::default())
.init();
Ok(())
}
/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
/// than printing to stdout.
///
/// By default, the verbosity level for the generated events is `DEBUG`, but
/// this can be customized.
#[macro_export]
macro_rules! trace_dbg {
(target: $target:expr, level: $level:expr, $ex:expr) => {{
match $ex {
value => {
tracing::event!(target: $target, $level, ?value, stringify!($ex));
value
}
}
}};
(level: $level:expr, $ex:expr) => {
trace_dbg!(target: module_path!(), level: $level, $ex)
};
(target: $target:expr, $ex:expr) => {
trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
};
($ex:expr) => {
trace_dbg!(level: tracing::Level::DEBUG, $ex)
};
}

View file

@ -0,0 +1,7 @@
//
mod actor;
pub mod components;
pub mod logging;
pub use actor::messages::Tick;
pub use actor::Tui;

View file

@ -1,4 +1,4 @@
//
use git_next_core::message;
message!(ShutdownWebhook: "Request to shutdown the Webhook actor");
message!(ShutdownWebhook, "Request to shutdown the Webhook actor");

View file

@ -12,6 +12,7 @@ use std::net::SocketAddr;
use tracing::Instrument;
#[allow(clippy::module_name_repetitions)]
#[derive(Debug)]
pub struct WebhookActor {
socket_addr: SocketAddr,

View file

@ -10,29 +10,29 @@ use crate::repo::messages::WebhookNotification;
use git_next_core::{ForgeAlias, RepoAlias};
pub struct WebhookRouter {
pub struct WebhookRouterActor {
span: tracing::Span,
recipients: BTreeMap<ForgeAlias, BTreeMap<RepoAlias, Recipient<WebhookNotification>>>,
}
impl Default for WebhookRouter {
impl Default for WebhookRouterActor {
fn default() -> Self {
Self::new()
}
}
impl WebhookRouter {
impl WebhookRouterActor {
pub fn new() -> Self {
let span = tracing::info_span!("WebhookRouter");
Self {
span,
recipients: Default::default(),
recipients: BTreeMap::default(),
}
}
}
impl Actor for WebhookRouter {
impl Actor for WebhookRouterActor {
type Context = Context<Self>;
}
impl Handler<WebhookNotification> for WebhookRouter {
impl Handler<WebhookNotification> for WebhookRouterActor {
type Result = ();
fn handle(&mut self, msg: WebhookNotification, _ctx: &mut Self::Context) -> Self::Result {
@ -59,7 +59,7 @@ pub struct AddWebhookRecipient {
pub repo_alias: RepoAlias,
pub recipient: Recipient<WebhookNotification>,
}
impl Handler<AddWebhookRecipient> for WebhookRouter {
impl Handler<AddWebhookRecipient> for WebhookRouterActor {
type Result = ();
fn handle(&mut self, msg: AddWebhookRecipient, _ctx: &mut Self::Context) -> Self::Result {

View file

@ -50,7 +50,7 @@ pub async fn start(
));
recipient
.try_send(message)
.map(|_| {
.map(|()| {
info!("Message sent ok");
warp::reply::with_status("OK", warp::http::StatusCode::OK)
})

View file

@ -6,6 +6,15 @@ license = { workspace = true }
repository = { workspace = true }
description = "core for git-next, the trunk-based development manager"
[lints.clippy]
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
[features]
default = ["forgejo", "github"]
forgejo = []
@ -48,18 +57,12 @@ serde_json = { workspace = true }
mockall = { workspace = true }
#iters
take-until = { workspace = true }
[dev-dependencies]
# Testing
assert2 = { workspace = true }
rand = { workspace = true }
test-log = { workspace = true }
pretty_assertions = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
# pedantic = "warn"
unwrap_used = "warn"
expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

@ -1,6 +1,6 @@
/// The API Token for the [user]
/// ForgeJo: https://{hostname}/user/settings/applications
/// Github: https://github.com/settings/tokens
/// `ForgeJo`: <https://{hostname}/user/settings/applications>
/// `Github`: <https://github.com/settings/tokens>
#[derive(Clone, Debug, derive_more::Constructor)]
pub struct ApiToken(secrecy::Secret<String>);
/// The API Token is in effect a password, so it must be explicitly exposed to access its value
@ -11,6 +11,6 @@ impl secrecy::ExposeSecret<String> for ApiToken {
}
impl Default for ApiToken {
fn default() -> Self {
Self("".to_string().into())
Self(String::new().into())
}
}

View file

@ -1,4 +1,14 @@
use derive_more::derive::Display;
use serde::Serialize;
crate::newtype!(BranchName: String, Display, Default, Hash, Serialize: "The name of a Git branch");
use crate::newtype;
newtype!(
BranchName,
String,
Display,
Default,
Hash,
Serialize,
"The name of a Git branch"
);

View file

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

View file

@ -3,6 +3,7 @@ use crate::config::{
RepoConfig, RepoConfigSource, RepoPath, User,
};
#[must_use]
pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
ForgeDetails::new(
forge_name(n),
@ -10,37 +11,39 @@ pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
hostname(n),
user(n),
api_token(n),
None,
)
}
pub fn api_token(n: u32) -> ApiToken {
ApiToken::new(format!("api-{}", n).into())
pub(crate) fn api_token(n: u32) -> ApiToken {
ApiToken::new(format!("api-{n}").into())
}
pub fn user(n: u32) -> User {
User::new(format!("user-{}", n))
pub(crate) fn user(n: u32) -> User {
User::new(format!("user-{n}"))
}
pub fn hostname(n: u32) -> Hostname {
Hostname::new(format!("hostname-{}", n))
pub(crate) fn hostname(n: u32) -> Hostname {
Hostname::new(format!("hostname-{n}"))
}
pub fn forge_name(n: u32) -> ForgeAlias {
ForgeAlias::new(format!("forge-name-{}", n))
pub(crate) fn forge_name(n: u32) -> ForgeAlias {
ForgeAlias::new(format!("forge-name-{n}"))
}
pub fn branch_name(n: u32) -> BranchName {
BranchName::new(format!("branch-name-{}", n))
pub(crate) fn branch_name(n: u32) -> BranchName {
BranchName::new(format!("branch-name-{n}"))
}
pub fn repo_path(n: u32) -> RepoPath {
RepoPath::new(format!("repo-path-{}", n))
pub(crate) fn repo_path(n: u32) -> RepoPath {
RepoPath::new(format!("repo-path-{n}"))
}
pub fn repo_alias(n: u32) -> RepoAlias {
RepoAlias::new(format!("repo-alias-{}", n))
pub(crate) fn repo_alias(n: u32) -> RepoAlias {
RepoAlias::new(format!("repo-alias-{n}"))
}
#[must_use]
pub fn repo_config(n: u32, source: RepoConfigSource) -> RepoConfig {
RepoConfig::new(
RepoBranches::new(format!("main-{n}"), format!("next-{n}"), format!("dev-{n}")),

View file

@ -1,6 +1,18 @@
use serde::Serialize;
crate::newtype!(ForgeAlias: String, Hash, PartialOrd, Ord, derive_more::Display, Default, Serialize: "The name of a Forge to connect to");
use crate::newtype;
newtype!(
ForgeAlias,
String,
Hash,
PartialOrd,
Ord,
derive_more::Display,
Default,
Serialize,
"The name of a Forge to connect to"
);
impl From<&ForgeAlias> for std::path::PathBuf {
fn from(value: &ForgeAlias) -> Self {
Self::from(&value.0)

Some files were not shown because too many files have changed in this diff Show more