Compare commits
98 commits
Author | SHA1 | Date | |
---|---|---|---|
|
0f78fc731a | ||
|
1784f3f28b | ||
|
b794a21dd9 | ||
|
d989da659c | ||
23de987444 | |||
|
9d6271a176 | ||
|
ddc22867b3 | ||
|
5e4e287562 | ||
|
6a0e0580dc | ||
|
7bd6347dd8 | ||
|
360b7f2cf7 | ||
f3a5b9cb4c | |||
18a537b18e | |||
ef6474ef9f | |||
dbf1a0db27 | |||
|
91c5973e31 | ||
978205b823 | |||
8359d0d7ca | |||
93cf6f83df | |||
681d85aac1 | |||
d4f16e6f5e | |||
048111202a | |||
3ea7f36c98 | |||
6c60e3fb7a | |||
313d6d79c5 | |||
189d579d33 | |||
a77c6335a6 | |||
|
82241de0dd | ||
664e424d1a | |||
df6b96fbfd | |||
566125f5c0 | |||
80af909ab0 | |||
ecd460cdfb | |||
|
35c2057f05 | ||
d2e2d00fe1 | |||
e759e495fd | |||
|
3672fd5d45 | ||
1f0b5e867c | |||
8ca7aad3c3 | |||
d923e831f0 | |||
5e0cf270dd | |||
b4a4631a1d | |||
181ec8eb0f | |||
47cbbad8e7 | |||
e793c18215 | |||
|
224b63deb1 | ||
3c01a822fd | |||
4160b6d6ee | |||
853b862f10 | |||
ca70c03e8b | |||
f475095f4a | |||
eae351d8a4 | |||
c1564807f8 | |||
b24005c3fe | |||
22ce2d431a | |||
be41842dae | |||
9720fd01fc | |||
8550adf79e | |||
2b09872131 | |||
d2048d8a34 | |||
02609fdc11 | |||
01f54d79ae | |||
1df982005e | |||
2abb36ad6c | |||
576eaaf990 | |||
97b685363a | |||
a2940ec753 | |||
d5d313064a | |||
f9e305afa4 | |||
4555b3ae09 | |||
64da1d8a34 | |||
a650996ecd | |||
eca556f976 | |||
a3dd82705f | |||
7504ab5a2d | |||
eb42745383 | |||
126d5d3ef5 | |||
4f6669548c | |||
52bd9cc30b | |||
2959bdfad4 | |||
f85cbce4c6 | |||
4517fe62e4 | |||
c6bf287ed1 | |||
35e3676930 | |||
95e9209e17 | |||
d1a685ae34 | |||
e489fb36e9 | |||
09ff4c3a54 | |||
48a5ed7a3b | |||
|
76ae37a9a5 | ||
5d9915bdbd | |||
|
f504b62ff6 | ||
47dbc1a8a4 | |||
7a4f9a45a6 | |||
622e144986 | |||
0632225752 | |||
08d2377404 | |||
e34c6e0ef6 |
109 changed files with 4525 additions and 1214 deletions
|
@ -22,13 +22,13 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run release-plz release-pr
|
||||
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: release-plz release-pr --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
- name: Run release-plz release
|
||||
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
|
||||
env:
|
||||
|
|
|
@ -13,26 +13,40 @@ jobs:
|
|||
build:
|
||||
runs-on: docker
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
toolchain:
|
||||
- name: stable
|
||||
- name: nightly
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Format
|
||||
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
|
||||
- name: Check TODOs
|
||||
uses: kemitix/todo-checker@v1.1.0
|
||||
|
||||
- name: Machete
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: cargo fmt --all -- --check
|
||||
args: ${{ matrix.toolchain.name }} cargo machete
|
||||
|
||||
- name: Format
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
|
||||
|
||||
- name: Clippy
|
||||
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: cargo clippy
|
||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset clippy
|
||||
|
||||
- name: Build
|
||||
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: cargo build
|
||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset build
|
||||
|
||||
- name: Test
|
||||
uses: https://git.kemitix.net/kemitix/rust@v1.80.0-2
|
||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
||||
with:
|
||||
args: cargo test
|
||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset test
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
# git-next ui logs
|
||||
.local/
|
||||
|
||||
# ---> Rust
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
steps:
|
||||
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
|
||||
docker-build:
|
||||
when:
|
||||
- event: push
|
||||
branch: next
|
||||
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
|
||||
image: docker.io/woodpeckerci/plugin-docker-buildx:5.0.0
|
||||
settings:
|
||||
# git-next-woodpecker-todo-checker - read:issue
|
||||
repository_token: "776a3b928b852472c2af727a360c85c00af64b9f"
|
||||
prefix_regex: "(#|//) (TODO|FIXME): "
|
||||
debug: false
|
||||
username: kemitix
|
||||
repo: git.kemitix.net/kemitix/git-next
|
||||
dockerfile: Dockerfile
|
||||
auto_tag: false
|
||||
dry-run: true # don't push to remote repo
|
||||
|
|
|
@ -4,7 +4,7 @@ steps:
|
|||
- event: tag
|
||||
ref: refs/tags/v*
|
||||
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
|
||||
image: docker.io/woodpeckerci/plugin-docker-buildx:4.2.0
|
||||
image: docker.io/woodpeckerci/plugin-docker-buildx:5.0.0
|
||||
settings:
|
||||
username: kemitix
|
||||
repo: git.kemitix.net/kemitix/git-next
|
||||
|
|
153
CHANGELOG.md
153
CHANGELOG.md
|
@ -2,6 +2,159 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## `git-next-core` - [0.13.11](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.10...git-next-core-v0.13.11) - 2024-09-14
|
||||
|
||||
### Added
|
||||
- should fetch repo on startup when not cloning
|
||||
- Remove branches when fetching from remote
|
||||
|
||||
### Other
|
||||
- reimplement git fetch using git
|
||||
|
||||
## `git-next` - [0.13.11](https://git.kemitix.net/kemitix/git-next/compare/v0.13.10...v0.13.11) - 2024-09-14
|
||||
|
||||
### Added
|
||||
- *(tui)* add time and version in border
|
||||
- should fetch repo on startup when not cloning
|
||||
- Remove branches when fetching from remote
|
||||
|
||||
### Other
|
||||
- Update TUI sooner when receiving CI status
|
||||
- reimplement git fetch using git
|
||||
- mark tui as complete on roadmap
|
||||
- Add missing port mapping parameter for running in docker
|
||||
|
||||
## `git-next-forge-github` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.9...git-next-forge-github-v0.13.10) - 2024-09-12
|
||||
|
||||
### Added
|
||||
- optionally specify max commits between dev and main
|
||||
|
||||
## `git-next-forge-forgejo` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.9...git-next-forge-forgejo-v0.13.10) - 2024-09-12
|
||||
|
||||
### Added
|
||||
- optionally specify max commits between dev and main
|
||||
|
||||
## `git-next-core` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.9...git-next-core-v0.13.10) - 2024-09-12
|
||||
|
||||
### Added
|
||||
- optionally specify max commits between dev and main
|
||||
|
||||
## `git-next` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/v0.13.9...v0.13.10) - 2024-09-12
|
||||
|
||||
### Added
|
||||
- optionally specify max commits between dev and main
|
||||
|
||||
### Fixed
|
||||
- *(tui)* make tui work from docker image
|
||||
- *(tui)* alerts, such as WIP aren't being reset
|
||||
- *(test)* tests requiring .git pass when not present
|
||||
- *(tui)* update ui when push next or main finishes
|
||||
- *(tui)* don't set background for normal repo alias
|
||||
|
||||
## `git-next` - [0.13.9](https://git.kemitix.net/kemitix/git-next/compare/v0.13.8...v0.13.9) - 2024-09-04
|
||||
|
||||
### Fixed
|
||||
- *(tui)* alerts are cleared on next repo update
|
||||
- shutdown properly on error
|
||||
- shutdown properly on file parse error
|
||||
|
||||
### Other
|
||||
- Expand docker docmentation
|
||||
|
||||
## `git-next-forge-forgejo` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.7...git-next-forge-forgejo-v0.13.8) - 2024-09-01
|
||||
|
||||
### Other
|
||||
- flatten nested blocks with early returns
|
||||
- rename method as peel
|
||||
|
||||
## `git-next-core` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.7...git-next-core-v0.13.8) - 2024-09-01
|
||||
|
||||
### Fixed
|
||||
- use configured branch names in user notification
|
||||
- create git graph log to after doing a fetch
|
||||
|
||||
### Other
|
||||
- flatten nested blocks with early returns
|
||||
- rename method as peel
|
||||
|
||||
## `git-next` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/v0.13.7...v0.13.8) - 2024-09-01
|
||||
|
||||
### Added
|
||||
- improved error display when startup fails
|
||||
- *(tui)* clean up alert display
|
||||
- *(tui)* remove some borders to clean up appearance
|
||||
- *(tui)* make progression of branches clearer
|
||||
- *(tui)* remove label from repo identity widget
|
||||
- *(tui)* hightlight repo alias in red when in alert
|
||||
- *(tui)* branch names look more like 'pills'
|
||||
- *(tui)* highlight branchs in log
|
||||
- *(tui)* hightlight status message in colour
|
||||
- *(tui)* use moving heart emoji as liveness indicator
|
||||
- *(tui)* add scrolling when overflow screen
|
||||
- *(tui)* forge widgets only use required lines
|
||||
- *(tui)* repo widgets only use required lines
|
||||
- *(tui)* move forge alias to left and add prefix
|
||||
- *(tui)* remove count of forges
|
||||
- *(tui)* remove duplicate messages from repo body
|
||||
- *(tui)* highlight user interventions in red
|
||||
|
||||
### Fixed
|
||||
- use configured branch names in user notification
|
||||
- remove unused imports
|
||||
- *(tui)* remove logging from inside ui loop
|
||||
- *(tui)* don't show HEAD in log
|
||||
- *(tui)* improve colour contrast on light background
|
||||
- *(tui)* remove unused import
|
||||
- *(alert)* typo in email message
|
||||
- *(repo)* avoid blocking threads when pausing
|
||||
- *(test)* give actix more time to process message
|
||||
- *(test)* give actix more time to process message
|
||||
- *(test)* give actix more time to process message
|
||||
- *(tui)* improve reliability of status updates
|
||||
- create git graph log to after doing a fetch
|
||||
- *(tui)* remove logging of tui updates
|
||||
|
||||
### Other
|
||||
- flatten nested blocks with early returns
|
||||
- merge identical match branches
|
||||
- *(tui)* add regex dependency
|
||||
- *(tui)* introduce LogLine to wrap log formatting
|
||||
- *(tui)* simplify repo identity widget
|
||||
- rename method as peel
|
||||
- *(tui)* child widget can provide constraint to container
|
||||
- *(tui)* merge repo widgets into one
|
||||
|
||||
## `git-next-core` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.6...git-next-core-v0.13.7) - 2024-08-25
|
||||
|
||||
### Added
|
||||
- *(tui)* (experimental) show repo state, messages and git log
|
||||
|
||||
## `git-next` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/v0.13.6...v0.13.7) - 2024-08-25
|
||||
|
||||
### Added
|
||||
- *(tui)* (experimental) show repo state, messages and git log
|
||||
|
||||
## `git-next-forge-github` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.5...git-next-forge-github-v0.13.6) - 2024-08-23
|
||||
|
||||
### Fixed
|
||||
- *(github)* register webhook with valid callback url
|
||||
|
||||
## `git-next-core` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.5...git-next-core-v0.13.6) - 2024-08-23
|
||||
|
||||
### Added
|
||||
- *(tui)* (experimental) tui option
|
||||
|
||||
## `git-next` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/v0.13.5...v0.13.6) - 2024-08-23
|
||||
|
||||
### Added
|
||||
- *(tui)* (experimental) tui option
|
||||
|
||||
### Fixed
|
||||
- file_watcher runs on own thread
|
||||
|
||||
### Other
|
||||
- test all feature combinations
|
||||
|
||||
## `git-next` - [0.13.5](https://git.kemitix.net/kemitix/git-next/compare/git-next-v0.13.4...git-next-v0.13.5) - 2024-08-10
|
||||
|
||||
### Added
|
||||
|
|
1671
Cargo.lock
generated
1671
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
22
Cargo.toml
22
Cargo.toml
|
@ -3,7 +3,7 @@ resolver = "2"
|
|||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.13.5"
|
||||
version = "0.13.11"
|
||||
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -27,14 +27,21 @@ git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.13" }
|
|||
git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
|
||||
|
||||
# TUI
|
||||
ratatui = "0.28"
|
||||
ratatui = "0.29"
|
||||
directories = "5.0"
|
||||
lazy_static = "1.5"
|
||||
color-eyre = "0.6"
|
||||
tui-scrollview = "0.5"
|
||||
regex = "1.10"
|
||||
chrono = "0.4"
|
||||
|
||||
# CLI parsing
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
|
||||
# logging
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-error = "0.2.0"
|
||||
|
||||
# base64 decoding
|
||||
base64 = "0.22"
|
||||
|
@ -45,8 +52,7 @@ sha2 = "0.10"
|
|||
hex = "0.4"
|
||||
|
||||
# git
|
||||
# gix = "0.62"
|
||||
gix = { version = "0.64", features = [
|
||||
gix = { version = "0.67", features = [
|
||||
"dirwalk",
|
||||
"blocking-http-transport-reqwest-rust-tls",
|
||||
] }
|
||||
|
@ -62,7 +68,7 @@ serde_json = "1.0"
|
|||
toml = "0.8"
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = "0.8"
|
||||
secrecy = "0.10"
|
||||
|
||||
# Conventional Commit check
|
||||
git-conventional = "0.12"
|
||||
|
@ -75,6 +81,7 @@ time = "0.3"
|
|||
standardwebhooks = "1.0"
|
||||
|
||||
# boilerplate
|
||||
bon = "2.0"
|
||||
derive_more = { version = "1.0.0-beta", features = [
|
||||
"as_ref",
|
||||
"constructor",
|
||||
|
@ -91,7 +98,7 @@ pike = "0.1"
|
|||
take-until = "0.2"
|
||||
|
||||
# file watcher
|
||||
notify = "6.1"
|
||||
notify = "7.0"
|
||||
|
||||
# Actors
|
||||
actix = "0.13"
|
||||
|
@ -111,3 +118,4 @@ pretty_assertions = "1.4"
|
|||
rand = "0.8"
|
||||
mockall = "0.13"
|
||||
test-log = "0.2"
|
||||
rstest = { version = "0.23", features = ["async-timeout"] }
|
||||
|
|
15
Dockerfile
15
Dockerfile
|
@ -12,20 +12,19 @@ FROM chef AS builder
|
|||
COPY --from=planner /app/recipe.json recipe.json
|
||||
RUN cargo chef cook --profile release --recipe-path recipe.json
|
||||
COPY . .
|
||||
RUN cargo build --release --bin git-next && \
|
||||
RUN cargo build --release --bin git-next --all-features && \
|
||||
strip target/release/git-next
|
||||
|
||||
FROM docker.io/debian:stable-20240722-slim AS runtime
|
||||
FROM docker.io/debian:stable-20240904-slim AS runtime
|
||||
WORKDIR /app
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
git=1:2.39.2-1.1 \
|
||||
libssl3=3.0.13-1~deb12u1 \
|
||||
libdbus-1-dev=1.14.10-1~deb12u1 \
|
||||
ca-certificates=20230311 \
|
||||
apt-get satisfy -y "git (>=2.39), libssl3 (>=3.0.14), libdbus-1-dev (>=1.14.10), ca-certificates (>=20230311)" \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
USER 1000
|
||||
COPY --from=builder /app/target/release/git-next /usr/local/bin
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/git-next", "server", "start" ]
|
||||
ENV HOME=/app
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/git-next" ]
|
||||
CMD [ "server", "start" ]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM docker.io/rust:1.80.1-bookworm
|
||||
FROM docker.io/rust:1.82.0-bookworm
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y libdbus-1-dev && \
|
||||
|
|
|
@ -6,4 +6,7 @@
|
|||
development workflows where each commit must pass CI before being included in
|
||||
the main branch.
|
||||
|
||||
![Demo](./demo.gif)
|
||||
|
||||
|
||||
See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information.
|
||||
|
|
18
RELEASE.md
Normal file
18
RELEASE.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# How to create a new Release
|
||||
|
||||
## TLDR
|
||||
|
||||
1. Merge PR `chore: release`
|
||||
2. Wait for [`push-main` workflow](https://git.kemitix.net/kemitix/git-next/actions?workflow=push-main.yml) to complete
|
||||
3. Replace [Release Notes](https://git.kemitix.net/kemitix/git-next/releases) body with details from [CHANGELOG](https://git.kemitix.net/kemitix/git-next/src/branch/main/CHANGELOG.md) (remove crates and duplicates)
|
||||
4. Update thread: <https://mitra.kemitix.net/post/01907ef5-5bd9-b0b6-2b8a-e29762541d78>
|
||||
|
||||
## Detail
|
||||
|
||||
The workflow in `.forgejo/workflows/push-main.yaml` will create or update a PR whenever `main` branch is updated.
|
||||
|
||||
The create the new release, merge this PR. It will automatically create the Release on the `git-next` repo, and publish crates to <https://crates/io>.
|
||||
|
||||
The Release notes that are included with the create Release are currently incorrect, and will need to be manually updated from the `CHANGELOG.md`. Remove crate headers and any resulting duplicates.
|
||||
|
||||
Post an update to the Fediverse. See link above.
|
|
@ -12,10 +12,11 @@ keywords = { workspace = true }
|
|||
categories = { workspace = true }
|
||||
|
||||
[features]
|
||||
# default = ["forgejo", "github"]
|
||||
default = ["forgejo", "github", "tui"]
|
||||
forgejo = ["git-next-forge-forgejo"]
|
||||
github = ["git-next-forge-github"]
|
||||
tui = ["ratatui"]
|
||||
tui = ["ratatui", "directories", "lazy_static", "tui-scrollview", "regex", "chrono"]
|
||||
|
||||
[dependencies]
|
||||
git-next-core = { workspace = true }
|
||||
|
@ -24,6 +25,12 @@ git-next-forge-github = { workspace = true, optional = true }
|
|||
|
||||
# TUI
|
||||
ratatui = { workspace = true, optional = true }
|
||||
directories = { workspace = true, optional = true }
|
||||
lazy_static = { workspace = true, optional = true }
|
||||
color-eyre = { workspace = true }
|
||||
tui-scrollview = { workspace = true, optional = true }
|
||||
regex = { workspace = true, optional = true }
|
||||
chrono = { workspace = true, optional = true }
|
||||
|
||||
# CLI parsing
|
||||
clap = { workspace = true }
|
||||
|
@ -34,6 +41,7 @@ kxio = { workspace = true }
|
|||
# logging
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-error.workspace = true
|
||||
|
||||
# Conventional Commit check
|
||||
git-conventional = { workspace = true }
|
||||
|
@ -44,8 +52,10 @@ toml = { workspace = true }
|
|||
# Actors
|
||||
actix = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
bon = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
derive-with = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
|
@ -77,6 +87,7 @@ test-log = { workspace = true }
|
|||
rand = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
mockall = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
|
|
|
@ -59,7 +59,7 @@ cargo install --path crates/cli
|
|||
- [x] cli
|
||||
- [x] server
|
||||
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
||||
- [ ] tui overview
|
||||
- [x] tui overview
|
||||
- [ ] webui overview
|
||||
|
||||
## Branch Names
|
||||
|
@ -198,13 +198,14 @@ forge_type = "GitHub"
|
|||
hostname = "github.com"
|
||||
user = "username"
|
||||
token = "api-key"
|
||||
max_dev_commits = 25
|
||||
```
|
||||
|
||||
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
||||
- **hostname** - the hostname for the forge.
|
||||
- **user** - the user to authenticate as
|
||||
- **token** - application token for the user. See below for the permissions
|
||||
required for on each forge.
|
||||
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
|
||||
- **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
|
||||
|
||||
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
||||
to `next`.
|
||||
|
@ -576,6 +577,58 @@ world = { repo = "user/world", branch = "master", main = "master", next = "upcom
|
|||
|
||||
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
||||
|
||||
## Docker
|
||||
|
||||
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
|
||||
|
||||
```shell
|
||||
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Here is an example `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
server:
|
||||
image: git.kemitix.net/kemitix/git-next:latest
|
||||
container_name: git-next-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RUST_LOG: "hyper=warn,info"
|
||||
ports:
|
||||
- 8080:8092
|
||||
volumes:
|
||||
- ./:/app/
|
||||
```
|
||||
|
||||
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
|
||||
`8092` and that you are using a reverse proxy to route traffic arriving at
|
||||
`listen.url` to port `8080`.
|
||||
|
||||
### Docker Run
|
||||
|
||||
This will run with the `server start` options:
|
||||
|
||||
```shell
|
||||
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
|
||||
```
|
||||
|
||||
To perform `server init`:
|
||||
|
||||
```shell
|
||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
|
||||
```
|
||||
|
||||
To perform repo `init`:
|
||||
|
||||
```shell
|
||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
|
||||
```
|
||||
|
||||
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
||||
|
|
|
@ -18,23 +18,24 @@ impl Handler<NotifyUser> for AlertsActor {
|
|||
};
|
||||
let net = self.net.clone();
|
||||
let shout = shout.clone();
|
||||
if let Some(user_notification) = self.history.sendable(msg.unwrap()) {
|
||||
async move {
|
||||
if let Some(webhook_config) = shout.webhook() {
|
||||
send_webhook(&user_notification, webhook_config, &net).await;
|
||||
}
|
||||
if let Some(email_config) = shout.email() {
|
||||
send_email(&user_notification, email_config);
|
||||
}
|
||||
if let Some(desktop) = shout.desktop() {
|
||||
if desktop {
|
||||
send_desktop_notification(&user_notification);
|
||||
}
|
||||
let Some(user_notification) = self.history.sendable(msg.peel()) else {
|
||||
return;
|
||||
};
|
||||
async move {
|
||||
if let Some(webhook_config) = shout.webhook() {
|
||||
send_webhook(&user_notification, webhook_config, &net).await;
|
||||
}
|
||||
if let Some(email_config) = shout.email() {
|
||||
send_email(&user_notification, email_config);
|
||||
}
|
||||
if let Some(desktop) = shout.desktop() {
|
||||
if desktop {
|
||||
send_desktop_notification(&user_notification);
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,6 @@ impl Handler<UpdateShout> for AlertsActor {
|
|||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.shout.replace(msg.unwrap());
|
||||
self.shout.replace(msg.peel());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,34 +30,8 @@ impl Actor for AlertsActor {
|
|||
}
|
||||
|
||||
fn short_message(user_notification: &UserNotification) -> String {
|
||||
let tail = match user_notification {
|
||||
UserNotification::CICheckFailed {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
commit,
|
||||
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}")
|
||||
let (forge_alias, repo_alias) = user_notification.aliases();
|
||||
format!("[git-next] {forge_alias}/{repo_alias}: {user_notification}")
|
||||
}
|
||||
|
||||
fn full_message(user_notification: &UserNotification) -> String {
|
||||
|
@ -71,7 +45,7 @@ fn full_message(user_notification: &UserNotification) -> String {
|
|||
let sha = commit.sha();
|
||||
let message = commit.message();
|
||||
[
|
||||
"CI Checks had Failed".to_string(),
|
||||
"CI Checks have Failed".to_string(),
|
||||
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
|
||||
format!("Commit:\n - {sha}\n - {message}"),
|
||||
"Log:".to_string(),
|
||||
|
|
|
@ -3,13 +3,17 @@ use actix::prelude::*;
|
|||
|
||||
use actix::Recipient;
|
||||
use anyhow::{Context, Result};
|
||||
use notify::event::ModifyKind;
|
||||
use notify::Watcher;
|
||||
use tracing::error;
|
||||
use tracing::info;
|
||||
use notify::{event::ModifyKind, Watcher};
|
||||
use tracing::{error, info};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[derive(Debug, Message)]
|
||||
#[rtype(result = "()")]
|
||||
|
@ -20,21 +24,26 @@ pub enum Error {
|
|||
#[error("io")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
pub async fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<()> {
|
||||
pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Arc<AtomicBool>> {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let shutdown = Arc::new(AtomicBool::default());
|
||||
|
||||
let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
|
||||
handler
|
||||
.watch(&path, notify::RecursiveMode::NonRecursive)
|
||||
.with_context(|| format!("Watch file: {path:?}"))?;
|
||||
info!("Watching: {:?}", path);
|
||||
async move {
|
||||
.with_context(|| format!("Watching: {path:?}"))?;
|
||||
let thread_shutdown = shutdown.clone();
|
||||
actix_rt::task::spawn_blocking(move || {
|
||||
loop {
|
||||
if thread_shutdown.load(Ordering::Relaxed) {
|
||||
drop(handler);
|
||||
break;
|
||||
}
|
||||
for result in rx.try_iter() {
|
||||
match result {
|
||||
Ok(event) => match event.kind {
|
||||
notify::EventKind::Modify(ModifyKind::Data(_)) => {
|
||||
tracing::info!("File modified");
|
||||
info!("File modified");
|
||||
recipient.do_send(FileUpdated);
|
||||
break;
|
||||
}
|
||||
|
@ -50,9 +59,8 @@ pub async fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Res
|
|||
}
|
||||
}
|
||||
}
|
||||
actix_rt::time::sleep(Duration::from_millis(1000)).await;
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
}
|
||||
}
|
||||
.await;
|
||||
Ok(())
|
||||
});
|
||||
Ok(shutdown)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
//
|
||||
use git_next_core::{
|
||||
git::{ForgeLike, RepoDetails},
|
||||
ForgeType,
|
||||
};
|
||||
use git_next_core::git::{ForgeLike, RepoDetails};
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
use git_next_forge_forgejo::ForgeJo;
|
||||
|
@ -19,10 +16,14 @@ impl Forge {
|
|||
pub fn create(repo_details: RepoDetails, net: Network) -> Box<dyn ForgeLike> {
|
||||
match repo_details.forge.forge_type() {
|
||||
#[cfg(feature = "forgejo")]
|
||||
ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
|
||||
git_next_core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
|
||||
#[cfg(feature = "github")]
|
||||
ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
|
||||
ForgeType::MockForge => unreachable!(),
|
||||
git_next_core::ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
|
||||
_ => {
|
||||
drop(repo_details);
|
||||
drop(net);
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
//
|
||||
#[cfg(any(feature = "forgejo", feature = "github"))]
|
||||
use super::*;
|
||||
|
||||
use git_next_core::{
|
||||
|
@ -7,31 +8,30 @@ use git_next_core::{
|
|||
GitDir, RepoConfigSource, StoragePathType,
|
||||
};
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
#[test]
|
||||
fn test_forgejo_name() {
|
||||
let net = Network::new_mock();
|
||||
let repo_details = given_repo_details(ForgeType::ForgeJo);
|
||||
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
|
||||
let forge = Forge::create(repo_details, net);
|
||||
assert_eq!(forge.name(), "forgejo");
|
||||
}
|
||||
|
||||
#[cfg(feature = "github")]
|
||||
#[test]
|
||||
fn test_github_name() {
|
||||
let net = Network::new_mock();
|
||||
let repo_details = given_repo_details(ForgeType::GitHub);
|
||||
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
|
||||
let forge = Forge::create(repo_details, net);
|
||||
assert_eq!(forge.name(), "github");
|
||||
}
|
||||
|
||||
fn given_fs() -> kxio::fs::FileSystem {
|
||||
kxio::fs::temp().unwrap_or_else(|e| {
|
||||
#[allow(dead_code)]
|
||||
fn given_repo_details(forge_type: git_next_core::ForgeType) -> RepoDetails {
|
||||
let fs = kxio::fs::temp().unwrap_or_else(|e| {
|
||||
println!("{e}");
|
||||
panic!("fs")
|
||||
})
|
||||
}
|
||||
|
||||
fn given_repo_details(forge_type: ForgeType) -> RepoDetails {
|
||||
let fs = given_fs();
|
||||
});
|
||||
git::repo_details(
|
||||
1,
|
||||
git::Generation::default(),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use anyhow::{Context, Result};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use kxio::fs::FileSystem;
|
||||
|
||||
pub fn run(fs: &FileSystem) -> Result<()> {
|
||||
|
|
|
@ -1,21 +1,26 @@
|
|||
//
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
mod alerts;
|
||||
mod file_watcher;
|
||||
mod forge;
|
||||
mod init;
|
||||
mod repo;
|
||||
mod server;
|
||||
mod webhook;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
mod tui;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod webhook;
|
||||
|
||||
use git_next_core::git;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use kxio::{fs, network::Network};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
@ -33,7 +38,12 @@ enum Command {
|
|||
#[derive(Parser, Debug)]
|
||||
enum Server {
|
||||
Init,
|
||||
Start,
|
||||
Start {
|
||||
/// Display a UI (experimental)
|
||||
#[cfg(feature = "tui")]
|
||||
#[arg(long, required = false)]
|
||||
ui: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
@ -43,18 +53,25 @@ fn main() -> Result<()> {
|
|||
let commands = Commands::parse();
|
||||
|
||||
match commands.command {
|
||||
Command::Init => {
|
||||
init::run(&fs)?;
|
||||
}
|
||||
Command::Init => init::run(&fs),
|
||||
Command::Server(server) => match server {
|
||||
Server::Init => {
|
||||
server::init(&fs)?;
|
||||
}
|
||||
Server::Start => {
|
||||
let sleep_duration = std::time::Duration::from_secs(10);
|
||||
server::start(fs, net, repository_factory, sleep_duration)?;
|
||||
}
|
||||
Server::Init => server::init(&fs),
|
||||
#[cfg(not(feature = "tui"))]
|
||||
Server::Start {} => server::start(
|
||||
false,
|
||||
fs,
|
||||
net,
|
||||
repository_factory,
|
||||
std::time::Duration::from_secs(10),
|
||||
),
|
||||
#[cfg(feature = "tui")]
|
||||
Server::Start { ui } => server::start(
|
||||
ui,
|
||||
fs,
|
||||
net,
|
||||
repository_factory,
|
||||
std::time::Duration::from_secs(10),
|
||||
),
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -17,21 +17,19 @@ use tracing::{info, instrument, warn};
|
|||
// advance next to the next commit towards the head of the dev branch
|
||||
#[instrument(fields(next), skip_all)]
|
||||
pub fn advance_next(
|
||||
next: &Commit,
|
||||
main: &Commit,
|
||||
dev_commit_history: &[Commit],
|
||||
repo_details: RepoDetails,
|
||||
commit: Option<Commit>,
|
||||
force: git_next_core::git::push::Force,
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: RepoConfig,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
message_token: MessageToken,
|
||||
) -> Result<MessageToken> {
|
||||
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
|
||||
let commit = commit.ok_or_else(|| Error::NextAtDev)?;
|
||||
validate_commit_message(commit.message())?;
|
||||
info!("Advancing next to commit '{}'", commit);
|
||||
reset(
|
||||
open_repository,
|
||||
&repo_details,
|
||||
repo_details,
|
||||
&repo_config.branches().next(),
|
||||
&commit.into(),
|
||||
&force,
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_core::RepoConfigSource;
|
||||
use git_next_core::{git, RepoConfigSource};
|
||||
|
||||
use tracing::warn;
|
||||
|
||||
use crate::repo::{
|
||||
branch::advance_main,
|
||||
do_send,
|
||||
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
|
||||
RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
branch::advance_main,
|
||||
do_send,
|
||||
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
|
||||
RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<AdvanceMain> for RepoActor {
|
||||
|
@ -26,24 +29,31 @@ impl Handler<AdvanceMain> for RepoActor {
|
|||
let repo_details = self.repo_details.clone();
|
||||
let addr = ctx.address();
|
||||
let message_token = self.message_token;
|
||||
let commit = msg.peel();
|
||||
|
||||
match advance_main(
|
||||
msg.unwrap(),
|
||||
&repo_details,
|
||||
&repo_config,
|
||||
&**open_repository,
|
||||
) {
|
||||
Err(err) => {
|
||||
warn!("advance main: {err}");
|
||||
self.update_tui(RepoUpdate::AdvancingMain {
|
||||
commit: commit.clone(),
|
||||
});
|
||||
|
||||
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
|
||||
warn!("advance main: {err}");
|
||||
self.alert_tui(format!("advance main: {err}"));
|
||||
} else {
|
||||
self.update_tui(RepoUpdate::MainUpdated);
|
||||
if let Some(open_repository) = &self.open_repository {
|
||||
match open_repository.fetch() {
|
||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
||||
}
|
||||
}
|
||||
Ok(()) => match repo_config.source() {
|
||||
match repo_config.source() {
|
||||
RepoConfigSource::Repo => {
|
||||
do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
|
||||
}
|
||||
RepoConfigSource::Server => {
|
||||
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::warn;
|
||||
use git_next_core::git;
|
||||
use tracing::{warn, Instrument};
|
||||
|
||||
use crate::repo::{
|
||||
branch::advance_next,
|
||||
do_send,
|
||||
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
||||
RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
branch::{advance_next, find_next_commit_on_dev},
|
||||
do_send,
|
||||
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
||||
RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<AdvanceNext> for RepoActor {
|
||||
|
@ -20,30 +24,52 @@ impl Handler<AdvanceNext> for RepoActor {
|
|||
let Some(open_repository) = &self.open_repository else {
|
||||
return;
|
||||
};
|
||||
|
||||
let AdvanceNextPayload {
|
||||
next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
} = msg.unwrap();
|
||||
} = msg.peel();
|
||||
let repo_details = self.repo_details.clone();
|
||||
let repo_config = repo_config.clone();
|
||||
let addr = ctx.address();
|
||||
|
||||
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
|
||||
if let Some(commit) = &commit {
|
||||
self.update_tui(RepoUpdate::AdvancingNext {
|
||||
commit: commit.clone(),
|
||||
force: force.clone(),
|
||||
});
|
||||
};
|
||||
match advance_next(
|
||||
&next,
|
||||
&main,
|
||||
&dev_commit_history,
|
||||
repo_details,
|
||||
commit,
|
||||
force,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&**open_repository,
|
||||
self.message_token,
|
||||
) {
|
||||
Ok(message_token) => {
|
||||
// pause to allow any CI checks to be started
|
||||
std::thread::sleep(self.sleep_duration);
|
||||
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||
self.update_tui(RepoUpdate::NextUpdated);
|
||||
match open_repository.fetch() {
|
||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
||||
}
|
||||
// INFO: pause to allow any CI checks to be started
|
||||
let sleep_duration = self.sleep_duration;
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("advance next: {err}");
|
||||
self.alert_tui(err.to_string());
|
||||
}
|
||||
Err(err) => warn!("advance next: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,13 @@ use actix::prelude::*;
|
|||
|
||||
use tracing::{debug, Instrument as _};
|
||||
|
||||
use crate::repo::{
|
||||
do_send,
|
||||
messages::{CheckCIStatus, ReceiveCIStatus},
|
||||
RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send,
|
||||
messages::{CheckCIStatus, ReceiveCIStatus},
|
||||
RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<CheckCIStatus> for RepoActor {
|
||||
|
@ -14,11 +17,13 @@ impl Handler<CheckCIStatus> for RepoActor {
|
|||
|
||||
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
||||
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
|
||||
|
||||
let addr = ctx.address();
|
||||
let forge = self.forge.duplicate();
|
||||
let next = msg.unwrap();
|
||||
let next = msg.peel();
|
||||
let log = self.log.clone();
|
||||
|
||||
self.update_tui(RepoUpdate::CheckingCI);
|
||||
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
||||
async move {
|
||||
let status = forge.commit_status(&next).await;
|
||||
|
|
|
@ -4,10 +4,13 @@ use actix::prelude::*;
|
|||
use git_next_core::git;
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use crate::repo::{
|
||||
do_send, logger,
|
||||
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
|
||||
RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, logger,
|
||||
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
|
||||
RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<CloneRepo> for RepoActor {
|
||||
|
@ -15,11 +18,13 @@ impl Handler<CloneRepo> for RepoActor {
|
|||
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
|
||||
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||
logger(self.log.as_ref(), "Handler: CloneRepo: start");
|
||||
self.update_tui(RepoUpdate::Opening);
|
||||
debug!("Handler: CloneRepo: start");
|
||||
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
||||
Ok(repository) => {
|
||||
logger(self.log.as_ref(), "open okay");
|
||||
debug!("open okay");
|
||||
self.update_tui(RepoUpdate::Opened);
|
||||
self.open_repository.replace(repository);
|
||||
if self.repo_details.repo_config.is_none() {
|
||||
do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref());
|
||||
|
@ -30,6 +35,7 @@ impl Handler<CloneRepo> for RepoActor {
|
|||
Err(err) => {
|
||||
logger(self.log.as_ref(), "open failed");
|
||||
warn!("Could not open repo: {err:?}");
|
||||
self.alert_tui(err.to_string());
|
||||
}
|
||||
}
|
||||
debug!("Handler: CloneRepo: finish");
|
||||
|
|
|
@ -5,10 +5,13 @@ use git_next_core::git::UserNotification;
|
|||
|
||||
use tracing::{debug, instrument, Instrument as _};
|
||||
|
||||
use crate::repo::{
|
||||
do_send, load,
|
||||
messages::{LoadConfigFromRepo, ReceiveRepoConfig},
|
||||
notify_user, RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, load,
|
||||
messages::{LoadConfigFromRepo, ReceiveRepoConfig},
|
||||
notify_user, RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<LoadConfigFromRepo> for RepoActor {
|
||||
|
@ -16,6 +19,7 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
|
|||
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
|
||||
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||
debug!("Handler: LoadConfigFromRepo: start");
|
||||
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
|
||||
let Some(open_repository) = &self.open_repository else {
|
||||
return;
|
||||
};
|
||||
|
|
|
@ -2,54 +2,73 @@
|
|||
use actix::prelude::*;
|
||||
|
||||
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
|
||||
use tracing::debug;
|
||||
use tracing::{debug, Instrument};
|
||||
|
||||
use crate::repo::{
|
||||
delay_send, do_send, logger,
|
||||
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
|
||||
notify_user, RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, logger,
|
||||
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
|
||||
notify_user, RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<ReceiveCIStatus> for RepoActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
||||
let log = self.log.clone();
|
||||
logger(log.as_ref(), "start: ReceiveCIStatus");
|
||||
logger(self.log.as_ref(), "start: ReceiveCIStatus");
|
||||
let (next, status) = msg.peel();
|
||||
self.update_tui(RepoUpdate::ReceiveCIStatus {
|
||||
status: status.clone(),
|
||||
});
|
||||
debug!(?status, "");
|
||||
let graph_log = graph::log(&self.repo_details);
|
||||
self.update_tui_log(graph_log.clone());
|
||||
|
||||
let addr = ctx.address();
|
||||
let (next, status) = msg.unwrap();
|
||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
||||
let repo_alias = self.repo_details.repo_alias.clone();
|
||||
let message_token = self.message_token;
|
||||
let sleep_duration = self.sleep_duration;
|
||||
|
||||
debug!(?status, "");
|
||||
match status {
|
||||
Status::Pass => {
|
||||
do_send(&addr, AdvanceMain::new(next), self.log.as_ref());
|
||||
}
|
||||
Status::Pending => {
|
||||
std::thread::sleep(sleep_duration);
|
||||
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
Status::Fail => {
|
||||
tracing::warn!("Checks have failed");
|
||||
|
||||
notify_user(
|
||||
self.notify_user_recipient.as_ref(),
|
||||
UserNotification::CICheckFailed {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
commit: next,
|
||||
log: graph::log(&self.repo_details),
|
||||
log: graph_log,
|
||||
},
|
||||
log.as_ref(),
|
||||
);
|
||||
delay_send(
|
||||
&addr,
|
||||
sleep_duration,
|
||||
ValidateRepo::new(message_token),
|
||||
self.log.as_ref(),
|
||||
);
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
debug!("sleeping before retrying...");
|
||||
logger(log.as_ref(), "before sleep");
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
logger(log.as_ref(), "after sleep");
|
||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,25 @@
|
|||
use actix::prelude::*;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::repo::{
|
||||
do_send,
|
||||
messages::{ReceiveRepoConfig, RegisterWebhook},
|
||||
RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send,
|
||||
messages::{ReceiveRepoConfig, RegisterWebhook},
|
||||
RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<ReceiveRepoConfig> for RepoActor {
|
||||
type Result = ();
|
||||
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
||||
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||
let repo_config = msg.unwrap();
|
||||
let repo_config = msg.peel();
|
||||
self.update_tui(RepoUpdate::ReceiveRepoConfig {
|
||||
repo_config: repo_config.clone(),
|
||||
});
|
||||
self.repo_details.repo_config.replace(repo_config);
|
||||
|
||||
self.update_tui_branches();
|
||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{debug, Instrument as _};
|
||||
use tracing::{debug, error, Instrument as _};
|
||||
|
||||
use crate::repo::{
|
||||
do_send,
|
||||
messages::{RegisterWebhook, WebhookRegistered},
|
||||
notify_user, RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send,
|
||||
messages::{RegisterWebhook, WebhookRegistered},
|
||||
notify_user, RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
use git_next_core::git::UserNotification;
|
||||
|
@ -25,11 +28,12 @@ impl Handler<RegisterWebhook> for RepoActor {
|
|||
let addr = ctx.address();
|
||||
let notify_user_recipient = self.notify_user_recipient.clone();
|
||||
let log = self.log.clone();
|
||||
self.update_tui(RepoUpdate::RegisteringWebhook);
|
||||
debug!("registering webhook");
|
||||
async move {
|
||||
match forge.register_webhook(&repo_listen_url).await {
|
||||
Ok(registered_webhook) => {
|
||||
debug!(?registered_webhook, "");
|
||||
debug!(?registered_webhook, "webhook registered");
|
||||
do_send(
|
||||
&addr,
|
||||
WebhookRegistered::from(registered_webhook),
|
||||
|
@ -37,6 +41,7 @@ impl Handler<RegisterWebhook> for RepoActor {
|
|||
);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(?err, "failed to register webhook");
|
||||
notify_user(
|
||||
notify_user_recipient.as_ref(),
|
||||
UserNotification::WebhookRegistration {
|
||||
|
@ -52,6 +57,8 @@ impl Handler<RegisterWebhook> for RepoActor {
|
|||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
} else {
|
||||
self.alert_tui("already have a webhook id - cant register webhook");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,25 +3,30 @@ use actix::prelude::*;
|
|||
|
||||
use tracing::{debug, warn, Instrument as _};
|
||||
|
||||
use crate::repo::{messages::UnRegisterWebhook, RepoActor};
|
||||
use crate::{
|
||||
repo::{messages::UnRegisterWebhook, RepoActor},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<UnRegisterWebhook> for RepoActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
||||
if let Some(webhook_id) = self.webhook_id.take() {
|
||||
let forge = self.forge.duplicate();
|
||||
debug!("unregistering webhook");
|
||||
async move {
|
||||
match forge.unregister_webhook(&webhook_id).await {
|
||||
Ok(()) => debug!("unregistered webhook"),
|
||||
Err(err) => warn!(?err, "unregistering webhook"),
|
||||
}
|
||||
let Some(webhook_id) = self.webhook_id.take() else {
|
||||
return;
|
||||
};
|
||||
self.update_tui(RepoUpdate::UnregisteringWebhook);
|
||||
let forge = self.forge.duplicate();
|
||||
debug!("unregistering webhook");
|
||||
async move {
|
||||
match forge.unregister_webhook(&webhook_id).await {
|
||||
Ok(()) => debug!("unregistered webhook"),
|
||||
Err(err) => warn!(?err, "unregistering webhook"),
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
debug!("unregistering webhook done");
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
debug!("unregistering webhook done");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use tracing::{debug, instrument, Instrument as _};
|
||||
use tracing::{info, instrument, Instrument as _};
|
||||
|
||||
use crate::repo::{
|
||||
do_send, logger,
|
||||
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
|
||||
notify_user, RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, logger,
|
||||
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
|
||||
notify_user, RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
use git_next_core::git::validation::positions::{validate, Error, Positions};
|
||||
use git_next_core::git::{
|
||||
push::Force,
|
||||
validation::positions::{validate, Error, Positions},
|
||||
UserNotification,
|
||||
};
|
||||
|
||||
impl Handler<ValidateRepo> for RepoActor {
|
||||
type Result = ();
|
||||
|
@ -19,7 +26,7 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
logger(self.log.as_ref(), "start: ValidateRepo");
|
||||
|
||||
// Message Token - make sure we are only triggered for the latest/current token
|
||||
match self.token_status(msg.unwrap()) {
|
||||
match self.token_status(msg.peel()) {
|
||||
TokenStatus::Current => {} // do nothing
|
||||
TokenStatus::Expired => {
|
||||
logger(
|
||||
|
@ -41,30 +48,44 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
format!("accepted token: {}", self.message_token),
|
||||
);
|
||||
|
||||
self.update_tui(RepoUpdate::ValidateRepo);
|
||||
|
||||
// Repository positions
|
||||
let Some(ref open_repository) = self.open_repository else {
|
||||
logger(self.log.as_ref(), "no open repository");
|
||||
self.alert_tui("repo not open");
|
||||
return;
|
||||
};
|
||||
logger(self.log.as_ref(), "have open repository");
|
||||
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
||||
logger(self.log.as_ref(), "no repo config");
|
||||
self.alert_tui("no repo config");
|
||||
return;
|
||||
};
|
||||
logger(self.log.as_ref(), "have repo config");
|
||||
|
||||
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
||||
Ok(Positions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history,
|
||||
next_is_valid,
|
||||
}) => {
|
||||
debug!(%main, %next, %dev, "positions");
|
||||
Ok((
|
||||
Positions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history,
|
||||
next_is_valid,
|
||||
},
|
||||
git_log,
|
||||
)) => {
|
||||
info!(%main, %next, %dev, "positions");
|
||||
self.update_tui_log(git_log);
|
||||
if next_is_valid && next != main {
|
||||
info!("Checking CI");
|
||||
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
|
||||
} else if next != dev {
|
||||
info!("Advance next");
|
||||
self.update_tui(RepoUpdate::AdvancingNext {
|
||||
commit: next.clone(),
|
||||
force: Force::No,
|
||||
});
|
||||
do_send(
|
||||
&ctx.address(),
|
||||
AdvanceNext::new(AdvanceNextPayload {
|
||||
|
@ -75,17 +96,20 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
self.log.as_ref(),
|
||||
);
|
||||
} else {
|
||||
// do nothing
|
||||
info!("do nothing");
|
||||
self.update_tui(RepoUpdate::Okay { main, next, dev });
|
||||
}
|
||||
}
|
||||
Err(Error::Retryable(message)) => {
|
||||
info!(?message, "Retryable");
|
||||
self.alert_tui(format!("retryable: {message}"));
|
||||
logger(self.log.as_ref(), message);
|
||||
let addr = ctx.address();
|
||||
let message_token = self.message_token;
|
||||
let sleep_duration = self.sleep_duration;
|
||||
let log = self.log.clone();
|
||||
async move {
|
||||
debug!("sleeping before retrying...");
|
||||
info!("sleeping before retrying...");
|
||||
logger(log.as_ref(), "before sleep");
|
||||
actix_rt::time::sleep(sleep_duration).await;
|
||||
logger(log.as_ref(), "after sleep");
|
||||
|
@ -95,12 +119,23 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
.into_actor(self)
|
||||
.wait(ctx);
|
||||
}
|
||||
Err(Error::UserIntervention(user_notification)) => notify_user(
|
||||
self.notify_user_recipient.as_ref(),
|
||||
user_notification,
|
||||
self.log.as_ref(),
|
||||
),
|
||||
Err(Error::UserIntervention(user_notification)) => {
|
||||
info!(?user_notification, "User Intervention");
|
||||
self.alert_tui(format!("USER INTERVENTION: {user_notification}"));
|
||||
if let UserNotification::CICheckFailed { log, .. }
|
||||
| UserNotification::DevNotBasedOnMain { log, .. } = &user_notification
|
||||
{
|
||||
self.update_tui_log(log.clone());
|
||||
}
|
||||
notify_user(
|
||||
self.notify_user_recipient.as_ref(),
|
||||
user_notification,
|
||||
self.log.as_ref(),
|
||||
);
|
||||
}
|
||||
Err(Error::NonRetryable(message)) => {
|
||||
info!(?message, "NonRetryable");
|
||||
self.alert_tui(format!("Error: {message}"));
|
||||
logger(self.log.as_ref(), message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,13 @@ use actix::prelude::*;
|
|||
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use crate::repo::{
|
||||
do_send, logger,
|
||||
messages::{ValidateRepo, WebhookNotification},
|
||||
ActorLog, RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, logger,
|
||||
messages::{ValidateRepo, WebhookNotification},
|
||||
ActorLog, RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
use git_next_core::{
|
||||
|
@ -52,6 +55,10 @@ impl Handler<WebhookNotification> for RepoActor {
|
|||
return;
|
||||
}
|
||||
Some(Branch::Main) => {
|
||||
self.update_tui(RepoUpdate::WebhookReceived {
|
||||
branch: Branch::Main,
|
||||
push: push.clone(),
|
||||
});
|
||||
if handle_push(
|
||||
push,
|
||||
&config.branches().main(),
|
||||
|
@ -64,6 +71,10 @@ impl Handler<WebhookNotification> for RepoActor {
|
|||
};
|
||||
}
|
||||
Some(Branch::Next) => {
|
||||
self.update_tui(RepoUpdate::WebhookReceived {
|
||||
branch: Branch::Next,
|
||||
push: push.clone(),
|
||||
});
|
||||
if handle_push(
|
||||
push,
|
||||
&config.branches().next(),
|
||||
|
@ -76,6 +87,10 @@ impl Handler<WebhookNotification> for RepoActor {
|
|||
};
|
||||
}
|
||||
Some(Branch::Dev) => {
|
||||
self.update_tui(RepoUpdate::WebhookReceived {
|
||||
branch: Branch::Dev,
|
||||
push: push.clone(),
|
||||
});
|
||||
if handle_push(
|
||||
push,
|
||||
&config.branches().dev(),
|
||||
|
@ -135,7 +150,7 @@ fn handle_push(
|
|||
last_commit: &mut Option<Commit>,
|
||||
log: Option<&ActorLog>,
|
||||
) -> Result<(), ()> {
|
||||
logger(log, "message is for dev branch");
|
||||
logger(log, format!("message is for {branch} branch"));
|
||||
let commit = Commit::from(push);
|
||||
if last_commit.as_ref() == Some(&commit) {
|
||||
logger(log, format!("not a new commit on {branch}"));
|
||||
|
|
|
@ -2,16 +2,20 @@
|
|||
use actix::prelude::*;
|
||||
use tracing::instrument;
|
||||
|
||||
use crate::repo::{
|
||||
do_send,
|
||||
messages::{ValidateRepo, WebhookRegistered},
|
||||
RepoActor,
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send,
|
||||
messages::{ValidateRepo, WebhookRegistered},
|
||||
RepoActor,
|
||||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
impl Handler<WebhookRegistered> for RepoActor {
|
||||
type Result = ();
|
||||
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
|
||||
fn handle(&mut self, msg: WebhookRegistered, ctx: &mut Self::Context) -> Self::Result {
|
||||
self.update_tui(RepoUpdate::RegisteredWebhook);
|
||||
self.webhook_id.replace(msg.webhook_id().clone());
|
||||
self.webhook_auth.replace(msg.webhook_auth().clone());
|
||||
do_send(
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use crate::alerts::messages::NotifyUser;
|
||||
use crate::{
|
||||
alerts::messages::NotifyUser,
|
||||
server::{actor::messages::RepoUpdate, ServerActor},
|
||||
};
|
||||
use derive_more::Deref;
|
||||
use kxio::network::Network;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn, Instrument};
|
||||
use tracing::{info, instrument, warn, Instrument};
|
||||
|
||||
use git_next_core::{
|
||||
git::{
|
||||
|
@ -59,6 +61,7 @@ pub struct RepoActor {
|
|||
forge: Box<dyn git::ForgeLike>,
|
||||
log: Option<ActorLog>,
|
||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||
server_addr: Option<Addr<ServerActor>>,
|
||||
}
|
||||
impl RepoActor {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -71,6 +74,7 @@ impl RepoActor {
|
|||
repository_factory: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||
server_addr: Option<Addr<ServerActor>>,
|
||||
) -> Self {
|
||||
let message_token = messages::MessageToken::default();
|
||||
Self {
|
||||
|
@ -90,12 +94,56 @@ impl RepoActor {
|
|||
sleep_duration,
|
||||
log: None,
|
||||
notify_user_recipient,
|
||||
server_addr,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_tui_branches(&self) {
|
||||
if cfg!(feature = "tui") {
|
||||
use crate::server::actor::messages::RepoUpdate;
|
||||
let Some(repo_config) = &self.repo_details.repo_config else {
|
||||
return;
|
||||
};
|
||||
let branches = repo_config.branches().clone();
|
||||
self.update_tui(RepoUpdate::Branches { branches });
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn update_tui_log(&self, log: git::graph::Log) {
|
||||
if cfg!(feature = "tui") {
|
||||
self.update_tui(RepoUpdate::Log { log });
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn alert_tui(&self, alert: impl Into<String>) {
|
||||
if cfg!(feature = "tui") {
|
||||
self.update_tui(RepoUpdate::Alert {
|
||||
alert: alert.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn update_tui(&self, repo_update: RepoUpdate) {
|
||||
if cfg!(feature = "tui") {
|
||||
let Some(server_addr) = &self.server_addr else {
|
||||
return;
|
||||
};
|
||||
|
||||
let update = crate::server::actor::messages::ServerUpdate::RepoUpdate {
|
||||
forge_alias: self.repo_details.forge.forge_alias().clone(),
|
||||
repo_alias: self.repo_details.repo_alias.clone(),
|
||||
repo_update,
|
||||
};
|
||||
server_addr.do_send(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Actor for RepoActor {
|
||||
type Context = Context<Self>;
|
||||
#[tracing::instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
|
||||
#[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||
tracing::debug!("stopping");
|
||||
info!("Checking webhook");
|
||||
|
@ -119,19 +167,6 @@ impl Actor for RepoActor {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn delay_send<M>(addr: &Addr<RepoActor>, delay: Duration, msg: M, log: Option<&ActorLog>)
|
||||
where
|
||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||
RepoActor: actix::Handler<M>,
|
||||
<M as actix::Message>::Result: Send,
|
||||
{
|
||||
let log_message = format!("send-after-delay: {msg:?}");
|
||||
tracing::debug!(log_message);
|
||||
logger(log, log_message);
|
||||
std::thread::sleep(delay);
|
||||
do_send(addr, msg, log);
|
||||
}
|
||||
|
||||
pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>)
|
||||
where
|
||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||
|
@ -139,7 +174,7 @@ where
|
|||
<M as actix::Message>::Result: Send,
|
||||
{
|
||||
let log_message = format!("send: {msg:?}");
|
||||
tracing::debug!(log_message);
|
||||
info!(log_message);
|
||||
logger(log, log_message);
|
||||
if cfg!(not(test)) {
|
||||
// #[cfg(not(test))]
|
||||
|
|
|
@ -1,6 +1,28 @@
|
|||
//
|
||||
use crate::repo::branch::find_next_commit_on_dev;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn advance_next_sut(
|
||||
next: &Commit,
|
||||
main: &Commit,
|
||||
dev_commit_history: &[Commit],
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: RepoConfig,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
message_token: MessageToken,
|
||||
) -> branch::Result<MessageToken> {
|
||||
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
|
||||
branch::advance_next(
|
||||
commit,
|
||||
force,
|
||||
repo_details,
|
||||
repo_config,
|
||||
open_repository,
|
||||
message_token,
|
||||
)
|
||||
}
|
||||
|
||||
mod when_at_dev {
|
||||
// next and dev branches are the same
|
||||
use super::*;
|
||||
|
@ -16,11 +38,11 @@ mod when_at_dev {
|
|||
// no on_push defined - so any call to push will cause an error
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Err(err) = branch::advance_next(
|
||||
Err(err) = advance_next_sut(
|
||||
&next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
repo_details,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
|
@ -51,11 +73,11 @@ mod can_advance {
|
|||
// no on_push defined - so any call to push will cause an error
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Err(err) = branch::advance_next(
|
||||
Err(err) = advance_next_sut(
|
||||
&next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
repo_details,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
|
@ -82,11 +104,11 @@ mod can_advance {
|
|||
// no on_push defined - so any call to push will cause an error
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Err(err) = branch::advance_next(
|
||||
Err(err) = advance_next_sut(
|
||||
&next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
repo_details,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
|
@ -122,11 +144,11 @@ mod can_advance {
|
|||
expect::push(&mut open_repository, Err(git::push::Error::Lock));
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Err(err) = branch::advance_next(
|
||||
Err(err) = advance_next_sut(
|
||||
&next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
repo_details,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
|
@ -154,11 +176,11 @@ mod can_advance {
|
|||
expect::push_ok(&mut open_repository);
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Ok(mt) = branch::advance_next(
|
||||
Ok(mt) = advance_next_sut(
|
||||
&next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
repo_details,
|
||||
&repo_details,
|
||||
repo_config,
|
||||
&open_repository,
|
||||
message_token,
|
||||
|
|
|
@ -69,6 +69,22 @@ pub fn a_name() -> String {
|
|||
generate(5)
|
||||
}
|
||||
|
||||
pub fn maybe_a_number() -> Option<u32> {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
if Rng::gen_ratio(&mut rng, 1, 2) {
|
||||
Some(a_number())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn a_number() -> u32 {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
rng.gen_range(0..100)
|
||||
}
|
||||
|
||||
pub fn a_webhook_id() -> WebhookId {
|
||||
WebhookId::new(a_name())
|
||||
}
|
||||
|
@ -89,6 +105,7 @@ pub fn a_forge_config() -> ForgeConfig {
|
|||
a_name(),
|
||||
a_name(),
|
||||
a_name(),
|
||||
maybe_a_number(),
|
||||
BTreeMap::default(), // no repos
|
||||
)
|
||||
}
|
||||
|
@ -195,6 +212,7 @@ pub fn a_repo_actor(
|
|||
repository_factory,
|
||||
std::time::Duration::from_nanos(1),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.with_log(actors_log),
|
||||
log,
|
||||
|
|
|
@ -25,6 +25,11 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
|
|||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_, _, _, _| Ok(()));
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
|
@ -70,6 +75,11 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
|
|||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_, _, _, _| Ok(()));
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
|
@ -83,10 +93,6 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
|
|||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: ValidateRepo")));
|
||||
})?;
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::repo::messages::AdvanceNextPayload;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[actix::test]
|
||||
#[test_log::test(actix::test)]
|
||||
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -26,6 +28,11 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
|||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|_, _, _, _| Ok(()));
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.in_sequence(&mut seq)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
|
@ -41,14 +48,11 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
|||
},
|
||||
))
|
||||
.await?;
|
||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
||||
System::current().stop();
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: ValidateRepo")));
|
||||
})?;
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -43,6 +43,10 @@ async fn should_open() -> TestResult {
|
|||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
||||
// factory opens a repository
|
||||
let mut repository_factory = MockRepositoryFactory::new();
|
||||
|
@ -79,6 +83,10 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
|
|||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let _repo_config = repo_details.repo_config.take().unwrap();
|
||||
|
||||
|
@ -106,6 +114,10 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
|
|||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
#[allow(clippy::unwrap_used)]
|
||||
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
||||
|
||||
|
@ -129,6 +141,10 @@ async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
|
|||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Ok(()));
|
||||
|
||||
given::has_remote_defaults(
|
||||
&mut open_repository,
|
||||
|
@ -158,15 +174,10 @@ async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
|
|||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
||||
given::has_remote_defaults(
|
||||
&mut open_repository,
|
||||
HashMap::from([
|
||||
(Direction::Push, repo_details.remote_url()),
|
||||
(Direction::Fetch, None),
|
||||
]),
|
||||
);
|
||||
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
.return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
|
||||
let mut repository_factory = MockRepositoryFactory::new();
|
||||
expect::open_repository(&mut repository_factory, open_repository);
|
||||
fs.dir_create(&repo_details.gitdir)?;
|
||||
|
|
|
@ -32,7 +32,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[test_log::test(actix::test)]
|
||||
async fn should_register_webhook() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -54,10 +54,6 @@ async fn should_register_webhook() -> TestResult {
|
|||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: RegisterWebhook")));
|
||||
})?;
|
||||
log.require_message_containing("send: RegisterWebhook")?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::time::Duration;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
|
@ -49,15 +51,12 @@ async fn when_pending_should_recheck_ci_status() -> TestResult {
|
|||
git::forge::commit::Status::Pending,
|
||||
)))
|
||||
.await?;
|
||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
||||
System::current().stop();
|
||||
|
||||
//then
|
||||
tracing::debug!(?log, "");
|
||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||
assert!(l
|
||||
.iter()
|
||||
.any(|message| message.contains("send: ValidateRepo")));
|
||||
})?;
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -79,11 +78,11 @@ async fn when_fail_should_recheck_after_delay() -> TestResult {
|
|||
git::forge::commit::Status::Fail,
|
||||
)))
|
||||
.await?;
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await;
|
||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
||||
System::current().stop();
|
||||
|
||||
//then
|
||||
log.require_message_containing("send-after-delay: ValidateRepo")?;
|
||||
log.require_message_containing("send: ValidateRepo")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
mod file_updated;
|
||||
mod receive_app_config;
|
||||
mod receive_valid_app_config;
|
||||
mod server_update;
|
||||
mod shutdown;
|
||||
mod shutdown_trigger;
|
||||
mod subscribe_updates;
|
||||
|
|
|
@ -25,7 +25,7 @@ impl Handler<ReceiveAppConfig> for ServerActor {
|
|||
|
||||
self.do_send(
|
||||
ReceiveValidAppConfig::new(ValidAppConfig::new(
|
||||
msg.unwrap(),
|
||||
msg.peel(),
|
||||
socket_addr,
|
||||
server_storage,
|
||||
)),
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
alerts::messages::UpdateShout,
|
||||
repo::{messages::CloneRepo, RepoActor},
|
||||
server::actor::{
|
||||
messages::{ReceiveValidAppConfig, ValidAppConfig},
|
||||
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
|
||||
ServerActor,
|
||||
},
|
||||
webhook::{
|
||||
|
@ -21,12 +21,12 @@ use crate::{
|
|||
impl Handler<ReceiveValidAppConfig> for ServerActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ReceiveValidAppConfig, _ctx: &mut Self::Context) -> Self::Result {
|
||||
fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||
let ValidAppConfig {
|
||||
app_config,
|
||||
socket_address,
|
||||
storage: server_storage,
|
||||
} = msg.unwrap();
|
||||
} = msg.peel();
|
||||
// shutdown any existing webhook actor
|
||||
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
|
||||
webhook_actor_addr.do_send(ShutdownWebhook);
|
||||
|
@ -37,6 +37,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
|||
let webhook_router = WebhookRouterActor::default().start();
|
||||
let listen_url = app_config.listen().url();
|
||||
let notify_user_recipient = self.alerts.clone().recipient();
|
||||
let server_addr = Some(ctx.address());
|
||||
// Forge Actors
|
||||
for (forge_alias, forge_config) in app_config.forges() {
|
||||
let repo_actors = self
|
||||
|
@ -46,6 +47,7 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
|||
&server_storage,
|
||||
listen_url,
|
||||
¬ify_user_recipient,
|
||||
server_addr.clone(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(start_repo_actor)
|
||||
|
@ -69,7 +71,17 @@ impl Handler<ReceiveValidAppConfig> for ServerActor {
|
|||
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
||||
self.webhook_actor_addr.replace(webhook_actor_addr);
|
||||
let shout = app_config.shout().clone();
|
||||
self.app_config.replace(app_config);
|
||||
self.app_config.replace(app_config.clone());
|
||||
self.do_send(
|
||||
ServerUpdate::AppConfigLoaded {
|
||||
app_config: ValidAppConfig {
|
||||
app_config,
|
||||
socket_address,
|
||||
storage: server_storage,
|
||||
},
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
self.alerts.do_send(UpdateShout::new(shout));
|
||||
}
|
||||
}
|
||||
|
|
14
crates/cli/src/server/actor/handlers/server_update.rs
Normal file
14
crates/cli/src/server/actor/handlers/server_update.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
use actix::Handler;
|
||||
|
||||
//
|
||||
use crate::server::{actor::messages::ServerUpdate, ServerActor};
|
||||
|
||||
impl Handler<ServerUpdate> for ServerActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.subscribers.iter().for_each(move |subscriber| {
|
||||
subscriber.do_send(msg.clone());
|
||||
});
|
||||
}
|
||||
}
|
12
crates/cli/src/server/actor/handlers/shutdown_trigger.rs
Normal file
12
crates/cli/src/server/actor/handlers/shutdown_trigger.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
use actix::Handler;
|
||||
|
||||
use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
|
||||
|
||||
impl Handler<ShutdownTrigger> for ServerActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.shutdown_trigger.replace(msg.peel());
|
||||
}
|
||||
}
|
10
crates/cli/src/server/actor/handlers/subscribe_updates.rs
Normal file
10
crates/cli/src/server/actor/handlers/subscribe_updates.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use crate::server::actor::{messages::SubscribeToUpdates, ServerActor};
|
||||
|
||||
//
|
||||
impl actix::Handler<SubscribeToUpdates> for ServerActor {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: SubscribeToUpdates, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.subscribers.push(msg.peel());
|
||||
}
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
//-
|
||||
//
|
||||
use actix::{Message, Recipient};
|
||||
use derive_more::Constructor;
|
||||
|
||||
use git_next_core::{
|
||||
git::{self, forge::commit::Status, graph::Log, Commit},
|
||||
message,
|
||||
server::{AppConfig, Storage},
|
||||
webhook::{push::Branch, Push},
|
||||
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
||||
};
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
@ -33,3 +37,77 @@ message!(
|
|||
);
|
||||
|
||||
message!(Shutdown, "Notification to shutdown the server actor");
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub enum ServerUpdate {
|
||||
/// List of all configured forges and aliases
|
||||
AppConfigLoaded { app_config: ValidAppConfig },
|
||||
|
||||
RepoUpdate {
|
||||
forge_alias: ForgeAlias,
|
||||
repo_alias: RepoAlias,
|
||||
repo_update: RepoUpdate,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RepoUpdate {
|
||||
Branches {
|
||||
branches: RepoBranches,
|
||||
},
|
||||
Log {
|
||||
log: Log,
|
||||
},
|
||||
ValidateRepo,
|
||||
Okay {
|
||||
main: Commit,
|
||||
next: Commit,
|
||||
dev: Commit,
|
||||
},
|
||||
Alert {
|
||||
alert: String,
|
||||
},
|
||||
CheckingCI,
|
||||
AdvancingNext {
|
||||
commit: git::Commit,
|
||||
force: git::push::Force,
|
||||
},
|
||||
AdvancingMain {
|
||||
commit: git::Commit,
|
||||
},
|
||||
Opening,
|
||||
LoadingConfigFromRepo,
|
||||
ReceiveCIStatus {
|
||||
status: Status,
|
||||
},
|
||||
ReceiveRepoConfig {
|
||||
repo_config: RepoConfig,
|
||||
},
|
||||
RegisteringWebhook,
|
||||
UnregisteringWebhook,
|
||||
WebhookReceived {
|
||||
branch: Branch,
|
||||
push: Push,
|
||||
},
|
||||
RegisteredWebhook,
|
||||
Opened,
|
||||
NextUpdated,
|
||||
MainUpdated,
|
||||
}
|
||||
|
||||
message!(
|
||||
SubscribeToUpdates,
|
||||
Recipient<ServerUpdate>,
|
||||
"Subscribe to receive updates from the server"
|
||||
);
|
||||
|
||||
/// Sends a channel to be used to shutdown the server
|
||||
#[derive(Message, Constructor)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
|
||||
impl ShutdownTrigger {
|
||||
pub fn peel(self) -> std::sync::mpsc::Sender<String> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use messages::ReceiveAppConfig;
|
||||
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
|
||||
use tracing::error;
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -58,6 +58,9 @@ pub struct ServerActor {
|
|||
sleep_duration: std::time::Duration,
|
||||
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
||||
|
||||
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>,
|
||||
subscribers: Vec<Recipient<ServerUpdate>>,
|
||||
|
||||
// testing
|
||||
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
||||
}
|
||||
|
@ -82,6 +85,8 @@ impl ServerActor {
|
|||
net,
|
||||
alerts,
|
||||
repository_factory: repo,
|
||||
shutdown_trigger: None,
|
||||
subscribers: Vec::default(),
|
||||
sleep_duration,
|
||||
repo_actors: BTreeMap::new(),
|
||||
message_log: None,
|
||||
|
@ -115,6 +120,7 @@ impl ServerActor {
|
|||
server_storage: &Storage,
|
||||
listen_url: &ListenUrl,
|
||||
notify_user_recipient: &Recipient<NotifyUser>,
|
||||
server_addr: Option<Addr<Self>>,
|
||||
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
||||
let span =
|
||||
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
||||
|
@ -122,8 +128,13 @@ impl ServerActor {
|
|||
let _guard = span.enter();
|
||||
tracing::info!("Creating Forge");
|
||||
let mut repos = vec![];
|
||||
let creator =
|
||||
self.create_actor(forge_name, forge_config.clone(), server_storage, listen_url);
|
||||
let creator = self.create_actor(
|
||||
forge_name,
|
||||
forge_config.clone(),
|
||||
server_storage,
|
||||
listen_url,
|
||||
server_addr,
|
||||
);
|
||||
for (repo_alias, server_repo_config) in forge_config.repos() {
|
||||
let forge_repo = creator((
|
||||
repo_alias,
|
||||
|
@ -145,6 +156,7 @@ impl ServerActor {
|
|||
forge_config: ForgeConfig,
|
||||
server_storage: &Storage,
|
||||
listen_url: &ListenUrl,
|
||||
server_addr: Option<Addr<Self>>,
|
||||
) -> impl Fn(
|
||||
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
||||
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
||||
|
@ -191,6 +203,7 @@ impl ServerActor {
|
|||
repository_factory.duplicate(),
|
||||
sleep_duration,
|
||||
Some(notify_user_recipient),
|
||||
server_addr.clone(),
|
||||
);
|
||||
(forge_name.clone(), repo_alias, actor)
|
||||
}
|
||||
|
@ -217,10 +230,15 @@ impl ServerActor {
|
|||
}
|
||||
|
||||
/// Attempts to gracefully shutdown the server before stopping the system.
|
||||
fn abort(&self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
|
||||
tracing::error!("Aborting: {}", message.into());
|
||||
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
|
||||
self.do_send(crate::server::actor::messages::Shutdown, ctx);
|
||||
System::current().stop_with_code(1);
|
||||
if let Some(t) = self.shutdown_trigger.take() {
|
||||
let _ = t.send(message.into());
|
||||
} else {
|
||||
error!("{}", message.into());
|
||||
self.do_send(Shutdown, ctx);
|
||||
// System::current().stop_with_code(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn do_send<M>(&self, msg: M, ctx: &<Self as actix::Actor>::Context)
|
||||
|
|
|
@ -1,23 +1,32 @@
|
|||
//
|
||||
mod actor;
|
||||
pub mod actor;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use actix::prelude::*;
|
||||
use actix_rt::signal;
|
||||
use actor::messages::ShutdownTrigger;
|
||||
|
||||
use crate::{
|
||||
alerts::{AlertsActor, History},
|
||||
file_watcher::{watch_file, FileUpdated},
|
||||
};
|
||||
use actor::ServerActor;
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub use actor::ServerActor;
|
||||
|
||||
use git_next_core::git::RepositoryFactory;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use color_eyre::{eyre::Context, Result};
|
||||
use kxio::{fs::FileSystem, network::Network};
|
||||
use tracing::info;
|
||||
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{atomic::Ordering, mpsc::channel, Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
|
@ -37,45 +46,131 @@ pub fn init(fs: &FileSystem) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn start(
|
||||
ui: bool,
|
||||
fs: FileSystem,
|
||||
net: Network,
|
||||
repo: Box<dyn RepositoryFactory>,
|
||||
sleep_duration: std::time::Duration,
|
||||
) -> Result<()> {
|
||||
init_logging();
|
||||
if ui {
|
||||
#[cfg(feature = "tui")]
|
||||
{
|
||||
crate::tui::logging::initialize_logging()?;
|
||||
}
|
||||
} else {
|
||||
init_logging();
|
||||
}
|
||||
|
||||
let shutdown_message_holder: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
|
||||
let shutdown_message_holder_exec = shutdown_message_holder.clone();
|
||||
let file_watcher_err_holder: Arc<RwLock<Option<anyhow::Error>>> = Arc::new(RwLock::new(None));
|
||||
let file_watcher_err_holder_exec = file_watcher_err_holder.clone();
|
||||
let execution = async move {
|
||||
info!("Starting Alert Dispatcher...");
|
||||
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
|
||||
|
||||
info!("Starting Server...");
|
||||
let server = ServerActor::new(
|
||||
fs.clone(),
|
||||
net.clone(),
|
||||
alerts_addr.clone(),
|
||||
repo,
|
||||
sleep_duration,
|
||||
)
|
||||
.start();
|
||||
server.do_send(FileUpdated);
|
||||
let server =
|
||||
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
|
||||
|
||||
info!("Starting File Watcher...");
|
||||
#[allow(clippy::expect_used)]
|
||||
watch_file("git-next-server.toml".into(), server.clone().recipient())
|
||||
.await
|
||||
.expect("file watcher");
|
||||
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
|
||||
let fw_shutdown = match watch_file {
|
||||
Ok(fw_shutdown) => fw_shutdown,
|
||||
Err(err) => {
|
||||
// shutdown now
|
||||
server.do_send(crate::server::actor::messages::Shutdown);
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
System::current().stop();
|
||||
let _ = file_watcher_err_holder_exec
|
||||
.write()
|
||||
.map(|mut o| o.replace(err));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Server running - Press Ctrl-C to stop...");
|
||||
let _ = actix_rt::signal::ctrl_c().await;
|
||||
info!("Ctrl-C received, shutting down...");
|
||||
let (tx_shutdown, rx_shutdown) = channel::<String>();
|
||||
if ui {
|
||||
#[cfg(feature = "tui")]
|
||||
{
|
||||
use crate::server::actor::messages::SubscribeToUpdates;
|
||||
use crate::tui;
|
||||
|
||||
let tui_addr = tui::Tui::new(tx_shutdown.clone()).start();
|
||||
server.do_send(SubscribeToUpdates::new(tui_addr.clone().recipient()));
|
||||
server.do_send(ShutdownTrigger::new(tx_shutdown));
|
||||
server.do_send(FileUpdated); // update file after ui subscription in place
|
||||
loop {
|
||||
let _ = tui_addr.send(tui::Tick).await;
|
||||
if let Ok(message) = rx_shutdown.try_recv() {
|
||||
let _ = shutdown_message_holder_exec
|
||||
.write()
|
||||
.map(|mut o| o.replace(message));
|
||||
break;
|
||||
}
|
||||
actix_rt::time::sleep(Duration::from_millis(16)).await;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server.do_send(ShutdownTrigger::new(tx_shutdown.clone()));
|
||||
server.do_send(FileUpdated);
|
||||
|
||||
info!("Server running - Press Ctrl-C to stop...");
|
||||
tokio::select! {
|
||||
_r = signal::ctrl_c() => {
|
||||
info!("Ctrl-C received, shutting down...");
|
||||
}
|
||||
_x = async move {
|
||||
loop{
|
||||
if let Ok(message) = rx_shutdown.try_recv() {
|
||||
let _ = shutdown_message_holder_exec
|
||||
.write()
|
||||
.map(|mut o| o.replace(message));
|
||||
break;
|
||||
}
|
||||
actix_rt::task::yield_now().await;
|
||||
}
|
||||
} => {
|
||||
info!("signaled shutdown");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// shutdown
|
||||
fw_shutdown.store(true, Ordering::Relaxed);
|
||||
server.do_send(crate::server::actor::messages::Shutdown);
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
System::current().stop();
|
||||
};
|
||||
|
||||
let system = System::new();
|
||||
Arbiter::current().spawn(execution);
|
||||
system.run()?;
|
||||
|
||||
// check for error from server thread
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if let Some(err) = &*shutdown_message_holder.read().unwrap() {
|
||||
#[cfg(feature = "tui")]
|
||||
if ui {
|
||||
ratatui::restore();
|
||||
}
|
||||
if !err.is_empty() {
|
||||
return Err(color_eyre::eyre::eyre!(format!("{err}")));
|
||||
}
|
||||
}
|
||||
|
||||
// check for error from file watcher thread
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if let Some(err) = &*file_watcher_err_holder.read().unwrap() {
|
||||
#[cfg(feature = "tui")]
|
||||
if ui {
|
||||
ratatui::restore();
|
||||
}
|
||||
return Err(color_eyre::eyre::eyre!(format!("{err}")));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use git_next_core::{
|
|||
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
||||
StoragePathType, User,
|
||||
};
|
||||
use secrecy::Secret;
|
||||
use secrecy::SecretString;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
|
@ -59,10 +59,13 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
|
|||
repo_details.forge = repo_details
|
||||
.forge
|
||||
.with_user(User::new("git".to_string()))
|
||||
.with_token(ApiToken::new(Secret::new(String::new())))
|
||||
.with_token(ApiToken::new(SecretString::from(String::new())))
|
||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
|
||||
let open_repository = git::repository::factory::real().open(&repo_details)?;
|
||||
let Ok(open_repository) = git::repository::factory::real().open(&repo_details) else {
|
||||
// .git directory may not be present on dev environment
|
||||
return Ok(());
|
||||
};
|
||||
let_assert!(
|
||||
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
|
||||
"Default Push Remote not found"
|
||||
|
@ -92,13 +95,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
|||
repo_details.forge = repo_details
|
||||
.forge
|
||||
.with_user(User::new("git".to_string()))
|
||||
.with_token(ApiToken::new(Secret::new(String::new())))
|
||||
.with_token(ApiToken::new(SecretString::from(String::new())))
|
||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||
tracing::debug!("opening...");
|
||||
let_assert!(
|
||||
Ok(repository) = git::repository::factory::real().open(&repo_details),
|
||||
"open repository"
|
||||
);
|
||||
let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
|
||||
// .git directory may not be present on dev environment
|
||||
return Ok(());
|
||||
};
|
||||
tracing::debug!("open okay");
|
||||
tracing::info!(?repository, "FOO");
|
||||
tracing::info!(?repo_details, "BAR");
|
||||
|
@ -108,11 +111,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
|
||||
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() {
|
||||
let_assert!(
|
||||
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
|
||||
);
|
||||
eprintln!("cli_crate_dir: {cli_crate_dir:?}");
|
||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||
eprintln!("root: {root:?}");
|
||||
let mut repo_details = git::repo_details(
|
||||
1,
|
||||
git::Generation::default(),
|
||||
|
@ -124,16 +129,17 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
|
|||
repo_details.forge = repo_details
|
||||
.forge
|
||||
.with_user(User::new("git".to_string()))
|
||||
.with_token(ApiToken::new(Secret::new(String::new())))
|
||||
.with_token(ApiToken::new(SecretString::from(String::new())))
|
||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||
let repository = git::repository::factory::real().open(&repo_details)?;
|
||||
let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
|
||||
// .git directory may not be present on dev environment
|
||||
return;
|
||||
};
|
||||
let mut repo_details = repo_details.clone();
|
||||
repo_details.forge = repo_details
|
||||
.forge
|
||||
.with_hostname(Hostname::new("code.kemitix.net"));
|
||||
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -38,3 +38,44 @@ mod init {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
mod file_watcher {
|
||||
use std::{sync::atomic::Ordering, time::Duration};
|
||||
|
||||
use actix::{Actor, Context, Handler};
|
||||
use rstest::*;
|
||||
|
||||
use crate::file_watcher::{self, FileUpdated};
|
||||
|
||||
use super::TestResult;
|
||||
|
||||
#[rstest]
|
||||
#[actix::test]
|
||||
#[timeout(Duration::from_millis(80))]
|
||||
async fn should_not_block_calling_thread() -> TestResult {
|
||||
let fs = kxio::fs::temp()?;
|
||||
let path = fs.base().join("file");
|
||||
fs.file_write(&path, "foo")?;
|
||||
|
||||
let listener = Listener;
|
||||
let l_addr = listener.start();
|
||||
let recipient = l_addr.recipient();
|
||||
|
||||
let fw_shutdown = file_watcher::watch_file(path, recipient)?;
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
fw_shutdown.store(true, Ordering::Relaxed);
|
||||
|
||||
Ok(()) // was not blocked
|
||||
}
|
||||
|
||||
struct Listener;
|
||||
impl Actor for Listener {
|
||||
type Context = Context<Self>;
|
||||
}
|
||||
impl Handler<FileUpdated> for Listener {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, _msg: FileUpdated, _ctx: &mut Self::Context) -> Self::Result {
|
||||
// todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
40
crates/cli/src/tui/README.md
Normal file
40
crates/cli/src/tui/README.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Terminal UI
|
||||
|
||||
Currently the Terminal UI is an experimental feature, controlled by the feature flag `tui`.
|
||||
|
||||
## Build & Run
|
||||
|
||||
The build `git-next` with the Terminal UI use: `cargo install git-next --features tui`
|
||||
|
||||
To run `git-next` with the Terminal UI use: `git-next server start --ui`
|
||||
|
||||
### Docker
|
||||
|
||||
If using the docker image you will need to create a directory to mount that contains the
|
||||
`git-next-server.toml` file. Mount this directory as `/app`. In the example below we use
|
||||
the current directory for this.
|
||||
|
||||
If you want to persist the clones of your monitored repos then point `storage.path` in
|
||||
`git-next-server.toml` to the the directory `/app`, (e.g. `path = "/app/data"`).
|
||||
|
||||
Map the port your webhook notifications are arriving on to the port specified in `listen.http.port`.
|
||||
|
||||
`docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest server start --ui`
|
||||
|
||||
## logs
|
||||
|
||||
When the Terminal UI is enabled via the `--ui` parameter, logs are written to the file:
|
||||
|
||||
- `???` on Linux
|
||||
- `~/Library/Application Support/net.kemitix.git-next/git-next.log` on MacOS
|
||||
- `???` on Windows
|
||||
|
||||
## Keys
|
||||
|
||||
- `q` - Quit
|
||||
- `j` - Down
|
||||
- `k` - Up
|
||||
- `f` - Page Down
|
||||
- `b` - Page Up
|
||||
- `g` - Top/Home
|
||||
- `G` - Bottom/End
|
3
crates/cli/src/tui/actor/handlers/mod.rs
Normal file
3
crates/cli/src/tui/actor/handlers/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
//
|
||||
mod server_update;
|
||||
mod tick;
|
104
crates/cli/src/tui/actor/handlers/server_update.rs
Normal file
104
crates/cli/src/tui/actor/handlers/server_update.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
use actix::Handler;
|
||||
use ratatui::style::Color;
|
||||
|
||||
use crate::{
|
||||
server::actor::messages::{RepoUpdate, ServerUpdate},
|
||||
tui::{actor::ServerState, Tui},
|
||||
};
|
||||
|
||||
static OKAY: Color = Color::Green;
|
||||
static PREP: Color = Color::Gray;
|
||||
static ACTING: Color = Color::LightBlue;
|
||||
static WARN: Color = Color::Red;
|
||||
|
||||
impl Handler<ServerUpdate> for Tui {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ServerUpdate, _ctx: &mut Self::Context) -> Self::Result {
|
||||
self.state.tap();
|
||||
match msg {
|
||||
ServerUpdate::AppConfigLoaded { app_config } => {
|
||||
self.state.mode = ServerState::from(app_config);
|
||||
}
|
||||
|
||||
ServerUpdate::RepoUpdate {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
repo_update,
|
||||
} => {
|
||||
if let ServerState::Configured { forges } = &mut self.state.mode {
|
||||
let Some(forge_state) = forges.get_mut(&forge_alias) else {
|
||||
return;
|
||||
};
|
||||
let Some(repo_state) = forge_state.repos.get_mut(&repo_alias) else {
|
||||
return;
|
||||
};
|
||||
repo_state.clear_alert();
|
||||
match repo_update {
|
||||
RepoUpdate::Branches { branches } => {
|
||||
repo_state.update_branches(branches);
|
||||
}
|
||||
RepoUpdate::Log { log } => {
|
||||
repo_state.update_log(log);
|
||||
}
|
||||
RepoUpdate::ValidateRepo => repo_state.update_message("polling...", ACTING),
|
||||
RepoUpdate::Okay { main, next, dev } => {
|
||||
repo_state.clear_alert();
|
||||
repo_state.update_message("okay", OKAY);
|
||||
*repo_state = repo_state.clone().ready(main, next, dev);
|
||||
}
|
||||
RepoUpdate::Alert { alert } => {
|
||||
repo_state.alert(alert);
|
||||
}
|
||||
RepoUpdate::CheckingCI => {
|
||||
repo_state.update_message("Checking CI status", ACTING);
|
||||
}
|
||||
RepoUpdate::AdvancingNext { commit, force: _ } => {
|
||||
repo_state
|
||||
.update_message(format!("advancing next to {commit}"), ACTING);
|
||||
}
|
||||
RepoUpdate::NextUpdated => {
|
||||
repo_state.update_message("next updated - pause while CI starts", OKAY);
|
||||
}
|
||||
RepoUpdate::AdvancingMain { commit } => {
|
||||
repo_state
|
||||
.update_message(format!("advancing main to {commit}"), ACTING);
|
||||
}
|
||||
RepoUpdate::MainUpdated => {
|
||||
repo_state.update_message("main updated", OKAY);
|
||||
}
|
||||
RepoUpdate::Opening => {
|
||||
repo_state.update_message("opening...", PREP);
|
||||
}
|
||||
RepoUpdate::Opened => {
|
||||
repo_state.update_message("opened", PREP);
|
||||
}
|
||||
RepoUpdate::LoadingConfigFromRepo => {
|
||||
repo_state.update_message("loading config from repo...", PREP);
|
||||
}
|
||||
RepoUpdate::ReceiveCIStatus { status } => {
|
||||
repo_state.update_message(format!("ci status: {status:?}"), WARN);
|
||||
}
|
||||
RepoUpdate::ReceiveRepoConfig { repo_config: _ } => {
|
||||
repo_state.update_message("loaded config from repo", PREP);
|
||||
}
|
||||
RepoUpdate::RegisteringWebhook => {
|
||||
repo_state.update_message("registering webhook...", PREP);
|
||||
}
|
||||
RepoUpdate::UnregisteringWebhook => {
|
||||
repo_state.update_message("unregistering webhook...", PREP);
|
||||
}
|
||||
RepoUpdate::WebhookReceived { branch, push: _ } => {
|
||||
repo_state
|
||||
.update_message(format!("webhook update: {branch:?}"), ACTING);
|
||||
}
|
||||
RepoUpdate::RegisteredWebhook => {
|
||||
repo_state.update_message("registered webhook", PREP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
crates/cli/src/tui/actor/handlers/tick.rs
Normal file
15
crates/cli/src/tui/actor/handlers/tick.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
use actix::Handler;
|
||||
|
||||
use crate::tui::actor::{messages::Tick, Tui};
|
||||
|
||||
impl Handler<Tick> for Tui {
|
||||
type Result = std::io::Result<()>;
|
||||
|
||||
fn handle(&mut self, _msg: Tick, ctx: &mut Self::Context) -> Self::Result {
|
||||
self.state.tap();
|
||||
self.draw()?;
|
||||
self.handle_input(ctx)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
4
crates/cli/src/tui/actor/messages.rs
Normal file
4
crates/cli/src/tui/actor/messages.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
//
|
||||
use git_next_core::message;
|
||||
|
||||
message!(Tick => std::io::Result<()>, "Update the TUI");
|
97
crates/cli/src/tui/actor/mod.rs
Normal file
97
crates/cli/src/tui/actor/mod.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
mod handlers;
|
||||
pub mod messages;
|
||||
mod model;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use actix::{Actor, ActorContext as _, Context};
|
||||
|
||||
pub use model::*;
|
||||
|
||||
use ratatui::{
|
||||
crossterm::event::{self, KeyCode, KeyEventKind},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use tui_scrollview::ScrollViewState;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Tui {
|
||||
terminal: Option<DefaultTerminal>,
|
||||
signal_shutdown: Sender<String>,
|
||||
pub state: State,
|
||||
scroll_view_state: ScrollViewState,
|
||||
}
|
||||
impl Actor for Tui {
|
||||
type Context = Context<Self>;
|
||||
fn started(&mut self, _ctx: &mut Self::Context) {
|
||||
self.terminal.replace(ratatui::init());
|
||||
}
|
||||
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
||||
self.terminal.take();
|
||||
ratatui::restore();
|
||||
}
|
||||
}
|
||||
impl Tui {
|
||||
pub fn new(signal_shutdown: Sender<String>) -> Self {
|
||||
Self {
|
||||
terminal: None,
|
||||
signal_shutdown,
|
||||
state: State::initial(),
|
||||
scroll_view_state: ScrollViewState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self) -> std::io::Result<()> {
|
||||
let t = self.terminal.take();
|
||||
let scroll_view_state = &mut self.scroll_view_state;
|
||||
let state = &self.state;
|
||||
if let Some(mut terminal) = t {
|
||||
terminal.draw(|frame| {
|
||||
frame.render_stateful_widget(state, frame.area(), scroll_view_state);
|
||||
})?;
|
||||
self.terminal = Some(terminal);
|
||||
} else {
|
||||
eprintln!("No terminal setup");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_input(&mut self, ctx: &mut <Self as actix::Actor>::Context) -> std::io::Result<()> {
|
||||
if event::poll(std::time::Duration::from_millis(16))? {
|
||||
let event::Event::Key(key) = event::read()? else {
|
||||
return Ok(());
|
||||
};
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
ctx.stop();
|
||||
if let Err(err) = self.signal_shutdown.send(String::new()) {
|
||||
tracing::error!(?err, "Failed to signal shutdown");
|
||||
}
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
|
||||
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
|
||||
KeyCode::Char('f') | KeyCode::PageDown => {
|
||||
self.scroll_view_state.scroll_page_down();
|
||||
}
|
||||
KeyCode::Char('b') | KeyCode::PageUp => {
|
||||
self.scroll_view_state.scroll_page_up();
|
||||
}
|
||||
KeyCode::Char('g') | KeyCode::Home => {
|
||||
self.scroll_view_state.scroll_to_top();
|
||||
}
|
||||
KeyCode::Char('G') | KeyCode::End => {
|
||||
self.scroll_view_state.scroll_to_bottom();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
388
crates/cli/src/tui/actor/model.rs
Normal file
388
crates/cli/src/tui/actor/model.rs
Normal file
|
@ -0,0 +1,388 @@
|
|||
//
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
prelude::{Buffer, Rect},
|
||||
style::{Color, Style, Stylize as _},
|
||||
symbols::border,
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph, StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
use git_next_core::{
|
||||
git::{self, graph::Log, Commit},
|
||||
ForgeAlias, RepoAlias, RepoBranches,
|
||||
};
|
||||
use tracing::info;
|
||||
use tui_scrollview::ScrollViewState;
|
||||
|
||||
use std::{collections::BTreeMap, fmt::Display, time::Instant};
|
||||
|
||||
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct State {
|
||||
last_update: Instant,
|
||||
started: Instant,
|
||||
pub mode: ServerState,
|
||||
}
|
||||
impl State {
|
||||
pub fn initial() -> Self {
|
||||
Self {
|
||||
last_update: Instant::now(),
|
||||
started: Instant::now(),
|
||||
mode: ServerState::Initial { tick: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tap(&mut self) {
|
||||
self.last_update = Instant::now();
|
||||
if let ServerState::Initial { tick } = &mut self.mode {
|
||||
*tick += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn beating_heart(&self) -> String {
|
||||
if self.last_update.duration_since(self.started).as_secs() % 2 == 0 {
|
||||
"💚 "
|
||||
} else {
|
||||
" 💚"
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn time() -> String {
|
||||
chrono::Local::now().format("%H:%M").to_string()
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ServerState {
|
||||
/// UI has started but has no information on the state of the server
|
||||
Initial { tick: usize }, // NOTE: for use with throbber-widgets-tui ?
|
||||
|
||||
/// The application configuration has been loaded, individual forges and repos have their own
|
||||
/// states
|
||||
Configured {
|
||||
forges: BTreeMap<ForgeAlias, ForgeState>,
|
||||
},
|
||||
}
|
||||
impl ServerState {
|
||||
pub fn update_branches(
|
||||
&mut self,
|
||||
forge_alias: &ForgeAlias,
|
||||
repo_alias: &RepoAlias,
|
||||
branches: RepoBranches,
|
||||
) {
|
||||
if let Self::Configured { forges } = self {
|
||||
let Some(forge_state) = forges.get_mut(forge_alias) else {
|
||||
return;
|
||||
};
|
||||
let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else {
|
||||
return;
|
||||
};
|
||||
match repo_state {
|
||||
RepoState::Configured {
|
||||
branches: state_branches,
|
||||
..
|
||||
}
|
||||
| RepoState::Ready {
|
||||
branches: state_branches,
|
||||
..
|
||||
} => *state_branches = branches,
|
||||
|
||||
RepoState::Identified { .. } => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_log(&mut self, forge_alias: &ForgeAlias, repo_alias: &RepoAlias, log: Log) {
|
||||
if let Self::Configured { forges } = self {
|
||||
let Some(forge_state) = forges.get_mut(forge_alias) else {
|
||||
return;
|
||||
};
|
||||
let Some(repo_state) = forge_state.repos.get_mut(repo_alias) else {
|
||||
return;
|
||||
};
|
||||
match repo_state {
|
||||
RepoState::Ready { log: state_log, .. } => *state_log = log,
|
||||
|
||||
RepoState::Identified { .. } | RepoState::Configured { .. } => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<ValidAppConfig> for ServerState {
|
||||
fn from(app_config: ValidAppConfig) -> Self {
|
||||
Self::Configured {
|
||||
forges: app_config
|
||||
.app_config
|
||||
.forges()
|
||||
.map(|(forge_alias, config)| {
|
||||
(
|
||||
forge_alias,
|
||||
config
|
||||
.repos()
|
||||
.map(|(repo_alias, server_repo_config)| {
|
||||
(repo_alias, server_repo_config.repo_config())
|
||||
})
|
||||
.map(
|
||||
|(repo_alias, option_repo_config)| match option_repo_config {
|
||||
Some(rc) => (
|
||||
repo_alias.clone(),
|
||||
RepoState::Configured {
|
||||
repo_alias,
|
||||
message: RepoMessage::builder()
|
||||
.text("configured".into())
|
||||
.style(Style::default().fg(Color::LightGreen))
|
||||
.build(),
|
||||
alert: None,
|
||||
branches: rc.branches().clone(),
|
||||
log: git::graph::Log::default(),
|
||||
},
|
||||
),
|
||||
None => (
|
||||
repo_alias.clone(),
|
||||
RepoState::Identified {
|
||||
repo_alias,
|
||||
message: RepoMessage::builder()
|
||||
.text("identified".into())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.build(),
|
||||
alert: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.map(|(forge_alias, vec_repo_alias_state)| {
|
||||
let forge_state: ForgeState = ForgeState {
|
||||
alias: forge_alias.clone(),
|
||||
view_state: ViewState::default(),
|
||||
repos: vec_repo_alias_state.into_iter().collect::<BTreeMap<_, _>>(),
|
||||
};
|
||||
(forge_alias, forge_state)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ViewState {
|
||||
Collapsed,
|
||||
#[default]
|
||||
Expanded,
|
||||
}
|
||||
impl Display for ViewState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let view_state = match self {
|
||||
Self::Collapsed => "+",
|
||||
Self::Expanded => "-",
|
||||
};
|
||||
write!(f, "{view_state}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct ForgeState {
|
||||
pub alias: ForgeAlias,
|
||||
pub view_state: ViewState,
|
||||
pub repos: BTreeMap<RepoAlias, RepoState>,
|
||||
}
|
||||
|
||||
#[bon::builder]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RepoMessage {
|
||||
text: String,
|
||||
style: Style,
|
||||
}
|
||||
impl From<&RepoMessage> for Span<'_> {
|
||||
fn from(value: &RepoMessage) -> Self {
|
||||
Self::default()
|
||||
.content(value.text.clone())
|
||||
.style(value.style)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RepoState {
|
||||
Identified {
|
||||
repo_alias: RepoAlias,
|
||||
message: RepoMessage,
|
||||
alert: Option<String>,
|
||||
},
|
||||
Configured {
|
||||
repo_alias: RepoAlias,
|
||||
message: RepoMessage,
|
||||
alert: Option<String>,
|
||||
branches: RepoBranches,
|
||||
log: Log,
|
||||
},
|
||||
Ready {
|
||||
repo_alias: RepoAlias,
|
||||
message: RepoMessage,
|
||||
alert: Option<String>,
|
||||
branches: RepoBranches,
|
||||
view_state: ViewState,
|
||||
main: Commit,
|
||||
next: Commit,
|
||||
dev: Commit,
|
||||
log: Log,
|
||||
},
|
||||
}
|
||||
impl RepoState {
|
||||
#[tracing::instrument]
|
||||
pub fn update_branches(&mut self, branches: RepoBranches) {
|
||||
match self {
|
||||
Self::Configured {
|
||||
branches: state_branches,
|
||||
..
|
||||
}
|
||||
| Self::Ready {
|
||||
branches: state_branches,
|
||||
..
|
||||
} => {
|
||||
*state_branches = branches;
|
||||
}
|
||||
|
||||
Self::Identified { .. } => (),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn update_log(&mut self, log: Log) {
|
||||
match self {
|
||||
Self::Configured { log: state_log, .. } | Self::Ready { log: state_log, .. } => {
|
||||
*state_log = log;
|
||||
}
|
||||
|
||||
Self::Identified { .. } => {
|
||||
info!("git graph log ignored by ui");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn update_message(&mut self, msg: impl Into<String> + std::fmt::Debug, colour: Color) {
|
||||
match self {
|
||||
Self::Identified { message, .. }
|
||||
| Self::Configured { message, .. }
|
||||
| Self::Ready { message, .. } => {
|
||||
info!(?msg, "updating ui");
|
||||
*message = RepoMessage::builder()
|
||||
.text(msg.into())
|
||||
.style(Style::default().fg(colour))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn clear_alert(&mut self) {
|
||||
match self {
|
||||
Self::Identified { alert, .. }
|
||||
| Self::Configured { alert, .. }
|
||||
| Self::Ready { alert, .. } => {
|
||||
*alert = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn alert(&mut self, msg: impl Into<String> + std::fmt::Debug) {
|
||||
let msg: String = msg.into();
|
||||
tracing::info!(%msg, "new tui alert");
|
||||
self.update_message("ALERT", Color::Red);
|
||||
match self {
|
||||
Self::Identified { alert, .. }
|
||||
| Self::Configured { alert, .. }
|
||||
| Self::Ready { alert, .. } => *alert = Some(msg),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ready(self, main: Commit, next: Commit, dev: Commit) -> Self {
|
||||
match self {
|
||||
Self::Identified {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
} => Self::Identified {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
},
|
||||
Self::Configured {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
branches,
|
||||
log,
|
||||
} => Self::Ready {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
branches,
|
||||
view_state: ViewState::Expanded,
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
log,
|
||||
},
|
||||
Self::Ready {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
branches,
|
||||
view_state,
|
||||
log,
|
||||
.. // drop existing main, next and dev to use parameters
|
||||
} => Self::Ready {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
branches,
|
||||
view_state,
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
log,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidget for &State {
|
||||
type State = ScrollViewState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let block = Block::bordered()
|
||||
.title_top(
|
||||
Line::from(format!(" Git-Next v{} ", clap::crate_version!()).bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.title_bottom(
|
||||
Line::from(vec![
|
||||
" [q]uit ".into(),
|
||||
self.beating_heart().into(),
|
||||
" ".into(),
|
||||
])
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.title_bottom(Line::from(format!(" {} ", time())).alignment(Alignment::Right))
|
||||
.border_set(border::THICK);
|
||||
let interior = block.inner(area);
|
||||
block.render(area, buf);
|
||||
match &self.mode {
|
||||
ServerState::Initial { tick } => Paragraph::new(format!("Loading...{tick}"))
|
||||
.centered()
|
||||
.render(interior, buf),
|
||||
ServerState::Configured { forges } => {
|
||||
ConfiguredAppWidget { forges }.render(interior, buf, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
99
crates/cli/src/tui/actor/tests.rs
Normal file
99
crates/cli/src/tui/actor/tests.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
mod model {
|
||||
mod repo_state {
|
||||
use git_next_core::{git::graph::Log, RepoBranches};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::{
|
||||
repo::tests::given,
|
||||
tui::actor::{RepoMessage, RepoState, ViewState},
|
||||
};
|
||||
type Alert = Option<String>;
|
||||
|
||||
fn identified_with_alert(alert: Alert) -> RepoState {
|
||||
RepoState::Identified {
|
||||
repo_alias: given::a_repo_alias(),
|
||||
message: RepoMessage::builder()
|
||||
.text(given::a_name())
|
||||
.style(Style::default())
|
||||
.build(),
|
||||
alert,
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_with_alert(alert: Alert) -> RepoState {
|
||||
RepoState::Configured {
|
||||
repo_alias: given::a_repo_alias(),
|
||||
message: RepoMessage::builder()
|
||||
.text(given::a_name())
|
||||
.style(Style::default())
|
||||
.build(),
|
||||
alert,
|
||||
branches: RepoBranches::new(String::new(), String::new(), String::new()),
|
||||
log: Log::default(),
|
||||
}
|
||||
}
|
||||
fn ready_with_alert(alert: Alert) -> RepoState {
|
||||
RepoState::Ready {
|
||||
repo_alias: given::a_repo_alias(),
|
||||
message: RepoMessage::builder()
|
||||
.text(given::a_name())
|
||||
.style(Style::default())
|
||||
.build(),
|
||||
alert,
|
||||
branches: RepoBranches::new(String::new(), String::new(), String::new()),
|
||||
log: Log::default(),
|
||||
view_state: ViewState::default(),
|
||||
main: given::a_commit(),
|
||||
next: given::a_commit(),
|
||||
dev: given::a_commit(),
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[case(identified_with_alert(None))]
|
||||
#[case(configured_with_alert(None))]
|
||||
#[case(ready_with_alert(None))]
|
||||
fn none_alert_remains_none(#[case] mut repo_state: RepoState) {
|
||||
// given
|
||||
match &repo_state {
|
||||
RepoState::Identified { alert, .. }
|
||||
| RepoState::Configured { alert, .. }
|
||||
| RepoState::Ready { alert, .. } => {
|
||||
assert!(alert.is_none(), "should be none at start");
|
||||
}
|
||||
}
|
||||
// when
|
||||
repo_state.clear_alert();
|
||||
// then
|
||||
match &repo_state {
|
||||
RepoState::Identified { alert, .. }
|
||||
| RepoState::Configured { alert, .. }
|
||||
| RepoState::Ready { alert, .. } => assert!(alert.is_none(), "should remain none"),
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[case(identified_with_alert(Some(String::new())))]
|
||||
#[case(configured_with_alert(Some(String::new())))]
|
||||
#[case(ready_with_alert(Some(String::new())))]
|
||||
fn some_alert_becomes_none(#[case] mut repo_state: RepoState) {
|
||||
// given
|
||||
match &repo_state {
|
||||
RepoState::Identified { alert, .. }
|
||||
| RepoState::Configured { alert, .. }
|
||||
| RepoState::Ready { alert, .. } => {
|
||||
assert!(alert.is_some(), "should be some at start");
|
||||
}
|
||||
}
|
||||
// when
|
||||
repo_state.clear_alert();
|
||||
// then
|
||||
match &repo_state {
|
||||
RepoState::Identified { alert, .. }
|
||||
| RepoState::Configured { alert, .. }
|
||||
| RepoState::Ready { alert, .. } => assert!(alert.is_none(), "should become none"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
68
crates/cli/src/tui/components/configured_app.rs
Normal file
68
crates/cli/src/tui/components/configured_app.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use git_next_core::ForgeAlias;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Direction, Layout, Rect, Size},
|
||||
widgets::StatefulWidget,
|
||||
};
|
||||
use tui_scrollview::{ScrollView, ScrollViewState};
|
||||
|
||||
use crate::tui::actor::ForgeState;
|
||||
|
||||
use super::{forge::ForgeWidget, HeightContraintLength};
|
||||
|
||||
pub struct ConfiguredAppWidget<'a> {
|
||||
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
|
||||
}
|
||||
impl<'a> HeightContraintLength for ConfiguredAppWidget<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
self.children()
|
||||
.iter()
|
||||
.map(HeightContraintLength::height_constraint_length)
|
||||
.sum::<u16>()
|
||||
+ 2 // top + bottom borders
|
||||
}
|
||||
}
|
||||
impl<'a> StatefulWidget for ConfiguredAppWidget<'a> {
|
||||
type State = ScrollViewState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let height = self
|
||||
.children()
|
||||
.iter()
|
||||
.map(HeightContraintLength::height_constraint_length)
|
||||
.sum::<u16>();
|
||||
let mut scroll = ScrollView::new(Size::new(area.width - 1, height));
|
||||
let layout_forge_list = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
self.children()
|
||||
.iter()
|
||||
.map(HeightContraintLength::height_constraint_length),
|
||||
)
|
||||
.split(scroll.area());
|
||||
|
||||
self.children()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, w)| scroll.render_widget(w, layout_forge_list[i]));
|
||||
|
||||
scroll.render(area, buf, state);
|
||||
}
|
||||
}
|
||||
impl<'a> ConfiguredAppWidget<'a> {
|
||||
fn children(&self) -> Vec<ForgeWidget<'a>> {
|
||||
self.forges
|
||||
.iter()
|
||||
.map(|(forge_alias, state)| ForgeWidget {
|
||||
forge_alias,
|
||||
repos: &state.repos,
|
||||
view_state: state.view_state,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
22
crates/cli/src/tui/components/forge/collapsed.rs
Normal file
22
crates/cli/src/tui/components/forge/collapsed.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
use git_next_core::ForgeAlias;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
|
||||
|
||||
use crate::tui::components::HeightContraintLength;
|
||||
|
||||
pub struct CollapsedForgeWidget<'a> {
|
||||
pub forge_alias: &'a ForgeAlias,
|
||||
}
|
||||
impl<'a> HeightContraintLength for CollapsedForgeWidget<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
1
|
||||
}
|
||||
}
|
||||
impl<'a> Widget for CollapsedForgeWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Text::from(format!("- {}", self.forge_alias)).render(area, buf);
|
||||
}
|
||||
}
|
61
crates/cli/src/tui/components/forge/expanded.rs
Normal file
61
crates/cli/src/tui/components/forge/expanded.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
//
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use git_next_core::{ForgeAlias, RepoAlias};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Direction, Layout, Rect},
|
||||
text::Line,
|
||||
widgets::{Block, Widget},
|
||||
};
|
||||
|
||||
use crate::tui::{
|
||||
actor::RepoState,
|
||||
components::{repo::RepoWidget, HeightContraintLength},
|
||||
};
|
||||
|
||||
pub struct ExpandedForgeWidget<'a> {
|
||||
pub forge_alias: &'a ForgeAlias,
|
||||
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
|
||||
}
|
||||
impl<'a> HeightContraintLength for ExpandedForgeWidget<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
self.children()
|
||||
.iter()
|
||||
.map(HeightContraintLength::height_constraint_length)
|
||||
.sum::<u16>()
|
||||
+ 2 // top title + bottom padding
|
||||
}
|
||||
}
|
||||
impl<'a> Widget for ExpandedForgeWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let block = Block::default().title_top(
|
||||
Line::from(format!(" forge: {} ", self.forge_alias)).alignment(Alignment::Left),
|
||||
);
|
||||
let children = self.children();
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
children
|
||||
.iter()
|
||||
.map(HeightContraintLength::height_constraint_length),
|
||||
)
|
||||
.split(block.inner(area));
|
||||
block.render(area, buf);
|
||||
children
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, w)| w.render(layout[i], buf));
|
||||
}
|
||||
}
|
||||
impl<'a> ExpandedForgeWidget<'a> {
|
||||
fn children(&self) -> Vec<RepoWidget<'a>> {
|
||||
self.repos
|
||||
.values()
|
||||
.map(|repo_state| RepoWidget { repo_state })
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
53
crates/cli/src/tui/components/forge/mod.rs
Normal file
53
crates/cli/src/tui/components/forge/mod.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
mod collapsed;
|
||||
mod expanded;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use collapsed::CollapsedForgeWidget;
|
||||
use expanded::ExpandedForgeWidget;
|
||||
use git_next_core::{ForgeAlias, RepoAlias};
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
use crate::tui::actor::{RepoState, ViewState};
|
||||
|
||||
use super::HeightContraintLength;
|
||||
|
||||
pub struct ForgeWidget<'a> {
|
||||
pub forge_alias: &'a ForgeAlias,
|
||||
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
|
||||
pub view_state: ViewState,
|
||||
}
|
||||
impl<'a> HeightContraintLength for ForgeWidget<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
match self.view_state {
|
||||
ViewState::Collapsed => CollapsedForgeWidget {
|
||||
forge_alias: self.forge_alias,
|
||||
}
|
||||
.height_constraint_length(),
|
||||
ViewState::Expanded => ExpandedForgeWidget {
|
||||
forge_alias: self.forge_alias,
|
||||
repos: self.repos,
|
||||
}
|
||||
.height_constraint_length(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Widget for ForgeWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self.view_state {
|
||||
ViewState::Collapsed => CollapsedForgeWidget {
|
||||
forge_alias: self.forge_alias,
|
||||
}
|
||||
.render(area, buf),
|
||||
ViewState::Expanded => ExpandedForgeWidget {
|
||||
forge_alias: self.forge_alias,
|
||||
repos: self.repos,
|
||||
}
|
||||
.render(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
128
crates/cli/src/tui/components/history.rs
Normal file
128
crates/cli/src/tui/components/history.rs
Normal file
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
use git_next_core::git::graph::Log;
|
||||
use ratatui::{
|
||||
style::{Color, Style},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Paragraph, Widget},
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
use super::HeightContraintLength;
|
||||
|
||||
pub struct CommitLog<'a> {
|
||||
pub log: &'a Log,
|
||||
}
|
||||
impl<'a> HeightContraintLength for CommitLog<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
u16::try_from(self.log.len()).unwrap_or(u16::MAX)
|
||||
}
|
||||
}
|
||||
impl<'a> Widget for CommitLog<'a> {
|
||||
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Paragraph::new(Text::from(
|
||||
self.log
|
||||
.iter()
|
||||
.map(LogLine::new)
|
||||
.map(Line::from)
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct LogLine {
|
||||
raw: String,
|
||||
}
|
||||
impl LogLine {
|
||||
fn new(raw: impl Into<String>) -> Self {
|
||||
Self { raw: raw.into() }
|
||||
}
|
||||
}
|
||||
lazy_static::lazy_static! {
|
||||
static ref RE: Regex =
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Regex::new(
|
||||
r"^(?<pre>.*)\s(?<hash>[0-9a-f]{7})\s\((?<branches>.*?)\)\s(?<message>.*)",
|
||||
).unwrap();
|
||||
|
||||
static ref BRANCHES: Regex =
|
||||
#[allow(clippy::unwrap_used)]
|
||||
Regex::new(
|
||||
r"origin\/(?<branch>[^,]+)",
|
||||
).unwrap();
|
||||
}
|
||||
impl From<LogLine> for Line<'_> {
|
||||
fn from(value: LogLine) -> Self {
|
||||
match RE.captures(&value.raw) {
|
||||
Some(caps) => {
|
||||
let pre = caps["pre"].to_owned();
|
||||
let hash = caps["hash"].to_owned();
|
||||
let message = caps["message"].to_owned();
|
||||
let mut branches = BRANCHES
|
||||
.captures_iter(&caps["branches"])
|
||||
.map(|captures| captures["branch"].to_owned())
|
||||
.filter(|branch| branch != "HEAD")
|
||||
.collect::<Vec<_>>();
|
||||
if branches.is_empty() {
|
||||
// line without branches
|
||||
Line::from(vec![
|
||||
pre.into(),
|
||||
" ".into(),
|
||||
hash.into(),
|
||||
" ".into(),
|
||||
message.into(),
|
||||
])
|
||||
} else {
|
||||
// line withbranches
|
||||
let mut spans = vec![pre.into(), " ".into(), hash.into(), " ".into()];
|
||||
branches.sort();
|
||||
branches
|
||||
.into_iter()
|
||||
.map(|branch| format!("({branch})"))
|
||||
.map(Span::from)
|
||||
.map(|span| span.style(Style::default().fg(Color::White).bg(Color::Blue)))
|
||||
.for_each(|span| spans.push(span));
|
||||
spans.push(" ".into());
|
||||
spans.push(message.into());
|
||||
Line::from(spans)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// non-commit line
|
||||
Line::from(value.raw.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tracing::info;
|
||||
|
||||
use super::RE;
|
||||
|
||||
#[test_log::test]
|
||||
fn parse_log_line() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let line = "* 97b6853 (origin/next, origin/main, origin/dev, origin/HEAD) refactor(tui): simplify repo identity widget";
|
||||
RE.captures(line).map_or_else(
|
||||
|| Err("Failed to capture".into()),
|
||||
|caps| {
|
||||
info!(?caps, "");
|
||||
assert_eq!(&caps["pre"], "*");
|
||||
assert_eq!(&caps["hash"], "97b6853");
|
||||
assert_eq!(
|
||||
&caps["branches"],
|
||||
"origin/next, origin/main, origin/dev, origin/HEAD"
|
||||
);
|
||||
assert_eq!(
|
||||
&caps["message"],
|
||||
"refactor(tui): simplify repo identity widget"
|
||||
);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
12
crates/cli/src/tui/components/mod.rs
Normal file
12
crates/cli/src/tui/components/mod.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
mod configured_app;
|
||||
mod forge;
|
||||
mod history;
|
||||
mod repo;
|
||||
|
||||
pub use configured_app::ConfiguredAppWidget;
|
||||
pub use history::CommitLog;
|
||||
|
||||
pub trait HeightContraintLength {
|
||||
fn height_constraint_length(&self) -> u16;
|
||||
}
|
87
crates/cli/src/tui/components/repo/identity.rs
Normal file
87
crates/cli/src/tui/components/repo/identity.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use std::string::ToString;
|
||||
|
||||
//
|
||||
use git_next_core::{RepoAlias, RepoBranches};
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
style::{Color, Style, Stylize as _},
|
||||
text::{Line, Span},
|
||||
widgets::block::Title,
|
||||
};
|
||||
|
||||
use crate::tui::actor::RepoMessage;
|
||||
|
||||
pub struct Identity<'a> {
|
||||
pub repo_alias: &'a RepoAlias,
|
||||
pub alert: Option<&'a str>,
|
||||
pub message: &'a RepoMessage,
|
||||
pub repo_branches: Option<&'a RepoBranches>,
|
||||
}
|
||||
impl<'a> Identity<'a> {
|
||||
pub const fn new(
|
||||
repo_alias: &'a RepoAlias,
|
||||
alert: Option<&'a str>,
|
||||
message: &'a RepoMessage,
|
||||
repo_branches: Option<&'a RepoBranches>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo_alias,
|
||||
alert,
|
||||
message,
|
||||
repo_branches,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Identity<'a> {
|
||||
fn spans(self) -> Vec<Span<'a>> {
|
||||
let alert = self
|
||||
.alert
|
||||
.map(ToString::to_string)
|
||||
.map(|alert| alert.fg(Color::White).bg(Color::Red));
|
||||
let message = self.message;
|
||||
let main = self
|
||||
.repo_branches
|
||||
.map(RepoBranches::main)
|
||||
.map_or_else(|| "_".to_string(), |b| b.to_string());
|
||||
let next = self
|
||||
.repo_branches
|
||||
.map(RepoBranches::next)
|
||||
.map_or_else(|| "_".to_string(), |b| b.to_string());
|
||||
let dev = self
|
||||
.repo_branches
|
||||
.map(RepoBranches::dev)
|
||||
.map_or_else(|| "_".to_string(), |b| b.to_string());
|
||||
let mut spans = vec![" ".into()];
|
||||
match alert {
|
||||
None => spans.push(
|
||||
Span::from(self.repo_alias.to_string()).style(Style::default().fg(Color::Cyan)),
|
||||
),
|
||||
Some(alert) => {
|
||||
spans.push(
|
||||
Span::from(self.repo_alias.to_string())
|
||||
.style(Style::default().fg(Color::White).bg(Color::Red)),
|
||||
);
|
||||
spans.push(" ".into());
|
||||
spans.push(alert);
|
||||
}
|
||||
}
|
||||
spans.push(" ".into());
|
||||
spans.push(format!("({main} -> {next} -> {dev}) ").into());
|
||||
spans.push(message.into());
|
||||
spans.push(" ".into());
|
||||
spans
|
||||
}
|
||||
}
|
||||
impl<'a> From<Identity<'a>> for Title<'a> {
|
||||
fn from(identity: Identity<'a>) -> Self {
|
||||
Self {
|
||||
content: Line {
|
||||
spans: identity.spans(),
|
||||
style: Style::reset(),
|
||||
alignment: None,
|
||||
},
|
||||
alignment: Some(Alignment::Left),
|
||||
position: None,
|
||||
}
|
||||
}
|
||||
}
|
114
crates/cli/src/tui/components/repo/mod.rs
Normal file
114
crates/cli/src/tui/components/repo/mod.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
mod identity;
|
||||
|
||||
use std::string::String;
|
||||
|
||||
use git_next_core::{RepoAlias, RepoBranches};
|
||||
|
||||
use crate::{
|
||||
git,
|
||||
tui::{
|
||||
actor::{RepoMessage, RepoState},
|
||||
components::CommitLog,
|
||||
},
|
||||
};
|
||||
|
||||
use identity::Identity;
|
||||
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{Block, Borders, Widget},
|
||||
};
|
||||
|
||||
use super::HeightContraintLength;
|
||||
|
||||
pub struct RepoWidget<'a> {
|
||||
pub repo_state: &'a RepoState,
|
||||
}
|
||||
impl<'a> HeightContraintLength for RepoWidget<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
self.inner().height_constraint_length() + 2 // top + bottom borders
|
||||
}
|
||||
}
|
||||
impl<'a> RepoWidget<'a> {
|
||||
fn inner(&self) -> InnerRepoWidget {
|
||||
match self.repo_state {
|
||||
RepoState::Identified {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
} => InnerRepoWidget {
|
||||
repo_alias,
|
||||
message,
|
||||
alert: alert.as_ref().map(String::as_str),
|
||||
branches: None,
|
||||
log: None,
|
||||
},
|
||||
|
||||
RepoState::Configured {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
branches,
|
||||
log,
|
||||
}
|
||||
| RepoState::Ready {
|
||||
repo_alias,
|
||||
message,
|
||||
alert,
|
||||
branches,
|
||||
log,
|
||||
..
|
||||
} => InnerRepoWidget {
|
||||
repo_alias,
|
||||
message,
|
||||
alert: alert.as_ref().map(String::as_str),
|
||||
branches: Some(branches),
|
||||
log: Some(log),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a> Widget for RepoWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.inner().render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
struct InnerRepoWidget<'a> {
|
||||
pub repo_alias: &'a RepoAlias,
|
||||
pub message: &'a RepoMessage,
|
||||
pub alert: Option<&'a str>,
|
||||
pub branches: Option<&'a RepoBranches>,
|
||||
pub log: Option<&'a git::graph::Log>,
|
||||
}
|
||||
impl<'a> HeightContraintLength for InnerRepoWidget<'a> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
self.log
|
||||
.map(|log| CommitLog { log })
|
||||
.map_or(0, |w| w.height_constraint_length())
|
||||
}
|
||||
}
|
||||
impl<'a> Widget for InnerRepoWidget<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let block = Block::default()
|
||||
.title(Identity::new(
|
||||
self.repo_alias,
|
||||
self.alert,
|
||||
self.message,
|
||||
self.branches,
|
||||
))
|
||||
.borders(Borders::TOP);
|
||||
if let Some(log) = self.log {
|
||||
CommitLog { log }.render(block.inner(area), buf);
|
||||
}
|
||||
block.render(area, buf);
|
||||
}
|
||||
}
|
86
crates/cli/src/tui/logging.rs
Normal file
86
crates/cli/src/tui/logging.rs
Normal file
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
use std::path::PathBuf;
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
use directories::ProjectDirs;
|
||||
use lazy_static::lazy_static;
|
||||
use tracing_error::ErrorLayer;
|
||||
use tracing_subscriber::{self, layer::SubscriberExt, util::SubscriberInitExt, Layer};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PROJECT_NAME: String = env!("CARGO_CRATE_NAME").to_uppercase();
|
||||
pub static ref DATA_FOLDER: Option<PathBuf> =
|
||||
std::env::var(format!("{}_DATA", PROJECT_NAME.clone()))
|
||||
.ok()
|
||||
.map(PathBuf::from);
|
||||
pub static ref LOG_ENV: String = format!("{}_LOGLEVEL", PROJECT_NAME.clone());
|
||||
pub static ref LOG_FILE: String = format!("{}.log", env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
|
||||
fn project_directory() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("net", "kemitix", env!("CARGO_PKG_NAME"))
|
||||
}
|
||||
|
||||
pub fn get_data_dir() -> PathBuf {
|
||||
let directory = DATA_FOLDER.clone().map_or_else(
|
||||
|| {
|
||||
project_directory().map_or_else(
|
||||
|| PathBuf::from(".").join(".data"),
|
||||
|proj_dirs| proj_dirs.data_local_dir().to_path_buf(),
|
||||
)
|
||||
},
|
||||
|data_folder| data_folder,
|
||||
);
|
||||
directory
|
||||
}
|
||||
|
||||
pub fn initialize_logging() -> Result<()> {
|
||||
let directory = get_data_dir();
|
||||
std::fs::create_dir_all(directory.clone())?;
|
||||
let log_path = directory.join(LOG_FILE.clone());
|
||||
let log_file = std::fs::File::create(log_path)?;
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
std::env::var("RUST_LOG")
|
||||
.or_else(|_| std::env::var(LOG_ENV.clone()))
|
||||
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
|
||||
);
|
||||
let file_subscriber = tracing_subscriber::fmt::layer()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(log_file)
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
|
||||
tracing_subscriber::registry()
|
||||
.with(file_subscriber)
|
||||
.with(ErrorLayer::default())
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Similar to the `std::dbg!` macro, but generates `tracing` events rather
|
||||
/// than printing to stdout.
|
||||
///
|
||||
/// By default, the verbosity level for the generated events is `DEBUG`, but
|
||||
/// this can be customized.
|
||||
#[macro_export]
|
||||
macro_rules! trace_dbg {
|
||||
(target: $target:expr, level: $level:expr, $ex:expr) => {{
|
||||
match $ex {
|
||||
value => {
|
||||
tracing::event!(target: $target, $level, ?value, stringify!($ex));
|
||||
value
|
||||
}
|
||||
}
|
||||
}};
|
||||
(level: $level:expr, $ex:expr) => {
|
||||
trace_dbg!(target: module_path!(), level: $level, $ex)
|
||||
};
|
||||
(target: $target:expr, $ex:expr) => {
|
||||
trace_dbg!(target: $target, level: tracing::Level::DEBUG, $ex)
|
||||
};
|
||||
($ex:expr) => {
|
||||
trace_dbg!(level: tracing::Level::DEBUG, $ex)
|
||||
};
|
||||
}
|
7
crates/cli/src/tui/mod.rs
Normal file
7
crates/cli/src/tui/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
//
|
||||
mod actor;
|
||||
pub mod components;
|
||||
pub mod logging;
|
||||
|
||||
pub use actor::messages::Tick;
|
||||
pub use actor::Tui;
|
|
@ -2,10 +2,10 @@
|
|||
/// `ForgeJo`: <https://{hostname}/user/settings/applications>
|
||||
/// `Github`: <https://github.com/settings/tokens>
|
||||
#[derive(Clone, Debug, derive_more::Constructor)]
|
||||
pub struct ApiToken(secrecy::Secret<String>);
|
||||
pub struct ApiToken(secrecy::SecretString);
|
||||
/// The API Token is in effect a password, so it must be explicitly exposed to access its value
|
||||
impl secrecy::ExposeSecret<String> for ApiToken {
|
||||
fn expose_secret(&self) -> &String {
|
||||
impl secrecy::ExposeSecret<str> for ApiToken {
|
||||
fn expose_secret(&self) -> &str {
|
||||
self.0.expose_secret()
|
||||
}
|
||||
}
|
||||
|
|
4
crates/core/src/config/commit_count.rs
Normal file
4
crates/core/src/config/commit_count.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
//
|
||||
use crate::newtype;
|
||||
|
||||
newtype!(CommitCount, u32, Default, "A number of commits");
|
|
@ -11,6 +11,7 @@ pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
|
|||
hostname(n),
|
||||
user(n),
|
||||
api_token(n),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ use std::collections::BTreeMap;
|
|||
|
||||
use crate::config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User};
|
||||
|
||||
use super::CommitCount;
|
||||
|
||||
/// Defines a Forge to connect to
|
||||
/// Maps from `git-next-server.toml` at `forge.{forge}`
|
||||
#[derive(
|
||||
|
@ -22,6 +24,7 @@ pub struct ForgeConfig {
|
|||
hostname: String,
|
||||
user: String,
|
||||
token: String,
|
||||
max_dev_commits: Option<u32>,
|
||||
repos: BTreeMap<String, ServerRepoConfig>,
|
||||
}
|
||||
impl ForgeConfig {
|
||||
|
@ -41,6 +44,10 @@ impl ForgeConfig {
|
|||
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)> {
|
||||
self.repos
|
||||
.iter()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::config::{ApiToken, ForgeAlias, ForgeConfig, ForgeType, Hostname, User};
|
||||
|
||||
use super::CommitCount;
|
||||
|
||||
/// The derived information about a Forge, used to create interactions with it
|
||||
#[derive(Clone, Default, Debug, derive_more::Constructor, derive_with::With)]
|
||||
pub struct ForgeDetails {
|
||||
|
@ -8,8 +10,7 @@ pub struct ForgeDetails {
|
|||
hostname: Hostname,
|
||||
user: User,
|
||||
token: ApiToken,
|
||||
// API Token
|
||||
// Private SSH Key Path
|
||||
max_dev_commits: Option<CommitCount>,
|
||||
}
|
||||
impl ForgeDetails {
|
||||
#[must_use]
|
||||
|
@ -35,15 +36,21 @@ impl ForgeDetails {
|
|||
pub const fn token(&self) -> &ApiToken {
|
||||
&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 {
|
||||
fn from(forge: (&ForgeAlias, &ForgeConfig)) -> Self {
|
||||
fn from((forge_alias, forge_config): (&ForgeAlias, &ForgeConfig)) -> Self {
|
||||
Self {
|
||||
forge_alias: forge.0.clone(),
|
||||
forge_type: forge.1.forge_type(),
|
||||
hostname: forge.1.hostname(),
|
||||
user: forge.1.user(),
|
||||
token: forge.1.token(),
|
||||
forge_alias: forge_alias.clone(),
|
||||
forge_type: forge_config.forge_type(),
|
||||
hostname: forge_config.hostname(),
|
||||
user: forge_config.user(),
|
||||
token: forge_config.token(),
|
||||
max_dev_commits: forge_config.max_dev_commits(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//
|
||||
mod api_token;
|
||||
mod branch_name;
|
||||
mod commit_count;
|
||||
pub mod common;
|
||||
mod forge_alias;
|
||||
mod forge_config;
|
||||
|
@ -26,6 +27,7 @@ mod tests;
|
|||
|
||||
pub use api_token::ApiToken;
|
||||
pub use branch_name::BranchName;
|
||||
pub use commit_count::CommitCount;
|
||||
pub use forge_alias::ForgeAlias;
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub use forge_config::ForgeConfig;
|
||||
|
|
|
@ -10,7 +10,7 @@ use std::{
|
|||
|
||||
use derive_more::{Constructor, Display};
|
||||
use kxio::fs::FileSystem;
|
||||
use secrecy::Secret;
|
||||
use secrecy::SecretString;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
|
@ -242,8 +242,11 @@ impl Shout {
|
|||
self.webhook.clone().map(|x| x.url)
|
||||
}
|
||||
|
||||
pub fn webhook_secret(&self) -> Option<Secret<String>> {
|
||||
self.webhook.clone().map(|x| x.secret).map(Secret::new)
|
||||
pub fn webhook_secret(&self) -> Option<SecretString> {
|
||||
self.webhook
|
||||
.clone()
|
||||
.map(|x| x.secret)
|
||||
.map(SecretString::from)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
@ -278,8 +281,8 @@ impl OutboundWebhook {
|
|||
self.url.as_ref()
|
||||
}
|
||||
#[must_use]
|
||||
pub fn secret(&self) -> Secret<String> {
|
||||
Secret::new(self.secret.clone())
|
||||
pub fn secret(&self) -> SecretString {
|
||||
SecretString::from(self.secret.clone())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,8 @@ impl ServerRepoConfig {
|
|||
}
|
||||
|
||||
/// Returns a `RepoConfig` from the server configuration if ALL THREE branches were provided
|
||||
pub(crate) fn repo_config(&self) -> Option<RepoConfig> {
|
||||
#[must_use]
|
||||
pub fn repo_config(&self) -> Option<RepoConfig> {
|
||||
match (&self.main, &self.next, &self.dev) {
|
||||
(Some(main), Some(next), Some(dev)) => Some(RepoConfig::new(
|
||||
RepoBranches::new(main.to_string(), next.to_string(), dev.to_string()),
|
||||
|
|
|
@ -149,6 +149,8 @@ mod repo_config {
|
|||
}
|
||||
}
|
||||
mod forge_config {
|
||||
use given::maybe_a_number;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -166,7 +168,7 @@ mod forge_config {
|
|||
let mut repos = BTreeMap::new();
|
||||
repos.insert(red_name.clone(), red.clone());
|
||||
repos.insert(blue_name.clone(), blue.clone());
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos);
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
|
||||
|
||||
let returned_repos = fc.repos().collect::<Vec<_>>();
|
||||
|
||||
|
@ -186,7 +188,7 @@ mod forge_config {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos);
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
|
||||
|
||||
assert_eq!(fc.forge_type(), ForgeType::MockForge);
|
||||
}
|
||||
|
@ -197,7 +199,14 @@ mod forge_config {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname.clone(), user, token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname.clone(),
|
||||
user,
|
||||
token,
|
||||
maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.hostname(), Hostname::new(hostname));
|
||||
}
|
||||
|
@ -208,7 +217,14 @@ mod forge_config {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user.clone(), token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user.clone(),
|
||||
token,
|
||||
maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.user(), User::new(user));
|
||||
}
|
||||
|
@ -219,7 +235,14 @@ mod forge_config {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token.clone(), repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token.clone(),
|
||||
maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.token().expose_secret(), token.as_str());
|
||||
}
|
||||
|
@ -237,7 +260,7 @@ mod forge_config {
|
|||
let mut repos = BTreeMap::new();
|
||||
repos.insert(red_name.clone(), red.clone());
|
||||
repos.insert(blue_name, blue);
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos);
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, maybe_a_number(), repos);
|
||||
|
||||
let returned_repo = fc.get_repo(red_name.as_str());
|
||||
|
||||
|
@ -255,8 +278,14 @@ mod forge_details {
|
|||
let user = User::new(given::a_name());
|
||||
let token = ApiToken::new(given::a_name().into());
|
||||
let forge_alias = ForgeAlias::new(given::a_name());
|
||||
let forge_details =
|
||||
ForgeDetails::new(forge_alias.clone(), forge_type, hostname, user, token);
|
||||
let forge_details = ForgeDetails::new(
|
||||
forge_alias.clone(),
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number().map(CommitCount::from),
|
||||
);
|
||||
|
||||
let result = forge_details.forge_alias();
|
||||
|
||||
|
@ -269,7 +298,14 @@ mod forge_details {
|
|||
let user = User::new(given::a_name());
|
||||
let token = ApiToken::new(given::a_name().into());
|
||||
let forge_name = ForgeAlias::new(given::a_name());
|
||||
let forge_details = ForgeDetails::new(forge_name, forge_type, hostname, user, token);
|
||||
let forge_details = ForgeDetails::new(
|
||||
forge_name,
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number().map(CommitCount::from),
|
||||
);
|
||||
|
||||
let result = forge_details.forge_type();
|
||||
|
||||
|
@ -282,8 +318,14 @@ mod forge_details {
|
|||
let user = User::new(given::a_name());
|
||||
let token = ApiToken::new(given::a_name().into());
|
||||
let forge_name = ForgeAlias::new(given::a_name());
|
||||
let forge_details =
|
||||
ForgeDetails::new(forge_name, forge_type, hostname.clone(), user, token);
|
||||
let forge_details = ForgeDetails::new(
|
||||
forge_name,
|
||||
forge_type,
|
||||
hostname.clone(),
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number().map(CommitCount::from),
|
||||
);
|
||||
|
||||
let result = forge_details.hostname();
|
||||
|
||||
|
@ -296,8 +338,14 @@ mod forge_details {
|
|||
let user = User::new(given::a_name());
|
||||
let token = ApiToken::new(given::a_name().into());
|
||||
let forge_name = ForgeAlias::new(given::a_name());
|
||||
let forge_details =
|
||||
ForgeDetails::new(forge_name, forge_type, hostname, user.clone(), token);
|
||||
let forge_details = ForgeDetails::new(
|
||||
forge_name,
|
||||
forge_type,
|
||||
hostname,
|
||||
user.clone(),
|
||||
token,
|
||||
given::maybe_a_number().map(CommitCount::from),
|
||||
);
|
||||
|
||||
let result = forge_details.user();
|
||||
|
||||
|
@ -310,8 +358,14 @@ mod forge_details {
|
|||
let user = User::new(given::a_name());
|
||||
let token = ApiToken::new(given::a_name().into());
|
||||
let forge_name = ForgeAlias::new(given::a_name());
|
||||
let forge_details =
|
||||
ForgeDetails::new(forge_name, forge_type, hostname, user, token.clone());
|
||||
let forge_details = ForgeDetails::new(
|
||||
forge_name,
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token.clone(),
|
||||
given::maybe_a_number().map(CommitCount::from),
|
||||
);
|
||||
|
||||
let result = forge_details.token();
|
||||
|
||||
|
@ -325,7 +379,14 @@ mod forge_details {
|
|||
let user = User::new(given::a_name());
|
||||
let token = ApiToken::new(given::a_name().into());
|
||||
let forge_name = ForgeAlias::new(given::a_name());
|
||||
let forge_details = ForgeDetails::new(forge_name, forge_type, hostname, user, token);
|
||||
let forge_details = ForgeDetails::new(
|
||||
forge_name,
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number().map(CommitCount::from),
|
||||
);
|
||||
|
||||
let result = forge_details.with_hostname(other_hostname.clone());
|
||||
|
||||
|
@ -340,12 +401,14 @@ mod forge_details {
|
|||
let user = User::new(user_value.clone());
|
||||
let token_value = given::a_name();
|
||||
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_config = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname_value,
|
||||
user_value,
|
||||
token_value,
|
||||
max_dev_commits,
|
||||
BTreeMap::new(),
|
||||
);
|
||||
|
||||
|
@ -355,6 +418,12 @@ mod forge_details {
|
|||
assert_eq!(forge_details.hostname(), &hostname);
|
||||
assert_eq!(forge_details.user(), &user);
|
||||
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 {
|
||||
|
@ -470,7 +539,7 @@ mod server {
|
|||
let shout_webhook_url = shout.webhook_url().unwrap_or_default();
|
||||
let shout_webhook_secret = shout
|
||||
.webhook_secret()
|
||||
.map(|secret| secret.expose_secret().clone())
|
||||
.map(|secret| secret.expose_secret().to_string())
|
||||
.unwrap_or_default();
|
||||
let_assert!(Some(shout_email) = shout.email());
|
||||
let shout_email_from = shout_email.from();
|
||||
|
@ -492,6 +561,12 @@ mod server {
|
|||
let forge_hostname = forge_default.hostname();
|
||||
let forge_user = forge_default.user();
|
||||
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![];
|
||||
for (repo_alias, server_repo_config) in forge_default.repos() {
|
||||
let repo_path = server_repo_config.repo();
|
||||
|
@ -542,6 +617,7 @@ forge_type = "{forge_type}"
|
|||
hostname = "{forge_hostname}"
|
||||
user = "{forge_user}"
|
||||
token = "{forge_token}"
|
||||
{optional_max_dev_commits}
|
||||
|
||||
[forge.{forge_alias}.repos]
|
||||
{repos}
|
||||
|
@ -726,6 +802,23 @@ mod given {
|
|||
}
|
||||
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 {
|
||||
AppConfig::new(
|
||||
a_listen(),
|
||||
|
@ -785,6 +878,7 @@ mod given {
|
|||
a_name(), // hostname
|
||||
a_name(), // user
|
||||
a_name(), // token
|
||||
maybe_a_number(),
|
||||
some_server_repo_configs(),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
use crate::config::{BranchName, RepoBranches};
|
||||
use derive_more::Constructor;
|
||||
|
||||
#[derive(Debug, Constructor, derive_with::With)]
|
||||
#[derive(Clone, Debug, Constructor, PartialEq, Eq, derive_with::With)]
|
||||
pub struct Push {
|
||||
branch: BranchName,
|
||||
sha: String,
|
||||
|
@ -34,7 +34,7 @@ impl Push {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Branch {
|
||||
Main,
|
||||
Next,
|
||||
|
|
|
@ -62,7 +62,7 @@ newtype!(
|
|||
"The commit message for a git commit."
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Histories {
|
||||
pub main: Vec<Commit>,
|
||||
pub next: Vec<Commit>,
|
||||
|
|
|
@ -3,6 +3,9 @@ pub type Result<T> = core::result::Result<T, Error>;
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("io")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("unable to open repo: {0}")]
|
||||
UnableToOpenRepo(String),
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//
|
||||
|
||||
use std::borrow::ToOwned;
|
||||
|
||||
use take_until::TakeUntilExt;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use crate::{git, git::repository::open::OpenRepositoryLike, BranchName};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Force {
|
||||
No,
|
||||
From(git::GitRef),
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
|||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use secrecy::{ExposeSecret, Secret};
|
||||
use secrecy::{ExposeSecret, SecretString};
|
||||
use tracing::instrument;
|
||||
|
||||
/// The derived information about a repo, used to interact with it
|
||||
|
@ -49,10 +49,11 @@ impl RepoDetails {
|
|||
forge_config.hostname(),
|
||||
forge_config.user(),
|
||||
forge_config.token(),
|
||||
forge_config.max_dev_commits(),
|
||||
),
|
||||
}
|
||||
}
|
||||
pub(crate) fn origin(&self) -> secrecy::Secret<String> {
|
||||
pub(crate) fn origin(&self) -> secrecy::SecretString {
|
||||
let repo_details = self;
|
||||
let user = &repo_details.forge.user();
|
||||
let hostname = &repo_details.forge.hostname();
|
||||
|
@ -77,7 +78,7 @@ impl RepoDetails {
|
|||
}
|
||||
|
||||
// url is a secret as it contains auth token
|
||||
pub(crate) fn url(&self) -> Secret<String> {
|
||||
pub(crate) fn url(&self) -> SecretString {
|
||||
let user = self.forge.user();
|
||||
let token = self.forge.token().expose_secret();
|
||||
let auth_delim = if token.is_empty() { "" } else { ":" };
|
||||
|
@ -94,12 +95,7 @@ impl RepoDetails {
|
|||
|> GitDir::pathbuf
|
||||
|> gix::ThreadSafeRepository::open
|
||||
}?;
|
||||
let repo = pike! {
|
||||
gix_repo
|
||||
|> RwLock::new
|
||||
|> Arc::new
|
||||
|> RealOpenRepository::new
|
||||
};
|
||||
let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo)), self.forge.clone());
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
|
|
|
@ -60,13 +60,14 @@ impl RepositoryFactory for RealRepositoryFactory {
|
|||
|
||||
fn git_clone(&self, repo_details: &RepoDetails) -> Result<Box<dyn OpenRepositoryLike>> {
|
||||
tracing::info!("creating");
|
||||
let (gix_repo, _outcome) = gix::prepare_clone_bare(
|
||||
repo_details.origin().expose_secret().as_str(),
|
||||
&*repo_details.gitdir,
|
||||
)?
|
||||
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
|
||||
let (gix_repo, _outcome) =
|
||||
gix::prepare_clone_bare(repo_details.origin().expose_secret(), &*repo_details.gitdir)?
|
||||
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
|
||||
tracing::info!("created");
|
||||
let repo = RealOpenRepository::new(Arc::new(RwLock::new(gix_repo.into())));
|
||||
let repo = RealOpenRepository::new(
|
||||
Arc::new(RwLock::new(gix_repo.into())),
|
||||
repo_details.forge.clone(),
|
||||
);
|
||||
Ok(Box::new(repo))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//
|
||||
use crate::{
|
||||
git::{
|
||||
self,
|
||||
repository::{
|
||||
open::{OpenRepository, OpenRepositoryLike},
|
||||
test::TestRepository,
|
||||
|
@ -31,8 +32,11 @@ pub enum Repository {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) const fn test(fs: kxio::fs::FileSystem) -> TestRepository {
|
||||
TestRepository::new(fs, vec![], vec![])
|
||||
pub(crate) const fn test(
|
||||
fs: kxio::fs::FileSystem,
|
||||
forge_details: crate::ForgeDetails,
|
||||
) -> TestRepository {
|
||||
TestRepository::new(fs, vec![], vec![], forge_details)
|
||||
}
|
||||
|
||||
/// Opens a repository, cloning if necessary
|
||||
|
@ -44,7 +48,9 @@ pub fn open(
|
|||
) -> Result<Box<dyn OpenRepositoryLike>> {
|
||||
let open_repository = if repo_details.gitdir.exists() {
|
||||
info!("Local copy found - opening...");
|
||||
repository_factory.open(repo_details)?
|
||||
let repo = repository_factory.open(repo_details)?;
|
||||
repo.fetch()?;
|
||||
repo
|
||||
} else {
|
||||
info!("Local copy not found - cloning...");
|
||||
repository_factory.git_clone(repo_details)?
|
||||
|
@ -114,6 +120,9 @@ pub enum Error {
|
|||
#[error("git clone: {0}")]
|
||||
Clone(String),
|
||||
|
||||
#[error("git fetch: {0}")]
|
||||
FetchError(#[from] git::fetch::Error),
|
||||
|
||||
#[error("open: {0}")]
|
||||
Open(String),
|
||||
|
||||
|
|
|
@ -37,10 +37,11 @@ pub enum OpenRepository {
|
|||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn real(gix_repo: gix::Repository) -> OpenRepository {
|
||||
OpenRepository::Real(oreal::RealOpenRepository::new(Arc::new(RwLock::new(
|
||||
gix_repo.into(),
|
||||
))))
|
||||
pub fn real(gix_repo: gix::Repository, forge_details: crate::ForgeDetails) -> OpenRepository {
|
||||
OpenRepository::Real(oreal::RealOpenRepository::new(
|
||||
Arc::new(RwLock::new(gix_repo.into())),
|
||||
forge_details,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))] // don't test mocks
|
||||
|
@ -49,8 +50,15 @@ pub(crate) fn test(
|
|||
fs: &kxio::fs::FileSystem,
|
||||
on_fetch: Vec<otest::OnFetch>,
|
||||
on_push: Vec<otest::OnPush>,
|
||||
forge_details: crate::ForgeDetails,
|
||||
) -> OpenRepository {
|
||||
OpenRepository::Test(TestOpenRepository::new(gitdir, fs, on_fetch, on_push))
|
||||
OpenRepository::Test(TestOpenRepository::new(
|
||||
gitdir,
|
||||
fs,
|
||||
on_fetch,
|
||||
on_push,
|
||||
forge_details,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
|
@ -94,7 +102,8 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync {
|
|||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if there are any network connectivity issues with the remote server.
|
||||
/// Will return `Err` if there are any problems with the branch name being invalid, or any
|
||||
/// corruption of the git repository.
|
||||
fn commit_log(
|
||||
&self,
|
||||
branch_name: &BranchName,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use crate::{
|
||||
git::{self, repository::OpenRepositoryLike},
|
||||
BranchName, Hostname, RemoteUrl, RepoPath,
|
||||
BranchName, ForgeDetails, Hostname, RemoteUrl, RepoPath,
|
||||
};
|
||||
|
||||
use derive_more::Constructor;
|
||||
|
@ -16,11 +16,14 @@ use std::{
|
|||
};
|
||||
|
||||
#[derive(Clone, Debug, Constructor)]
|
||||
pub struct RealOpenRepository(Arc<RwLock<gix::ThreadSafeRepository>>);
|
||||
pub struct RealOpenRepository {
|
||||
inner: Arc<RwLock<gix::ThreadSafeRepository>>,
|
||||
forge_details: ForgeDetails,
|
||||
}
|
||||
impl super::OpenRepositoryLike for RealOpenRepository {
|
||||
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>> {
|
||||
let refs = self
|
||||
.0
|
||||
.inner
|
||||
.read()
|
||||
.map_err(|_| git::push::Error::Lock)
|
||||
.and_then(|repo| {
|
||||
|
@ -44,7 +47,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
|||
|
||||
#[tracing::instrument]
|
||||
fn find_default_remote(&self, direction: git::repository::Direction) -> Option<RemoteUrl> {
|
||||
let Ok(repository) = self.0.read() else {
|
||||
let Ok(repository) = self.inner.read() else {
|
||||
#[cfg(not(tarpaulin_include))] // don't test mutex lock failure
|
||||
tracing::debug!("no repository");
|
||||
return None;
|
||||
|
@ -64,34 +67,32 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
|||
#[tracing::instrument(skip_all)]
|
||||
#[cfg(not(tarpaulin_include))] // would require writing to external service
|
||||
fn fetch(&self) -> Result<(), git::fetch::Error> {
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
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
|
||||
if self
|
||||
.find_default_remote(git::repository::Direction::Fetch)
|
||||
.is_none()
|
||||
{
|
||||
return Err(git::fetch::Error::NoFetchRemoteFound);
|
||||
};
|
||||
remote
|
||||
.connect(gix::remote::Direction::Fetch)
|
||||
.map_err(|gix| git::fetch::Error::Connect(gix.to_string()))?
|
||||
.prepare_fetch(
|
||||
gix::progress::Discard,
|
||||
gix::remote::ref_map::Options::default(),
|
||||
)
|
||||
.map_err(|gix| git::fetch::Error::Prepare(gix.to_string()))?
|
||||
.receive(gix::progress::Discard, &AtomicBool::default())
|
||||
.map_err(|gix| git::fetch::Error::Receive(gix.to_string()))?;
|
||||
}
|
||||
info!("Fetching");
|
||||
gix::command::prepare("/usr/bin/git fetch --prune")
|
||||
.with_context(gix::diff::command::Context {
|
||||
git_dir: Some(
|
||||
self.inner
|
||||
.read()
|
||||
.map_err(|_| git::fetch::Error::Lock)
|
||||
.map(|r| r.git_dir().to_path_buf())?,
|
||||
),
|
||||
..Default::default()
|
||||
})
|
||||
.with_shell_allow_argument_splitting()
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
info!("Fetch okay");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: (#72) reimplement using `gix`
|
||||
#[cfg(not(tarpaulin_include))] // would require writing to external service
|
||||
#[tracing::instrument(skip_all)]
|
||||
fn push(
|
||||
|
@ -111,13 +112,13 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
|||
}
|
||||
};
|
||||
// INFO: never log the command as it contains the API token within the 'origin'
|
||||
let command: secrecy::Secret<String> = format!(
|
||||
let command: secrecy::SecretString = format!(
|
||||
"/usr/bin/git push {} {to_commit}:{branch_name} {force}",
|
||||
origin.expose_secret()
|
||||
)
|
||||
.into();
|
||||
let git_dir = self
|
||||
.0
|
||||
.inner
|
||||
.read()
|
||||
.map_err(|_| git::push::Error::Lock)
|
||||
.map(|r| r.git_dir().to_path_buf())?;
|
||||
|
@ -140,8 +141,14 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
|||
branch_name: &BranchName,
|
||||
find_commits: &[git::Commit],
|
||||
) -> Result<Vec<git::Commit>, git::commit::log::Error> {
|
||||
let limit = if find_commits.is_empty() { 1 } else { 25 };
|
||||
self.0
|
||||
let limit: usize = if find_commits.is_empty() {
|
||||
1
|
||||
} else {
|
||||
self.forge_details
|
||||
.max_dev_commits()
|
||||
.map_or(25, |commit_count| commit_count.clone().peel() as usize)
|
||||
};
|
||||
self.inner
|
||||
.read()
|
||||
.map_err(|_| git::commit::log::Error::Lock)
|
||||
.map(|repo| {
|
||||
|
@ -196,7 +203,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
|||
|
||||
#[tracing::instrument(skip_all, fields(%branch_name, ?file_name))]
|
||||
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String> {
|
||||
self.0
|
||||
self.inner
|
||||
.read()
|
||||
.map_err(|_| git::file::Error::Lock)
|
||||
.and_then(|repo| {
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
self,
|
||||
repository::open::{OpenRepositoryLike, RealOpenRepository},
|
||||
},
|
||||
BranchName, GitDir, RemoteUrl, RepoBranches,
|
||||
BranchName, ForgeDetails, GitDir, RemoteUrl, RepoBranches,
|
||||
};
|
||||
|
||||
use derive_more::Constructor;
|
||||
|
@ -99,7 +99,6 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
|||
.fetch_counter
|
||||
.read()
|
||||
.map_err(|_| git::fetch::Error::Lock)?;
|
||||
println!("Fetch: {i}");
|
||||
self.fetch_counter
|
||||
.write()
|
||||
.map_err(|_| git::fetch::Error::Lock)
|
||||
|
@ -156,6 +155,7 @@ impl TestOpenRepository {
|
|||
fs: &kxio::fs::FileSystem,
|
||||
on_fetch: Vec<OnFetch>,
|
||||
on_push: Vec<OnPush>,
|
||||
forge_details: ForgeDetails,
|
||||
) -> Self {
|
||||
let pathbuf = fs.base().join(gitdir.to_path_buf());
|
||||
#[allow(clippy::expect_used)]
|
||||
|
@ -166,7 +166,7 @@ impl TestOpenRepository {
|
|||
fetch_counter: Arc::new(RwLock::new(0)),
|
||||
on_push,
|
||||
push_counter: Arc::new(RwLock::new(0)),
|
||||
real: RealOpenRepository::new(Arc::new(RwLock::new(gix.into()))),
|
||||
real: RealOpenRepository::new(Arc::new(RwLock::new(gix.into())), forge_details),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use crate::CommitCount;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
|
@ -6,7 +8,8 @@ use super::*;
|
|||
fn should_return_single_item_in_commit_log_when_not_searching() -> TestResult {
|
||||
let_assert!(Ok(fs) = kxio::fs::temp());
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
|
||||
let repo_config = &given::a_repo_config();
|
||||
let branches = repo_config.branches();
|
||||
|
@ -23,7 +26,10 @@ fn should_return_capacity_25_in_commit_log_when_searching_for_garbage() -> TestR
|
|||
let_assert!(Ok(fs) = kxio::fs::temp());
|
||||
let branch_name = given::a_branch_name();
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(25)));
|
||||
let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
|
||||
assert!(**max_dev_commits >= 25);
|
||||
let test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
|
||||
for _ in [0; 25] {
|
||||
then::create_a_commit_on_branch(&fs, &gitdir, &branch_name)?;
|
||||
|
@ -39,7 +45,10 @@ fn should_return_5_in_commit_log_when_searching_for_5th_item() -> TestResult {
|
|||
let_assert!(Ok(fs) = kxio::fs::temp(), "create temp directory");
|
||||
let branch_name = given::a_branch_name();
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details().with_max_dev_commits(Some(CommitCount::from(10)));
|
||||
let_assert!(Some(max_dev_commits) = forge_details.max_dev_commits());
|
||||
assert!(**max_dev_commits > 5);
|
||||
let test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let_assert!(
|
||||
Ok(open_repository) = test_repository.open(&gitdir),
|
||||
"open repository"
|
||||
|
|
|
@ -14,7 +14,14 @@ fn should_return_repos() {
|
|||
let mut repos = BTreeMap::new();
|
||||
repos.insert(red_name.clone(), red.clone());
|
||||
repos.insert(blue_name.clone(), blue.clone());
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
let returned_repos = fc.repos().collect::<Vec<_>>();
|
||||
|
||||
|
@ -35,7 +42,14 @@ fn should_return_forge_type() {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.forge_type(), ForgeType::MockForge);
|
||||
}
|
||||
|
@ -47,7 +61,14 @@ fn should_return_hostname() {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname.clone(), user, token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname.clone(),
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.hostname(), Hostname::new(hostname));
|
||||
}
|
||||
|
@ -59,7 +80,14 @@ fn should_return_user() {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user.clone(), token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user.clone(),
|
||||
token,
|
||||
given::maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.user(), User::new(user));
|
||||
}
|
||||
|
@ -71,7 +99,14 @@ fn should_return_token() {
|
|||
let user = given::a_name();
|
||||
let token = given::a_name();
|
||||
let repos = BTreeMap::new();
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token.clone(), repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token.clone(),
|
||||
given::maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
assert_eq!(fc.token().expose_secret(), token.as_str());
|
||||
}
|
||||
|
@ -90,7 +125,14 @@ fn should_return_repo() {
|
|||
let mut repos = BTreeMap::new();
|
||||
repos.insert(red_name.clone(), red.clone());
|
||||
repos.insert(blue_name, blue);
|
||||
let fc = ForgeConfig::new(forge_type, hostname, user, token, repos);
|
||||
let fc = ForgeConfig::new(
|
||||
forge_type,
|
||||
hostname,
|
||||
user,
|
||||
token,
|
||||
given::maybe_a_number(),
|
||||
repos,
|
||||
);
|
||||
|
||||
let returned_repo = fc.get_repo(red_name.as_str());
|
||||
|
||||
|
|
|
@ -9,8 +9,9 @@ fn should_return_file() -> TestResult {
|
|||
let file_name = given::a_pathbuf();
|
||||
let contents = given::a_name();
|
||||
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));
|
||||
then::commit_named_file_to_branch(
|
||||
&file_name,
|
||||
|
@ -33,7 +34,8 @@ fn should_return_file() -> TestResult {
|
|||
fn should_error_on_missing_file() -> TestResult {
|
||||
let_assert!(Ok(fs) = kxio::fs::temp());
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
|
||||
let repo_config = &given::a_repo_config();
|
||||
let branches = repo_config.branches();
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
},
|
||||
RepoDetails,
|
||||
},
|
||||
GitDir,
|
||||
ForgeDetails, GitDir,
|
||||
};
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
|
@ -22,6 +22,7 @@ pub struct TestRepository {
|
|||
fs: kxio::fs::FileSystem,
|
||||
on_fetch: Vec<git::repository::open::otest::OnFetch>,
|
||||
on_push: Vec<git::repository::open::otest::OnPush>,
|
||||
forge_details: ForgeDetails,
|
||||
}
|
||||
impl TestRepository {
|
||||
pub fn on_fetch(&mut self, on_fetch: OnFetch) {
|
||||
|
@ -39,6 +40,7 @@ impl RepositoryLike for TestRepository {
|
|||
&self.fs,
|
||||
self.on_fetch.clone(),
|
||||
self.on_push.clone(),
|
||||
self.forge_details.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@ mod repo_details {
|
|||
"host".to_string(),
|
||||
"user".to_string(),
|
||||
"token".to_string(),
|
||||
given::maybe_a_number(), // max dev commits
|
||||
BTreeMap::new(),
|
||||
),
|
||||
GitDir::new(PathBuf::default().join("foo"), StoragePathType::Internal),
|
||||
|
@ -184,6 +185,8 @@ mod repo_details {
|
|||
}
|
||||
}
|
||||
pub mod given {
|
||||
use crate::ForgeDetails;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn repo_branches() -> RepoBranches {
|
||||
|
@ -219,6 +222,22 @@ pub mod given {
|
|||
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 {
|
||||
BranchName::new(a_name())
|
||||
}
|
||||
|
@ -235,14 +254,19 @@ pub mod given {
|
|||
format!("hostname-{}", a_name()),
|
||||
format!("user-{}", a_name()),
|
||||
format!("token-{}", a_name()),
|
||||
BTreeMap::default(), // no repos
|
||||
given::maybe_a_number(), // max dev commits
|
||||
BTreeMap::default(), // no repos
|
||||
)
|
||||
}
|
||||
|
||||
pub fn forge_details() -> ForgeDetails {
|
||||
(&a_forge_alias(), &a_forge_config()).into()
|
||||
}
|
||||
|
||||
pub fn a_server_repo_config() -> ServerRepoConfig {
|
||||
let main = a_branch_name().unwrap();
|
||||
let next = a_branch_name().unwrap();
|
||||
let dev = a_branch_name().unwrap();
|
||||
let main = a_branch_name().peel();
|
||||
let next = a_branch_name().peel();
|
||||
let dev = a_branch_name().peel();
|
||||
ServerRepoConfig::new(
|
||||
format!("{}/{}", a_name(), a_name()),
|
||||
main.clone(),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
//
|
||||
use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias};
|
||||
use serde_json::json;
|
||||
|
@ -114,4 +116,49 @@ 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 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let message = match self {
|
||||
Self::CICheckFailed { commit, .. } => format!("CI Check Failed [{commit}]"),
|
||||
Self::RepoConfigLoadFailure { reason, .. } => {
|
||||
format!("Failed to load repo config: {reason}")
|
||||
}
|
||||
Self::WebhookRegistration { reason, .. } => {
|
||||
format!("Failed to register webhook: {reason}")
|
||||
}
|
||||
Self::DevNotBasedOnMain {
|
||||
dev_branch,
|
||||
main_branch,
|
||||
..
|
||||
} => format!("{dev_branch} not based on {main_branch}"),
|
||||
};
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//
|
||||
use crate::{
|
||||
git::{self, graph::log, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
|
||||
git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
|
||||
BranchName, RepoConfig,
|
||||
};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
|
@ -27,12 +28,14 @@ pub fn validate(
|
|||
open_repository: &dyn OpenRepositoryLike,
|
||||
repo_details: &git::RepoDetails,
|
||||
repo_config: &RepoConfig,
|
||||
) -> Result<Positions> {
|
||||
) -> Result<(Positions, git::graph::Log)> {
|
||||
let main_branch = repo_config.branches().main();
|
||||
let next_branch = repo_config.branches().next();
|
||||
let dev_branch = repo_config.branches().dev();
|
||||
// Collect Commit Histories for `main`, `next` and `dev` branches
|
||||
open_repository.fetch()?;
|
||||
let git_log = git::graph::log(repo_details);
|
||||
|
||||
let commit_histories = get_commit_histories(open_repository, repo_config)?;
|
||||
// branch tips
|
||||
let main = commit_histories
|
||||
|
@ -61,7 +64,7 @@ pub fn validate(
|
|||
main_branch,
|
||||
dev_commit: dev,
|
||||
main_commit: main,
|
||||
log: log(repo_details),
|
||||
log: git_log,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -77,23 +80,38 @@ pub fn validate(
|
|||
&main,
|
||||
) {
|
||||
tracing::info!("Main not on same commit as next, or it's parent - resetting next to main",);
|
||||
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
|
||||
return Err(reset_next_to_main(
|
||||
open_repository,
|
||||
repo_details,
|
||||
&main,
|
||||
&next,
|
||||
&next_branch,
|
||||
));
|
||||
}
|
||||
// verify that next is an ancestor of dev, else reset it back to main if dev not ahead of main
|
||||
if is_not_based_on(&commit_histories.dev, &next)
|
||||
&& commit_histories.main.first() == commit_histories.dev.first()
|
||||
{
|
||||
tracing::info!("Next is not an ancestor of dev - resetting next to main");
|
||||
return reset_next_to_main(open_repository, repo_details, &main, &next, &next_branch);
|
||||
return Err(reset_next_to_main(
|
||||
open_repository,
|
||||
repo_details,
|
||||
&main,
|
||||
&next,
|
||||
&next_branch,
|
||||
));
|
||||
}
|
||||
let next_is_valid = is_based_on(&commit_histories.dev, &next);
|
||||
Ok(git::validation::positions::Positions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history: commit_histories.dev,
|
||||
next_is_valid,
|
||||
})
|
||||
Ok((
|
||||
git::validation::positions::Positions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history: commit_histories.dev,
|
||||
next_is_valid,
|
||||
},
|
||||
git_log,
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::result_large_err)]
|
||||
|
@ -103,22 +121,19 @@ fn reset_next_to_main(
|
|||
main: &git::Commit,
|
||||
next: &git::Commit,
|
||||
next_branch: &BranchName,
|
||||
) -> Result<Positions> {
|
||||
git::push::reset(
|
||||
) -> Error {
|
||||
match git::push::reset(
|
||||
open_repository,
|
||||
repo_details,
|
||||
next_branch,
|
||||
&main.clone().into(),
|
||||
&git::push::Force::From(next.clone().into()),
|
||||
)
|
||||
.map_err(|err| {
|
||||
Error::NonRetryable(format!(
|
||||
) {
|
||||
Ok(()) => Error::Retryable(format!("Branch {next_branch} has been reset")),
|
||||
Err(err) => Error::NonRetryable(format!(
|
||||
"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 {
|
||||
|
@ -129,13 +144,23 @@ fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
|
|||
commits.iter().any(|commit| commit == needle)
|
||||
}
|
||||
|
||||
fn get_commit_histories(
|
||||
/// Returns the commit logs for the main, next and dev branches
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if there are any problems with the branch names being invalid, or any
|
||||
/// corruption of the git repository.
|
||||
#[instrument]
|
||||
pub fn get_commit_histories(
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
repo_config: &RepoConfig,
|
||||
) -> git::commit::log::Result<git::commit::Histories> {
|
||||
debug!("main...");
|
||||
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
|
||||
let main_head = [main[0].clone()];
|
||||
debug!("next");
|
||||
let next = open_repository.commit_log(&repo_config.branches().next(), &main_head)?;
|
||||
debug!("dev");
|
||||
let dev = open_repository.commit_log(&repo_config.branches().dev(), &main_head)?;
|
||||
let histories = git::commit::Histories { main, next, dev };
|
||||
Ok(histories)
|
||||
|
|
|
@ -207,7 +207,8 @@ mod positions {
|
|||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let mut test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let mut test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let repo_config = given::a_repo_config();
|
||||
test_repository.on_fetch(OnFetch::new(
|
||||
repo_config.branches().clone(),
|
||||
|
@ -257,7 +258,8 @@ mod positions {
|
|||
//given
|
||||
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let mut test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let mut test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let repo_config = given::a_repo_config();
|
||||
test_repository.on_fetch(OnFetch::new(
|
||||
repo_config.branches().clone(),
|
||||
|
@ -343,7 +345,8 @@ mod positions {
|
|||
//given
|
||||
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let mut test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let mut test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let repo_config = given::a_repo_config();
|
||||
test_repository.on_fetch(OnFetch::new(
|
||||
repo_config.branches().clone(),
|
||||
|
@ -416,7 +419,8 @@ mod positions {
|
|||
//given
|
||||
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let mut test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let mut test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let repo_config = given::a_repo_config();
|
||||
test_repository.on_fetch(OnFetch::new(
|
||||
repo_config.branches().clone(),
|
||||
|
@ -501,7 +505,8 @@ mod positions {
|
|||
//given
|
||||
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let mut test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let mut test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let repo_config = given::a_repo_config();
|
||||
test_repository.on_fetch(OnFetch::new(
|
||||
repo_config.branches().clone(),
|
||||
|
@ -567,7 +572,8 @@ mod positions {
|
|||
|
||||
//when
|
||||
let_assert!(
|
||||
Ok(positions) = validate(&*open_repository, &repo_details, &repo_config),
|
||||
Ok((positions, _git_log)) =
|
||||
validate(&*open_repository, &repo_details, &repo_config),
|
||||
"validate"
|
||||
);
|
||||
|
||||
|
@ -597,7 +603,8 @@ mod positions {
|
|||
//given
|
||||
let_assert!(Ok(fs) = kxio::fs::temp(), "temp fs");
|
||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
||||
let mut test_repository = git::repository::test(fs.clone());
|
||||
let forge_details = given::forge_details();
|
||||
let mut test_repository = git::repository::test(fs.clone(), forge_details);
|
||||
let repo_config = given::a_repo_config();
|
||||
test_repository.on_fetch(OnFetch::new(
|
||||
repo_config.branches().clone(),
|
||||
|
@ -626,7 +633,8 @@ mod positions {
|
|||
|
||||
//when
|
||||
let_assert!(
|
||||
Ok(positions) = validate(&*open_repository, &repo_details, &repo_config),
|
||||
Ok((positions, _git_log)) =
|
||||
validate(&*open_repository, &repo_details, &repo_config),
|
||||
"validate"
|
||||
);
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue