Compare commits
No commits in common. "main" and "v0.12.0" have entirely different histories.
198 changed files with 1703 additions and 10790 deletions
|
@ -1,7 +1,17 @@
|
||||||
# ./cargo/config.toml
|
# ./cargo/config.toml
|
||||||
|
[target.x86_64-unknown-linux-gnu]
|
||||||
|
linker = "/usr/bin/clang-16"
|
||||||
|
rustflags = [
|
||||||
|
"-C",
|
||||||
|
"link-arg=--ld-path=/usr/bin/mold",
|
||||||
|
"--cfg",
|
||||||
|
"tokio_unstable",
|
||||||
|
]
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
RUST_LOG = "hyper=warn,git_url_parse=warn,debug"
|
RUST_LOG = "hyper=warn,git_url_parse=warn,debug"
|
||||||
|
RUSTFLAGS = "--cfg tokio_unstable"
|
||||||
|
|
25
.forgejo/workflows/publish-to-crates-io.yml.disabled
Normal file
25
.forgejo/workflows/publish-to-crates-io.yml.disabled
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: Publish to crates.io
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login
|
||||||
|
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
|
||||||
|
with:
|
||||||
|
args: login "$CARGO_REGISTRY_TOKEN"
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
|
||||||
|
with:
|
||||||
|
args: publish --registry crates-io --no-verify
|
|
@ -1,35 +0,0 @@
|
||||||
name: Release Please
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release-plz:
|
|
||||||
name: Release-plz
|
|
||||||
runs-on: docker
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Run release-plz release-pr
|
|
||||||
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@v2.3.0
|
|
||||||
with:
|
|
||||||
args: release-plz release --backend gitea --git-token ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
env:
|
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
|
@ -13,40 +13,26 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
toolchain:
|
|
||||||
- name: stable
|
|
||||||
- name: nightly
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check TODOs
|
|
||||||
uses: kemitix/todo-checker@v1.1.0
|
|
||||||
|
|
||||||
- name: Machete
|
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
|
||||||
with:
|
|
||||||
args: ${{ matrix.toolchain.name }} cargo machete
|
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
|
||||||
with:
|
with:
|
||||||
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
|
args: fmt --all -- --check
|
||||||
|
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
|
||||||
with:
|
with:
|
||||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset clippy
|
args: clippy
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
|
||||||
with:
|
with:
|
||||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset build
|
args: build
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
uses: https://git.kemitix.net/kemitix/rust@v2.3.0
|
uses: https://git.kemitix.net/kemitix/rust@v0.3.1
|
||||||
with:
|
with:
|
||||||
args: ${{ matrix.toolchain.name }} cargo hack --feature-powerset test
|
args: test
|
||||||
|
|
6
.git-next.toml
Normal file
6
.git-next.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[branches]
|
||||||
|
main = "main"
|
||||||
|
next = "next"
|
||||||
|
dev = "dev"
|
||||||
|
|
||||||
|
[options]
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,6 +1,3 @@
|
||||||
# git-next ui logs
|
|
||||||
.local/
|
|
||||||
|
|
||||||
# ---> Rust
|
# ---> Rust
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
|
@ -12,7 +9,7 @@ target/
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
# Cargo.lock
|
Cargo.lock
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
# These are backup files generated by rustfmt
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
steps:
|
steps:
|
||||||
docker-build:
|
todo_check:
|
||||||
|
# INFO: This doesn't have an equivalent yet for Forgejo Actions
|
||||||
|
# INFO: https://woodpecker-ci.org/plugins/TODO-Checker
|
||||||
|
image: codeberg.org/epsilon_02/todo-checker:1.1
|
||||||
when:
|
when:
|
||||||
- event: push
|
- event: push
|
||||||
branch: next
|
branch: next
|
||||||
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
|
|
||||||
image: docker.io/woodpeckerci/plugin-docker-buildx:5.0.0
|
|
||||||
settings:
|
settings:
|
||||||
username: kemitix
|
# git-next-woodpecker-todo-checker - read:issue
|
||||||
repo: git.kemitix.net/kemitix/git-next
|
repository_token: "776a3b928b852472c2af727a360c85c00af64b9f"
|
||||||
dockerfile: Dockerfile
|
prefix_regex: "(#|//) (TODO|FIXME): "
|
||||||
auto_tag: false
|
debug: false
|
||||||
dry-run: true # don't push to remote repo
|
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
steps:
|
steps:
|
||||||
|
publish-to-forgejo:
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/v*
|
||||||
|
# INFO: https://woodpecker-ci.org/plugins/Gitea%20Release
|
||||||
|
image: docker.io/woodpeckerci/plugin-gitea-release:0.3.2
|
||||||
|
settings:
|
||||||
|
base_url: https://git.kemitix.net
|
||||||
|
api_key:
|
||||||
|
from_secret: FORGEJO_RELEASE_PLUGIN
|
||||||
|
target: main
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
when:
|
when:
|
||||||
- event: tag
|
- event: tag
|
||||||
ref: refs/tags/v*
|
ref: refs/tags/v*
|
||||||
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
|
# INFO: https://woodpecker-ci.org/plugins/Docker%20Buildx
|
||||||
image: docker.io/woodpeckerci/plugin-docker-buildx:5.0.0
|
image: docker.io/woodpeckerci/plugin-docker-buildx:4.0.0
|
||||||
settings:
|
settings:
|
||||||
username: kemitix
|
username: kemitix
|
||||||
repo: git.kemitix.net/kemitix/git-next
|
repo: git.kemitix.net/kemitix/git-next
|
||||||
|
|
298
CHANGELOG.md
298
CHANGELOG.md
|
@ -2,305 +2,8 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## `git-next-core` - [0.13.11](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.10...git-next-core-v0.13.11) - 2024-09-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- should fetch repo on startup when not cloning
|
|
||||||
- Remove branches when fetching from remote
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- reimplement git fetch using git
|
|
||||||
|
|
||||||
## `git-next` - [0.13.11](https://git.kemitix.net/kemitix/git-next/compare/v0.13.10...v0.13.11) - 2024-09-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- *(tui)* add time and version in border
|
|
||||||
- should fetch repo on startup when not cloning
|
|
||||||
- Remove branches when fetching from remote
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- Update TUI sooner when receiving CI status
|
|
||||||
- reimplement git fetch using git
|
|
||||||
- mark tui as complete on roadmap
|
|
||||||
- Add missing port mapping parameter for running in docker
|
|
||||||
|
|
||||||
## `git-next-forge-github` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.9...git-next-forge-github-v0.13.10) - 2024-09-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- optionally specify max commits between dev and main
|
|
||||||
|
|
||||||
## `git-next-forge-forgejo` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.9...git-next-forge-forgejo-v0.13.10) - 2024-09-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- optionally specify max commits between dev and main
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.9...git-next-core-v0.13.10) - 2024-09-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- optionally specify max commits between dev and main
|
|
||||||
|
|
||||||
## `git-next` - [0.13.10](https://git.kemitix.net/kemitix/git-next/compare/v0.13.9...v0.13.10) - 2024-09-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- optionally specify max commits between dev and main
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- *(tui)* make tui work from docker image
|
|
||||||
- *(tui)* alerts, such as WIP aren't being reset
|
|
||||||
- *(test)* tests requiring .git pass when not present
|
|
||||||
- *(tui)* update ui when push next or main finishes
|
|
||||||
- *(tui)* don't set background for normal repo alias
|
|
||||||
|
|
||||||
## `git-next` - [0.13.9](https://git.kemitix.net/kemitix/git-next/compare/v0.13.8...v0.13.9) - 2024-09-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- *(tui)* alerts are cleared on next repo update
|
|
||||||
- shutdown properly on error
|
|
||||||
- shutdown properly on file parse error
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- Expand docker docmentation
|
|
||||||
|
|
||||||
## `git-next-forge-forgejo` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.7...git-next-forge-forgejo-v0.13.8) - 2024-09-01
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- flatten nested blocks with early returns
|
|
||||||
- rename method as peel
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.7...git-next-core-v0.13.8) - 2024-09-01
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- use configured branch names in user notification
|
|
||||||
- create git graph log to after doing a fetch
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- flatten nested blocks with early returns
|
|
||||||
- rename method as peel
|
|
||||||
|
|
||||||
## `git-next` - [0.13.8](https://git.kemitix.net/kemitix/git-next/compare/v0.13.7...v0.13.8) - 2024-09-01
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- improved error display when startup fails
|
|
||||||
- *(tui)* clean up alert display
|
|
||||||
- *(tui)* remove some borders to clean up appearance
|
|
||||||
- *(tui)* make progression of branches clearer
|
|
||||||
- *(tui)* remove label from repo identity widget
|
|
||||||
- *(tui)* hightlight repo alias in red when in alert
|
|
||||||
- *(tui)* branch names look more like 'pills'
|
|
||||||
- *(tui)* highlight branchs in log
|
|
||||||
- *(tui)* hightlight status message in colour
|
|
||||||
- *(tui)* use moving heart emoji as liveness indicator
|
|
||||||
- *(tui)* add scrolling when overflow screen
|
|
||||||
- *(tui)* forge widgets only use required lines
|
|
||||||
- *(tui)* repo widgets only use required lines
|
|
||||||
- *(tui)* move forge alias to left and add prefix
|
|
||||||
- *(tui)* remove count of forges
|
|
||||||
- *(tui)* remove duplicate messages from repo body
|
|
||||||
- *(tui)* highlight user interventions in red
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- use configured branch names in user notification
|
|
||||||
- remove unused imports
|
|
||||||
- *(tui)* remove logging from inside ui loop
|
|
||||||
- *(tui)* don't show HEAD in log
|
|
||||||
- *(tui)* improve colour contrast on light background
|
|
||||||
- *(tui)* remove unused import
|
|
||||||
- *(alert)* typo in email message
|
|
||||||
- *(repo)* avoid blocking threads when pausing
|
|
||||||
- *(test)* give actix more time to process message
|
|
||||||
- *(test)* give actix more time to process message
|
|
||||||
- *(test)* give actix more time to process message
|
|
||||||
- *(tui)* improve reliability of status updates
|
|
||||||
- create git graph log to after doing a fetch
|
|
||||||
- *(tui)* remove logging of tui updates
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- flatten nested blocks with early returns
|
|
||||||
- merge identical match branches
|
|
||||||
- *(tui)* add regex dependency
|
|
||||||
- *(tui)* introduce LogLine to wrap log formatting
|
|
||||||
- *(tui)* simplify repo identity widget
|
|
||||||
- rename method as peel
|
|
||||||
- *(tui)* child widget can provide constraint to container
|
|
||||||
- *(tui)* merge repo widgets into one
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.6...git-next-core-v0.13.7) - 2024-08-25
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- *(tui)* (experimental) show repo state, messages and git log
|
|
||||||
|
|
||||||
## `git-next` - [0.13.7](https://git.kemitix.net/kemitix/git-next/compare/v0.13.6...v0.13.7) - 2024-08-25
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- *(tui)* (experimental) show repo state, messages and git log
|
|
||||||
|
|
||||||
## `git-next-forge-github` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.5...git-next-forge-github-v0.13.6) - 2024-08-23
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- *(github)* register webhook with valid callback url
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.5...git-next-core-v0.13.6) - 2024-08-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- *(tui)* (experimental) tui option
|
|
||||||
|
|
||||||
## `git-next` - [0.13.6](https://git.kemitix.net/kemitix/git-next/compare/v0.13.5...v0.13.6) - 2024-08-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- *(tui)* (experimental) tui option
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- file_watcher runs on own thread
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- test all feature combinations
|
|
||||||
|
|
||||||
## `git-next` - [0.13.5](https://git.kemitix.net/kemitix/git-next/compare/git-next-v0.13.4...git-next-v0.13.5) - 2024-08-10
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- make forge and repo alias more prominent in email
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- invalid config section typo in README
|
|
||||||
|
|
||||||
## `git-next-forge-github` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.3...git-next-forge-github-v0.13.4) - 2024-08-08
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- cleanup pedantic clippy in forge-github crate
|
|
||||||
|
|
||||||
## `git-next-forge-forgejo` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.3...git-next-forge-forgejo-v0.13.4) - 2024-08-08
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- cleanup pedantic clippy in forge-forgejo crate
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.3...git-next-core-v0.13.4) - 2024-08-08
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- add short git log graph to notifications
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- macros use a more common syntax
|
|
||||||
- cleanup pedantic clippy in core crate
|
|
||||||
|
|
||||||
## `git-next` - [0.13.4](https://git.kemitix.net/kemitix/git-next/compare/v0.13.3...v0.13.4) - 2024-08-08
|
|
||||||
|
|
||||||
### Added
|
|
||||||
- add short git log graph to notifications
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- remove dependcy on clang & mold
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- macros use a more common syntax
|
|
||||||
- cleanup pedantic clippy in core crate
|
|
||||||
- cleanup pedantic clippy in cli crate
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.3](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.2...git-next-core-v0.13.3) - 2024-08-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- shout.desktop should be optional
|
|
||||||
|
|
||||||
## `git-next` - [0.13.3](https://git.kemitix.net/kemitix/git-next/compare/v0.13.2...v0.13.3) - 2024-08-04
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- shout.desktop should be optional
|
|
||||||
|
|
||||||
## `git-next` - [0.13.2](https://git.kemitix.net/kemitix/git-next/compare/v0.13.1...v0.13.2) - 2024-08-04
|
|
||||||
|
|
||||||
### Other
|
|
||||||
- timing test waits longer than expiry
|
|
||||||
|
|
||||||
## `git-next-forge-github` - [0.13.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.13.0...git-next-forge-github-v0.13.1) - 2024-08-04
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- remove unused dependencies
|
|
||||||
|
|
||||||
## `git-next-forge-forgejo` - [0.13.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.13.0...git-next-forge-forgejo-v0.13.1) - 2024-08-04
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- remove unused dependencies
|
|
||||||
|
|
||||||
## `git-next-core` - [0.13.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.13.0...git-next-core-v0.13.1) - 2024-08-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- prevent duplicate alerts
|
|
||||||
- add support for desktop notifications
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- remove unused dependencies
|
|
||||||
- update tests to check for email config parsing
|
|
||||||
|
|
||||||
## `git-next` - [0.13.1](https://git.kemitix.net/kemitix/git-next/compare/v0.13.0...v0.13.1) - 2024-08-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- prevent duplicate alerts
|
|
||||||
- add support for desktop notifications
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- add example email config to server default template
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- remove unused dependencies
|
|
||||||
- extract alerts into own actor
|
|
||||||
- add example to readme for listen, shout & storage
|
|
||||||
- add config details for sending emails
|
|
||||||
|
|
||||||
## `git-next-forge-github` [0.13.0](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.12.1...git-next-forge-github-v0.13.0) - 2024-08-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- [**breaking**] restructured server config into listen & shout sections
|
|
||||||
|
|
||||||
## `git-next-forge-forgejo` [0.13.0](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.12.1...git-next-forge-forgejo-v0.13.0) - 2024-08-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- [**breaking**] restructured server config into listen & shout sections
|
|
||||||
|
|
||||||
## `git-next-core` [0.13.0](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.12.1...git-next-core-v0.13.0) - 2024-08-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- send email notifications (sendmail/smtp)
|
|
||||||
- [**breaking**] restructured server config into listen & shout sections
|
|
||||||
- remove notification.type
|
|
||||||
- [**breaking**] reduce the max commit dev can be ahead of main
|
|
||||||
|
|
||||||
## `git-next` [0.13.0](https://git.kemitix.net/kemitix/git-next/compare/git-next-v0.12.1...git-next-v0.13.0) - 2024-08-02
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- send email notifications (sendmail/smtp)
|
|
||||||
- [**breaking**] restructured server config into listen & shout sections
|
|
||||||
- remove notification.type
|
|
||||||
- terminate process if config file is invalid
|
|
||||||
- return better errors to user on server failure
|
|
||||||
- return better errors to the user on init
|
|
||||||
|
|
||||||
## [0.12.1] - 2024-07-29
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
- Webhook secret doesn't need to be base64 encoded ([691a733](https://git.kemitix.net/kemitix/git-next/commit/691a733fc37cfba5d9be72b57e24c5b9d3c1218a))
|
|
||||||
- Remove requirement for RUSTFLAGS to be set ([e56d6a3](https://git.kemitix.net/kemitix/git-next/commit/e56d6a3ebbb4b4bfcaacc986269ba898ffbd1bc6))
|
|
||||||
- Make default server config example valid ([b7abe94](https://git.kemitix.net/kemitix/git-next/commit/b7abe949e2067e1c3663d45a520385d967f19af8))
|
|
||||||
|
|
||||||
### Miscellaneous Tasks
|
|
||||||
|
|
||||||
- Update create publishing command ([bf12712](https://git.kemitix.net/kemitix/git-next/commit/bf12712bcaaefe6ae7da113e03b739b42d860fcf))
|
|
||||||
- Remove deprecated crates ([5dc0de8](https://git.kemitix.net/kemitix/git-next/commit/5dc0de8a05d610c3a5b7be00aac1033763a76949))
|
|
||||||
|
|
||||||
## [0.12.0] - 2024-07-28
|
## [0.12.0] - 2024-07-28
|
||||||
|
|
||||||
[656ec4a](https://git.kemitix.net/kemitix/git-next/commit/656ec4a534b5b55ddceb05eee6ed610207ac33d4)...[b89431b](https://git.kemitix.net/kemitix/git-next/commit/b89431b7798dec0ab80010d76327bef89b94eeb0)
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
- Don't log content of internal messages ([3ae1132](https://git.kemitix.net/kemitix/git-next/commit/3ae113212af3ee43f36383a22984e03e3f44f3f2))
|
- Don't log content of internal messages ([3ae1132](https://git.kemitix.net/kemitix/git-next/commit/3ae113212af3ee43f36383a22984e03e3f44f3f2))
|
||||||
|
@ -320,7 +23,6 @@ All notable changes to this project will be documented in this file.
|
||||||
- Remove deprecated crates ([5a595ec](https://git.kemitix.net/kemitix/git-next/commit/5a595ec9eed77cf961f01c671c69ca2bc7988092))
|
- Remove deprecated crates ([5a595ec](https://git.kemitix.net/kemitix/git-next/commit/5a595ec9eed77cf961f01c671c69ca2bc7988092))
|
||||||
- Bump gix from 0.63 to 0.64 ([b24675d](https://git.kemitix.net/kemitix/git-next/commit/b24675d48a3e35a9d780a7f7f8cbfb1477765a7b))
|
- Bump gix from 0.63 to 0.64 ([b24675d](https://git.kemitix.net/kemitix/git-next/commit/b24675d48a3e35a9d780a7f7f8cbfb1477765a7b))
|
||||||
- Bump mockall from 0.12 to 0.13 ([22faa85](https://git.kemitix.net/kemitix/git-next/commit/22faa851dcdd99451c736290bc17b17cbe6aa55c))
|
- Bump mockall from 0.12 to 0.13 ([22faa85](https://git.kemitix.net/kemitix/git-next/commit/22faa851dcdd99451c736290bc17b17cbe6aa55c))
|
||||||
- Release 0.12.0 ([b89431b](https://git.kemitix.net/kemitix/git-next/commit/b89431b7798dec0ab80010d76327bef89b94eeb0))
|
|
||||||
|
|
||||||
### Refactor
|
### Refactor
|
||||||
|
|
||||||
|
|
5060
Cargo.lock
generated
5060
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
59
Cargo.toml
59
Cargo.toml
|
@ -3,7 +3,7 @@ resolver = "2"
|
||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.13.11"
|
version = "0.12.0" # Update git-next-* under workspace.dependencies
|
||||||
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -15,33 +15,32 @@ documentation = "https://git.kemitix.net/kemitix/git-next/src/branch/main/README
|
||||||
keywords = ["git", "cli", "server", "tool"]
|
keywords = ["git", "cli", "server", "tool"]
|
||||||
categories = ["development-tools"]
|
categories = ["development-tools"]
|
||||||
|
|
||||||
# [workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
# pedantic = { level = "warn", priority = -1 }
|
nursery = { level = "warn", priority = -1 }
|
||||||
# nursery = { level = "warn", priority = -1 }
|
# pedantic = "warn"
|
||||||
# unwrap_used = "warn"
|
unwrap_used = "warn"
|
||||||
# expect_used = "warn"
|
expect_used = "warn"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
git-next-core = { path = "crates/core", version = "0.13" }
|
git-next-core = { path = "crates/core", version = "0.12" }
|
||||||
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.13" }
|
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.12" }
|
||||||
git-next-forge-github = { path = "crates/forge-github", version = "0.13" }
|
git-next-forge-github = { path = "crates/forge-github", version = "0.12" }
|
||||||
|
|
||||||
# TUI
|
# remove after 0.12.0
|
||||||
ratatui = "0.29"
|
git-next-server = { path = "crates/server", version = "0.12" }
|
||||||
directories = "5.0"
|
git-next-server-actor = { path = "crates/server-actor", version = "0.12" }
|
||||||
lazy_static = "1.5"
|
git-next-forge = { path = "crates/forge", version = "0.12" }
|
||||||
color-eyre = "0.6"
|
git-next-repo-actor = { path = "crates/repo-actor", version = "0.12" }
|
||||||
tui-scrollview = "0.5"
|
git-next-webhook-actor = { path = "crates/webhook-actor", version = "0.12" }
|
||||||
regex = "1.10"
|
git-next-file-watcher-actor = { path = "crates/file-watcher-actor", version = "0.12" }
|
||||||
chrono = "0.4"
|
|
||||||
|
|
||||||
# CLI parsing
|
# CLI parsing
|
||||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
|
console-subscriber = "0.3"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = "0.3"
|
||||||
tracing-error = "0.2.0"
|
|
||||||
|
|
||||||
# base64 decoding
|
# base64 decoding
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
@ -52,7 +51,8 @@ sha2 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
||||||
# git
|
# git
|
||||||
gix = { version = "0.67", features = [
|
# gix = "0.62"
|
||||||
|
gix = { version = "0.64", features = [
|
||||||
"dirwalk",
|
"dirwalk",
|
||||||
"blocking-http-transport-reqwest-rust-tls",
|
"blocking-http-transport-reqwest-rust-tls",
|
||||||
] }
|
] }
|
||||||
|
@ -68,7 +68,7 @@ serde_json = "1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|
||||||
# Secrets and Password
|
# Secrets and Password
|
||||||
secrecy = "0.10"
|
secrecy = "0.8"
|
||||||
|
|
||||||
# Conventional Commit check
|
# Conventional Commit check
|
||||||
git-conventional = "0.12"
|
git-conventional = "0.12"
|
||||||
|
@ -81,8 +81,7 @@ time = "0.3"
|
||||||
standardwebhooks = "1.0"
|
standardwebhooks = "1.0"
|
||||||
|
|
||||||
# boilerplate
|
# boilerplate
|
||||||
bon = "2.0"
|
derive_more = { version = "1.0.0-beta.6", features = [
|
||||||
derive_more = { version = "1.0.0-beta", features = [
|
|
||||||
"as_ref",
|
"as_ref",
|
||||||
"constructor",
|
"constructor",
|
||||||
"display",
|
"display",
|
||||||
|
@ -90,32 +89,20 @@ derive_more = { version = "1.0.0-beta", features = [
|
||||||
"from",
|
"from",
|
||||||
] }
|
] }
|
||||||
derive-with = "0.5"
|
derive-with = "0.5"
|
||||||
anyhow = "1.0"
|
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
pike = "0.1"
|
pike = "0.1"
|
||||||
|
|
||||||
# iters
|
|
||||||
take-until = "0.2"
|
|
||||||
|
|
||||||
# file watcher
|
# file watcher
|
||||||
notify = "7.0"
|
notify = "6.1"
|
||||||
|
|
||||||
# Actors
|
# Actors
|
||||||
actix = "0.13"
|
actix = "0.13"
|
||||||
actix-rt = "2.9"
|
actix-rt = "2.9"
|
||||||
tokio = { version = "1.37", features = ["rt", "macros"] }
|
tokio = { version = "1.37", features = ["rt", "macros"] }
|
||||||
|
|
||||||
# email
|
|
||||||
lettre = "0.11"
|
|
||||||
sendmail = "2.0"
|
|
||||||
|
|
||||||
# desktop notifications
|
|
||||||
notifica = "3.0"
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
assert2 = "0.3"
|
assert2 = "0.3"
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
mockall = "0.13"
|
mockall = "0.13"
|
||||||
test-log = "0.2"
|
test-log = "0.2"
|
||||||
rstest = { version = "0.23", features = ["async-timeout"] }
|
|
||||||
|
|
16
Dockerfile
16
Dockerfile
|
@ -1,6 +1,6 @@
|
||||||
# Leveraging the pre-built Docker images with
|
# Leveraging the pre-built Docker images with
|
||||||
# cargo-chef and the Rust toolchain
|
# cargo-chef and the Rust toolchain
|
||||||
FROM git.kemitix.net/kemitix/git-next-builder:2024.08.04 AS chef
|
FROM git.kemitix.net/kemitix/git-next-builder:2024.07.05 AS chef
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM chef AS planner
|
FROM chef AS planner
|
||||||
|
@ -12,19 +12,19 @@ FROM chef AS builder
|
||||||
COPY --from=planner /app/recipe.json recipe.json
|
COPY --from=planner /app/recipe.json recipe.json
|
||||||
RUN cargo chef cook --profile release --recipe-path recipe.json
|
RUN cargo chef cook --profile release --recipe-path recipe.json
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN cargo build --release --bin git-next --all-features && \
|
RUN cargo build --release --bin git-next && \
|
||||||
strip target/release/git-next
|
strip target/release/git-next
|
||||||
|
|
||||||
FROM docker.io/debian:stable-20240904-slim AS runtime
|
FROM docker.io/debian:stable-20240701-slim AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get satisfy -y "git (>=2.39), libssl3 (>=3.0.14), libdbus-1-dev (>=1.14.10), ca-certificates (>=20230311)" \
|
apt-get install --no-install-recommends -y \
|
||||||
|
git=1:2.39.2-1.1 \
|
||||||
|
libssl3=3.0.13-1~deb12u1 \
|
||||||
|
ca-certificates=20230311 \
|
||||||
&& \
|
&& \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
USER 1000
|
USER 1000
|
||||||
COPY --from=builder /app/target/release/git-next /usr/local/bin
|
COPY --from=builder /app/target/release/git-next /usr/local/bin
|
||||||
|
|
||||||
ENV HOME=/app
|
ENTRYPOINT [ "/usr/local/bin/git-next", "server", "start" ]
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/git-next" ]
|
|
||||||
CMD [ "server", "start" ]
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
FROM docker.io/rust:1.82.0-bookworm
|
FROM docker.io/rust:1.79.0-bookworm
|
||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y libdbus-1-dev && \
|
apt-get install -y clang-16 mold && \
|
||||||
curl -L https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz -o cargo-binstall.tgz && \
|
curl -L https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz -o cargo-binstall.tgz && \
|
||||||
tar -xzf cargo-binstall.tgz && \
|
tar -xzf cargo-binstall.tgz && \
|
||||||
rm cargo-binstall.tgz && \
|
rm cargo-binstall.tgz && \
|
||||||
|
@ -15,6 +15,8 @@ RUN cargo chef --version
|
||||||
RUN rustfmt --version
|
RUN rustfmt --version
|
||||||
RUN cargo fmt --version
|
RUN cargo fmt --version
|
||||||
RUN cargo clippy --version
|
RUN cargo clippy --version
|
||||||
|
RUN mold --version
|
||||||
|
RUN clang-16 --version
|
||||||
RUN cargo --version
|
RUN cargo --version
|
||||||
RUN rustc --version
|
RUN rustc --version
|
||||||
RUN rustup --version
|
RUN rustup --version
|
||||||
|
|
|
@ -6,7 +6,4 @@
|
||||||
development workflows where each commit must pass CI before being included in
|
development workflows where each commit must pass CI before being included in
|
||||||
the main branch.
|
the main branch.
|
||||||
|
|
||||||
![Demo](./demo.gif)
|
|
||||||
|
|
||||||
|
|
||||||
See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information.
|
See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information.
|
||||||
|
|
18
RELEASE.md
18
RELEASE.md
|
@ -1,18 +0,0 @@
|
||||||
# How to create a new Release
|
|
||||||
|
|
||||||
## TLDR
|
|
||||||
|
|
||||||
1. Merge PR `chore: release`
|
|
||||||
2. Wait for [`push-main` workflow](https://git.kemitix.net/kemitix/git-next/actions?workflow=push-main.yml) to complete
|
|
||||||
3. Replace [Release Notes](https://git.kemitix.net/kemitix/git-next/releases) body with details from [CHANGELOG](https://git.kemitix.net/kemitix/git-next/src/branch/main/CHANGELOG.md) (remove crates and duplicates)
|
|
||||||
4. Update thread: <https://mitra.kemitix.net/post/01907ef5-5bd9-b0b6-2b8a-e29762541d78>
|
|
||||||
|
|
||||||
## Detail
|
|
||||||
|
|
||||||
The workflow in `.forgejo/workflows/push-main.yaml` will create or update a PR whenever `main` branch is updated.
|
|
||||||
|
|
||||||
The create the new release, merge this PR. It will automatically create the Release on the `git-next` repo, and publish crates to <https://crates/io>.
|
|
||||||
|
|
||||||
The Release notes that are included with the create Release are currently incorrect, and will need to be manually updated from the `CHANGELOG.md`. Remove crate headers and any resulting duplicates.
|
|
||||||
|
|
||||||
Post an update to the Fediverse. See link above.
|
|
|
@ -12,26 +12,16 @@ keywords = { workspace = true }
|
||||||
categories = { workspace = true }
|
categories = { workspace = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# default = ["forgejo", "github"]
|
default = ["forgejo", "github"]
|
||||||
default = ["forgejo", "github", "tui"]
|
|
||||||
forgejo = ["git-next-forge-forgejo"]
|
forgejo = ["git-next-forge-forgejo"]
|
||||||
github = ["git-next-forge-github"]
|
github = ["git-next-forge-github"]
|
||||||
tui = ["ratatui", "directories", "lazy_static", "tui-scrollview", "regex", "chrono"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
git-next-core = { workspace = true }
|
git-next-core = { workspace = true }
|
||||||
|
git-next-server-actor = { workspace = true }
|
||||||
git-next-forge-forgejo = { workspace = true, optional = true }
|
git-next-forge-forgejo = { workspace = true, optional = true }
|
||||||
git-next-forge-github = { workspace = true, optional = true }
|
git-next-forge-github = { workspace = true, optional = true }
|
||||||
|
|
||||||
# TUI
|
|
||||||
ratatui = { workspace = true, optional = true }
|
|
||||||
directories = { workspace = true, optional = true }
|
|
||||||
lazy_static = { workspace = true, optional = true }
|
|
||||||
color-eyre = { workspace = true }
|
|
||||||
tui-scrollview = { workspace = true, optional = true }
|
|
||||||
regex = { workspace = true, optional = true }
|
|
||||||
chrono = { workspace = true, optional = true }
|
|
||||||
|
|
||||||
# CLI parsing
|
# CLI parsing
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
|
|
||||||
|
@ -39,9 +29,12 @@ clap = { workspace = true }
|
||||||
kxio = { workspace = true }
|
kxio = { workspace = true }
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
|
console-subscriber = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true }
|
tracing-subscriber = { workspace = true }
|
||||||
tracing-error.workspace = true
|
|
||||||
|
# git
|
||||||
|
async-trait = { workspace = true }
|
||||||
|
|
||||||
# Conventional Commit check
|
# Conventional Commit check
|
||||||
git-conventional = { workspace = true }
|
git-conventional = { workspace = true }
|
||||||
|
@ -49,19 +42,20 @@ git-conventional = { workspace = true }
|
||||||
# TOML parsing
|
# TOML parsing
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
||||||
|
# base64 decoding
|
||||||
|
base64 = { workspace = true }
|
||||||
|
|
||||||
# Actors
|
# Actors
|
||||||
actix = { workspace = true }
|
actix = { workspace = true }
|
||||||
actix-rt = { workspace = true }
|
actix-rt = { workspace = true }
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|
||||||
# boilerplate
|
# boilerplate
|
||||||
bon = { workspace = true }
|
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
derive-with = { workspace = true }
|
derive-with = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
# Webhooks
|
# Webhooks
|
||||||
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
time = { workspace = true }
|
time = { workspace = true }
|
||||||
|
@ -73,13 +67,6 @@ warp = { workspace = true }
|
||||||
# file watcher (linux)
|
# file watcher (linux)
|
||||||
notify = { workspace = true }
|
notify = { workspace = true }
|
||||||
|
|
||||||
# email
|
|
||||||
lettre = { workspace = true }
|
|
||||||
sendmail = { workspace = true }
|
|
||||||
|
|
||||||
# desktop notifications
|
|
||||||
notifica = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# Testing
|
# Testing
|
||||||
assert2 = { workspace = true }
|
assert2 = { workspace = true }
|
||||||
|
@ -87,13 +74,9 @@ test-log = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
mockall = { workspace = true }
|
mockall = { workspace = true }
|
||||||
rstest = { workspace = true }
|
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
nursery = { level = "warn", priority = -1 }
|
nursery = { level = "warn", priority = -1 }
|
||||||
pedantic = { level = "warn", priority = -1 }
|
# pedantic = "warn"
|
||||||
unwrap_used = "warn"
|
unwrap_used = "warn"
|
||||||
expect_used = "warn"
|
expect_used = "warn"
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
|
||||||
|
|
|
@ -29,8 +29,13 @@ See [Behaviour](#behaviour) to learn how we do this.
|
||||||
- Rust 1.76.0 or later - https://www.rust-lang.org
|
- Rust 1.76.0 or later - https://www.rust-lang.org
|
||||||
- pgk-config
|
- pgk-config
|
||||||
- libssl-dev
|
- libssl-dev
|
||||||
- libdbus-1-dev (ubuntu/debian)
|
|
||||||
- dbus-devel (fedora)
|
### x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
|
Additionally for this platform, to improved compilation times:
|
||||||
|
|
||||||
|
- clang-16
|
||||||
|
- mold
|
||||||
|
|
||||||
See `.cargo/config.toml` for how they are configured.
|
See `.cargo/config.toml` for how they are configured.
|
||||||
|
|
||||||
|
@ -59,7 +64,7 @@ cargo install --path crates/cli
|
||||||
- [x] cli
|
- [x] cli
|
||||||
- [x] server
|
- [x] server
|
||||||
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
||||||
- [x] tui overview
|
- [ ] tui overview
|
||||||
- [ ] webui overview
|
- [ ] webui overview
|
||||||
|
|
||||||
## Branch Names
|
## Branch Names
|
||||||
|
@ -82,17 +87,7 @@ happen to have those same names.
|
||||||
|
|
||||||
The server is configured by the `git-next-server.toml` file.
|
The server is configured by the `git-next-server.toml` file.
|
||||||
|
|
||||||
#### listen
|
#### http
|
||||||
|
|
||||||
The server should listen for webhook notifications from each forge.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[listen]
|
|
||||||
http = { addr = "0.0.0.0", port = 8080 }
|
|
||||||
url = "https://localhost:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
##### http
|
|
||||||
|
|
||||||
The server needs to be able to receive webhook notifications from your forge,
|
The server needs to be able to receive webhook notifications from your forge,
|
||||||
(e.g. github.com). You can do this via any method that suits your environment,
|
(e.g. github.com). You can do this via any method that suits your environment,
|
||||||
|
@ -105,9 +100,17 @@ This is the address and port that your reverse proxy should route traffic to.
|
||||||
- **addr** - the IP address the server should bind to
|
- **addr** - the IP address the server should bind to
|
||||||
- **port** - the IP port the server should bind to
|
- **port** - the IP port the server should bind to
|
||||||
|
|
||||||
##### url
|
#### notification
|
||||||
|
|
||||||
The HTTPS URL for forges to send webhooks to.
|
The server should be able to notify the user when manual intervention is required.
|
||||||
|
Currently this is only available via sending a Webhook message.
|
||||||
|
|
||||||
|
- **type** - one of `None` or `Webhook`
|
||||||
|
- **webhook** - the URL to POST the notification to
|
||||||
|
|
||||||
|
See [Notifications](#notifications) for more details.
|
||||||
|
|
||||||
|
#### webhook
|
||||||
|
|
||||||
Your forges need to know where they should route webhooks to. This should be
|
Your forges need to know where they should route webhooks to. This should be
|
||||||
an address this is accessible to the forge. So, for github.com, it would need
|
an address this is accessible to the forge. So, for github.com, it would need
|
||||||
|
@ -115,68 +118,10 @@ to be a publicly accessible HTTPS URL. For a self-hosted forge, e.g. ForgeJo,
|
||||||
on your own network, then it only needs to be accessible from the server your
|
on your own network, then it only needs to be accessible from the server your
|
||||||
forge is running on.
|
forge is running on.
|
||||||
|
|
||||||
#### shout
|
- **url** - the HTTPS URL for forges to send webhook to
|
||||||
|
|
||||||
The server should be able to notify the user when manual intervention is required.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[shout]
|
|
||||||
desktop = true
|
|
||||||
|
|
||||||
[shout.webhook]
|
|
||||||
url = "https//localhost:9090"
|
|
||||||
secret = "secret-password"
|
|
||||||
|
|
||||||
[shout.email]
|
|
||||||
from = "git-next@example.com"
|
|
||||||
to = "developer@example.com"
|
|
||||||
|
|
||||||
[shout.email.smtp]
|
|
||||||
hostname = "smtp.example.com"
|
|
||||||
username = "git-next@example.com"
|
|
||||||
password = "MySecretEmailPassword42"
|
|
||||||
```
|
|
||||||
|
|
||||||
##### desktop
|
|
||||||
|
|
||||||
When specified as `true`, desktop notifications will be sent for some events.
|
|
||||||
|
|
||||||
##### webhook
|
|
||||||
|
|
||||||
Will send a POST request for some events.
|
|
||||||
|
|
||||||
- **url** - the URL to POST the notification to and the
|
|
||||||
- **secret** - the sync key used to sign the webhook payload
|
|
||||||
|
|
||||||
See [Notifications](#notifications) for more details.
|
|
||||||
|
|
||||||
##### email
|
|
||||||
|
|
||||||
Will send an email for some events.
|
|
||||||
|
|
||||||
- **from** - the email address to send the email from
|
|
||||||
- **to** - the email address to send the email to
|
|
||||||
|
|
||||||
With just `from` and `to` specified, `git-next` will attempt to send emails
|
|
||||||
with `sendmail` if it is configured.
|
|
||||||
|
|
||||||
Alternativly, you can use an SMTP relay.
|
|
||||||
|
|
||||||
###### smtp
|
|
||||||
|
|
||||||
Will send emails using an SMTP relay.
|
|
||||||
|
|
||||||
- **hostname** - the SMTP relay server
|
|
||||||
- **username** - the account to authenticate as
|
|
||||||
- **password** - the password to authenticate with
|
|
||||||
|
|
||||||
#### storage
|
#### storage
|
||||||
|
|
||||||
```toml
|
|
||||||
[storage]
|
|
||||||
path = "./data"
|
|
||||||
```
|
|
||||||
|
|
||||||
`git-next` will create a bare clone of each repo that you configure it to
|
`git-next` will create a bare clone of each repo that you configure it to
|
||||||
monitor. They will all be created in the directory specified here. This data
|
monitor. They will all be created in the directory specified here. This data
|
||||||
does not need to be backed up, as any missing information will be cloned when
|
does not need to be backed up, as any missing information will be cloned when
|
||||||
|
@ -198,14 +143,13 @@ forge_type = "GitHub"
|
||||||
hostname = "github.com"
|
hostname = "github.com"
|
||||||
user = "username"
|
user = "username"
|
||||||
token = "api-key"
|
token = "api-key"
|
||||||
max_dev_commits = 25
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
||||||
- **hostname** - the hostname for the forge.
|
- **hostname** - the hostname for the forge.
|
||||||
- **user** - the user to authenticate as
|
- **user** - the user to authenticate as
|
||||||
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
|
- **token** - application token for the user. See below for the permissions
|
||||||
- **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
|
required for on each forge.
|
||||||
|
|
||||||
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
||||||
to `next`.
|
to `next`.
|
||||||
|
@ -270,11 +214,13 @@ Currently `git-next` can only use a `gitdir` if the forge and repo is the
|
||||||
same one specified as the `origin` remote. Otherwise the behaviour is
|
same one specified as the `origin` remote. Otherwise the behaviour is
|
||||||
untested and undefined.
|
untested and undefined.
|
||||||
|
|
||||||
## Webhook Notifications
|
## Notifications
|
||||||
|
|
||||||
When sending a Webhook Notification to a user they are sent using the
|
`git-next` can send a number of notification to the user when intervention is required.
|
||||||
Standard Webhooks format. That means all POST messages have the
|
Currently, only WebHooks are supported.
|
||||||
following headers:
|
|
||||||
|
Webhooks are sent using the Standard Webhooks format. That means all POST messages have
|
||||||
|
the following headers:
|
||||||
|
|
||||||
- `Webhook-Id`
|
- `Webhook-Id`
|
||||||
- `Webhook-Signature`
|
- `Webhook-Signature`
|
||||||
|
@ -298,13 +244,7 @@ Sample payload:
|
||||||
"main": "main"
|
"main": "main"
|
||||||
},
|
},
|
||||||
"forge_alias": "jo",
|
"forge_alias": "jo",
|
||||||
"repo_alias": "kxio",
|
"repo_alias": "kxio"
|
||||||
"log": [
|
|
||||||
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
|
|
||||||
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
|
|
||||||
"|/",
|
|
||||||
"* 8c19680 refactor: macros use a more common syntax"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"timestamp": "1721760933",
|
"timestamp": "1721760933",
|
||||||
"type": "branch.dev.not-on-main"
|
"type": "branch.dev.not-on-main"
|
||||||
|
@ -325,16 +265,11 @@ Sample payload:
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"commit": {
|
"commit": {
|
||||||
"sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
|
"sha": "98abef1af6825f9770d725a681e5cfc09d7fd4f2",
|
||||||
"message": "feat: add log graph to notifications (1 of 2)"
|
"message": "feat: add foo to bar template"
|
||||||
},
|
},
|
||||||
"forge_alias": "jo",
|
"forge_alias": "jo",
|
||||||
"repo_alias": "kxio",
|
"repo_alias": "kxio"
|
||||||
"log": [
|
|
||||||
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
|
|
||||||
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
|
|
||||||
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"timestamp": "1721760933",
|
"timestamp": "1721760933",
|
||||||
"type": "cicheck.failed"
|
"type": "cicheck.failed"
|
||||||
|
@ -577,58 +512,6 @@ world = { repo = "user/world", branch = "master", main = "master", next = "upcom
|
||||||
|
|
||||||
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
Here is an example `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
server:
|
|
||||||
image: git.kemitix.net/kemitix/git-next:latest
|
|
||||||
container_name: git-next-server
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
RUST_LOG: "hyper=warn,info"
|
|
||||||
ports:
|
|
||||||
- 8080:8092
|
|
||||||
volumes:
|
|
||||||
- ./:/app/
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
|
|
||||||
`8092` and that you are using a reverse proxy to route traffic arriving at
|
|
||||||
`listen.url` to port `8080`.
|
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
This will run with the `server start` options:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
To perform `server init`:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
|
|
||||||
```
|
|
||||||
|
|
||||||
To perform repo `init`:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
|
|
||||||
```
|
|
||||||
|
|
||||||
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
//
|
|
||||||
use crate::alerts::short_message;
|
|
||||||
use git_next_core::git::UserNotification;
|
|
||||||
|
|
||||||
pub(super) fn send_desktop_notification(user_notification: &UserNotification) {
|
|
||||||
let message = short_message(user_notification);
|
|
||||||
if let Err(err) = notifica::notify("git-next", &message) {
|
|
||||||
tracing::warn!(?err, "failed to send desktop notification");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
//
|
|
||||||
use git_next_core::{
|
|
||||||
git::UserNotification,
|
|
||||||
server::{EmailConfig, SmtpConfig},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::alerts::{full_message, short_message};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct EmailMessage {
|
|
||||||
from: String,
|
|
||||||
to: String,
|
|
||||||
subject: String,
|
|
||||||
body: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn send_email(user_notification: &UserNotification, email_config: &EmailConfig) {
|
|
||||||
let email_message = EmailMessage {
|
|
||||||
from: email_config.from().to_string(),
|
|
||||||
to: email_config.to().to_string(),
|
|
||||||
subject: short_message(user_notification),
|
|
||||||
body: full_message(user_notification),
|
|
||||||
};
|
|
||||||
match email_config.smtp() {
|
|
||||||
Some(smtp) => send_email_smtp(email_message, smtp),
|
|
||||||
None => send_email_sendmail(&email_message),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_email_sendmail(email_message: &EmailMessage) {
|
|
||||||
use sendmail::email;
|
|
||||||
match email::send(
|
|
||||||
&email_message.from,
|
|
||||||
[email_message.to.as_ref()],
|
|
||||||
&email_message.subject,
|
|
||||||
&email_message.body,
|
|
||||||
) {
|
|
||||||
Ok(()) => tracing::info!("Email sent successfully!"),
|
|
||||||
Err(e) => tracing::warn!("Could not send email: {:?}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) {
|
|
||||||
if let Err(err) = do_send_email_smtp(email_message, smtp) {
|
|
||||||
tracing::warn!(?err, "sending email");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_send_email_smtp(email_message: EmailMessage, smtp: &SmtpConfig) -> Result<(), anyhow::Error> {
|
|
||||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
|
||||||
let email = Message::builder()
|
|
||||||
.from(email_message.from.parse()?)
|
|
||||||
.to(email_message.to.parse()?)
|
|
||||||
.subject(email_message.subject)
|
|
||||||
.body(email_message.body)?;
|
|
||||||
let creds = Credentials::new(smtp.username().to_string(), smtp.password().to_string());
|
|
||||||
let mailer = SmtpTransport::relay(smtp.hostname())?
|
|
||||||
.credentials(creds)
|
|
||||||
.build();
|
|
||||||
Ok(mailer
|
|
||||||
.send(&email)
|
|
||||||
.map(|response| {
|
|
||||||
response
|
|
||||||
.message()
|
|
||||||
.map(ToString::to_string)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.map(|response| {
|
|
||||||
tracing::info!(?response, "email sent via smtp");
|
|
||||||
})?)
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
mod notify_user;
|
|
||||||
mod update_shout;
|
|
|
@ -1,41 +0,0 @@
|
||||||
//
|
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use tracing::{info, Instrument as _};
|
|
||||||
|
|
||||||
use crate::alerts::{
|
|
||||||
desktop::send_desktop_notification, email::send_email, messages::NotifyUser,
|
|
||||||
webhook::send_webhook, AlertsActor,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Handler<NotifyUser> for AlertsActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
let Some(shout) = &self.shout else {
|
|
||||||
info!("No shout config available");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let net = self.net.clone();
|
|
||||||
let shout = shout.clone();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//
|
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use crate::alerts::{messages::UpdateShout, AlertsActor};
|
|
||||||
|
|
||||||
impl Handler<UpdateShout> for AlertsActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: UpdateShout, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
self.shout.replace(msg.peel());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,50 +0,0 @@
|
||||||
//
|
|
||||||
use git_next_core::git::UserNotification;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct History {
|
|
||||||
/// The maximum age of an item in the history.
|
|
||||||
///
|
|
||||||
/// Items older than this will be dropped.
|
|
||||||
max_age_seconds: Duration,
|
|
||||||
|
|
||||||
/// Maps a user notification to when it was last seen.
|
|
||||||
///
|
|
||||||
/// The user notification will not be sent until after `max_age_seconds` from last seen.
|
|
||||||
///
|
|
||||||
/// Each time we see a given user notification, the last seen time will be updated.
|
|
||||||
items: HashMap<UserNotification, Instant>,
|
|
||||||
}
|
|
||||||
impl History {
|
|
||||||
pub fn new(max_age_seconds: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
max_age_seconds,
|
|
||||||
items: HashMap::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sendable(&mut self, user_notification: UserNotification) -> Option<UserNotification> {
|
|
||||||
let now = Instant::now();
|
|
||||||
self.prune(&now); // remove expired items first
|
|
||||||
let contains_key = self.items.contains_key(&user_notification);
|
|
||||||
self.items.insert(user_notification.clone(), now);
|
|
||||||
if contains_key {
|
|
||||||
info!("previously sent");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
info!("not sent before");
|
|
||||||
Some(user_notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prune(&mut self, now: &Instant) {
|
|
||||||
if let Some(threshold) = now.checked_sub(self.max_age_seconds) {
|
|
||||||
self.items.retain(|_, last_seen| *last_seen > threshold);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
//
|
|
||||||
use git_next_core::{git::UserNotification, message, server::Shout};
|
|
||||||
|
|
||||||
message!(UpdateShout, Shout, "Updated Shout configuration");
|
|
||||||
|
|
||||||
message!(
|
|
||||||
NotifyUser,
|
|
||||||
UserNotification,
|
|
||||||
"Request to send the message payload to the notification webhook"
|
|
||||||
);
|
|
|
@ -1,93 +0,0 @@
|
||||||
//
|
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use derive_more::derive::Constructor;
|
|
||||||
|
|
||||||
use git_next_core::{git::UserNotification, server::Shout};
|
|
||||||
|
|
||||||
pub use history::History;
|
|
||||||
|
|
||||||
mod desktop;
|
|
||||||
mod email;
|
|
||||||
mod handlers;
|
|
||||||
mod history;
|
|
||||||
pub mod messages;
|
|
||||||
mod webhook;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
#[derive(Debug, Constructor)]
|
|
||||||
pub struct AlertsActor {
|
|
||||||
shout: Option<Shout>, // config for sending alerts to users
|
|
||||||
history: History, // record of alerts sent recently (e.g. 24 hours)
|
|
||||||
net: kxio::network::Network,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Actor for AlertsActor {
|
|
||||||
type Context = Context<Self>;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn short_message(user_notification: &UserNotification) -> String {
|
|
||||||
let (forge_alias, repo_alias) = user_notification.aliases();
|
|
||||||
format!("[git-next] {forge_alias}/{repo_alias}: {user_notification}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn full_message(user_notification: &UserNotification) -> String {
|
|
||||||
match user_notification {
|
|
||||||
UserNotification::CICheckFailed {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
commit,
|
|
||||||
log,
|
|
||||||
} => {
|
|
||||||
let sha = commit.sha();
|
|
||||||
let message = commit.message();
|
|
||||||
[
|
|
||||||
"CI Checks have Failed".to_string(),
|
|
||||||
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
|
|
||||||
format!("Commit:\n - {sha}\n - {message}"),
|
|
||||||
"Log:".to_string(),
|
|
||||||
log.join("\n"),
|
|
||||||
]
|
|
||||||
.join("\n\n")
|
|
||||||
}
|
|
||||||
UserNotification::RepoConfigLoadFailure {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
reason,
|
|
||||||
} => [
|
|
||||||
"Failed to read or parse the .git-next.toml file from repo".to_string(),
|
|
||||||
format!(" - {reason}"),
|
|
||||||
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
|
|
||||||
]
|
|
||||||
.join("\n\n"),
|
|
||||||
UserNotification::WebhookRegistration {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
reason,
|
|
||||||
} => [
|
|
||||||
"Failed to register webhook with the forge".to_string(),
|
|
||||||
format!(" - {reason}"),
|
|
||||||
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
|
|
||||||
]
|
|
||||||
.join("\n\n"),
|
|
||||||
UserNotification::DevNotBasedOnMain {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
dev_branch,
|
|
||||||
main_branch,
|
|
||||||
dev_commit: _,
|
|
||||||
main_commit: _,
|
|
||||||
log,
|
|
||||||
} => [
|
|
||||||
format!("The branch '{dev_branch}' is not based on the branch '{main_branch}'."),
|
|
||||||
format!("TODO: Rebase '{dev_branch}' onto '{main_branch}'."),
|
|
||||||
format!("Forge: {forge_alias}\nRepo : {repo_alias}"),
|
|
||||||
"Log:".to_string(),
|
|
||||||
log.join("\n"),
|
|
||||||
]
|
|
||||||
.join("\n\n"),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use assert2::let_assert;
|
|
||||||
use git_next_core::git::UserNotification;
|
|
||||||
|
|
||||||
use crate::{alerts::History, repo::tests::given};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn when_history_is_empty_then_message_is_passed() {
|
|
||||||
let mut history = History::new(Duration::from_millis(1));
|
|
||||||
|
|
||||||
let user_notification = UserNotification::RepoConfigLoadFailure {
|
|
||||||
forge_alias: given::a_forge_alias(),
|
|
||||||
repo_alias: given::a_repo_alias(),
|
|
||||||
reason: given::a_name(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = history.sendable(user_notification);
|
|
||||||
|
|
||||||
assert!(result.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn when_history_has_expired_then_message_is_passed() {
|
|
||||||
let dur = Duration::from_millis(1);
|
|
||||||
let mut history = History::new(dur);
|
|
||||||
|
|
||||||
let user_notification = UserNotification::RepoConfigLoadFailure {
|
|
||||||
forge_alias: given::a_forge_alias(),
|
|
||||||
repo_alias: given::a_repo_alias(),
|
|
||||||
reason: given::a_name(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// add message to history
|
|
||||||
let result = history.sendable(user_notification);
|
|
||||||
let_assert!(Some(user_notification) = result);
|
|
||||||
|
|
||||||
std::thread::sleep(dur);
|
|
||||||
// twice the expiry duration to avoid things happening in the wrong order
|
|
||||||
std::thread::sleep(dur);
|
|
||||||
|
|
||||||
// after dur, message has expired, so is still valid
|
|
||||||
let result = history.sendable(user_notification);
|
|
||||||
assert!(result.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn when_history_has_unexpired_then_message_is_blocked() {
|
|
||||||
let dur = Duration::from_secs(1);
|
|
||||||
let mut history = History::new(dur);
|
|
||||||
|
|
||||||
let user_notification = UserNotification::RepoConfigLoadFailure {
|
|
||||||
forge_alias: given::a_forge_alias(),
|
|
||||||
repo_alias: given::a_repo_alias(),
|
|
||||||
reason: given::a_name(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// add message to history
|
|
||||||
let result = history.sendable(user_notification);
|
|
||||||
let_assert!(Some(user_notification) = result);
|
|
||||||
|
|
||||||
// no time passed
|
|
||||||
// std::thread::sleep(dur);
|
|
||||||
|
|
||||||
// after dur, message has expired, so is still valid
|
|
||||||
let result = history.sendable(user_notification);
|
|
||||||
assert!(result.is_none());
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
mod history;
|
|
|
@ -1,54 +0,0 @@
|
||||||
//
|
|
||||||
use git_next_core::{git::UserNotification, server::OutboundWebhook};
|
|
||||||
use kxio::network::{NetRequest, NetUrl, RequestBody, ResponseType};
|
|
||||||
use secrecy::ExposeSecret as _;
|
|
||||||
use standardwebhooks::Webhook;
|
|
||||||
|
|
||||||
pub(super) async fn send_webhook(
|
|
||||||
user_notification: &UserNotification,
|
|
||||||
webhook_config: &OutboundWebhook,
|
|
||||||
net: &kxio::network::Network,
|
|
||||||
) {
|
|
||||||
let Ok(webhook) =
|
|
||||||
Webhook::from_bytes(webhook_config.secret().expose_secret().as_bytes().into())
|
|
||||||
else {
|
|
||||||
tracing::warn!("Invalid notification configuration (signer) - can't sent notification");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
do_send_webhook(user_notification, webhook, webhook_config, net).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_send_webhook(
|
|
||||||
user_notification: &UserNotification,
|
|
||||||
webhook: Webhook,
|
|
||||||
webhook_config: &OutboundWebhook,
|
|
||||||
net: &kxio::network::Network,
|
|
||||||
) {
|
|
||||||
let message_id = format!("msg_{}", ulid::Ulid::new());
|
|
||||||
let timestamp = time::OffsetDateTime::now_utc();
|
|
||||||
let payload = user_notification.as_json(timestamp);
|
|
||||||
let timestamp = timestamp.unix_timestamp();
|
|
||||||
let to_sign = format!("{message_id}.{timestamp}.{payload}");
|
|
||||||
tracing::info!(?to_sign, "");
|
|
||||||
#[allow(clippy::expect_used)]
|
|
||||||
let signature = webhook
|
|
||||||
.sign(&message_id, timestamp, payload.to_string().as_ref())
|
|
||||||
.expect("signature");
|
|
||||||
tracing::info!(?signature, "");
|
|
||||||
let url = webhook_config.url();
|
|
||||||
|
|
||||||
let net_url = NetUrl::new(url.to_string());
|
|
||||||
let request = NetRequest::post(net_url)
|
|
||||||
.body(RequestBody::Json(payload))
|
|
||||||
.header("webhook-id", &message_id)
|
|
||||||
.header("webhook-timestamp", ×tamp.to_string())
|
|
||||||
.header("webhook-signature", &signature)
|
|
||||||
.response_type(ResponseType::None)
|
|
||||||
.build();
|
|
||||||
net.post_json::<()>(request).await.map_or_else(
|
|
||||||
|err| {
|
|
||||||
tracing::warn!(?err, "sending webhook");
|
|
||||||
},
|
|
||||||
|_| (),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -2,18 +2,13 @@
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use actix::Recipient;
|
use actix::Recipient;
|
||||||
use anyhow::{Context, Result};
|
use notify::event::ModifyKind;
|
||||||
use notify::{event::ModifyKind, Watcher};
|
use notify::Watcher;
|
||||||
use tracing::{error, info};
|
use tracing::error;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
path::PathBuf,
|
use std::time::Duration;
|
||||||
sync::{
|
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Message)]
|
#[derive(Debug, Message)]
|
||||||
#[rtype(result = "()")]
|
#[rtype(result = "()")]
|
||||||
|
@ -24,26 +19,23 @@ pub enum Error {
|
||||||
#[error("io")]
|
#[error("io")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Arc<AtomicBool>> {
|
pub async fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) {
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
let shutdown = Arc::new(AtomicBool::default());
|
|
||||||
|
|
||||||
let mut handler = notify::recommended_watcher(tx).context("file watcher")?;
|
#[allow(clippy::expect_used)]
|
||||||
|
let mut handler = notify::recommended_watcher(tx).expect("file watcher");
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
handler
|
handler
|
||||||
.watch(&path, notify::RecursiveMode::NonRecursive)
|
.watch(&path, notify::RecursiveMode::NonRecursive)
|
||||||
.with_context(|| format!("Watching: {path:?}"))?;
|
.expect("watch file");
|
||||||
let thread_shutdown = shutdown.clone();
|
info!("Watching: {:?}", path);
|
||||||
actix_rt::task::spawn_blocking(move || {
|
async move {
|
||||||
loop {
|
loop {
|
||||||
if thread_shutdown.load(Ordering::Relaxed) {
|
|
||||||
drop(handler);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
for result in rx.try_iter() {
|
for result in rx.try_iter() {
|
||||||
match result {
|
match result {
|
||||||
Ok(event) => match event.kind {
|
Ok(event) => match event.kind {
|
||||||
notify::EventKind::Modify(ModifyKind::Data(_)) => {
|
notify::EventKind::Modify(ModifyKind::Data(_)) => {
|
||||||
info!("File modified");
|
tracing::info!("File modified");
|
||||||
recipient.do_send(FileUpdated);
|
recipient.do_send(FileUpdated);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -59,8 +51,8 @@ pub fn watch_file(path: PathBuf, recipient: Recipient<FileUpdated>) -> Result<Ar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(1000));
|
actix_rt::time::sleep(Duration::from_millis(1000)).await;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
Ok(shutdown)
|
.await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
//
|
//
|
||||||
use git_next_core::git::{ForgeLike, RepoDetails};
|
use git_next_core::{
|
||||||
|
git::{ForgeLike, RepoDetails},
|
||||||
|
ForgeType,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "forgejo")]
|
#[cfg(feature = "forgejo")]
|
||||||
use git_next_forge_forgejo::ForgeJo;
|
use git_next_forge_forgejo::ForgeJo;
|
||||||
|
@ -16,14 +19,10 @@ impl Forge {
|
||||||
pub fn create(repo_details: RepoDetails, net: Network) -> Box<dyn ForgeLike> {
|
pub fn create(repo_details: RepoDetails, net: Network) -> Box<dyn ForgeLike> {
|
||||||
match repo_details.forge.forge_type() {
|
match repo_details.forge.forge_type() {
|
||||||
#[cfg(feature = "forgejo")]
|
#[cfg(feature = "forgejo")]
|
||||||
git_next_core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
|
ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
|
||||||
#[cfg(feature = "github")]
|
#[cfg(feature = "github")]
|
||||||
git_next_core::ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
|
ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
|
||||||
_ => {
|
ForgeType::MockForge => unreachable!(),
|
||||||
drop(repo_details);
|
|
||||||
drop(net);
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
//
|
//
|
||||||
#[cfg(any(feature = "forgejo", feature = "github"))]
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
|
@ -8,30 +7,31 @@ use git_next_core::{
|
||||||
GitDir, RepoConfigSource, StoragePathType,
|
GitDir, RepoConfigSource, StoragePathType,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "forgejo")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_forgejo_name() {
|
fn test_forgejo_name() {
|
||||||
let net = Network::new_mock();
|
let net = Network::new_mock();
|
||||||
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
|
let repo_details = given_repo_details(ForgeType::ForgeJo);
|
||||||
let forge = Forge::create(repo_details, net);
|
let forge = Forge::create(repo_details, net);
|
||||||
assert_eq!(forge.name(), "forgejo");
|
assert_eq!(forge.name(), "forgejo");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "github")]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_github_name() {
|
fn test_github_name() {
|
||||||
let net = Network::new_mock();
|
let net = Network::new_mock();
|
||||||
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
|
let repo_details = given_repo_details(ForgeType::GitHub);
|
||||||
let forge = Forge::create(repo_details, net);
|
let forge = Forge::create(repo_details, net);
|
||||||
assert_eq!(forge.name(), "github");
|
assert_eq!(forge.name(), "github");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
fn given_fs() -> kxio::fs::FileSystem {
|
||||||
fn given_repo_details(forge_type: git_next_core::ForgeType) -> RepoDetails {
|
kxio::fs::temp().unwrap_or_else(|e| {
|
||||||
let fs = kxio::fs::temp().unwrap_or_else(|e| {
|
|
||||||
println!("{e}");
|
println!("{e}");
|
||||||
panic!("fs")
|
panic!("fs")
|
||||||
});
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn given_repo_details(forge_type: ForgeType) -> RepoDetails {
|
||||||
|
let fs = given_fs();
|
||||||
git::repo_details(
|
git::repo_details(
|
||||||
1,
|
1,
|
||||||
git::Generation::default(),
|
git::Generation::default(),
|
||||||
|
|
|
@ -1,18 +1,29 @@
|
||||||
//
|
//
|
||||||
use color_eyre::{eyre::Context, Result};
|
|
||||||
use kxio::fs::FileSystem;
|
use kxio::fs::FileSystem;
|
||||||
|
|
||||||
pub fn run(fs: &FileSystem) -> Result<()> {
|
pub fn run(fs: FileSystem) {
|
||||||
let pathbuf = fs.base().join(".git-next.toml");
|
let file_name = ".git-next.toml";
|
||||||
if fs
|
let pathbuf = fs.base().join(file_name);
|
||||||
.path_exists(&pathbuf)
|
match fs.path_exists(&pathbuf) {
|
||||||
.with_context(|| format!("Checking for existing file: {pathbuf:?}"))?
|
Ok(exists) => {
|
||||||
{
|
if exists {
|
||||||
eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",);
|
eprintln!(
|
||||||
} else {
|
"The configuration file already exists at {} - not overwritting it.",
|
||||||
fs.file_write(&pathbuf, include_str!("../default.toml"))
|
file_name
|
||||||
.with_context(|| format!("Writing file: {pathbuf:?}"))?;
|
);
|
||||||
println!("Created a default configuration file at {pathbuf:?}");
|
} else {
|
||||||
|
match fs.file_write(&pathbuf, include_str!("../default.toml")) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Created a default configuration file at {}", file_name);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to write to the configuration file: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Could not check if file exist: {} - {err:?}", file_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,19 @@
|
||||||
//
|
//
|
||||||
#![allow(clippy::module_name_repetitions)]
|
|
||||||
|
|
||||||
mod alerts;
|
|
||||||
mod file_watcher;
|
mod file_watcher;
|
||||||
mod forge;
|
mod forge;
|
||||||
mod init;
|
mod init;
|
||||||
mod repo;
|
mod repo;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod webhook;
|
||||||
#[cfg(feature = "tui")]
|
|
||||||
mod tui;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
mod webhook;
|
|
||||||
|
|
||||||
use git_next_core::git;
|
use git_next_core::git;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use color_eyre::Result;
|
|
||||||
use kxio::{fs, network::Network};
|
use kxio::{fs, network::Network};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -38,40 +31,27 @@ enum Command {
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
enum Server {
|
enum Server {
|
||||||
Init,
|
Init,
|
||||||
Start {
|
Start,
|
||||||
/// Display a UI (experimental)
|
|
||||||
#[cfg(feature = "tui")]
|
|
||||||
#[arg(long, required = false)]
|
|
||||||
ui: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() {
|
||||||
let fs = fs::new(PathBuf::default());
|
let fs = fs::new(PathBuf::default());
|
||||||
let net = Network::new_real();
|
let net = Network::new_real();
|
||||||
let repository_factory = git::repository::factory::real();
|
let repository_factory = git::repository::factory::real();
|
||||||
let commands = Commands::parse();
|
let commands = Commands::parse();
|
||||||
|
|
||||||
match commands.command {
|
match commands.command {
|
||||||
Command::Init => init::run(&fs),
|
Command::Init => {
|
||||||
|
init::run(fs);
|
||||||
|
}
|
||||||
Command::Server(server) => match server {
|
Command::Server(server) => match server {
|
||||||
Server::Init => server::init(&fs),
|
Server::Init => {
|
||||||
#[cfg(not(feature = "tui"))]
|
server::init(fs);
|
||||||
Server::Start {} => server::start(
|
}
|
||||||
false,
|
Server::Start => {
|
||||||
fs,
|
let sleep_duration = std::time::Duration::from_secs(10);
|
||||||
net,
|
server::start(fs, net, repository_factory, sleep_duration);
|
||||||
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),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,19 +17,21 @@ use tracing::{info, instrument, warn};
|
||||||
// advance next to the next commit towards the head of the dev branch
|
// advance next to the next commit towards the head of the dev branch
|
||||||
#[instrument(fields(next), skip_all)]
|
#[instrument(fields(next), skip_all)]
|
||||||
pub fn advance_next(
|
pub fn advance_next(
|
||||||
commit: Option<Commit>,
|
next: &Commit,
|
||||||
force: git_next_core::git::push::Force,
|
main: &Commit,
|
||||||
repo_details: &RepoDetails,
|
dev_commit_history: &[Commit],
|
||||||
|
repo_details: RepoDetails,
|
||||||
repo_config: RepoConfig,
|
repo_config: RepoConfig,
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
message_token: MessageToken,
|
message_token: MessageToken,
|
||||||
) -> Result<MessageToken> {
|
) -> Result<MessageToken> {
|
||||||
|
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
|
||||||
let commit = commit.ok_or_else(|| Error::NextAtDev)?;
|
let commit = commit.ok_or_else(|| Error::NextAtDev)?;
|
||||||
validate_commit_message(commit.message())?;
|
validate_commit_message(commit.message())?;
|
||||||
info!("Advancing next to commit '{}'", commit);
|
info!("Advancing next to commit '{}'", commit);
|
||||||
reset(
|
reset(
|
||||||
open_repository,
|
open_repository,
|
||||||
repo_details,
|
&repo_details,
|
||||||
&repo_config.branches().next(),
|
&repo_config.branches().next(),
|
||||||
&commit.into(),
|
&commit.into(),
|
||||||
&force,
|
&force,
|
||||||
|
@ -64,7 +66,7 @@ pub fn find_next_commit_on_dev(
|
||||||
) -> (Option<Commit>, Force) {
|
) -> (Option<Commit>, Force) {
|
||||||
let mut next_commit: Option<&Commit> = None;
|
let mut next_commit: Option<&Commit> = None;
|
||||||
let mut force = Force::No;
|
let mut force = Force::No;
|
||||||
for commit in dev_commit_history {
|
for commit in dev_commit_history.iter() {
|
||||||
if commit == next {
|
if commit == next {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use git_next_core::{git, RepoConfigSource};
|
use git_next_core::RepoConfigSource;
|
||||||
|
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
branch::advance_main,
|
||||||
branch::advance_main,
|
do_send,
|
||||||
do_send,
|
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
|
||||||
messages::{AdvanceMain, LoadConfigFromRepo, ValidateRepo},
|
RepoActor,
|
||||||
RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<AdvanceMain> for RepoActor {
|
impl Handler<AdvanceMain> for RepoActor {
|
||||||
|
@ -29,31 +26,24 @@ impl Handler<AdvanceMain> for RepoActor {
|
||||||
let repo_details = self.repo_details.clone();
|
let repo_details = self.repo_details.clone();
|
||||||
let addr = ctx.address();
|
let addr = ctx.address();
|
||||||
let message_token = self.message_token;
|
let message_token = self.message_token;
|
||||||
let commit = msg.peel();
|
|
||||||
|
|
||||||
self.update_tui(RepoUpdate::AdvancingMain {
|
match advance_main(
|
||||||
commit: commit.clone(),
|
msg.unwrap(),
|
||||||
});
|
&repo_details,
|
||||||
|
&repo_config,
|
||||||
if let Err(err) = advance_main(commit, &repo_details, &repo_config, &**open_repository) {
|
&**open_repository,
|
||||||
warn!("advance main: {err}");
|
) {
|
||||||
self.alert_tui(format!("advance main: {err}"));
|
Err(err) => {
|
||||||
} else {
|
warn!("advance main: {err}");
|
||||||
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}")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
match repo_config.source() {
|
Ok(_) => match repo_config.source() {
|
||||||
RepoConfigSource::Repo => {
|
RepoConfigSource::Repo => {
|
||||||
do_send(&addr, LoadConfigFromRepo, self.log.as_ref());
|
do_send(addr, LoadConfigFromRepo, self.log.as_ref());
|
||||||
}
|
}
|
||||||
RepoConfigSource::Server => {
|
RepoConfigSource::Server => {
|
||||||
do_send(&addr, ValidateRepo::new(message_token), self.log.as_ref());
|
do_send(addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use git_next_core::git;
|
use tracing::warn;
|
||||||
use tracing::{warn, Instrument};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
branch::advance_next,
|
||||||
branch::{advance_next, find_next_commit_on_dev},
|
do_send,
|
||||||
do_send,
|
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
||||||
messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
RepoActor,
|
||||||
RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<AdvanceNext> for RepoActor {
|
impl Handler<AdvanceNext> for RepoActor {
|
||||||
|
@ -24,52 +20,30 @@ impl Handler<AdvanceNext> for RepoActor {
|
||||||
let Some(open_repository) = &self.open_repository else {
|
let Some(open_repository) = &self.open_repository else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let AdvanceNextPayload {
|
let AdvanceNextPayload {
|
||||||
next,
|
next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
} = msg.peel();
|
} = msg.unwrap();
|
||||||
let repo_details = self.repo_details.clone();
|
let repo_details = self.repo_details.clone();
|
||||||
let repo_config = repo_config.clone();
|
let repo_config = repo_config.clone();
|
||||||
let addr = ctx.address();
|
let addr = ctx.address();
|
||||||
|
|
||||||
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
|
|
||||||
if let Some(commit) = &commit {
|
|
||||||
self.update_tui(RepoUpdate::AdvancingNext {
|
|
||||||
commit: commit.clone(),
|
|
||||||
force: force.clone(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
match advance_next(
|
match advance_next(
|
||||||
commit,
|
&next,
|
||||||
force,
|
&main,
|
||||||
&repo_details,
|
&dev_commit_history,
|
||||||
|
repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&**open_repository,
|
&**open_repository,
|
||||||
self.message_token,
|
self.message_token,
|
||||||
) {
|
) {
|
||||||
Ok(message_token) => {
|
Ok(message_token) => {
|
||||||
self.update_tui(RepoUpdate::NextUpdated);
|
// pause to allow any CI checks to be started
|
||||||
match open_repository.fetch() {
|
std::thread::sleep(self.sleep_duration);
|
||||||
Ok(()) => self.update_tui_log(git::graph::log(&self.repo_details)),
|
do_send(addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||||
Err(err) => self.alert_tui(format!("fetching: {err}")),
|
|
||||||
}
|
|
||||||
// INFO: pause to allow any CI checks to be started
|
|
||||||
let sleep_duration = self.sleep_duration;
|
|
||||||
let log = self.log.clone();
|
|
||||||
async move {
|
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
|
||||||
}
|
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("advance next: {err}");
|
|
||||||
self.alert_tui(err.to_string());
|
|
||||||
}
|
}
|
||||||
|
Err(err) => warn!("advance next: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,10 @@ use actix::prelude::*;
|
||||||
|
|
||||||
use tracing::{debug, Instrument as _};
|
use tracing::{debug, Instrument as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send,
|
||||||
do_send,
|
messages::{CheckCIStatus, ReceiveCIStatus},
|
||||||
messages::{CheckCIStatus, ReceiveCIStatus},
|
RepoActor,
|
||||||
RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<CheckCIStatus> for RepoActor {
|
impl Handler<CheckCIStatus> for RepoActor {
|
||||||
|
@ -17,18 +14,16 @@ impl Handler<CheckCIStatus> for RepoActor {
|
||||||
|
|
||||||
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: CheckCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
||||||
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
|
crate::repo::logger(self.log.as_ref(), "start: CheckCIStatus");
|
||||||
|
|
||||||
let addr = ctx.address();
|
let addr = ctx.address();
|
||||||
let forge = self.forge.duplicate();
|
let forge = self.forge.duplicate();
|
||||||
let next = msg.peel();
|
let next = msg.unwrap();
|
||||||
let log = self.log.clone();
|
let log = self.log.clone();
|
||||||
|
|
||||||
self.update_tui(RepoUpdate::CheckingCI);
|
|
||||||
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
||||||
async move {
|
async move {
|
||||||
let status = forge.commit_status(&next).await;
|
let status = forge.commit_status(&next).await;
|
||||||
debug!("got status: {status:?}");
|
debug!("got status: {status:?}");
|
||||||
do_send(&addr, ReceiveCIStatus::new((next, status)), log.as_ref());
|
do_send(addr, ReceiveCIStatus::new((next, status)), log.as_ref());
|
||||||
}
|
}
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.into_actor(self)
|
.into_actor(self)
|
||||||
|
|
|
@ -4,13 +4,10 @@ use actix::prelude::*;
|
||||||
use git_next_core::git;
|
use git_next_core::git;
|
||||||
use tracing::{debug, instrument, warn};
|
use tracing::{debug, instrument, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send, logger,
|
||||||
do_send, logger,
|
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
|
||||||
messages::{CloneRepo, LoadConfigFromRepo, RegisterWebhook},
|
RepoActor,
|
||||||
RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<CloneRepo> for RepoActor {
|
impl Handler<CloneRepo> for RepoActor {
|
||||||
|
@ -18,24 +15,21 @@ impl Handler<CloneRepo> for RepoActor {
|
||||||
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
|
#[instrument(name = "RepoActor::CloneRepo", skip_all, fields(repo = %self.repo_details))]
|
||||||
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, _msg: CloneRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||||
logger(self.log.as_ref(), "Handler: CloneRepo: start");
|
logger(self.log.as_ref(), "Handler: CloneRepo: start");
|
||||||
self.update_tui(RepoUpdate::Opening);
|
|
||||||
debug!("Handler: CloneRepo: start");
|
debug!("Handler: CloneRepo: start");
|
||||||
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
match git::repository::open(&*self.repository_factory, &self.repo_details) {
|
||||||
Ok(repository) => {
|
Ok(repository) => {
|
||||||
logger(self.log.as_ref(), "open okay");
|
logger(self.log.as_ref(), "open okay");
|
||||||
debug!("open okay");
|
debug!("open okay");
|
||||||
self.update_tui(RepoUpdate::Opened);
|
|
||||||
self.open_repository.replace(repository);
|
self.open_repository.replace(repository);
|
||||||
if self.repo_details.repo_config.is_none() {
|
if self.repo_details.repo_config.is_none() {
|
||||||
do_send(&ctx.address(), LoadConfigFromRepo, self.log.as_ref());
|
do_send(ctx.address(), LoadConfigFromRepo, self.log.as_ref());
|
||||||
} else {
|
} else {
|
||||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
do_send(ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
logger(self.log.as_ref(), "open failed");
|
logger(self.log.as_ref(), "open failed");
|
||||||
warn!("Could not open repo: {err:?}");
|
warn!("Could not open repo: {err:?}")
|
||||||
self.alert_tui(err.to_string());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!("Handler: CloneRepo: finish");
|
debug!("Handler: CloneRepo: finish");
|
||||||
|
|
|
@ -5,13 +5,10 @@ use git_next_core::git::UserNotification;
|
||||||
|
|
||||||
use tracing::{debug, instrument, Instrument as _};
|
use tracing::{debug, instrument, Instrument as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send, load,
|
||||||
do_send, load,
|
messages::{LoadConfigFromRepo, ReceiveRepoConfig},
|
||||||
messages::{LoadConfigFromRepo, ReceiveRepoConfig},
|
notify_user, RepoActor,
|
||||||
notify_user, RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<LoadConfigFromRepo> for RepoActor {
|
impl Handler<LoadConfigFromRepo> for RepoActor {
|
||||||
|
@ -19,7 +16,6 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
|
||||||
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
|
#[instrument(name = "Repocrate::repo::LoadConfigFromRepo", skip_all, fields(repo = %self.repo_details))]
|
||||||
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, _msg: LoadConfigFromRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||||
debug!("Handler: LoadConfigFromRepo: start");
|
debug!("Handler: LoadConfigFromRepo: start");
|
||||||
self.update_tui(RepoUpdate::LoadingConfigFromRepo);
|
|
||||||
let Some(open_repository) = &self.open_repository else {
|
let Some(open_repository) = &self.open_repository else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
@ -32,9 +28,7 @@ impl Handler<LoadConfigFromRepo> for RepoActor {
|
||||||
let log = self.log.clone();
|
let log = self.log.clone();
|
||||||
async move {
|
async move {
|
||||||
match load::config_from_repository(repo_details, &*open_repository).await {
|
match load::config_from_repository(repo_details, &*open_repository).await {
|
||||||
Ok(repo_config) => {
|
Ok(repo_config) => do_send(addr, ReceiveRepoConfig::new(repo_config), log.as_ref()),
|
||||||
do_send(&addr, ReceiveRepoConfig::new(repo_config), log.as_ref());
|
|
||||||
}
|
|
||||||
Err(err) => notify_user(
|
Err(err) => notify_user(
|
||||||
notify_user_recipient.as_ref(),
|
notify_user_recipient.as_ref(),
|
||||||
UserNotification::RepoConfigLoadFailure {
|
UserNotification::RepoConfigLoadFailure {
|
||||||
|
|
|
@ -1,74 +1,54 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
|
use git_next_core::git::{forge::commit::Status, UserNotification};
|
||||||
use tracing::{debug, Instrument};
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
delay_send, do_send, logger,
|
||||||
do_send, logger,
|
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
|
||||||
messages::{AdvanceMain, ReceiveCIStatus, ValidateRepo},
|
notify_user, RepoActor,
|
||||||
notify_user, RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveCIStatus> for RepoActor {
|
impl Handler<ReceiveCIStatus> for RepoActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: ReceiveCIStatus, ctx: &mut Self::Context) -> Self::Result {
|
||||||
logger(self.log.as_ref(), "start: ReceiveCIStatus");
|
let log = self.log.clone();
|
||||||
let (next, status) = msg.peel();
|
logger(log.as_ref(), "start: ReceiveCIStatus");
|
||||||
self.update_tui(RepoUpdate::ReceiveCIStatus {
|
|
||||||
status: status.clone(),
|
|
||||||
});
|
|
||||||
debug!(?status, "");
|
|
||||||
let graph_log = graph::log(&self.repo_details);
|
|
||||||
self.update_tui_log(graph_log.clone());
|
|
||||||
|
|
||||||
let addr = ctx.address();
|
let addr = ctx.address();
|
||||||
|
let (next, status) = msg.unwrap();
|
||||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
||||||
let repo_alias = self.repo_details.repo_alias.clone();
|
let repo_alias = self.repo_details.repo_alias.clone();
|
||||||
let message_token = self.message_token;
|
let message_token = self.message_token;
|
||||||
let sleep_duration = self.sleep_duration;
|
let sleep_duration = self.sleep_duration;
|
||||||
|
|
||||||
|
debug!(?status, "");
|
||||||
match status {
|
match status {
|
||||||
Status::Pass => {
|
Status::Pass => {
|
||||||
do_send(&addr, AdvanceMain::new(next), self.log.as_ref());
|
do_send(addr, AdvanceMain::new(next), self.log.as_ref());
|
||||||
}
|
}
|
||||||
Status::Pending => {
|
Status::Pending => {
|
||||||
let log = self.log.clone();
|
std::thread::sleep(sleep_duration);
|
||||||
async move {
|
do_send(addr, ValidateRepo::new(message_token), self.log.as_ref());
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
|
||||||
}
|
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
Status::Fail => {
|
Status::Fail => {
|
||||||
tracing::warn!("Checks have failed");
|
tracing::warn!("Checks have failed");
|
||||||
|
|
||||||
notify_user(
|
notify_user(
|
||||||
self.notify_user_recipient.as_ref(),
|
self.notify_user_recipient.as_ref(),
|
||||||
UserNotification::CICheckFailed {
|
UserNotification::CICheckFailed {
|
||||||
forge_alias,
|
forge_alias,
|
||||||
repo_alias,
|
repo_alias,
|
||||||
commit: next,
|
commit: next,
|
||||||
log: graph_log,
|
|
||||||
},
|
},
|
||||||
|
log.as_ref(),
|
||||||
|
);
|
||||||
|
delay_send(
|
||||||
|
addr,
|
||||||
|
sleep_duration,
|
||||||
|
ValidateRepo::new(message_token),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
);
|
||||||
let log = self.log.clone();
|
|
||||||
async move {
|
|
||||||
debug!("sleeping before retrying...");
|
|
||||||
logger(log.as_ref(), "before sleep");
|
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
|
||||||
logger(log.as_ref(), "after sleep");
|
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
|
||||||
}
|
|
||||||
.in_current_span()
|
|
||||||
.into_actor(self)
|
|
||||||
.wait(ctx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,25 +2,19 @@
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send,
|
||||||
do_send,
|
messages::{ReceiveRepoConfig, RegisterWebhook},
|
||||||
messages::{ReceiveRepoConfig, RegisterWebhook},
|
RepoActor,
|
||||||
RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<ReceiveRepoConfig> for RepoActor {
|
impl Handler<ReceiveRepoConfig> for RepoActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
#[instrument(name = "RepoActor::ReceiveRepoConfig", skip_all, fields(repo = %self.repo_details, branches = ?msg))]
|
||||||
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: ReceiveRepoConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||||
let repo_config = msg.peel();
|
let repo_config = msg.unwrap();
|
||||||
self.update_tui(RepoUpdate::ReceiveRepoConfig {
|
|
||||||
repo_config: repo_config.clone(),
|
|
||||||
});
|
|
||||||
self.repo_details.repo_config.replace(repo_config);
|
self.repo_details.repo_config.replace(repo_config);
|
||||||
self.update_tui_branches();
|
|
||||||
do_send(&ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
do_send(ctx.address(), RegisterWebhook::new(), self.log.as_ref());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use tracing::{debug, error, Instrument as _};
|
use tracing::{debug, Instrument as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send,
|
||||||
do_send,
|
messages::{RegisterWebhook, WebhookRegistered},
|
||||||
messages::{RegisterWebhook, WebhookRegistered},
|
notify_user, RepoActor,
|
||||||
notify_user, RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use git_next_core::git::UserNotification;
|
use git_next_core::git::UserNotification;
|
||||||
|
@ -21,27 +18,23 @@ impl Handler<RegisterWebhook> for RepoActor {
|
||||||
if self.webhook_id.is_none() {
|
if self.webhook_id.is_none() {
|
||||||
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
let forge_alias = self.repo_details.forge.forge_alias().clone();
|
||||||
let repo_alias = self.repo_details.repo_alias.clone();
|
let repo_alias = self.repo_details.repo_alias.clone();
|
||||||
let repo_listen_url = self
|
let webhook_url = self.webhook.url(&forge_alias, &repo_alias);
|
||||||
.listen_url
|
|
||||||
.repo_url(forge_alias.clone(), repo_alias.clone());
|
|
||||||
let forge = self.forge.duplicate();
|
let forge = self.forge.duplicate();
|
||||||
let addr = ctx.address();
|
let addr = ctx.address();
|
||||||
let notify_user_recipient = self.notify_user_recipient.clone();
|
let notify_user_recipient = self.notify_user_recipient.clone();
|
||||||
let log = self.log.clone();
|
let log = self.log.clone();
|
||||||
self.update_tui(RepoUpdate::RegisteringWebhook);
|
|
||||||
debug!("registering webhook");
|
debug!("registering webhook");
|
||||||
async move {
|
async move {
|
||||||
match forge.register_webhook(&repo_listen_url).await {
|
match forge.register_webhook(&webhook_url).await {
|
||||||
Ok(registered_webhook) => {
|
Ok(registered_webhook) => {
|
||||||
debug!(?registered_webhook, "webhook registered");
|
debug!(?registered_webhook, "");
|
||||||
do_send(
|
do_send(
|
||||||
&addr,
|
addr,
|
||||||
WebhookRegistered::from(registered_webhook),
|
WebhookRegistered::from(registered_webhook),
|
||||||
log.as_ref(),
|
log.as_ref(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(?err, "failed to register webhook");
|
|
||||||
notify_user(
|
notify_user(
|
||||||
notify_user_recipient.as_ref(),
|
notify_user_recipient.as_ref(),
|
||||||
UserNotification::WebhookRegistration {
|
UserNotification::WebhookRegistration {
|
||||||
|
@ -57,8 +50,6 @@ impl Handler<RegisterWebhook> for RepoActor {
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.into_actor(self)
|
.into_actor(self)
|
||||||
.wait(ctx);
|
.wait(ctx);
|
||||||
} else {
|
|
||||||
self.alert_tui("already have a webhook id - cant register webhook");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,30 +3,25 @@ use actix::prelude::*;
|
||||||
|
|
||||||
use tracing::{debug, warn, Instrument as _};
|
use tracing::{debug, warn, Instrument as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{messages::UnRegisterWebhook, RepoActor};
|
||||||
repo::{messages::UnRegisterWebhook, RepoActor},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Handler<UnRegisterWebhook> for RepoActor {
|
impl Handler<UnRegisterWebhook> for RepoActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, _msg: UnRegisterWebhook, ctx: &mut Self::Context) -> Self::Result {
|
||||||
let Some(webhook_id) = self.webhook_id.take() else {
|
if let Some(webhook_id) = self.webhook_id.take() {
|
||||||
return;
|
let forge = self.forge.duplicate();
|
||||||
};
|
debug!("unregistering webhook");
|
||||||
self.update_tui(RepoUpdate::UnregisteringWebhook);
|
async move {
|
||||||
let forge = self.forge.duplicate();
|
match forge.unregister_webhook(&webhook_id).await {
|
||||||
debug!("unregistering webhook");
|
Ok(_) => debug!("unregistered webhook"),
|
||||||
async move {
|
Err(err) => warn!(?err, "unregistering webhook"),
|
||||||
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,32 +1,26 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use tracing::{info, instrument, Instrument as _};
|
use derive_more::Deref as _;
|
||||||
|
use tracing::{debug, instrument, Instrument as _};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send, logger,
|
||||||
do_send, logger,
|
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
|
||||||
messages::{AdvanceNext, AdvanceNextPayload, CheckCIStatus, MessageToken, ValidateRepo},
|
notify_user, RepoActor,
|
||||||
notify_user, RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use git_next_core::git::{
|
use git_next_core::git::validation::positions::{validate_positions, Error, Positions};
|
||||||
push::Force,
|
|
||||||
validation::positions::{validate, Error, Positions},
|
|
||||||
UserNotification,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Handler<ValidateRepo> for RepoActor {
|
impl Handler<ValidateRepo> for RepoActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %&*msg))]
|
#[instrument(name = "RepoActor::ValidateRepo", skip_all, fields(repo = %self.repo_details, token = %msg.deref()))]
|
||||||
fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result {
|
||||||
logger(self.log.as_ref(), "start: ValidateRepo");
|
logger(self.log.as_ref(), "start: ValidateRepo");
|
||||||
|
|
||||||
// Message Token - make sure we are only triggered for the latest/current token
|
// Message Token - make sure we are only triggered for the latest/current token
|
||||||
match self.token_status(msg.peel()) {
|
match self.token_status(msg.unwrap()) {
|
||||||
TokenStatus::Current => {} // do nothing
|
TokenStatus::Current => {} // do nothing
|
||||||
TokenStatus::Expired => {
|
TokenStatus::Expired => {
|
||||||
logger(
|
logger(
|
||||||
|
@ -48,94 +42,66 @@ impl Handler<ValidateRepo> for RepoActor {
|
||||||
format!("accepted token: {}", self.message_token),
|
format!("accepted token: {}", self.message_token),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.update_tui(RepoUpdate::ValidateRepo);
|
|
||||||
|
|
||||||
// Repository positions
|
// Repository positions
|
||||||
let Some(ref open_repository) = self.open_repository else {
|
let Some(ref open_repository) = self.open_repository else {
|
||||||
logger(self.log.as_ref(), "no open repository");
|
logger(self.log.as_ref(), "no open repository");
|
||||||
self.alert_tui("repo not open");
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
logger(self.log.as_ref(), "have open repository");
|
logger(self.log.as_ref(), "have open repository");
|
||||||
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
let Some(repo_config) = self.repo_details.repo_config.clone() else {
|
||||||
logger(self.log.as_ref(), "no repo config");
|
logger(self.log.as_ref(), "no repo config");
|
||||||
self.alert_tui("no repo config");
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
logger(self.log.as_ref(), "have repo config");
|
logger(self.log.as_ref(), "have repo config");
|
||||||
|
|
||||||
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
match validate_positions(&**open_repository, &self.repo_details, repo_config) {
|
||||||
Ok((
|
Ok(Positions {
|
||||||
Positions {
|
main,
|
||||||
main,
|
next,
|
||||||
next,
|
dev,
|
||||||
dev,
|
dev_commit_history,
|
||||||
dev_commit_history,
|
next_is_valid,
|
||||||
next_is_valid,
|
}) => {
|
||||||
},
|
debug!(%main, %next, %dev, "positions");
|
||||||
git_log,
|
|
||||||
)) => {
|
|
||||||
info!(%main, %next, %dev, "positions");
|
|
||||||
self.update_tui_log(git_log);
|
|
||||||
if next_is_valid && next != main {
|
if next_is_valid && next != main {
|
||||||
info!("Checking CI");
|
do_send(ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
|
||||||
do_send(&ctx.address(), CheckCIStatus::new(next), self.log.as_ref());
|
|
||||||
} else if next != dev {
|
} else if next != dev {
|
||||||
info!("Advance next");
|
|
||||||
self.update_tui(RepoUpdate::AdvancingNext {
|
|
||||||
commit: next.clone(),
|
|
||||||
force: Force::No,
|
|
||||||
});
|
|
||||||
do_send(
|
do_send(
|
||||||
&ctx.address(),
|
ctx.address(),
|
||||||
AdvanceNext::new(AdvanceNextPayload {
|
AdvanceNext::new(AdvanceNextPayload {
|
||||||
next,
|
next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
}),
|
}),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
info!("do nothing");
|
// do nothing
|
||||||
self.update_tui(RepoUpdate::Okay { main, next, dev });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Error::Retryable(message)) => {
|
Err(Error::Retryable(message)) => {
|
||||||
info!(?message, "Retryable");
|
|
||||||
self.alert_tui(format!("retryable: {message}"));
|
|
||||||
logger(self.log.as_ref(), message);
|
logger(self.log.as_ref(), message);
|
||||||
let addr = ctx.address();
|
let addr = ctx.address();
|
||||||
let message_token = self.message_token;
|
let message_token = self.message_token;
|
||||||
let sleep_duration = self.sleep_duration;
|
let sleep_duration = self.sleep_duration;
|
||||||
let log = self.log.clone();
|
let log = self.log.clone();
|
||||||
async move {
|
async move {
|
||||||
info!("sleeping before retrying...");
|
debug!("sleeping before retrying...");
|
||||||
logger(log.as_ref(), "before sleep");
|
logger(log.as_ref(), "before sleep");
|
||||||
actix_rt::time::sleep(sleep_duration).await;
|
actix_rt::time::sleep(sleep_duration).await;
|
||||||
logger(log.as_ref(), "after sleep");
|
logger(log.as_ref(), "after sleep");
|
||||||
do_send(&addr, ValidateRepo::new(message_token), log.as_ref());
|
do_send(addr, ValidateRepo::new(message_token), log.as_ref());
|
||||||
}
|
}
|
||||||
.in_current_span()
|
.in_current_span()
|
||||||
.into_actor(self)
|
.into_actor(self)
|
||||||
.wait(ctx);
|
.wait(ctx);
|
||||||
}
|
}
|
||||||
Err(Error::UserIntervention(user_notification)) => {
|
Err(Error::UserIntervention(user_notification)) => notify_user(
|
||||||
info!(?user_notification, "User Intervention");
|
self.notify_user_recipient.as_ref(),
|
||||||
self.alert_tui(format!("USER INTERVENTION: {user_notification}"));
|
user_notification,
|
||||||
if let UserNotification::CICheckFailed { log, .. }
|
self.log.as_ref(),
|
||||||
| 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)) => {
|
Err(Error::NonRetryable(message)) => {
|
||||||
info!(?message, "NonRetryable");
|
|
||||||
self.alert_tui(format!("Error: {message}"));
|
|
||||||
logger(self.log.as_ref(), message);
|
logger(self.log.as_ref(), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,13 +3,10 @@ use actix::prelude::*;
|
||||||
|
|
||||||
use tracing::{info, instrument, warn};
|
use tracing::{info, instrument, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send, logger,
|
||||||
do_send, logger,
|
messages::{ValidateRepo, WebhookNotification},
|
||||||
messages::{ValidateRepo, WebhookNotification},
|
RepoActor, RepoActorLog,
|
||||||
ActorLog, RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
|
@ -55,13 +52,9 @@ impl Handler<WebhookNotification> for RepoActor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Some(Branch::Main) => {
|
Some(Branch::Main) => {
|
||||||
self.update_tui(RepoUpdate::WebhookReceived {
|
|
||||||
branch: Branch::Main,
|
|
||||||
push: push.clone(),
|
|
||||||
});
|
|
||||||
if handle_push(
|
if handle_push(
|
||||||
push,
|
push,
|
||||||
&config.branches().main(),
|
config.branches().main(),
|
||||||
&mut self.last_main_commit,
|
&mut self.last_main_commit,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
@ -71,13 +64,9 @@ impl Handler<WebhookNotification> for RepoActor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Some(Branch::Next) => {
|
Some(Branch::Next) => {
|
||||||
self.update_tui(RepoUpdate::WebhookReceived {
|
|
||||||
branch: Branch::Next,
|
|
||||||
push: push.clone(),
|
|
||||||
});
|
|
||||||
if handle_push(
|
if handle_push(
|
||||||
push,
|
push,
|
||||||
&config.branches().next(),
|
config.branches().next(),
|
||||||
&mut self.last_next_commit,
|
&mut self.last_next_commit,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
@ -87,13 +76,9 @@ impl Handler<WebhookNotification> for RepoActor {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Some(Branch::Dev) => {
|
Some(Branch::Dev) => {
|
||||||
self.update_tui(RepoUpdate::WebhookReceived {
|
|
||||||
branch: Branch::Dev,
|
|
||||||
push: push.clone(),
|
|
||||||
});
|
|
||||||
if handle_push(
|
if handle_push(
|
||||||
push,
|
push,
|
||||||
&config.branches().dev(),
|
config.branches().dev(),
|
||||||
&mut self.last_dev_commit,
|
&mut self.last_dev_commit,
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
)
|
)
|
||||||
|
@ -110,7 +95,7 @@ impl Handler<WebhookNotification> for RepoActor {
|
||||||
"New commit"
|
"New commit"
|
||||||
);
|
);
|
||||||
do_send(
|
do_send(
|
||||||
&ctx.address(),
|
ctx.address(),
|
||||||
ValidateRepo::new(message_token),
|
ValidateRepo::new(message_token),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
);
|
||||||
|
@ -121,7 +106,7 @@ fn validate_notification(
|
||||||
msg: &WebhookNotification,
|
msg: &WebhookNotification,
|
||||||
webhook_auth: Option<&WebhookAuth>,
|
webhook_auth: Option<&WebhookAuth>,
|
||||||
forge: &dyn ForgeLike,
|
forge: &dyn ForgeLike,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&RepoActorLog>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
let Some(expected_authorization) = webhook_auth else {
|
let Some(expected_authorization) = webhook_auth else {
|
||||||
logger(log, "server has no auth token");
|
logger(log, "server has no auth token");
|
||||||
|
@ -146,11 +131,11 @@ fn validate_notification(
|
||||||
|
|
||||||
fn handle_push(
|
fn handle_push(
|
||||||
push: Push,
|
push: Push,
|
||||||
branch: &BranchName,
|
branch: BranchName,
|
||||||
last_commit: &mut Option<Commit>,
|
last_commit: &mut Option<Commit>,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&RepoActorLog>,
|
||||||
) -> Result<(), ()> {
|
) -> Result<(), ()> {
|
||||||
logger(log, format!("message is for {branch} branch"));
|
logger(log, "message is for dev branch");
|
||||||
let commit = Commit::from(push);
|
let commit = Commit::from(push);
|
||||||
if last_commit.as_ref() == Some(&commit) {
|
if last_commit.as_ref() == Some(&commit) {
|
||||||
logger(log, format!("not a new commit on {branch}"));
|
logger(log, format!("not a new commit on {branch}"));
|
||||||
|
|
|
@ -2,24 +2,20 @@
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::repo::{
|
||||||
repo::{
|
do_send,
|
||||||
do_send,
|
messages::{ValidateRepo, WebhookRegistered},
|
||||||
messages::{ValidateRepo, WebhookRegistered},
|
RepoActor,
|
||||||
RepoActor,
|
|
||||||
},
|
|
||||||
server::actor::messages::RepoUpdate,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<WebhookRegistered> for RepoActor {
|
impl Handler<WebhookRegistered> for RepoActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
#[instrument(name = "RepoActor::WebhookRegistered", skip_all, fields(repo = %self.repo_details, webhook_id = %msg.webhook_id()))]
|
#[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 {
|
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_id.replace(msg.webhook_id().clone());
|
||||||
self.webhook_auth.replace(msg.webhook_auth().clone());
|
self.webhook_auth.replace(msg.webhook_auth().clone());
|
||||||
do_send(
|
do_send(
|
||||||
&ctx.address(),
|
ctx.address(),
|
||||||
ValidateRepo::new(self.message_token),
|
ValidateRepo::new(self.message_token),
|
||||||
self.log.as_ref(),
|
self.log.as_ref(),
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,37 +6,22 @@ use git_next_core::{
|
||||||
message, newtype, ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId,
|
message, newtype, ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId,
|
||||||
};
|
};
|
||||||
|
|
||||||
message!(
|
message!(LoadConfigFromRepo: "Request to load the `git-next.toml` from the git repo.");
|
||||||
LoadConfigFromRepo,
|
message!(CloneRepo: "Request to clone (or open) the git repo.");
|
||||||
"Request to load the `git-next.toml` from the git repo."
|
message!(ReceiveRepoConfig: RepoConfig: r#"Notification that the `git-next.toml` file has been loaded from the repo and parsed.
|
||||||
);
|
|
||||||
message!(CloneRepo, "Request to clone (or open) the git repo.");
|
|
||||||
message!(
|
|
||||||
ReceiveRepoConfig,
|
|
||||||
RepoConfig,
|
|
||||||
r#"Notification that the `git-next.toml` file has been loaded from the repo and parsed.
|
|
||||||
|
|
||||||
Contains the parsed contents of the `git-next.toml` file."#
|
Contains the parsed contents of the `git-next.toml` file."#);
|
||||||
);
|
message!(ValidateRepo: MessageToken: r#"Request that the state of the branches in the git repo be assessed and generate any followup actions.
|
||||||
message!(
|
|
||||||
ValidateRepo,
|
|
||||||
MessageToken,
|
|
||||||
r#"Request that the state of the branches in the git repo be assessed and generate any followup actions.
|
|
||||||
|
|
||||||
This is the main function of `git-next` where decisions are made on what branches need to be updated and when.
|
This is the main function of `git-next` where decisions are made on what branches need to be updated and when.
|
||||||
|
|
||||||
Contains a [MessageToken] to reduce duplicate messages being sent. Only messages with the latest [MessageToken] are handled,
|
Contains a [MessageToken] to reduce duplicate messages being sent. Only messages with the latest [MessageToken] are handled,
|
||||||
all others are dropped."#
|
all others are dropped."#);
|
||||||
);
|
|
||||||
|
|
||||||
message!(
|
message!(WebhookRegistered: (WebhookId, WebhookAuth): r#"Notification that a webhook has been registered with a forge.
|
||||||
WebhookRegistered,
|
|
||||||
(WebhookId, WebhookAuth),
|
|
||||||
r#"Notification that a webhook has been registered with a forge.
|
|
||||||
|
|
||||||
Contains a tuple of the ID for the webhook returned from the forge, and the unique authorisation token that
|
Contains a tuple of the ID for the webhook returned from the forge, and the unique authorisation token that
|
||||||
incoming messages from the forge must provide."#
|
incoming messages from the forge must provide."#);
|
||||||
);
|
|
||||||
impl WebhookRegistered {
|
impl WebhookRegistered {
|
||||||
pub const fn webhook_id(&self) -> &WebhookId {
|
pub const fn webhook_id(&self) -> &WebhookId {
|
||||||
&self.0 .0
|
&self.0 .0
|
||||||
|
@ -53,51 +38,28 @@ impl From<RegisteredWebhook> for WebhookRegistered {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message!(
|
message!(UnRegisterWebhook: "Request that the webhook be removed from the forge, so they will stop notifying us.");
|
||||||
UnRegisterWebhook,
|
|
||||||
"Request that the webhook be removed from the forge, so they will stop notifying us."
|
|
||||||
);
|
|
||||||
|
|
||||||
newtype!(
|
newtype!(MessageToken: u32, Copy, Default, Display, PartialOrd, Ord: r#"An incremental token used to identify the current set of messages.
|
||||||
MessageToken,
|
|
||||||
u32,
|
|
||||||
Copy,
|
|
||||||
Default,
|
|
||||||
Display,
|
|
||||||
PartialOrd,
|
|
||||||
Ord,
|
|
||||||
r#"An incremental token used to identify the current set of messages.
|
|
||||||
|
|
||||||
Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is
|
Primarily used by [ValidateRepo] to reduce duplicate messages. The token is incremented when a new Webhook message is
|
||||||
received, marking that message the latest, and causing any previous messages still being processed to be dropped when
|
received, marking that message the latest, and causing any previous messages still being processed to be dropped when
|
||||||
they next send a [ValidateRepo] message."#
|
they next send a [ValidateRepo] message."#);
|
||||||
);
|
|
||||||
impl MessageToken {
|
impl MessageToken {
|
||||||
pub const fn next(self) -> Self {
|
pub const fn next(&self) -> Self {
|
||||||
Self(self.0 + 1)
|
Self(self.0 + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message!(
|
message!(RegisterWebhook: "Requests that a Webhook be registered with the forge.");
|
||||||
RegisterWebhook,
|
message!(CheckCIStatus: Commit: r#"Requests that the CI status for the commit be checked.
|
||||||
"Requests that a Webhook be registered with the forge."
|
|
||||||
);
|
|
||||||
message!(
|
|
||||||
CheckCIStatus,
|
|
||||||
Commit,
|
|
||||||
r#"Requests that the CI status for the commit be checked.
|
|
||||||
|
|
||||||
Once the CI Status has been received it will be sent via a [ReceiveCIStatus] message.
|
Once the CI Status has been received it will be sent via a [ReceiveCIStatus] message.
|
||||||
|
|
||||||
Contains the commit from the tip of the `next` branch."#
|
Contains the commit from the tip of the `next` branch."#); // next commit
|
||||||
); // next commit
|
message!(ReceiveCIStatus: (Commit, Status): r#"Notification of the status of the CI checks for the commit.
|
||||||
message!(
|
|
||||||
ReceiveCIStatus,
|
|
||||||
(Commit, Status),
|
|
||||||
r#"Notification of the status of the CI checks for the commit.
|
|
||||||
|
|
||||||
Contains a tuple of the commit that was checked (the tip of the `next` branch) and the status."#
|
Contains a tuple of the commit that was checked (the tip of the `next` branch) and the status."#); // commit and it's status
|
||||||
); // commit and it's status
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct AdvanceNextPayload {
|
pub struct AdvanceNextPayload {
|
||||||
|
@ -105,23 +67,7 @@ pub struct AdvanceNextPayload {
|
||||||
pub main: Commit,
|
pub main: Commit,
|
||||||
pub dev_commit_history: Vec<Commit>,
|
pub dev_commit_history: Vec<Commit>,
|
||||||
}
|
}
|
||||||
message!(
|
message!(AdvanceNext: AdvanceNextPayload: "Request to advance the `next` branch on to the next commit on the `dev branch."); // next commit and the dev commit history
|
||||||
AdvanceNext,
|
message!(AdvanceMain: Commit: "Request to advance the `main` branch on to same commit as the `next` branch."); // next commit
|
||||||
AdvanceNextPayload,
|
message!(WebhookNotification: ForgeNotification: "Notification of a webhook message from the forge.");
|
||||||
"Request to advance the `next` branch on to the next commit on the `dev branch."
|
message!(NotifyUser: UserNotification: "Request to send the message payload to the notification webhook");
|
||||||
); // next commit and the dev commit history
|
|
||||||
message!(
|
|
||||||
AdvanceMain,
|
|
||||||
Commit,
|
|
||||||
"Request to advance the `main` branch on to same commit as the `next` branch."
|
|
||||||
); // next commit
|
|
||||||
message!(
|
|
||||||
WebhookNotification,
|
|
||||||
ForgeNotification,
|
|
||||||
"Notification of a webhook message from the forge."
|
|
||||||
);
|
|
||||||
message!(
|
|
||||||
NotifyUser,
|
|
||||||
UserNotification,
|
|
||||||
"Request to send the message payload to the notification webhook"
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
|
||||||
alerts::messages::NotifyUser,
|
|
||||||
server::{actor::messages::RepoUpdate, ServerActor},
|
|
||||||
};
|
|
||||||
use derive_more::Deref;
|
use derive_more::Deref;
|
||||||
use kxio::network::Network;
|
use kxio::network::Network;
|
||||||
use tracing::{info, instrument, warn, Instrument};
|
use messages::NotifyUser;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{info, warn, Instrument};
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{
|
git::{
|
||||||
|
@ -15,8 +13,7 @@ use git_next_core::{
|
||||||
repository::{factory::RepositoryFactory, open::OpenRepositoryLike},
|
repository::{factory::RepositoryFactory, open::OpenRepositoryLike},
|
||||||
UserNotification,
|
UserNotification,
|
||||||
},
|
},
|
||||||
server::ListenUrl,
|
server, WebhookAuth, WebhookId,
|
||||||
WebhookAuth, WebhookId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod branch;
|
mod branch;
|
||||||
|
@ -26,11 +23,11 @@ pub mod messages;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests;
|
mod tests;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct ActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
|
pub struct RepoActorLog(std::sync::Arc<std::sync::RwLock<Vec<String>>>);
|
||||||
impl Deref for ActorLog {
|
impl Deref for RepoActorLog {
|
||||||
type Target = std::sync::Arc<std::sync::RwLock<Vec<String>>>;
|
type Target = std::sync::Arc<std::sync::RwLock<Vec<String>>>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -40,8 +37,7 @@ impl Deref for ActorLog {
|
||||||
|
|
||||||
/// An actor that represents a Git Repository.
|
/// An actor that represents a Git Repository.
|
||||||
///
|
///
|
||||||
/// When this actor is started it is sent the `CloneRepo` message.
|
/// When this actor is started it is sent the [CloneRepo] message.
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
#[derive(Debug, derive_more::Display, derive_with::With)]
|
#[derive(Debug, derive_more::Display, derive_with::With)]
|
||||||
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
|
#[display("{}:{}:{}", generation, repo_details.forge.forge_alias(), repo_details.repo_alias)]
|
||||||
pub struct RepoActor {
|
pub struct RepoActor {
|
||||||
|
@ -49,7 +45,7 @@ pub struct RepoActor {
|
||||||
generation: git::Generation,
|
generation: git::Generation,
|
||||||
message_token: messages::MessageToken,
|
message_token: messages::MessageToken,
|
||||||
repo_details: git::RepoDetails,
|
repo_details: git::RepoDetails,
|
||||||
listen_url: ListenUrl,
|
webhook: server::InboundWebhook,
|
||||||
webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
|
webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
|
||||||
webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured
|
webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured
|
||||||
last_main_commit: Option<git::Commit>,
|
last_main_commit: Option<git::Commit>,
|
||||||
|
@ -59,29 +55,27 @@ pub struct RepoActor {
|
||||||
open_repository: Option<Box<dyn OpenRepositoryLike>>,
|
open_repository: Option<Box<dyn OpenRepositoryLike>>,
|
||||||
net: Network,
|
net: Network,
|
||||||
forge: Box<dyn git::ForgeLike>,
|
forge: Box<dyn git::ForgeLike>,
|
||||||
log: Option<ActorLog>,
|
log: Option<RepoActorLog>,
|
||||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||||
server_addr: Option<Addr<ServerActor>>,
|
|
||||||
}
|
}
|
||||||
impl RepoActor {
|
impl RepoActor {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
repo_details: git::RepoDetails,
|
repo_details: git::RepoDetails,
|
||||||
forge: Box<dyn git::ForgeLike>,
|
forge: Box<dyn git::ForgeLike>,
|
||||||
listen_url: ListenUrl,
|
webhook: server::InboundWebhook,
|
||||||
generation: git::Generation,
|
generation: git::Generation,
|
||||||
net: Network,
|
net: Network,
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
notify_user_recipient: Option<Recipient<NotifyUser>>,
|
||||||
server_addr: Option<Addr<ServerActor>>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let message_token = messages::MessageToken::default();
|
let message_token = messages::MessageToken::default();
|
||||||
Self {
|
Self {
|
||||||
generation,
|
generation,
|
||||||
message_token,
|
message_token,
|
||||||
repo_details,
|
repo_details,
|
||||||
listen_url,
|
webhook,
|
||||||
webhook_id: None,
|
webhook_id: None,
|
||||||
webhook_auth: None,
|
webhook_auth: None,
|
||||||
last_main_commit: None,
|
last_main_commit: None,
|
||||||
|
@ -94,56 +88,12 @@ impl RepoActor {
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
log: None,
|
log: None,
|
||||||
notify_user_recipient,
|
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 {
|
impl Actor for RepoActor {
|
||||||
type Context = Context<Self>;
|
type Context = Context<Self>;
|
||||||
#[instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
|
#[tracing::instrument(name = "RepoActor::stopping", skip_all, fields(repo = %self.repo_details))]
|
||||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||||
tracing::debug!("stopping");
|
tracing::debug!("stopping");
|
||||||
info!("Checking webhook");
|
info!("Checking webhook");
|
||||||
|
@ -167,22 +117,33 @@ impl Actor for RepoActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_send<M>(addr: &Addr<RepoActor>, msg: M, log: Option<&ActorLog>)
|
pub fn delay_send<M>(addr: Addr<RepoActor>, delay: Duration, msg: M, log: Option<&RepoActorLog>)
|
||||||
where
|
where
|
||||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||||
RepoActor: actix::Handler<M>,
|
RepoActor: actix::Handler<M>,
|
||||||
<M as actix::Message>::Result: Send,
|
<M as actix::Message>::Result: Send,
|
||||||
{
|
{
|
||||||
let log_message = format!("send: {msg:?}");
|
let log_message = format!("send-after-delay: {:?}", msg);
|
||||||
info!(log_message);
|
tracing::debug!(log_message);
|
||||||
logger(log, log_message);
|
logger(log, log_message);
|
||||||
if cfg!(not(test)) {
|
std::thread::sleep(delay);
|
||||||
// #[cfg(not(test))]
|
do_send(addr, msg, log)
|
||||||
addr.do_send(msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logger(log: Option<&ActorLog>, message: impl Into<String>) {
|
pub fn do_send<M>(_addr: Addr<RepoActor>, msg: M, log: Option<&RepoActorLog>)
|
||||||
|
where
|
||||||
|
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||||
|
RepoActor: actix::Handler<M>,
|
||||||
|
<M as actix::Message>::Result: Send,
|
||||||
|
{
|
||||||
|
let log_message = format!("send: {:?}", msg);
|
||||||
|
tracing::debug!(log_message);
|
||||||
|
logger(log, log_message);
|
||||||
|
#[cfg(not(test))]
|
||||||
|
_addr.do_send(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn logger(log: Option<&RepoActorLog>, message: impl Into<String>) {
|
||||||
if let Some(log) = log {
|
if let Some(log) = log {
|
||||||
let message: String = message.into();
|
let message: String = message.into();
|
||||||
tracing::debug!(message);
|
tracing::debug!(message);
|
||||||
|
@ -192,10 +153,10 @@ pub fn logger(log: Option<&ActorLog>, message: impl Into<String>) {
|
||||||
pub fn notify_user(
|
pub fn notify_user(
|
||||||
recipient: Option<&Recipient<NotifyUser>>,
|
recipient: Option<&Recipient<NotifyUser>>,
|
||||||
user_notification: UserNotification,
|
user_notification: UserNotification,
|
||||||
log: Option<&ActorLog>,
|
log: Option<&RepoActorLog>,
|
||||||
) {
|
) {
|
||||||
let msg = NotifyUser::from(user_notification);
|
let msg = NotifyUser::from(user_notification);
|
||||||
let log_message = format!("send: {msg:?}");
|
let log_message = format!("send: {:?}", msg);
|
||||||
tracing::debug!(log_message);
|
tracing::debug!(log_message);
|
||||||
logger(log, log_message);
|
logger(log, log_message);
|
||||||
if let Some(recipient) = &recipient {
|
if let Some(recipient) = &recipient {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
//
|
//
|
||||||
|
use derive_more::Deref as _;
|
||||||
|
|
||||||
use crate::repo::messages::NotifyUser;
|
use crate::repo::messages::NotifyUser;
|
||||||
|
|
||||||
|
@ -9,12 +10,11 @@ use serde_json::json;
|
||||||
impl NotifyUser {
|
impl NotifyUser {
|
||||||
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
|
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
|
||||||
let timestamp = timestamp.unix_timestamp().to_string();
|
let timestamp = timestamp.unix_timestamp().to_string();
|
||||||
match &**self {
|
match self.deref() {
|
||||||
UserNotification::CICheckFailed {
|
UserNotification::CICheckFailed {
|
||||||
forge_alias,
|
forge_alias,
|
||||||
repo_alias,
|
repo_alias,
|
||||||
commit,
|
commit,
|
||||||
log,
|
|
||||||
} => json!({
|
} => json!({
|
||||||
"type": "cicheck.failed",
|
"type": "cicheck.failed",
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
|
@ -24,8 +24,7 @@ impl NotifyUser {
|
||||||
"commit": {
|
"commit": {
|
||||||
"sha": commit.sha(),
|
"sha": commit.sha(),
|
||||||
"message": commit.message()
|
"message": commit.message()
|
||||||
},
|
}
|
||||||
"log": **log
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
UserNotification::RepoConfigLoadFailure {
|
UserNotification::RepoConfigLoadFailure {
|
||||||
|
@ -59,9 +58,6 @@ impl NotifyUser {
|
||||||
repo_alias,
|
repo_alias,
|
||||||
dev_branch,
|
dev_branch,
|
||||||
main_branch,
|
main_branch,
|
||||||
dev_commit,
|
|
||||||
main_commit,
|
|
||||||
log,
|
|
||||||
} => json!({
|
} => json!({
|
||||||
"type": "branch.dev.not-on-main",
|
"type": "branch.dev.not-on-main",
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
|
@ -71,18 +67,7 @@ impl NotifyUser {
|
||||||
"branches": {
|
"branches": {
|
||||||
"dev": dev_branch,
|
"dev": dev_branch,
|
||||||
"main": main_branch
|
"main": main_branch
|
||||||
},
|
}
|
||||||
"commits": {
|
|
||||||
"dev": {
|
|
||||||
"sha": dev_commit.sha(),
|
|
||||||
"message": dev_commit.message()
|
|
||||||
},
|
|
||||||
"main": {
|
|
||||||
"sha": main_commit.sha(),
|
|
||||||
"message": main_commit.message()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": **log
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,11 @@
|
||||||
//
|
//
|
||||||
use crate::repo::branch::find_next_commit_on_dev;
|
|
||||||
|
|
||||||
use super::*;
|
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 {
|
mod when_at_dev {
|
||||||
// next and dev branches are the same
|
// next and dev branches are the same
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_not_push() {
|
fn should_not_push() -> TestResult {
|
||||||
let next = given::a_commit();
|
let next = given::a_commit();
|
||||||
let main = &next;
|
let main = &next;
|
||||||
let dev_commit_history = &[next.clone()];
|
let dev_commit_history = &[next.clone()];
|
||||||
|
@ -38,11 +15,11 @@ mod when_at_dev {
|
||||||
// no on_push defined - so any call to push will cause an error
|
// no on_push defined - so any call to push will cause an error
|
||||||
let message_token = given::a_message_token();
|
let message_token = given::a_message_token();
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Err(err) = advance_next_sut(
|
Err(err) = branch::advance_next(
|
||||||
&next,
|
&next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
|
@ -50,6 +27,7 @@ mod when_at_dev {
|
||||||
);
|
);
|
||||||
tracing::debug!("Got: {err}");
|
tracing::debug!("Got: {err}");
|
||||||
assert!(matches!(err, branch::Error::NextAtDev));
|
assert!(matches!(err, branch::Error::NextAtDev));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,7 +40,7 @@ mod can_advance {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_not_push() {
|
fn should_not_push() -> TestResult {
|
||||||
let next = given::a_commit();
|
let next = given::a_commit();
|
||||||
let main = &next;
|
let main = &next;
|
||||||
let dev = given::a_commit_with_message("wip: test: message".to_string());
|
let dev = given::a_commit_with_message("wip: test: message".to_string());
|
||||||
|
@ -73,11 +51,11 @@ mod can_advance {
|
||||||
// no on_push defined - so any call to push will cause an error
|
// no on_push defined - so any call to push will cause an error
|
||||||
let message_token = given::a_message_token();
|
let message_token = given::a_message_token();
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Err(err) = advance_next_sut(
|
Err(err) = branch::advance_next(
|
||||||
&next,
|
&next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
|
@ -85,6 +63,7 @@ mod can_advance {
|
||||||
);
|
);
|
||||||
tracing::debug!("Got: {err}");
|
tracing::debug!("Got: {err}");
|
||||||
assert!(matches!(err, branch::Error::IsWorkInProgress));
|
assert!(matches!(err, branch::Error::IsWorkInProgress));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,7 +72,7 @@ mod can_advance {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_not_push_and_error() {
|
fn should_not_push_and_error() -> TestResult {
|
||||||
let next = given::a_commit();
|
let next = given::a_commit();
|
||||||
let main = &next;
|
let main = &next;
|
||||||
let dev = given::a_commit();
|
let dev = given::a_commit();
|
||||||
|
@ -104,11 +83,11 @@ mod can_advance {
|
||||||
// no on_push defined - so any call to push will cause an error
|
// no on_push defined - so any call to push will cause an error
|
||||||
let message_token = given::a_message_token();
|
let message_token = given::a_message_token();
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Err(err) = advance_next_sut(
|
Err(err) = branch::advance_next(
|
||||||
&next,
|
&next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
|
@ -120,6 +99,7 @@ mod can_advance {
|
||||||
branch::Error::InvalidCommitMessage{reason}
|
branch::Error::InvalidCommitMessage{reason}
|
||||||
if reason == "Missing type in the commit summary, expected `type: description`"
|
if reason == "Missing type in the commit summary, expected `type: description`"
|
||||||
));
|
));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +112,7 @@ mod can_advance {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_error() {
|
fn should_error() -> TestResult {
|
||||||
let next = given::a_commit();
|
let next = given::a_commit();
|
||||||
let main = &next;
|
let main = &next;
|
||||||
let dev = given::a_commit_with_message("test: message".to_string());
|
let dev = given::a_commit_with_message("test: message".to_string());
|
||||||
|
@ -144,11 +124,11 @@ mod can_advance {
|
||||||
expect::push(&mut open_repository, Err(git::push::Error::Lock));
|
expect::push(&mut open_repository, Err(git::push::Error::Lock));
|
||||||
let message_token = given::a_message_token();
|
let message_token = given::a_message_token();
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Err(err) = advance_next_sut(
|
Err(err) = branch::advance_next(
|
||||||
&next,
|
&next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
|
@ -156,6 +136,7 @@ mod can_advance {
|
||||||
);
|
);
|
||||||
tracing::debug!("Got: {err:?}");
|
tracing::debug!("Got: {err:?}");
|
||||||
assert!(matches!(err, branch::Error::Push(git::push::Error::Lock)));
|
assert!(matches!(err, branch::Error::Push(git::push::Error::Lock)));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +145,7 @@ mod can_advance {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_ok() {
|
fn should_ok() -> TestResult {
|
||||||
let next = given::a_commit();
|
let next = given::a_commit();
|
||||||
let main = &next;
|
let main = &next;
|
||||||
let dev = given::a_commit_with_message("test: message".to_string());
|
let dev = given::a_commit_with_message("test: message".to_string());
|
||||||
|
@ -176,11 +157,11 @@ mod can_advance {
|
||||||
expect::push_ok(&mut open_repository);
|
expect::push_ok(&mut open_repository);
|
||||||
let message_token = given::a_message_token();
|
let message_token = given::a_message_token();
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(mt) = advance_next_sut(
|
Ok(mt) = branch::advance_next(
|
||||||
&next,
|
&next,
|
||||||
main,
|
main,
|
||||||
dev_commit_history,
|
dev_commit_history,
|
||||||
&repo_details,
|
repo_details,
|
||||||
repo_config,
|
repo_config,
|
||||||
&open_repository,
|
&open_repository,
|
||||||
message_token,
|
message_token,
|
||||||
|
@ -188,6 +169,7 @@ mod can_advance {
|
||||||
);
|
);
|
||||||
tracing::debug!("Got: {mt:?}");
|
tracing::debug!("Got: {mt:?}");
|
||||||
assert_eq!(mt, message_token);
|
assert_eq!(mt, message_token);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub fn fetch(open_repository: &mut MockOpenRepositoryLike, result: Result<(), fe
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_ok(open_repository: &mut MockOpenRepositoryLike) {
|
pub fn push_ok(open_repository: &mut MockOpenRepositoryLike) {
|
||||||
expect::push(open_repository, Ok(()));
|
expect::push(open_repository, Ok(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push(
|
pub fn push(
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use git_next_core::server::ListenUrl;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -20,12 +18,12 @@ pub fn has_remote_defaults(
|
||||||
open_repository: &mut MockOpenRepositoryLike,
|
open_repository: &mut MockOpenRepositoryLike,
|
||||||
remotes: HashMap<Direction, Option<RemoteUrl>>,
|
remotes: HashMap<Direction, Option<RemoteUrl>>,
|
||||||
) {
|
) {
|
||||||
for (direction, remote) in remotes {
|
remotes.into_iter().for_each(|(direction, remote)| {
|
||||||
open_repository
|
open_repository
|
||||||
.expect_find_default_remote()
|
.expect_find_default_remote()
|
||||||
.with(eq(direction))
|
.with(eq(direction))
|
||||||
.return_once(|_| remote);
|
.return_once(|_| remote);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_webhook_auth() -> WebhookAuth {
|
pub fn a_webhook_auth() -> WebhookAuth {
|
||||||
|
@ -52,8 +50,8 @@ pub fn a_network() -> kxio::network::MockNetwork {
|
||||||
kxio::network::MockNetwork::new()
|
kxio::network::MockNetwork::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_listen_url() -> ListenUrl {
|
pub fn a_webhook_url(forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> WebhookUrl {
|
||||||
ListenUrl::new(a_name())
|
InboundWebhook::new(a_name()).url(forge_alias, repo_alias)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_name() -> String {
|
pub fn a_name() -> String {
|
||||||
|
@ -69,22 +67,6 @@ pub fn a_name() -> String {
|
||||||
generate(5)
|
generate(5)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn maybe_a_number() -> Option<u32> {
|
|
||||||
use rand::Rng;
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
if Rng::gen_ratio(&mut rng, 1, 2) {
|
|
||||||
Some(a_number())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn a_number() -> u32 {
|
|
||||||
use rand::Rng;
|
|
||||||
let mut rng = rand::thread_rng();
|
|
||||||
rng.gen_range(0..100)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn a_webhook_id() -> WebhookId {
|
pub fn a_webhook_id() -> WebhookId {
|
||||||
WebhookId::new(a_name())
|
WebhookId::new(a_name())
|
||||||
}
|
}
|
||||||
|
@ -105,8 +87,7 @@ pub fn a_forge_config() -> ForgeConfig {
|
||||||
a_name(),
|
a_name(),
|
||||||
a_name(),
|
a_name(),
|
||||||
a_name(),
|
a_name(),
|
||||||
maybe_a_number(),
|
Default::default(), // no repos
|
||||||
BTreeMap::default(), // no repos
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +168,10 @@ pub fn a_message_token() -> MessageToken {
|
||||||
MessageToken::default()
|
MessageToken::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_box_returns)]
|
pub fn a_webhook(url: &WebhookUrl) -> InboundWebhook {
|
||||||
|
InboundWebhook::new(url.clone().into())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn a_forge() -> Box<MockForgeLike> {
|
pub fn a_forge() -> Box<MockForgeLike> {
|
||||||
Box::new(MockForgeLike::new())
|
Box::new(MockForgeLike::new())
|
||||||
}
|
}
|
||||||
|
@ -197,22 +181,24 @@ pub fn a_repo_actor(
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
forge: Box<dyn ForgeLike>,
|
forge: Box<dyn ForgeLike>,
|
||||||
net: kxio::network::Network,
|
net: kxio::network::Network,
|
||||||
) -> (RepoActor, ActorLog) {
|
) -> (RepoActor, RepoActorLog) {
|
||||||
let listen_url = given::a_listen_url();
|
let forge_alias = repo_details.forge.forge_alias();
|
||||||
|
let repo_alias = &repo_details.repo_alias;
|
||||||
|
let webhook_url = given::a_webhook_url(forge_alias, repo_alias);
|
||||||
|
let webhook = given::a_webhook(&webhook_url);
|
||||||
let generation = Generation::default();
|
let generation = Generation::default();
|
||||||
let log = ActorLog::default();
|
let log = RepoActorLog::default();
|
||||||
let actors_log = log.clone();
|
let actors_log = log.clone();
|
||||||
(
|
(
|
||||||
RepoActor::new(
|
RepoActor::new(
|
||||||
repo_details,
|
repo_details,
|
||||||
forge,
|
forge,
|
||||||
listen_url,
|
webhook,
|
||||||
generation,
|
generation,
|
||||||
net,
|
net,
|
||||||
repository_factory,
|
repository_factory,
|
||||||
std::time::Duration::from_nanos(1),
|
std::time::Duration::from_nanos(1),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
.with_log(actors_log),
|
.with_log(actors_log),
|
||||||
log,
|
log,
|
||||||
|
|
|
@ -25,11 +25,6 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
|
||||||
.times(1)
|
.times(1)
|
||||||
.in_sequence(&mut seq)
|
.in_sequence(&mut seq)
|
||||||
.return_once(|_, _, _, _| Ok(()));
|
.return_once(|_, _, _, _| Ok(()));
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.in_sequence(&mut seq)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
|
@ -46,13 +41,13 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
assert!(l
|
assert!(l
|
||||||
.iter()
|
.iter()
|
||||||
.any(|message| message.contains("send: LoadConfigFromRepo")));
|
.any(|message| message.contains("send: LoadConfigFromRepo")))
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix::test]
|
#[actix::test]
|
||||||
async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
async fn when_server_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
||||||
|
@ -75,11 +70,6 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
|
||||||
.times(1)
|
.times(1)
|
||||||
.in_sequence(&mut seq)
|
.in_sequence(&mut seq)
|
||||||
.return_once(|_, _, _, _| Ok(()));
|
.return_once(|_, _, _, _| Ok(()));
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.in_sequence(&mut seq)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
|
@ -93,6 +83,10 @@ async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
|
assert!(l
|
||||||
|
.iter()
|
||||||
|
.any(|message| message.contains("send: ValidateRepo")))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::repo::messages::AdvanceNextPayload;
|
use crate::repo::messages::AdvanceNextPayload;
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[actix::test]
|
||||||
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -28,11 +26,6 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
.times(1)
|
.times(1)
|
||||||
.in_sequence(&mut seq)
|
.in_sequence(&mut seq)
|
||||||
.return_once(|_, _, _, _| Ok(()));
|
.return_once(|_, _, _, _| Ok(()));
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.in_sequence(&mut seq)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
|
|
||||||
//when
|
//when
|
||||||
let (addr, log) = when::start_actor_with_open_repository(
|
let (addr, log) = when::start_actor_with_open_repository(
|
||||||
|
@ -48,11 +41,14 @@ async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
|
||||||
System::current().stop();
|
System::current().stop();
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
|
assert!(l
|
||||||
|
.iter()
|
||||||
|
.any(|message| message.contains("send: ValidateRepo")))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
assert!(l
|
assert!(l
|
||||||
.iter()
|
.iter()
|
||||||
.any(|message| message.contains("send: ReceiveCIStatus")));
|
.any(|message| message.contains("send: ReceiveCIStatus")))
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,10 +43,6 @@ async fn should_open() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
||||||
// factory opens a repository
|
// factory opens a repository
|
||||||
let mut repository_factory = MockRepositoryFactory::new();
|
let mut repository_factory = MockRepositoryFactory::new();
|
||||||
|
@ -83,10 +79,6 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
let _repo_config = repo_details.repo_config.take().unwrap();
|
let _repo_config = repo_details.repo_config.take().unwrap();
|
||||||
|
|
||||||
|
@ -114,10 +106,6 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
given::has_all_valid_remote_defaults(&mut open_repository, &repo_details);
|
||||||
|
|
||||||
|
@ -141,10 +129,6 @@ async fn opened_repo_with_no_default_push_should_not_proceed() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
|
||||||
.times(1)
|
|
||||||
.return_once(|| Ok(()));
|
|
||||||
|
|
||||||
given::has_remote_defaults(
|
given::has_remote_defaults(
|
||||||
&mut open_repository,
|
&mut open_repository,
|
||||||
|
@ -174,10 +158,15 @@ async fn opened_repo_with_no_default_fetch_should_not_proceed() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||||
open_repository
|
|
||||||
.expect_fetch()
|
given::has_remote_defaults(
|
||||||
.times(1)
|
&mut open_repository,
|
||||||
.return_once(|| Err(git::fetch::Error::NoFetchRemoteFound));
|
HashMap::from([
|
||||||
|
(Direction::Push, repo_details.remote_url()),
|
||||||
|
(Direction::Fetch, None),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
let mut repository_factory = MockRepositoryFactory::new();
|
let mut repository_factory = MockRepositoryFactory::new();
|
||||||
expect::open_repository(&mut repository_factory, open_repository);
|
expect::open_repository(&mut repository_factory, open_repository);
|
||||||
fs.dir_create(&repo_details.gitdir)?;
|
fs.dir_create(&repo_details.gitdir)?;
|
||||||
|
|
|
@ -32,7 +32,7 @@ async fn should_store_repo_config_in_actor() -> TestResult {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(actix::test)]
|
#[actix::test]
|
||||||
async fn should_register_webhook() -> TestResult {
|
async fn should_register_webhook() -> TestResult {
|
||||||
//given
|
//given
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
|
@ -54,6 +54,10 @@ async fn should_register_webhook() -> TestResult {
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: RegisterWebhook")?;
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
|
assert!(l
|
||||||
|
.iter()
|
||||||
|
.any(|message| message.contains("send: RegisterWebhook")))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -26,9 +24,9 @@ async fn when_pass_should_advance_main_to_next() -> TestResult {
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
let expected = format!("send: AdvanceMain({next_commit:?})");
|
let expected = format!("send: AdvanceMain({:?})", next_commit);
|
||||||
tracing::debug!(%expected,"");
|
tracing::debug!(%expected,"");
|
||||||
assert!(l.iter().any(|message| message.contains(&expected)));
|
assert!(l.iter().any(|message| message.contains(&expected)))
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -51,12 +49,15 @@ async fn when_pending_should_recheck_ci_status() -> TestResult {
|
||||||
git::forge::commit::Status::Pending,
|
git::forge::commit::Status::Pending,
|
||||||
)))
|
)))
|
||||||
.await?;
|
.await?;
|
||||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
|
||||||
System::current().stop();
|
System::current().stop();
|
||||||
|
|
||||||
//then
|
//then
|
||||||
tracing::debug!(?log, "");
|
tracing::debug!(?log, "");
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
|
assert!(l
|
||||||
|
.iter()
|
||||||
|
.any(|message| message.contains("send: ValidateRepo")))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,11 +79,11 @@ async fn when_fail_should_recheck_after_delay() -> TestResult {
|
||||||
git::forge::commit::Status::Fail,
|
git::forge::commit::Status::Fail,
|
||||||
)))
|
)))
|
||||||
.await?;
|
.await?;
|
||||||
actix_rt::time::sleep(Duration::from_millis(9)).await;
|
actix_rt::time::sleep(std::time::Duration::from_millis(2)).await;
|
||||||
System::current().stop();
|
System::current().stop();
|
||||||
|
|
||||||
//then
|
//then
|
||||||
log.require_message_containing("send: ValidateRepo")?;
|
log.require_message_containing("send-after-delay: ValidateRepo")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
|
||||||
log.read().map_err(|e| e.to_string()).map(|l| {
|
log.read().map_err(|e| e.to_string()).map(|l| {
|
||||||
assert!(l
|
assert!(l
|
||||||
.iter()
|
.iter()
|
||||||
.any(|message| message.contains("send: WebhookRegistered")));
|
.any(|message| message.contains("send: WebhookRegistered")))
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
|
@ -43,7 +43,7 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
|
@ -77,7 +77,7 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let mut forge = given::a_forge();
|
let mut forge = given::a_forge();
|
||||||
|
@ -115,7 +115,7 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let mut forge = given::a_forge();
|
let mut forge = given::a_forge();
|
||||||
|
@ -157,7 +157,7 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let mut forge = given::a_forge();
|
let mut forge = given::a_forge();
|
||||||
|
@ -200,7 +200,7 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let commit = given::a_commit();
|
let commit = given::a_commit();
|
||||||
|
@ -248,7 +248,7 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let commit = given::a_commit();
|
let commit = given::a_commit();
|
||||||
|
@ -297,7 +297,7 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let commit = given::a_commit();
|
let commit = given::a_commit();
|
||||||
|
@ -346,7 +346,7 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let commit = given::a_commit();
|
let commit = given::a_commit();
|
||||||
|
@ -395,7 +395,7 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let push_commit = given::a_commit();
|
let push_commit = given::a_commit();
|
||||||
|
@ -443,7 +443,7 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let push_commit = given::a_commit();
|
let push_commit = given::a_commit();
|
||||||
|
@ -491,7 +491,7 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
|
||||||
let forge_alias = given::a_forge_alias();
|
let forge_alias = given::a_forge_alias();
|
||||||
let repo_alias = given::a_repo_alias();
|
let repo_alias = given::a_repo_alias();
|
||||||
let headers = BTreeMap::new();
|
let headers = BTreeMap::new();
|
||||||
let body = Body::new(String::new());
|
let body = Body::new("".to_string());
|
||||||
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
let forge_notification = ForgeNotification::new(forge_alias, repo_alias, headers, body);
|
||||||
let repository_factory = MockRepositoryFactory::new();
|
let repository_factory = MockRepositoryFactory::new();
|
||||||
let push_commit = given::a_commit();
|
let push_commit = given::a_commit();
|
||||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
git,
|
git,
|
||||||
repo::{
|
repo::{
|
||||||
messages::{CloneRepo, MessageToken},
|
messages::{CloneRepo, MessageToken},
|
||||||
ActorLog, RepoActor,
|
RepoActor, RepoActorLog,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ use git_next_core::{
|
||||||
Commit, ForgeLike, Generation, MockForgeLike, RepoDetails,
|
Commit, ForgeLike, Generation, MockForgeLike, RepoDetails,
|
||||||
},
|
},
|
||||||
message,
|
message,
|
||||||
|
server::{InboundWebhook, WebhookUrl},
|
||||||
webhook::{forge_notification::Body, Push},
|
webhook::{forge_notification::Body, Push},
|
||||||
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname,
|
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname,
|
||||||
RegisteredWebhook, RemoteUrl, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource,
|
RegisteredWebhook, RemoteUrl, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource,
|
||||||
|
@ -45,7 +46,7 @@ mod handlers;
|
||||||
mod load;
|
mod load;
|
||||||
mod when;
|
mod when;
|
||||||
|
|
||||||
impl ActorLog {
|
impl RepoActorLog {
|
||||||
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult {
|
pub fn no_message_contains(&self, needle: impl AsRef<str> + std::fmt::Display) -> TestResult {
|
||||||
if self.find_in_messages(needle.as_ref())? {
|
if self.find_in_messages(needle.as_ref())? {
|
||||||
error!(?self, "");
|
error!(?self, "");
|
||||||
|
@ -78,7 +79,7 @@ impl ActorLog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message!(ExamineActor => RepoActorView, "Request a view of the current state of the [RepoActor].");
|
message!(ExamineActor => RepoActorView: "Request a view of the current state of the [RepoActor].");
|
||||||
impl Handler<ExamineActor> for RepoActor {
|
impl Handler<ExamineActor> for RepoActor {
|
||||||
type Result = RepoActorView;
|
type Result = RepoActorView;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ pub fn start_actor(
|
||||||
repository_factory: MockRepositoryFactory,
|
repository_factory: MockRepositoryFactory,
|
||||||
repo_details: RepoDetails,
|
repo_details: RepoDetails,
|
||||||
forge: Box<dyn ForgeLike>,
|
forge: Box<dyn ForgeLike>,
|
||||||
) -> (actix::Addr<RepoActor>, ActorLog) {
|
) -> (actix::Addr<RepoActor>, RepoActorLog) {
|
||||||
let (actor, log) = given::a_repo_actor(
|
let (actor, log) = given::a_repo_actor(
|
||||||
repo_details,
|
repo_details,
|
||||||
Box::new(repository_factory),
|
Box::new(repository_factory),
|
||||||
|
@ -19,7 +19,7 @@ pub fn start_actor_with_open_repository(
|
||||||
open_repository: Box<dyn OpenRepositoryLike>,
|
open_repository: Box<dyn OpenRepositoryLike>,
|
||||||
repo_details: RepoDetails,
|
repo_details: RepoDetails,
|
||||||
forge: Box<dyn ForgeLike>,
|
forge: Box<dyn ForgeLike>,
|
||||||
) -> (actix::Addr<RepoActor>, ActorLog) {
|
) -> (actix::Addr<RepoActor>, RepoActorLog) {
|
||||||
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into());
|
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, given::a_network().into());
|
||||||
let actor = actor.with_open_repository(Some(open_repository));
|
let actor = actor.with_open_repository(Some(open_repository));
|
||||||
(actor.start(), log)
|
(actor.start(), log)
|
||||||
|
|
|
@ -1,20 +1,24 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use git_next_core::server::AppConfig;
|
use git_next_core::server::ServerConfig;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
file_watcher::FileUpdated,
|
file_watcher::FileUpdated,
|
||||||
server::actor::{messages::ReceiveAppConfig, ServerActor},
|
server::actor::{messages::ReceiveServerConfig, ServerActor},
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Handler<FileUpdated> for ServerActor {
|
impl Handler<FileUpdated> for ServerActor {
|
||||||
type Result = ();
|
type Result = ();
|
||||||
|
|
||||||
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result {
|
fn handle(&mut self, _msg: FileUpdated, ctx: &mut Self::Context) -> Self::Result {
|
||||||
match AppConfig::load(&self.fs) {
|
let server_config = match ServerConfig::load(&self.fs) {
|
||||||
Ok(app_config) => self.do_send(ReceiveAppConfig::new(app_config), ctx),
|
Ok(server_config) => server_config,
|
||||||
Err(err) => self.abort(ctx, format!("Failed to load config file. Error: {err}")),
|
Err(err) => {
|
||||||
|
tracing::error!("Failed to load config file. Error: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
self.do_send(ReceiveServerConfig::new(server_config), ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
mod file_updated;
|
mod file_updated;
|
||||||
mod receive_app_config;
|
mod notify_user;
|
||||||
mod receive_valid_app_config;
|
mod receive_server_config;
|
||||||
mod server_update;
|
mod receive_valid_server_config;
|
||||||
mod shutdown;
|
mod shutdown;
|
||||||
mod shutdown_trigger;
|
|
||||||
mod subscribe_updates;
|
|
||||||
|
|
82
crates/cli/src/server/actor/handlers/notify_user.rs
Normal file
82
crates/cli/src/server/actor/handlers/notify_user.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
use actix::prelude::*;
|
||||||
|
|
||||||
|
use secrecy::ExposeSecret;
|
||||||
|
use standardwebhooks::Webhook;
|
||||||
|
use tracing::Instrument;
|
||||||
|
|
||||||
|
use crate::{repo::messages::NotifyUser, server::actor::ServerActor};
|
||||||
|
|
||||||
|
use git_next_core::server::{self, Notification, NotificationType};
|
||||||
|
|
||||||
|
impl Handler<NotifyUser> for ServerActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: NotifyUser, ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let Some(server_config) = &self.server_config else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let notification_config = server_config.notification().clone();
|
||||||
|
let net = self.net.clone();
|
||||||
|
async move {
|
||||||
|
match notification_config.r#type() {
|
||||||
|
NotificationType::None => { /* do nothing */ }
|
||||||
|
NotificationType::Webhook => send_webhook(msg, notification_config, net).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.in_current_span()
|
||||||
|
.into_actor(self)
|
||||||
|
.wait(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_webhook(
|
||||||
|
msg: NotifyUser,
|
||||||
|
notification_config: Notification,
|
||||||
|
net: kxio::network::Network,
|
||||||
|
) {
|
||||||
|
let Some(webhook_config) = notification_config.webhook() else {
|
||||||
|
tracing::warn!("Invalid notification configuration (config) - can't sent notification");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(webhook) = Webhook::new(webhook_config.secret().expose_secret()) else {
|
||||||
|
tracing::warn!("Invalid notification configuration (signer) - can't sent notification");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
do_send_webhook(msg, webhook, webhook_config, net).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn do_send_webhook(
|
||||||
|
msg: NotifyUser,
|
||||||
|
webhook: Webhook,
|
||||||
|
webhook_config: &server::OutboundWebhook,
|
||||||
|
net: kxio::network::Network,
|
||||||
|
) {
|
||||||
|
let message_id = format!("msg_{}", ulid::Ulid::new());
|
||||||
|
let timestamp = time::OffsetDateTime::now_utc();
|
||||||
|
let payload = msg.as_json(timestamp);
|
||||||
|
let timestamp = timestamp.unix_timestamp();
|
||||||
|
let to_sign = format!("{message_id}.{timestamp}.{payload}");
|
||||||
|
tracing::info!(?to_sign, "");
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
let signature = webhook
|
||||||
|
.sign(&message_id, timestamp, payload.to_string().as_ref())
|
||||||
|
.expect("signature");
|
||||||
|
tracing::info!(?signature, "");
|
||||||
|
let url = webhook_config.url();
|
||||||
|
use kxio::network::{NetRequest, NetUrl, RequestBody, ResponseType};
|
||||||
|
let net_url = NetUrl::new(url.to_string());
|
||||||
|
let request = NetRequest::post(net_url)
|
||||||
|
.body(RequestBody::Json(payload))
|
||||||
|
.header("webhook-id", &message_id)
|
||||||
|
.header("webhook-timestamp", ×tamp.to_string())
|
||||||
|
.header("webhook-signature", &signature)
|
||||||
|
.response_type(ResponseType::None)
|
||||||
|
.build();
|
||||||
|
net.post_json::<()>(request).await.map_or_else(
|
||||||
|
|err| {
|
||||||
|
tracing::warn!(?err, "sending webhook");
|
||||||
|
},
|
||||||
|
|_| (),
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use crate::server::actor::{
|
|
||||||
messages::{ReceiveAppConfig, ReceiveValidAppConfig, ValidAppConfig},
|
|
||||||
ServerActor,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Handler<ReceiveAppConfig> for ServerActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
#[allow(clippy::cognitive_complexity)]
|
|
||||||
fn handle(&mut self, msg: ReceiveAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
tracing::info!("recieved server config");
|
|
||||||
let Ok(socket_addr) = msg.listen_socket_addr() else {
|
|
||||||
return self.abort(ctx, "Unable to parse http.addr");
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(server_storage) = self.server_storage(&msg) else {
|
|
||||||
return self.abort(ctx, "Server storage not available");
|
|
||||||
};
|
|
||||||
|
|
||||||
if msg.listen().url().ends_with('/') {
|
|
||||||
return self.abort(ctx, "webhook.url must not end with a '/'");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.do_send(
|
|
||||||
ReceiveValidAppConfig::new(ValidAppConfig::new(
|
|
||||||
msg.peel(),
|
|
||||||
socket_addr,
|
|
||||||
server_storage,
|
|
||||||
)),
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
use actix::prelude::*;
|
||||||
|
|
||||||
|
use crate::server::actor::{
|
||||||
|
messages::{ReceiveServerConfig, ReceiveValidServerConfig, ValidServerConfig},
|
||||||
|
ServerActor,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Handler<ReceiveServerConfig> for ServerActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
|
fn handle(&mut self, msg: ReceiveServerConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
tracing::info!("recieved server config");
|
||||||
|
let Ok(socket_addr) = msg.http() else {
|
||||||
|
tracing::error!("Unable to parse http.addr");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(server_storage) = self.server_storage(&msg) else {
|
||||||
|
tracing::error!("Server storage not available");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if msg.inbound_webhook().base_url().ends_with('/') {
|
||||||
|
tracing::error!("webhook.url must not end with a '/'");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.do_send(
|
||||||
|
ReceiveValidServerConfig::new(ValidServerConfig::new(
|
||||||
|
msg.unwrap(),
|
||||||
|
socket_addr,
|
||||||
|
server_storage,
|
||||||
|
)),
|
||||||
|
ctx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,97 +0,0 @@
|
||||||
//
|
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use git_next_core::{ForgeAlias, RepoAlias};
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
alerts::messages::UpdateShout,
|
|
||||||
repo::{messages::CloneRepo, RepoActor},
|
|
||||||
server::actor::{
|
|
||||||
messages::{ReceiveValidAppConfig, ServerUpdate, ValidAppConfig},
|
|
||||||
ServerActor,
|
|
||||||
},
|
|
||||||
webhook::{
|
|
||||||
messages::ShutdownWebhook,
|
|
||||||
router::{AddWebhookRecipient, WebhookRouterActor},
|
|
||||||
WebhookActor,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Handler<ReceiveValidAppConfig> for ServerActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: ReceiveValidAppConfig, ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
let ValidAppConfig {
|
|
||||||
app_config,
|
|
||||||
socket_address,
|
|
||||||
storage: server_storage,
|
|
||||||
} = msg.peel();
|
|
||||||
// shutdown any existing webhook actor
|
|
||||||
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
|
|
||||||
webhook_actor_addr.do_send(ShutdownWebhook);
|
|
||||||
}
|
|
||||||
self.generation.inc();
|
|
||||||
// Webhook Server
|
|
||||||
info!("Starting Webhook Server...");
|
|
||||||
let webhook_router = WebhookRouterActor::default().start();
|
|
||||||
let listen_url = app_config.listen().url();
|
|
||||||
let notify_user_recipient = self.alerts.clone().recipient();
|
|
||||||
let server_addr = Some(ctx.address());
|
|
||||||
// Forge Actors
|
|
||||||
for (forge_alias, forge_config) in app_config.forges() {
|
|
||||||
let repo_actors = self
|
|
||||||
.create_forge_repos(
|
|
||||||
forge_config,
|
|
||||||
forge_alias.clone(),
|
|
||||||
&server_storage,
|
|
||||||
listen_url,
|
|
||||||
¬ify_user_recipient,
|
|
||||||
server_addr.clone(),
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.map(start_repo_actor)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
repo_actors
|
|
||||||
.iter()
|
|
||||||
.map(|(repo_alias, addr)| {
|
|
||||||
AddWebhookRecipient::new(
|
|
||||||
forge_alias.clone(),
|
|
||||||
repo_alias.clone(),
|
|
||||||
addr.clone().recipient(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.for_each(|msg| webhook_router.do_send(msg));
|
|
||||||
for (repo_alias, addr) in repo_actors {
|
|
||||||
self.repo_actors
|
|
||||||
.insert((forge_alias.clone(), repo_alias), addr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let webhook_actor_addr =
|
|
||||||
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
|
||||||
self.webhook_actor_addr.replace(webhook_actor_addr);
|
|
||||||
let shout = app_config.shout().clone();
|
|
||||||
self.app_config.replace(app_config.clone());
|
|
||||||
self.do_send(
|
|
||||||
ServerUpdate::AppConfigLoaded {
|
|
||||||
app_config: ValidAppConfig {
|
|
||||||
app_config,
|
|
||||||
socket_address,
|
|
||||||
storage: server_storage,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ctx,
|
|
||||||
);
|
|
||||||
self.alerts.do_send(UpdateShout::new(shout));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_repo_actor(actor: (ForgeAlias, RepoAlias, RepoActor)) -> (RepoAlias, Addr<RepoActor>) {
|
|
||||||
let (forge_name, repo_alias, actor) = actor;
|
|
||||||
let span = tracing::info_span!("start_repo_actor", forge = %forge_name, repo = %repo_alias);
|
|
||||||
let _guard = span.enter();
|
|
||||||
let addr = actor.start();
|
|
||||||
addr.do_send(CloneRepo);
|
|
||||||
tracing::info!("Started");
|
|
||||||
(repo_alias, addr)
|
|
||||||
}
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
//
|
||||||
|
use actix::prelude::*;
|
||||||
|
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
server::actor::{
|
||||||
|
messages::{ReceiveValidServerConfig, ValidServerConfig},
|
||||||
|
ServerActor,
|
||||||
|
},
|
||||||
|
webhook::{
|
||||||
|
messages::ShutdownWebhook,
|
||||||
|
router::{AddWebhookRecipient, WebhookRouter},
|
||||||
|
WebhookActor,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Handler<ReceiveValidServerConfig> for ServerActor {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: ReceiveValidServerConfig, ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
let ValidServerConfig {
|
||||||
|
server_config,
|
||||||
|
socket_address,
|
||||||
|
server_storage,
|
||||||
|
} = msg.unwrap();
|
||||||
|
// shutdown any existing webhook actor
|
||||||
|
if let Some(webhook_actor_addr) = self.webhook_actor_addr.take() {
|
||||||
|
webhook_actor_addr.do_send(ShutdownWebhook);
|
||||||
|
}
|
||||||
|
self.generation.inc();
|
||||||
|
// Webhook Server
|
||||||
|
info!("Starting Webhook Server...");
|
||||||
|
let webhook_router = WebhookRouter::default().start();
|
||||||
|
let inbound_webhook = server_config.inbound_webhook();
|
||||||
|
// Forge Actors
|
||||||
|
for (forge_alias, forge_config) in server_config.forges() {
|
||||||
|
let repo_actors = self
|
||||||
|
.create_forge_repos(
|
||||||
|
forge_config,
|
||||||
|
forge_alias.clone(),
|
||||||
|
&server_storage,
|
||||||
|
inbound_webhook,
|
||||||
|
ctx.address().recipient(),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| self.start_actor(a))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
repo_actors
|
||||||
|
.iter()
|
||||||
|
.map(|(repo_alias, addr)| {
|
||||||
|
AddWebhookRecipient::new(
|
||||||
|
forge_alias.clone(),
|
||||||
|
repo_alias.clone(),
|
||||||
|
addr.clone().recipient(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.for_each(|msg| webhook_router.do_send(msg));
|
||||||
|
repo_actors.into_iter().for_each(|(repo_alias, addr)| {
|
||||||
|
self.repo_actors
|
||||||
|
.insert((forge_alias.clone(), repo_alias), addr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let webhook_actor_addr =
|
||||||
|
WebhookActor::new(socket_address, webhook_router.recipient()).start();
|
||||||
|
self.webhook_actor_addr.replace(webhook_actor_addr);
|
||||||
|
self.server_config.replace(server_config);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +0,0 @@
|
||||||
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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//
|
|
||||||
use actix::Handler;
|
|
||||||
|
|
||||||
use crate::server::{actor::messages::ShutdownTrigger, ServerActor};
|
|
||||||
|
|
||||||
impl Handler<ShutdownTrigger> for ServerActor {
|
|
||||||
type Result = ();
|
|
||||||
|
|
||||||
fn handle(&mut self, msg: ShutdownTrigger, _ctx: &mut Self::Context) -> Self::Result {
|
|
||||||
self.shutdown_trigger.replace(msg.peel());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
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,113 +1,27 @@
|
||||||
//
|
//-
|
||||||
use actix::{Message, Recipient};
|
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{self, forge::commit::Status, graph::Log, Commit},
|
|
||||||
message,
|
message,
|
||||||
server::{AppConfig, Storage},
|
server::{ServerConfig, ServerStorage},
|
||||||
webhook::{push::Branch, Push},
|
|
||||||
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
// receive server config
|
// receive server config
|
||||||
message!(
|
message!(ReceiveServerConfig: ServerConfig: "Notification of newly loaded server configuration.
|
||||||
ReceiveAppConfig,
|
|
||||||
AppConfig,
|
|
||||||
"Notification of newly loaded server configuration.
|
|
||||||
|
|
||||||
This message will prompt the `git-next` server to stop and restart all repo-actors.
|
This message will prompt the `git-next` server to stop and restart all repo-actors.
|
||||||
|
|
||||||
Contains the new server configuration."
|
Contains the new server configuration.");
|
||||||
);
|
|
||||||
|
|
||||||
// receive valid server config
|
// receive valid server config
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Constructor)]
|
#[derive(Clone, Debug, PartialEq, Eq, Constructor)]
|
||||||
pub struct ValidAppConfig {
|
pub struct ValidServerConfig {
|
||||||
pub app_config: AppConfig,
|
pub server_config: ServerConfig,
|
||||||
pub socket_address: SocketAddr,
|
pub socket_address: SocketAddr,
|
||||||
pub storage: Storage,
|
pub server_storage: ServerStorage,
|
||||||
}
|
}
|
||||||
message!(
|
message!(ReceiveValidServerConfig: ValidServerConfig: "Notification of validated server configuration.");
|
||||||
ReceiveValidAppConfig,
|
|
||||||
ValidAppConfig,
|
|
||||||
"Notification of validated server configuration."
|
|
||||||
);
|
|
||||||
|
|
||||||
message!(Shutdown, "Notification to shutdown the server actor");
|
message!(Shutdown: "Notification to shutdown the server actor");
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Message)]
|
|
||||||
#[rtype(result = "()")]
|
|
||||||
pub enum ServerUpdate {
|
|
||||||
/// List of all configured forges and aliases
|
|
||||||
AppConfigLoaded { app_config: ValidAppConfig },
|
|
||||||
|
|
||||||
RepoUpdate {
|
|
||||||
forge_alias: ForgeAlias,
|
|
||||||
repo_alias: RepoAlias,
|
|
||||||
repo_update: RepoUpdate,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum RepoUpdate {
|
|
||||||
Branches {
|
|
||||||
branches: RepoBranches,
|
|
||||||
},
|
|
||||||
Log {
|
|
||||||
log: Log,
|
|
||||||
},
|
|
||||||
ValidateRepo,
|
|
||||||
Okay {
|
|
||||||
main: Commit,
|
|
||||||
next: Commit,
|
|
||||||
dev: Commit,
|
|
||||||
},
|
|
||||||
Alert {
|
|
||||||
alert: String,
|
|
||||||
},
|
|
||||||
CheckingCI,
|
|
||||||
AdvancingNext {
|
|
||||||
commit: git::Commit,
|
|
||||||
force: git::push::Force,
|
|
||||||
},
|
|
||||||
AdvancingMain {
|
|
||||||
commit: git::Commit,
|
|
||||||
},
|
|
||||||
Opening,
|
|
||||||
LoadingConfigFromRepo,
|
|
||||||
ReceiveCIStatus {
|
|
||||||
status: Status,
|
|
||||||
},
|
|
||||||
ReceiveRepoConfig {
|
|
||||||
repo_config: RepoConfig,
|
|
||||||
},
|
|
||||||
RegisteringWebhook,
|
|
||||||
UnregisteringWebhook,
|
|
||||||
WebhookReceived {
|
|
||||||
branch: Branch,
|
|
||||||
push: Push,
|
|
||||||
},
|
|
||||||
RegisteredWebhook,
|
|
||||||
Opened,
|
|
||||||
NextUpdated,
|
|
||||||
MainUpdated,
|
|
||||||
}
|
|
||||||
|
|
||||||
message!(
|
|
||||||
SubscribeToUpdates,
|
|
||||||
Recipient<ServerUpdate>,
|
|
||||||
"Subscribe to receive updates from the server"
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Sends a channel to be used to shutdown the server
|
|
||||||
#[derive(Message, Constructor)]
|
|
||||||
#[rtype(result = "()")]
|
|
||||||
pub struct ShutdownTrigger(std::sync::mpsc::Sender<String>);
|
|
||||||
impl ShutdownTrigger {
|
|
||||||
pub fn peel(self) -> std::sync::mpsc::Sender<String> {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use messages::{ReceiveAppConfig, ServerUpdate, Shutdown};
|
use messages::ReceiveServerConfig;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -10,13 +10,17 @@ mod handlers;
|
||||||
pub mod messages;
|
pub mod messages;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
alerts::messages::NotifyUser, alerts::AlertsActor, forge::Forge, repo::RepoActor,
|
forge::Forge,
|
||||||
|
repo::{
|
||||||
|
messages::{CloneRepo, NotifyUser},
|
||||||
|
RepoActor,
|
||||||
|
},
|
||||||
webhook::WebhookActor,
|
webhook::WebhookActor,
|
||||||
};
|
};
|
||||||
|
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
||||||
server::{self, AppConfig, ListenUrl, Storage},
|
server::{self, InboundWebhook, ServerConfig, ServerStorage},
|
||||||
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -44,23 +48,18 @@ pub enum Error {
|
||||||
}
|
}
|
||||||
type Result<T> = core::result::Result<T, Error>;
|
type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
#[derive(derive_with::With)]
|
#[derive(derive_with::With)]
|
||||||
#[with(message_log)]
|
#[with(message_log)]
|
||||||
pub struct ServerActor {
|
pub struct ServerActor {
|
||||||
app_config: Option<AppConfig>,
|
server_config: Option<ServerConfig>,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
webhook_actor_addr: Option<Addr<WebhookActor>>,
|
webhook_actor_addr: Option<Addr<WebhookActor>>,
|
||||||
fs: FileSystem,
|
fs: FileSystem,
|
||||||
net: Network,
|
net: Network,
|
||||||
alerts: Addr<AlertsActor>,
|
|
||||||
repository_factory: Box<dyn RepositoryFactory>,
|
repository_factory: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
repo_actors: BTreeMap<(ForgeAlias, RepoAlias), Addr<RepoActor>>,
|
||||||
|
|
||||||
shutdown_trigger: Option<std::sync::mpsc::Sender<String>>,
|
|
||||||
subscribers: Vec<Recipient<ServerUpdate>>,
|
|
||||||
|
|
||||||
// testing
|
// testing
|
||||||
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
message_log: Option<Arc<RwLock<Vec<String>>>>,
|
||||||
}
|
}
|
||||||
|
@ -72,21 +71,17 @@ impl ServerActor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
fs: FileSystem,
|
fs: FileSystem,
|
||||||
net: Network,
|
net: Network,
|
||||||
alerts: Addr<AlertsActor>,
|
|
||||||
repo: Box<dyn RepositoryFactory>,
|
repo: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let generation = Generation::default();
|
let generation = Generation::default();
|
||||||
Self {
|
Self {
|
||||||
app_config: None,
|
server_config: None,
|
||||||
generation,
|
generation,
|
||||||
webhook_actor_addr: None,
|
webhook_actor_addr: None,
|
||||||
fs,
|
fs,
|
||||||
net,
|
net,
|
||||||
alerts,
|
|
||||||
repository_factory: repo,
|
repository_factory: repo,
|
||||||
shutdown_trigger: None,
|
|
||||||
subscribers: Vec::default(),
|
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
repo_actors: BTreeMap::new(),
|
repo_actors: BTreeMap::new(),
|
||||||
message_log: None,
|
message_log: None,
|
||||||
|
@ -94,10 +89,10 @@ impl ServerActor {
|
||||||
}
|
}
|
||||||
fn create_forge_data_directories(
|
fn create_forge_data_directories(
|
||||||
&self,
|
&self,
|
||||||
app_config: &AppConfig,
|
server_config: &ServerConfig,
|
||||||
server_dir: &std::path::Path,
|
server_dir: &std::path::Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
for (forge_name, _forge_config) in app_config.forges() {
|
for (forge_name, _forge_config) in server_config.forges() {
|
||||||
let forge_dir: PathBuf = (&forge_name).into();
|
let forge_dir: PathBuf = (&forge_name).into();
|
||||||
let path = server_dir.join(&forge_dir);
|
let path = server_dir.join(&forge_dir);
|
||||||
if self.fs.path_exists(&path)? {
|
if self.fs.path_exists(&path)? {
|
||||||
|
@ -117,10 +112,9 @@ impl ServerActor {
|
||||||
&self,
|
&self,
|
||||||
forge_config: &ForgeConfig,
|
forge_config: &ForgeConfig,
|
||||||
forge_name: ForgeAlias,
|
forge_name: ForgeAlias,
|
||||||
server_storage: &Storage,
|
server_storage: &ServerStorage,
|
||||||
listen_url: &ListenUrl,
|
webhook: &InboundWebhook,
|
||||||
notify_user_recipient: &Recipient<NotifyUser>,
|
notify_user_recipient: Recipient<NotifyUser>,
|
||||||
server_addr: Option<Addr<Self>>,
|
|
||||||
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> {
|
||||||
let span =
|
let span =
|
||||||
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config);
|
||||||
|
@ -128,13 +122,7 @@ impl ServerActor {
|
||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
tracing::info!("Creating Forge");
|
tracing::info!("Creating Forge");
|
||||||
let mut repos = vec![];
|
let mut repos = vec![];
|
||||||
let creator = self.create_actor(
|
let creator = self.create_actor(forge_name, forge_config.clone(), server_storage, webhook);
|
||||||
forge_name,
|
|
||||||
forge_config.clone(),
|
|
||||||
server_storage,
|
|
||||||
listen_url,
|
|
||||||
server_addr,
|
|
||||||
);
|
|
||||||
for (repo_alias, server_repo_config) in forge_config.repos() {
|
for (repo_alias, server_repo_config) in forge_config.repos() {
|
||||||
let forge_repo = creator((
|
let forge_repo = creator((
|
||||||
repo_alias,
|
repo_alias,
|
||||||
|
@ -154,18 +142,18 @@ impl ServerActor {
|
||||||
&self,
|
&self,
|
||||||
forge_name: ForgeAlias,
|
forge_name: ForgeAlias,
|
||||||
forge_config: ForgeConfig,
|
forge_config: ForgeConfig,
|
||||||
server_storage: &Storage,
|
server_storage: &ServerStorage,
|
||||||
listen_url: &ListenUrl,
|
webhook: &InboundWebhook,
|
||||||
server_addr: Option<Addr<Self>>,
|
|
||||||
) -> impl Fn(
|
) -> impl Fn(
|
||||||
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
(RepoAlias, &ServerRepoConfig, Recipient<NotifyUser>),
|
||||||
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
) -> (ForgeAlias, RepoAlias, RepoActor) {
|
||||||
let server_storage = server_storage.clone();
|
let server_storage = server_storage.clone();
|
||||||
let listen_url = listen_url.clone();
|
let webhook = webhook.clone();
|
||||||
let net = self.net.clone();
|
let net = self.net.clone();
|
||||||
let repository_factory = self.repository_factory.duplicate();
|
let repository_factory = self.repository_factory.duplicate();
|
||||||
let generation = self.generation;
|
let generation = self.generation;
|
||||||
let sleep_duration = self.sleep_duration;
|
let sleep_duration = self.sleep_duration;
|
||||||
|
// let notify_user_recipient = server_addr.recipient();
|
||||||
move |(repo_alias, server_repo_config, notify_user_recipient)| {
|
move |(repo_alias, server_repo_config, notify_user_recipient)| {
|
||||||
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
|
let span = tracing::info_span!("create_actor", alias = %repo_alias, config = %server_repo_config);
|
||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
|
@ -197,20 +185,32 @@ impl ServerActor {
|
||||||
let actor = RepoActor::new(
|
let actor = RepoActor::new(
|
||||||
repo_details,
|
repo_details,
|
||||||
forge,
|
forge,
|
||||||
listen_url.clone(),
|
webhook.clone(),
|
||||||
generation,
|
generation,
|
||||||
net.clone(),
|
net.clone(),
|
||||||
repository_factory.duplicate(),
|
repository_factory.duplicate(),
|
||||||
sleep_duration,
|
sleep_duration,
|
||||||
Some(notify_user_recipient),
|
Some(notify_user_recipient),
|
||||||
server_addr.clone(),
|
|
||||||
);
|
);
|
||||||
(forge_name.clone(), repo_alias, actor)
|
(forge_name.clone(), repo_alias, actor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn server_storage(&self, app_config: &ReceiveAppConfig) -> Option<Storage> {
|
fn start_actor(
|
||||||
let server_storage = app_config.storage().clone();
|
&self,
|
||||||
|
actor: (ForgeAlias, RepoAlias, RepoActor),
|
||||||
|
) -> (RepoAlias, Addr<RepoActor>) {
|
||||||
|
let (forge_name, repo_alias, actor) = actor;
|
||||||
|
let span = tracing::info_span!("start_actor", forge = %forge_name, repo = %repo_alias);
|
||||||
|
let _guard = span.enter();
|
||||||
|
let addr = actor.start();
|
||||||
|
addr.do_send(CloneRepo);
|
||||||
|
tracing::info!("Started");
|
||||||
|
(repo_alias, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_storage(&self, server_config: &ReceiveServerConfig) -> Option<ServerStorage> {
|
||||||
|
let server_storage = server_config.storage().clone();
|
||||||
let dir = server_storage.path();
|
let dir = server_storage.path();
|
||||||
if !dir.exists() {
|
if !dir.exists() {
|
||||||
if let Err(err) = self.fs.dir_create(dir) {
|
if let Err(err) = self.fs.dir_create(dir) {
|
||||||
|
@ -222,39 +222,26 @@ impl ServerActor {
|
||||||
error!(?dir, "Failed to confirm server storage");
|
error!(?dir, "Failed to confirm server storage");
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if let Err(err) = self.create_forge_data_directories(app_config, &canon) {
|
if let Err(err) = self.create_forge_data_directories(server_config, &canon) {
|
||||||
error!(?err, "Failure creating forge storage");
|
error!(?err, "Failure creating forge storage");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some(server_storage)
|
Some(server_storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to gracefully shutdown the server before stopping the system.
|
fn do_send<M>(&mut self, msg: M, _ctx: &mut <Self as actix::Actor>::Context)
|
||||||
fn abort(&mut self, ctx: &<Self as actix::Actor>::Context, message: impl Into<String>) {
|
|
||||||
self.do_send(crate::server::actor::messages::Shutdown, ctx);
|
|
||||||
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)
|
|
||||||
where
|
where
|
||||||
M: actix::Message + Send + 'static + std::fmt::Debug,
|
M: actix::Message + Send + 'static + std::fmt::Debug,
|
||||||
Self: actix::Handler<M>,
|
Self: actix::Handler<M>,
|
||||||
<M as actix::Message>::Result: Send,
|
<M as actix::Message>::Result: Send,
|
||||||
{
|
{
|
||||||
if let Some(message_log) = &self.message_log {
|
if let Some(message_log) = &self.message_log {
|
||||||
let log_message = format!("send: {msg:?}");
|
let log_message = format!("send: {:?}", msg);
|
||||||
if let Ok(mut log) = message_log.write() {
|
if let Ok(mut log) = message_log.write() {
|
||||||
log.push(log_message);
|
log.push(log_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if cfg!(not(test)) {
|
#[cfg(not(test))]
|
||||||
ctx.address().do_send(msg);
|
_ctx.address().do_send(msg);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,3 @@
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use actix::prelude::*;
|
|
||||||
|
|
||||||
use crate::alerts::{AlertsActor, History};
|
|
||||||
|
|
||||||
//
|
|
||||||
pub fn a_filesystem() -> kxio::fs::FileSystem {
|
pub fn a_filesystem() -> kxio::fs::FileSystem {
|
||||||
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
|
kxio::fs::temp().unwrap_or_else(|e| panic!("{}", e))
|
||||||
}
|
}
|
||||||
|
@ -12,7 +5,3 @@ pub fn a_filesystem() -> kxio::fs::FileSystem {
|
||||||
pub fn a_network() -> kxio::network::MockNetwork {
|
pub fn a_network() -> kxio::network::MockNetwork {
|
||||||
kxio::network::MockNetwork::new()
|
kxio::network::MockNetwork::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn an_alerts_actor(net: kxio::network::Network) -> Addr<AlertsActor> {
|
|
||||||
AlertsActor::new(None, History::new(Duration::from_millis(1)), net).start()
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
mod receive_app_config;
|
mod receive_server_config;
|
||||||
|
|
||||||
mod given;
|
mod given;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
//
|
//
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
|
|
||||||
use crate::server::actor::{tests::given, ReceiveAppConfig, ServerActor};
|
use crate::server::actor::{tests::given, ReceiveServerConfig, ServerActor};
|
||||||
use git_next_core::{
|
use git_next_core::{
|
||||||
git,
|
git,
|
||||||
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
|
server::{Http, InboundWebhook, Notification, ServerConfig, ServerStorage},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -18,20 +18,17 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||||
// parameters
|
// parameters
|
||||||
let fs = given::a_filesystem();
|
let fs = given::a_filesystem();
|
||||||
let net = given::a_network();
|
let net = given::a_network();
|
||||||
let alerts = given::an_alerts_actor(net.clone().into());
|
|
||||||
let repo = git::repository::factory::mock();
|
let repo = git::repository::factory::mock();
|
||||||
let duration = std::time::Duration::from_millis(1);
|
let duration = std::time::Duration::from_millis(1);
|
||||||
|
|
||||||
// sut
|
// sut
|
||||||
let server = ServerActor::new(fs.clone(), net.into(), alerts, repo, duration);
|
let server = ServerActor::new(fs.clone(), net.into(), repo, duration);
|
||||||
|
|
||||||
// collaborators
|
// collaborators
|
||||||
let listen = Listen::new(
|
let http = Http::new("0.0.0.0".to_string(), 80);
|
||||||
Http::new("0.0.0.0".to_string(), 80),
|
let webhook = InboundWebhook::new("http://localhost/".to_string()); // With trailing slash
|
||||||
ListenUrl::new("http://localhost/".to_string()), // with trailing slash
|
let notifications = Notification::none();
|
||||||
);
|
let server_storage = ServerStorage::new((fs.base()).to_path_buf());
|
||||||
let shout = Shout::default();
|
|
||||||
let server_storage = Storage::new((fs.base()).to_path_buf());
|
|
||||||
let repos = BTreeMap::default();
|
let repos = BTreeMap::default();
|
||||||
|
|
||||||
// debugging
|
// debugging
|
||||||
|
@ -39,12 +36,15 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||||
let server = server.with_message_log(Some(message_log.clone()));
|
let server = server.with_message_log(Some(message_log.clone()));
|
||||||
|
|
||||||
//when
|
//when
|
||||||
server.start().do_send(ReceiveAppConfig::new(AppConfig::new(
|
server
|
||||||
listen,
|
.start()
|
||||||
shout,
|
.do_send(ReceiveServerConfig::new(ServerConfig::new(
|
||||||
server_storage,
|
http,
|
||||||
repos,
|
webhook,
|
||||||
)));
|
notifications,
|
||||||
|
server_storage,
|
||||||
|
repos,
|
||||||
|
)));
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
|
actix_rt::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||||
|
|
||||||
//then
|
//then
|
||||||
|
@ -52,5 +52,5 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() {
|
||||||
tracing::debug!(?message_log, "");
|
tracing::debug!(?message_log, "");
|
||||||
assert!(message_log.read().iter().any(|log| !log
|
assert!(message_log.read().iter().any(|log| !log
|
||||||
.iter()
|
.iter()
|
||||||
.any(|line| line == "send: ReceiveValidServerConfig")));
|
.any(|line| line != "send: ReceiveValidServerConfig")));
|
||||||
}
|
}
|
|
@ -1,190 +1,87 @@
|
||||||
//
|
//
|
||||||
pub mod actor;
|
mod actor;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use actix::prelude::*;
|
use actix::prelude::*;
|
||||||
use actix_rt::signal;
|
|
||||||
use actor::messages::ShutdownTrigger;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
alerts::{AlertsActor, History},
|
|
||||||
file_watcher::{watch_file, FileUpdated},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
pub use actor::ServerActor;
|
|
||||||
|
|
||||||
|
use crate::file_watcher::{watch_file, FileUpdated};
|
||||||
|
use actor::ServerActor;
|
||||||
use git_next_core::git::RepositoryFactory;
|
use git_next_core::git::RepositoryFactory;
|
||||||
|
|
||||||
use color_eyre::{eyre::Context, Result};
|
|
||||||
use kxio::{fs::FileSystem, network::Network};
|
use kxio::{fs::FileSystem, network::Network};
|
||||||
use tracing::info;
|
use tracing::{info, level_filters::LevelFilter};
|
||||||
|
|
||||||
use std::{
|
use std::path::PathBuf;
|
||||||
path::PathBuf,
|
|
||||||
sync::{atomic::Ordering, mpsc::channel, Arc, RwLock},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
const A_DAY: Duration = Duration::from_secs(24 * 60 * 60);
|
pub fn init(fs: FileSystem) {
|
||||||
|
|
||||||
pub fn init(fs: &FileSystem) -> Result<()> {
|
|
||||||
let file_name = "git-next-server.toml";
|
let file_name = "git-next-server.toml";
|
||||||
let pathbuf = PathBuf::from(file_name);
|
let pathbuf = PathBuf::from(file_name);
|
||||||
if fs
|
let Ok(exists) = fs.path_exists(&pathbuf) else {
|
||||||
.path_exists(&pathbuf)
|
eprintln!("Could not check if file exist: {}", file_name);
|
||||||
.with_context(|| format!("Checking for existing file: {pathbuf:?}"))?
|
return;
|
||||||
{
|
};
|
||||||
eprintln!("The configuration file already exists at {pathbuf:?} - not overwritting it.",);
|
if exists {
|
||||||
|
eprintln!(
|
||||||
|
"The configuration file already exists at {} - not overwritting it.",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
fs.file_write(&pathbuf, include_str!("server-default.toml"))
|
match fs.file_write(&pathbuf, include_str!("server-default.toml")) {
|
||||||
.with_context(|| format!("Writing file: {pathbuf:?}"))?;
|
Ok(_) => println!("Created a default configuration file at {}", file_name),
|
||||||
println!("Created a default configuration file at {pathbuf:?}",);
|
Err(e) => {
|
||||||
|
eprintln!("Failed to write to the configuration file: {}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
pub fn start(
|
pub fn start(
|
||||||
ui: bool,
|
|
||||||
fs: FileSystem,
|
fs: FileSystem,
|
||||||
net: Network,
|
net: Network,
|
||||||
repo: Box<dyn RepositoryFactory>,
|
repo: Box<dyn RepositoryFactory>,
|
||||||
sleep_duration: std::time::Duration,
|
sleep_duration: std::time::Duration,
|
||||||
) -> Result<()> {
|
) {
|
||||||
if ui {
|
init_logging();
|
||||||
#[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 {
|
let execution = async move {
|
||||||
info!("Starting Alert Dispatcher...");
|
|
||||||
let alerts_addr = AlertsActor::new(None, History::new(A_DAY), net.clone()).start();
|
|
||||||
|
|
||||||
info!("Starting Server...");
|
info!("Starting Server...");
|
||||||
let server =
|
let server = ServerActor::new(fs.clone(), net.clone(), repo, sleep_duration).start();
|
||||||
ServerActor::new(fs.clone(), net.clone(), alerts_addr, repo, sleep_duration).start();
|
server.do_send(FileUpdated);
|
||||||
|
|
||||||
info!("Starting File Watcher...");
|
info!("Starting File Watcher...");
|
||||||
let watch_file = watch_file("git-next-server.toml".into(), server.clone().recipient());
|
watch_file("git-next-server.toml".into(), server.clone().recipient()).await;
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let (tx_shutdown, rx_shutdown) = channel::<String>();
|
info!("Server running - Press Ctrl-C to stop...");
|
||||||
if ui {
|
let _ = actix_rt::signal::ctrl_c().await;
|
||||||
#[cfg(feature = "tui")]
|
info!("Ctrl-C received, shutting down...");
|
||||||
{
|
|
||||||
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);
|
server.do_send(crate::server::actor::messages::Shutdown);
|
||||||
actix_rt::time::sleep(std::time::Duration::from_millis(10)).await;
|
actix_rt::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||||
System::current().stop();
|
System::current().stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
let system = System::new();
|
let system = System::new();
|
||||||
Arbiter::current().spawn(execution);
|
Arbiter::current().spawn(execution);
|
||||||
system.run()?;
|
if let Err(err) = system.run() {
|
||||||
|
tracing::error!(?err, "")
|
||||||
// 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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_logging() {
|
pub fn init_logging() {
|
||||||
use tracing::Level;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::FmtSubscriber;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
let subscriber = FmtSubscriber::builder()
|
let subscriber = tracing_subscriber::fmt::layer()
|
||||||
.with_target(false)
|
.with_target(false)
|
||||||
.with_file(true)
|
.with_file(true)
|
||||||
.with_line_number(true)
|
.with_line_number(true)
|
||||||
.with_max_level(Level::INFO)
|
.with_filter(
|
||||||
.finish();
|
EnvFilter::builder()
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
#[allow(clippy::expect_used)]
|
.from_env_lossy(),
|
||||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
);
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(console_subscriber::ConsoleLayer::builder().spawn())
|
||||||
|
.with(subscriber)
|
||||||
|
.init();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,25 @@
|
||||||
[listen]
|
[http]
|
||||||
# The address and port to listen to for incoming webhooks from forges.
|
addr = "0.0.0.0"
|
||||||
http = { addr = "0.0.0.0", port = 8080 }
|
port = 8080
|
||||||
|
|
||||||
# The URL where forge should send updates to.
|
[webhook]
|
||||||
# This should be route to 'http.addr:http.port' above (e.g. using a reverse proxy)
|
|
||||||
url = "https://localhost:8080" # don't include any query path or a trailing slash
|
url = "https://localhost:8080" # don't include any query path or a trailing slash
|
||||||
|
|
||||||
[shout] # where updates from git-next should be sent to alert the user
|
[storage]
|
||||||
# webhook = { url = "https//localhost:9090", secret = "secret-password" }
|
|
||||||
# desktop = true # enable desktop notifications
|
|
||||||
|
|
||||||
# [shout.email]
|
|
||||||
# from = "git-next@example.com"
|
|
||||||
# to = "developer@example.com"
|
|
||||||
#
|
|
||||||
# [shout.email.smtp]
|
|
||||||
# hostname = "smtp.example.com"
|
|
||||||
# username = "git-next@example.com"
|
|
||||||
# password = "MySecretEmailPassword42"
|
|
||||||
|
|
||||||
[storage] # where local copies of repositories will be cloned (bare) into
|
|
||||||
path = "./data"
|
path = "./data"
|
||||||
|
|
||||||
[forge] # the forges to connect to
|
[notifications]
|
||||||
|
type = "WebHook"
|
||||||
|
webhook = { url = "https://localhost:9090" }
|
||||||
|
|
||||||
# [forge.default]
|
[forge]
|
||||||
# forge_type = "ForgeJo"
|
|
||||||
# hostname = "git.example.net"
|
[forge.default]
|
||||||
# user = "bob" # the user to perform actions as
|
forge_type = "ForgeJo"
|
||||||
# token = "API-Token"
|
hostname = "git.example.net"
|
||||||
#
|
user = "git-next" # the user to perform actions as
|
||||||
# [forge.default.repos] # the repos at the forge to manage
|
token = "API-Token"
|
||||||
# hello = { repo = "bob/hello", branch = "main", gitdir = "/opt/git/projects/bob/hello.git" }
|
|
||||||
# world = { repo = "bob/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" }
|
[forge.default.repos]
|
||||||
|
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://git.example.net/user/hello on the branch 'main'
|
||||||
|
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
||||||
|
|
|
@ -6,7 +6,7 @@ use git_next_core::{
|
||||||
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
||||||
StoragePathType, User,
|
StoragePathType, User,
|
||||||
};
|
};
|
||||||
use secrecy::SecretString;
|
use secrecy::Secret;
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ fn gitdir_should_display_as_pathbuf() {
|
||||||
//given
|
//given
|
||||||
let gitdir = GitDir::new("foo/dir".into(), StoragePathType::External);
|
let gitdir = GitDir::new("foo/dir".into(), StoragePathType::External);
|
||||||
//when
|
//when
|
||||||
let result = format!("{gitdir}");
|
let result = format!("{}", gitdir);
|
||||||
//then
|
//then
|
||||||
assert_eq!(result, "foo/dir");
|
assert_eq!(result, "foo/dir");
|
||||||
}
|
}
|
||||||
|
@ -59,13 +59,10 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
|
||||||
repo_details.forge = repo_details
|
repo_details.forge = repo_details
|
||||||
.forge
|
.forge
|
||||||
.with_user(User::new("git".to_string()))
|
.with_user(User::new("git".to_string()))
|
||||||
.with_token(ApiToken::new(SecretString::from(String::new())))
|
.with_token(ApiToken::new(Secret::new("".to_string())))
|
||||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||||
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
|
repo_details.repo_path = RepoPath::new("kemitix/git-next".to_string());
|
||||||
let Ok(open_repository) = git::repository::factory::real().open(&repo_details) else {
|
let open_repository = git::repository::factory::real().open(&repo_details)?;
|
||||||
// .git directory may not be present on dev environment
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
|
Some(found_git_remote) = open_repository.find_default_remote(Direction::Push),
|
||||||
"Default Push Remote not found"
|
"Default Push Remote not found"
|
||||||
|
@ -95,13 +92,13 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
||||||
repo_details.forge = repo_details
|
repo_details.forge = repo_details
|
||||||
.forge
|
.forge
|
||||||
.with_user(User::new("git".to_string()))
|
.with_user(User::new("git".to_string()))
|
||||||
.with_token(ApiToken::new(SecretString::from(String::new())))
|
.with_token(ApiToken::new(Secret::new("".to_string())))
|
||||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||||
tracing::debug!("opening...");
|
tracing::debug!("opening...");
|
||||||
let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
|
let_assert!(
|
||||||
// .git directory may not be present on dev environment
|
Ok(repository) = git::repository::factory::real().open(&repo_details),
|
||||||
return Ok(());
|
"open repository"
|
||||||
};
|
);
|
||||||
tracing::debug!("open okay");
|
tracing::debug!("open okay");
|
||||||
tracing::info!(?repository, "FOO");
|
tracing::info!(?repository, "FOO");
|
||||||
tracing::info!(?repo_details, "BAR");
|
tracing::info!(?repo_details, "BAR");
|
||||||
|
@ -111,13 +108,11 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() {
|
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
|
||||||
let_assert!(
|
let_assert!(
|
||||||
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
|
Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validation::remotes::Error::Io)
|
||||||
);
|
);
|
||||||
eprintln!("cli_crate_dir: {cli_crate_dir:?}");
|
|
||||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||||
eprintln!("root: {root:?}");
|
|
||||||
let mut repo_details = git::repo_details(
|
let mut repo_details = git::repo_details(
|
||||||
1,
|
1,
|
||||||
git::Generation::default(),
|
git::Generation::default(),
|
||||||
|
@ -129,17 +124,16 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() {
|
||||||
repo_details.forge = repo_details
|
repo_details.forge = repo_details
|
||||||
.forge
|
.forge
|
||||||
.with_user(User::new("git".to_string()))
|
.with_user(User::new("git".to_string()))
|
||||||
.with_token(ApiToken::new(SecretString::from(String::new())))
|
.with_token(ApiToken::new(Secret::new("".to_string())))
|
||||||
.with_hostname(Hostname::new("git.kemitix.net"));
|
.with_hostname(Hostname::new("git.kemitix.net"));
|
||||||
let Ok(repository) = git::repository::factory::real().open(&repo_details) else {
|
let repository = git::repository::factory::real().open(&repo_details)?;
|
||||||
// .git directory may not be present on dev environment
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut repo_details = repo_details.clone();
|
let mut repo_details = repo_details.clone();
|
||||||
repo_details.forge = repo_details
|
repo_details.forge = repo_details
|
||||||
.forge
|
.forge
|
||||||
.with_hostname(Hostname::new("code.kemitix.net"));
|
.with_hostname(Hostname::new("code.kemitix.net"));
|
||||||
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
|
let_assert!(Err(_) = validate_default_remotes(&*repository, &repo_details));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -8,7 +8,7 @@ mod init {
|
||||||
let file = fs.base().join(".git-next.toml");
|
let file = fs.base().join(".git-next.toml");
|
||||||
fs.file_write(&file, "contents")?;
|
fs.file_write(&file, "contents")?;
|
||||||
|
|
||||||
crate::init::run(&fs)?;
|
crate::init::run(fs.clone());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs.file_read_to_string(&file)?,
|
fs.file_read_to_string(&file)?,
|
||||||
|
@ -23,7 +23,7 @@ mod init {
|
||||||
fn should_create_default_file_if_not_exists() -> TestResult {
|
fn should_create_default_file_if_not_exists() -> TestResult {
|
||||||
let fs = kxio::fs::temp()?;
|
let fs = kxio::fs::temp()?;
|
||||||
|
|
||||||
crate::init::run(&fs)?;
|
crate::init::run(fs.clone());
|
||||||
|
|
||||||
let file = fs.base().join(".git-next.toml");
|
let file = fs.base().join(".git-next.toml");
|
||||||
|
|
||||||
|
@ -38,44 +38,3 @@ mod init {
|
||||||
Ok(())
|
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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,3 +0,0 @@
|
||||||
//
|
|
||||||
mod server_update;
|
|
||||||
mod tick;
|
|
|
@ -1,104 +0,0 @@
|
||||||
//
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
//
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
//
|
|
||||||
use git_next_core::message;
|
|
||||||
|
|
||||||
message!(Tick => std::io::Result<()>, "Update the TUI");
|
|
|
@ -1,97 +0,0 @@
|
||||||
//
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,388 +0,0 @@
|
||||||
//
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
//
|
|
||||||
mod model {
|
|
||||||
mod repo_state {
|
|
||||||
use git_next_core::{git::graph::Log, RepoBranches};
|
|
||||||
use ratatui::style::Style;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
repo::tests::given,
|
|
||||||
tui::actor::{RepoMessage, RepoState, ViewState},
|
|
||||||
};
|
|
||||||
type Alert = Option<String>;
|
|
||||||
|
|
||||||
fn identified_with_alert(alert: Alert) -> RepoState {
|
|
||||||
RepoState::Identified {
|
|
||||||
repo_alias: given::a_repo_alias(),
|
|
||||||
message: RepoMessage::builder()
|
|
||||||
.text(given::a_name())
|
|
||||||
.style(Style::default())
|
|
||||||
.build(),
|
|
||||||
alert,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configured_with_alert(alert: Alert) -> RepoState {
|
|
||||||
RepoState::Configured {
|
|
||||||
repo_alias: given::a_repo_alias(),
|
|
||||||
message: RepoMessage::builder()
|
|
||||||
.text(given::a_name())
|
|
||||||
.style(Style::default())
|
|
||||||
.build(),
|
|
||||||
alert,
|
|
||||||
branches: RepoBranches::new(String::new(), String::new(), String::new()),
|
|
||||||
log: Log::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn ready_with_alert(alert: Alert) -> RepoState {
|
|
||||||
RepoState::Ready {
|
|
||||||
repo_alias: given::a_repo_alias(),
|
|
||||||
message: RepoMessage::builder()
|
|
||||||
.text(given::a_name())
|
|
||||||
.style(Style::default())
|
|
||||||
.build(),
|
|
||||||
alert,
|
|
||||||
branches: RepoBranches::new(String::new(), String::new(), String::new()),
|
|
||||||
log: Log::default(),
|
|
||||||
view_state: ViewState::default(),
|
|
||||||
main: given::a_commit(),
|
|
||||||
next: given::a_commit(),
|
|
||||||
dev: given::a_commit(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest::rstest]
|
|
||||||
#[case(identified_with_alert(None))]
|
|
||||||
#[case(configured_with_alert(None))]
|
|
||||||
#[case(ready_with_alert(None))]
|
|
||||||
fn none_alert_remains_none(#[case] mut repo_state: RepoState) {
|
|
||||||
// given
|
|
||||||
match &repo_state {
|
|
||||||
RepoState::Identified { alert, .. }
|
|
||||||
| RepoState::Configured { alert, .. }
|
|
||||||
| RepoState::Ready { alert, .. } => {
|
|
||||||
assert!(alert.is_none(), "should be none at start");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// when
|
|
||||||
repo_state.clear_alert();
|
|
||||||
// then
|
|
||||||
match &repo_state {
|
|
||||||
RepoState::Identified { alert, .. }
|
|
||||||
| RepoState::Configured { alert, .. }
|
|
||||||
| RepoState::Ready { alert, .. } => assert!(alert.is_none(), "should remain none"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rstest::rstest]
|
|
||||||
#[case(identified_with_alert(Some(String::new())))]
|
|
||||||
#[case(configured_with_alert(Some(String::new())))]
|
|
||||||
#[case(ready_with_alert(Some(String::new())))]
|
|
||||||
fn some_alert_becomes_none(#[case] mut repo_state: RepoState) {
|
|
||||||
// given
|
|
||||||
match &repo_state {
|
|
||||||
RepoState::Identified { alert, .. }
|
|
||||||
| RepoState::Configured { alert, .. }
|
|
||||||
| RepoState::Ready { alert, .. } => {
|
|
||||||
assert!(alert.is_some(), "should be some at start");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// when
|
|
||||||
repo_state.clear_alert();
|
|
||||||
// then
|
|
||||||
match &repo_state {
|
|
||||||
RepoState::Identified { alert, .. }
|
|
||||||
| RepoState::Configured { alert, .. }
|
|
||||||
| RepoState::Ready { alert, .. } => assert!(alert.is_none(), "should become none"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
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<_>>()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
//
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
//
|
|
||||||
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<_>>()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
//
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
//
|
|
||||||
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(())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
//
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
//
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
//
|
|
||||||
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)
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
//
|
|
||||||
mod actor;
|
|
||||||
pub mod components;
|
|
||||||
pub mod logging;
|
|
||||||
|
|
||||||
pub use actor::messages::Tick;
|
|
||||||
pub use actor::Tui;
|
|
|
@ -1,4 +1,4 @@
|
||||||
//
|
//
|
||||||
use git_next_core::message;
|
use git_next_core::message;
|
||||||
|
|
||||||
message!(ShutdownWebhook, "Request to shutdown the Webhook actor");
|
message!(ShutdownWebhook: "Request to shutdown the Webhook actor");
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue