Compare commits

..

No commits in common. "dev" and "v0.13.7" have entirely different histories.
dev ... v0.13.7

92 changed files with 1236 additions and 2549 deletions

View file

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

View file

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

3
.gitignore vendored
View file

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

View file

@ -1,13 +1,13 @@
steps: steps:
docker-build: todo_check:
# INFO: This doesn't have an equivalent yet for Forgejo Actions
# INFO: https://woodpecker-ci.org/plugins/TODO-Checker
image: codeberg.org/epsilon_02/todo-checker:1.1
when: when:
- event: push - event: push
branch: next branch: next
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
image: docker.io/woodpeckerci/plugin-docker-buildx:5.0.0
settings: settings:
username: kemitix # git-next-woodpecker-todo-checker - read:issue
repo: git.kemitix.net/kemitix/git-next repository_token: "776a3b928b852472c2af727a360c85c00af64b9f"
dockerfile: Dockerfile prefix_regex: "(#|//) (TODO|FIXME): "
auto_tag: false debug: false
dry-run: true # don't push to remote repo

View file

@ -4,7 +4,7 @@ steps:
- event: tag - event: tag
ref: refs/tags/v* ref: refs/tags/v*
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx # INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
image: docker.io/woodpeckerci/plugin-docker-buildx:5.0.0 image: docker.io/woodpeckerci/plugin-docker-buildx:4.2.0
settings: settings:
username: kemitix username: kemitix
repo: git.kemitix.net/kemitix/git-next repo: git.kemitix.net/kemitix/git-next

View file

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

533
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.13.11" version = "0.13.7"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -27,13 +27,10 @@ git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.13" }
git-next-forge-github = { path = "crates/forge-github", version = "0.13" } git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
# TUI # TUI
ratatui = "0.29" ratatui = "0.28"
directories = "5.0" directories = "5.0.1"
lazy_static = "1.5" lazy_static = "1.5.0"
color-eyre = "0.6" color-eyre = "0.6.3"
tui-scrollview = "0.5"
regex = "1.10"
chrono = "0.4"
# CLI parsing # CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] } clap = { version = "4.5", features = ["cargo", "derive"] }
@ -52,7 +49,7 @@ sha2 = "0.10"
hex = "0.4" hex = "0.4"
# git # git
gix = { version = "0.67", features = [ gix = { version = "0.66", features = [
"dirwalk", "dirwalk",
"blocking-http-transport-reqwest-rust-tls", "blocking-http-transport-reqwest-rust-tls",
] } ] }
@ -68,7 +65,7 @@ serde_json = "1.0"
toml = "0.8" toml = "0.8"
# Secrets and Password # Secrets and Password
secrecy = "0.10" secrecy = "0.8"
# Conventional Commit check # Conventional Commit check
git-conventional = "0.12" git-conventional = "0.12"
@ -81,7 +78,6 @@ time = "0.3"
standardwebhooks = "1.0" standardwebhooks = "1.0"
# boilerplate # boilerplate
bon = "2.0"
derive_more = { version = "1.0.0-beta", features = [ derive_more = { version = "1.0.0-beta", features = [
"as_ref", "as_ref",
"constructor", "constructor",
@ -98,7 +94,7 @@ pike = "0.1"
take-until = "0.2" take-until = "0.2"
# file watcher # file watcher
notify = "7.0" notify = "6.1"
# Actors # Actors
actix = "0.13" actix = "0.13"
@ -118,4 +114,4 @@ pretty_assertions = "1.4"
rand = "0.8" rand = "0.8"
mockall = "0.13" mockall = "0.13"
test-log = "0.2" test-log = "0.2"
rstest = { version = "0.23", features = ["async-timeout"] } rstest = { version = "0.22", features = ["async-timeout"] }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ use git_next_core::{
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
StoragePathType, User, StoragePathType, User,
}; };
use secrecy::SecretString; use secrecy::Secret;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
@ -59,13 +59,10 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
repo_details.forge = repo_details repo_details.forge = repo_details
.forge .forge
.with_user(User::new("git".to_string())) .with_user(User::new("git".to_string()))
.with_token(ApiToken::new(SecretString::from(String::new()))) .with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string()); repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
let Ok(open_repository) = git::repository::factory::real().open(&repo_details) else { let open_repository = git::repository::factory::real().open(&repo_details)?;
// .git directory may not be present on dev environment
return Ok(());
};
let_assert!( let_assert!(
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push), Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
"Default Push Remote not found" "Default Push Remote not found"
@ -95,13 +92,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
repo_details.forge = repo_details repo_details.forge = repo_details
.forge .forge
.with_user(User::new("git".to_string())) .with_user(User::new("git".to_string()))
.with_token(ApiToken::new(SecretString::from(String::new()))) .with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
tracing::debug!("opening..."); tracing::debug!("opening...");
let Ok(repository) = git::repository::factory::real().open(&repo_details) else { let_assert!(
// .git directory may not be present on dev environment Ok(repository) = git::repository::factory::real().open(&repo_details),
return Ok(()); "open repository"
}; );
tracing::debug!("open okay"); tracing::debug!("open okay");
tracing::info!(?repository, "FOO"); tracing::info!(?repository, "FOO");
tracing::info!(?repo_details, "BAR"); tracing::info!(?repo_details, "BAR");
@ -111,13 +108,11 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
} }
#[test] #[test]
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() { fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
let_assert!( let_assert!(
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io) Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
); );
eprintln!("cli_crate_dir: {cli_crate_dir:?}");
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent())); let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
eprintln!("root: {root:?}");
let mut repo_details = git::repo_details( let mut repo_details = git::repo_details(
1, 1,
git::Generation::default(), git::Generation::default(),
@ -129,17 +124,16 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() {
repo_details.forge = repo_details repo_details.forge = repo_details
.forge .forge
.with_user(User::new("git".to_string())) .with_user(User::new("git".to_string()))
.with_token(ApiToken::new(SecretString::from(String::new()))) .with_token(ApiToken::new(Secret::new(String::new())))
.with_hostname(Hostname::new("git.kemitix.net")); .with_hostname(Hostname::new("git.kemitix.net"));
let Ok(repository) = git::repository::factory::real().open(&repo_details) else { let repository = git::repository::factory::real().open(&repo_details)?;
// .git directory may not be present on dev environment
return;
};
let mut repo_details = repo_details.clone(); let mut repo_details = repo_details.clone();
repo_details.forge = repo_details repo_details.forge = repo_details
.forge .forge
.with_hostname(Hostname::new("code.kemitix.net")); .with_hostname(Hostname::new("code.kemitix.net"));
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details)); let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
Ok(())
} }
#[test] #[test]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,10 +2,10 @@
/// `ForgeJo`: <https://{hostname}/user/settings/applications> /// `ForgeJo`: <https://{hostname}/user/settings/applications>
/// `Github`: <https://github.com/settings/tokens> /// `Github`: <https://github.com/settings/tokens>
#[derive(Clone, Debug, derive_more::Constructor)] #[derive(Clone, Debug, derive_more::Constructor)]
pub struct ApiToken(secrecy::SecretString); pub struct ApiToken(secrecy::Secret<String>);
/// The API Token is in effect a password, so it must be explicitly exposed to access its value /// The API Token is in effect a password, so it must be explicitly exposed to access its value
impl secrecy::ExposeSecret<str> for ApiToken { impl secrecy::ExposeSecret<String> for ApiToken {
fn expose_secret(&self) -> &str { fn expose_secret(&self) -> &String {
self.0.expose_secret() self.0.expose_secret()
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@ use std::{
use derive_more::{Constructor, Display}; use derive_more::{Constructor, Display};
use kxio::fs::FileSystem; use kxio::fs::FileSystem;
use secrecy::SecretString; use secrecy::Secret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
@ -242,11 +242,8 @@ impl Shout {
self.webhook.clone().map(|x| x.url) self.webhook.clone().map(|x| x.url)
} }
pub fn webhook_secret(&self) -> Option<SecretString> { pub fn webhook_secret(&self) -> Option<Secret<String>> {
self.webhook self.webhook.clone().map(|x| x.secret).map(Secret::new)
.clone()
.map(|x| x.secret)
.map(SecretString::from)
} }
#[must_use] #[must_use]
@ -281,8 +278,8 @@ impl OutboundWebhook {
self.url.as_ref() self.url.as_ref()
} }
#[must_use] #[must_use]
pub fn secret(&self) -> SecretString { pub fn secret(&self) -> Secret<String> {
SecretString::from(self.secret.clone()) Secret::new(self.secret.clone())
} }
} }

View file

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

View file

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

View file

@ -11,7 +11,7 @@ use crate::{
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use secrecy::{ExposeSecret, SecretString}; use secrecy::{ExposeSecret, Secret};
use tracing::instrument; use tracing::instrument;
/// The derived information about a repo, used to interact with it /// The derived information about a repo, used to interact with it
@ -49,11 +49,10 @@ impl RepoDetails {
forge_config.hostname(), forge_config.hostname(),
forge_config.user(), forge_config.user(),
forge_config.token(), forge_config.token(),
forge_config.max_dev_commits(),
), ),
} }
} }
pub(crate) fn origin(&self) -> secrecy::SecretString { pub(crate) fn origin(&self) -> secrecy::Secret<String> {
let repo_details = self; let repo_details = self;
let user = &repo_details.forge.user(); let user = &repo_details.forge.user();
let hostname = &repo_details.forge.hostname(); let hostname = &repo_details.forge.hostname();
@ -78,7 +77,7 @@ impl RepoDetails {
} }
// url is a secret as it contains auth token // url is a secret as it contains auth token
pub(crate) fn url(&self) -> SecretString { pub(crate) fn url(&self) -> Secret<String> {
let user = self.forge.user(); let user = self.forge.user();
let token = self.forge.token().expose_secret(); let token = self.forge.token().expose_secret();
let auth_delim = if token.is_empty() { "" } else { ":" }; let auth_delim = if token.is_empty() { "" } else { ":" };
@ -95,7 +94,12 @@ impl RepoDetails {
|> GitDir::pathbuf |> GitDir::pathbuf
|> gix::ThreadSafeRepository::open |> gix::ThreadSafeRepository::open
}?; }?;
let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo)), self.forge.clone()); let repo = pike! {
gix_repo
|> RwLock::new
|> Arc::new
|> RealOpenRepository::new
};
Ok(repo) Ok(repo)
} }

View file

@ -60,14 +60,13 @@ impl RepositoryFactory for RealRepositoryFactory {
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> { fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> {
tracing::info!("creating"); tracing::info!("creating");
let (gix_repo, _outcome) = let (gix_repo, _outcome) = gix::prepare_clone_bare(
gix::prepare_clone_bare(repo_details.origin().expose_secret(), &*repo_details.gitdir)? repo_details.origin().expose_secret().as_str(),
&*repo_details.gitdir,
)?
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
tracing::info!("created"); tracing::info!("created");
let repo = RealOpenRepository::new( let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo.into())));
Arc::new(RwLock::new(gix_repo.into())),
repo_details.forge.clone(),
);
Ok(Box::new(repo)) Ok(Box::new(repo))
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

447
demo.cast
View file

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

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

View file

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