Compare commits
No commits in common. "main" and "v0.14.0" have entirely different histories.
209 changed files with 2925 additions and 2624 deletions
|
@ -1 +0,0 @@
|
|||
target/
|
|
@ -1,49 +0,0 @@
|
|||
name: Daily with Nightly
|
||||
# at 2am every day build against the latets nightly version of rust
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- "0 2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
|
||||
container:
|
||||
image:
|
||||
git.kemitix.net/kemitix/rust:v4.0.1
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Ignored Files
|
||||
run: check-for-ignored
|
||||
|
||||
- name: Check TODOs
|
||||
uses: https://git.kemitix.net/kemitix/forgejo-todo-checker@v1.3.0
|
||||
|
||||
- name: Machete
|
||||
run: cargo +nightly machete
|
||||
|
||||
- name: Format
|
||||
run: cargo +nightly fmt --all --check
|
||||
|
||||
- name: Install dbus-dev
|
||||
run: apk add dbus-dev
|
||||
|
||||
- name: Clippy
|
||||
run: cargo +nightly hack --feature-powerset clippy
|
||||
|
||||
- name: Build
|
||||
run: cargo +nightly hack --feature-powerset build
|
||||
|
||||
- name: Test
|
||||
run: cargo +nightly hack --feature-powerset test
|
||||
|
||||
# - name: Mutations
|
||||
# run: cargo +nightly mutants -vV --in-place
|
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -2,33 +2,6 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## `git-next-forge-github` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.14.0...git-next-forge-github-v0.14.1) - 2025-01-19
|
||||
|
||||
### Other
|
||||
- *(deps)* update rust crate kxio to v5
|
||||
|
||||
## `git-next-forge-forgejo` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.14.0...git-next-forge-forgejo-v0.14.1) - 2025-01-19
|
||||
|
||||
### Other
|
||||
- *(deps)* update rust crate kxio to v5
|
||||
|
||||
## `git-next-core` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.14.0...git-next-core-v0.14.1) - 2025-01-19
|
||||
|
||||
### Added
|
||||
- remove openssl transitive dependency
|
||||
|
||||
### Other
|
||||
- *(deps)* update rust crate gix to 0.69
|
||||
|
||||
## `git-next` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/v0.14.0...v0.14.1) - 2025-01-19
|
||||
|
||||
### Added
|
||||
- *(ui)* filter repos
|
||||
- remove openssl transitive dependency
|
||||
|
||||
### Other
|
||||
- *(deps)* update rust crate kxio to v5
|
||||
|
||||
## [0.14.0] - 2025-01-16
|
||||
|
||||
### Bug Fixes
|
||||
|
|
1259
Cargo.lock
generated
1259
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
85
Cargo.toml
85
Cargo.toml
|
@ -1,41 +1,39 @@
|
|||
[package]
|
||||
name = "git-next"
|
||||
version = "0.14.1"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.14.0"
|
||||
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://git.kemitix.net/kemitix/git-next"
|
||||
description = "trunk-based development manager"
|
||||
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
||||
rust-version = "1.76"
|
||||
description = "trunk-based development manager"
|
||||
documentation = "https://git.kemitix.net/kemitix/git-next/src/branch/main/README.md"
|
||||
keywords = ["git", "cli", "server", "tool"]
|
||||
categories = ["development-tools"]
|
||||
|
||||
[features]
|
||||
# default = ["forgejo", "github"]
|
||||
default = ["forgejo", "github", "tui"]
|
||||
forgejo = []
|
||||
github = []
|
||||
tui = [
|
||||
"ratatui",
|
||||
"directories",
|
||||
"lazy_static",
|
||||
"tui-scrollview",
|
||||
"regex",
|
||||
"chrono",
|
||||
]
|
||||
# [workspace.lints.clippy]
|
||||
# pedantic = { level = "warn", priority = -1 }
|
||||
# nursery = { level = "warn", priority = -1 }
|
||||
# unwrap_used = "warn"
|
||||
# expect_used = "warn"
|
||||
|
||||
[dependencies]
|
||||
|
||||
color-eyre = "0.6"
|
||||
[workspace.dependencies]
|
||||
git-next-core = { path = "crates/core", version = "0.14" }
|
||||
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.14" }
|
||||
git-next-forge-github = { path = "crates/forge-github", version = "0.14" }
|
||||
|
||||
# TUI
|
||||
ratatui = { version = "0.29", optional = true }
|
||||
directories = { version = "6.0", optional = true }
|
||||
lazy_static = { version = "1.5", optional = true }
|
||||
tui-scrollview = { version = "0.5", optional = true }
|
||||
regex = { version = "1.10", optional = true }
|
||||
chrono = { version = "0.4", optional = true }
|
||||
ratatui = "0.29"
|
||||
directories = "6.0"
|
||||
lazy_static = "1.5"
|
||||
color-eyre = "0.6"
|
||||
tui-scrollview = "0.5"
|
||||
regex = "1.10"
|
||||
chrono = "0.4"
|
||||
|
||||
# CLI parsing
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
|
@ -46,7 +44,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||
tracing-error = "0.2.0"
|
||||
|
||||
# base64 decoding
|
||||
# base64 = { version = "0.22", optional = true }
|
||||
base64 = "0.22"
|
||||
|
||||
# sha256 encoding (e.g. verify github webhooks)
|
||||
hmac = "0.12"
|
||||
|
@ -54,7 +52,7 @@ sha2 = "0.10"
|
|||
hex = "0.4"
|
||||
|
||||
# git
|
||||
gix = { version = "0.70", features = [
|
||||
gix = { version = "0.68", features = [
|
||||
"dirwalk",
|
||||
"blocking-http-transport-reqwest-rust-tls",
|
||||
] }
|
||||
|
@ -62,8 +60,9 @@ async-trait = "0.1"
|
|||
git-url-parse = "0.4"
|
||||
|
||||
# fs/network
|
||||
kxio = "5.1"
|
||||
# kxio = { path = "../kxio/" }
|
||||
kxio = "4.0"
|
||||
native-tls = { version = "0.2", features = ["vendored"] }
|
||||
|
||||
|
||||
# TOML parsing
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -85,12 +84,11 @@ standardwebhooks = "1.0"
|
|||
|
||||
# boilerplate
|
||||
bon = "3.0"
|
||||
derive_more = { version = "1.0.0", features = [
|
||||
derive_more = { version = "1.0.0-beta", features = [
|
||||
"as_ref",
|
||||
"constructor",
|
||||
"display",
|
||||
"deref",
|
||||
"deref_mut",
|
||||
"from",
|
||||
] }
|
||||
derive-with = "0.5"
|
||||
|
@ -102,34 +100,23 @@ pike = "0.1"
|
|||
take-until = "0.2"
|
||||
|
||||
# file watcher
|
||||
notify = "8.0"
|
||||
notify = "7.0"
|
||||
|
||||
# Actors
|
||||
kameo = "0.14"
|
||||
kameo = "0.13"
|
||||
tokio = { version = "1.37", features = ["full"] }
|
||||
|
||||
# email
|
||||
lettre = { version = "0.11", default-features = false, features = ["builder", "rustls-tls", "smtp-transport"] }
|
||||
lettre = "0.11"
|
||||
sendmail = "2.0"
|
||||
|
||||
# desktop notifications
|
||||
notifica = "3.0"
|
||||
|
||||
mockall = "0.13"
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
assert2 = "0.3"
|
||||
|
||||
pretty_assertions = "1.4"
|
||||
rand = "0.8"
|
||||
rstest = { version = "0.24", features = ["async-timeout"] }
|
||||
mockall = "0.13"
|
||||
test-log = "0.2"
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
rstest = { version = "0.23", features = ["async-timeout"] }
|
||||
|
|
|
@ -6,7 +6,7 @@ RUN apk add --no-cache dbus-dev=1.14.10-r4 && rm -rf /vra/cache/apk/*
|
|||
|
||||
FROM chef AS planner
|
||||
COPY Cargo.toml ./
|
||||
COPY src src
|
||||
COPY crates crates
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM chef AS builder
|
||||
|
|
662
README.md
662
README.md
|
@ -2,671 +2,11 @@
|
|||
|
||||
## Trunk-based developement manager.
|
||||
|
||||
> A source-control branching model, where developers collaborate on code in a single branch
|
||||
> called ‘trunk’, resist any pressure to create other long-lived development branches by
|
||||
> employing documented techniques. They therefore avoid merge hell, do not break the build,
|
||||
> and live happily ever after. - [source](https://trunkbaseddevelopment.com)
|
||||
|
||||
`git-next` is a combined server and command-line tool that enables trunk-based
|
||||
development workflows where each commit must pass CI before being included in
|
||||
the main branch.
|
||||
|
||||
## Features
|
||||
|
||||
- Allows enforcing the requirement for each commit to pass the CI pipeline before being
|
||||
included in the main branch
|
||||
- Provides a server component that manages the trunk-based development process
|
||||
- Ensure a consistent, high-quality codebase by preventing untested changes
|
||||
from being added to main
|
||||
- Requires each commit uses conventional commit format.
|
||||
|
||||
See [Behaviour](#behaviour) to learn how we do this.
|
||||
|
||||
![Demo](./demo.gif)
|
||||
|
||||
## Prerequisits
|
||||
|
||||
- Rust 1.76.0 or later - https://www.rust-lang.org
|
||||
- pgk-config
|
||||
- libssl-dev
|
||||
- libdbus-1-dev (ubuntu/debian)
|
||||
- dbus-devel (fedora)
|
||||
|
||||
See `.cargo/config.toml` for how they are configured.
|
||||
|
||||
## Installation
|
||||
|
||||
You can install `git-next` from <https://crates.io/>:
|
||||
|
||||
```shell
|
||||
cargo install git-next
|
||||
```
|
||||
|
||||
If you use [mise](https://mise.jdx.dev):
|
||||
|
||||
```shell
|
||||
mise use -g cargo:git-next
|
||||
```
|
||||
|
||||
Or you can install `git-next` from source after cloning:
|
||||
|
||||
```shell
|
||||
cargo install --path crates/cli
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] cli
|
||||
- [x] server
|
||||
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
||||
- [x] tui overview
|
||||
- [ ] webui overview
|
||||
|
||||
## Branch Names
|
||||
|
||||
`git-next` uses three branches, `main`, `next` and `dev`, although they do not
|
||||
need to have those names. In the documentation we will use those names, but
|
||||
each repo must specify the names of the branches to use for each, even if they
|
||||
happen to have those same names.
|
||||
|
||||
## Configuration
|
||||
|
||||
- The branches to use for `main`, `next` and `dev` must be specified in either
|
||||
the `.git-next.toml` in the repo itself, or in the server configuration file,
|
||||
`git-next-server.toml`. See below for details.
|
||||
- CI checks should be configured to run when the `next` branch is `pushed`.
|
||||
- The `dev` branch _must_ have the `main` branch as an ancestor.
|
||||
- The `next` branch _must_ have the `main` branch as an ancestor.
|
||||
|
||||
### Server
|
||||
|
||||
The server is configured by the `git-next-server.toml` file.
|
||||
|
||||
#### listen
|
||||
|
||||
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,
|
||||
(e.g. github.com). You can do this via any method that suits your environment,
|
||||
e.g. ngrok or a reverse proxy from a web server that itself can route traffic
|
||||
to the machine you are running the git-next server on.
|
||||
|
||||
Specify the address and port the server should listen to for incoming webhooks.
|
||||
This is the address and port that your reverse proxy should route traffic to.
|
||||
|
||||
- **addr** - the IP address the server should bind to
|
||||
- **port** - the IP port the server should bind to
|
||||
|
||||
##### url
|
||||
|
||||
The HTTPS URL for forges to send webhooks to.
|
||||
|
||||
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
|
||||
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
|
||||
forge is running on.
|
||||
|
||||
#### shout
|
||||
|
||||
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
|
||||
|
||||
```toml
|
||||
[storage]
|
||||
path = "./data"
|
||||
```
|
||||
|
||||
`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
|
||||
does not need to be backed up, as any missing information will be cloned when
|
||||
the server starts up.
|
||||
|
||||
- **path** - directory to store local copies of monitored repos
|
||||
|
||||
#### forge
|
||||
|
||||
Within the forge tree, specify each forge you want to monitor repos on.
|
||||
|
||||
Give your forge an alias, e.g. `default`, `gh`, `github`.
|
||||
|
||||
e.g.
|
||||
|
||||
```toml
|
||||
[forge.github]
|
||||
forge_type = "GitHub"
|
||||
hostname = "github.com"
|
||||
user = "username"
|
||||
token = "api-key"
|
||||
max_dev_commits = 25
|
||||
```
|
||||
|
||||
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
||||
- **hostname** - the hostname for the forge.
|
||||
- **user** - the user to authenticate as
|
||||
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
|
||||
- **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
|
||||
|
||||
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
||||
to `next`.
|
||||
|
||||
#### repos
|
||||
|
||||
For each forge, you need to specify which repos on the forge you want to
|
||||
monitor. They do not need to be owned by the `user`, but they `user` must have
|
||||
the `push` and `force-push` permissions as mentioned above for each of the
|
||||
repositories.
|
||||
|
||||
e.g.
|
||||
|
||||
```toml
|
||||
[forge.github.repos]
|
||||
my-repo = { repo = "owner/repo", branch = "main", gitdir = "/home/pcampbell/project/my-repo" }
|
||||
|
||||
[forge.github.repos.other-repo]
|
||||
repo = "user/other"
|
||||
branch = "master"
|
||||
main = "master"
|
||||
next = "ci-testing"
|
||||
dev = "trunk"
|
||||
```
|
||||
|
||||
Note that toml allows specifying the values on one line, or across multiple
|
||||
lines. Both are equivalent. What is not equivalent between `my-repo` and
|
||||
`other-repo`, is that one will require a configuration file within the repo
|
||||
itself. `other-repo` specifies the `main`, `next` and `dev` branches to be
|
||||
used, but `my-repo` doesn't.
|
||||
|
||||
A sample `.git-next-toml` file that would need to exist in `my-repo`'s `owner/repo`
|
||||
repo, on the `main` branch:
|
||||
|
||||
```toml
|
||||
[branches]
|
||||
main = "main"
|
||||
next = "next"
|
||||
dev = "dev"
|
||||
```
|
||||
|
||||
- **repo** - the owner and name of the repo to be monitored
|
||||
- **branch** - the branch to look for a `.git-next.toml` file if needed
|
||||
- **gitdir** - (optional) you can use a local copy of the repo
|
||||
- **main** - the branch to use as `main`
|
||||
- **next** - the branch to use as `next`
|
||||
- **dev** - the branch to use as `dev`
|
||||
|
||||
##### gitdir
|
||||
|
||||
Additional notes on using `gitdir`:
|
||||
|
||||
When you specify the `gitdir` value, the repo cloned in that directory will
|
||||
be used for perform the equivalent of `git fetch`, `git push` and `git push
|
||||
--force-with-lease`.
|
||||
|
||||
These commands will not affect the contents of your working tree, nor will
|
||||
it change any local branches. Only the details about branches on the remote
|
||||
forge will be updated.
|
||||
|
||||
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
|
||||
untested and undefined.
|
||||
|
||||
## Webhook Notifications
|
||||
|
||||
When sending a Webhook Notification to a user they are sent using the
|
||||
Standard Webhooks format. That means all POST messages have the
|
||||
following headers:
|
||||
|
||||
- `Webhook-Id`
|
||||
- `Webhook-Signature`
|
||||
- `Webhook-Timestamp`
|
||||
|
||||
### Events
|
||||
|
||||
#### Dev Not Based on Main
|
||||
|
||||
This message `type` indicates that the `dev` branch is not based on `main`.
|
||||
|
||||
**Action Required**: Rebase the `dev` branch onto the `main` branch.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"branches": {
|
||||
"dev": "dev",
|
||||
"main": "main"
|
||||
},
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio",
|
||||
"log": [
|
||||
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
|
||||
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
|
||||
"|/",
|
||||
"* 8c19680 refactor: macros use a more common syntax"
|
||||
]
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "branch.dev.not-on-main"
|
||||
}
|
||||
```
|
||||
|
||||
#### CI Check Failed
|
||||
|
||||
This message `type` indicates that the commit on the tip of the `next` branch has failed the
|
||||
configured CI checks.
|
||||
|
||||
**Action Required**: Either update the commit to correct the issue CI raised, or, if the issue
|
||||
is transient (e.g. a network issue), re-run/re-start the job in your CI.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"commit": {
|
||||
"sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
|
||||
"message": "feat: add log graph to notifications (1 of 2)"
|
||||
},
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio",
|
||||
"log": [
|
||||
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
|
||||
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
|
||||
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
|
||||
]
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "cicheck.failed"
|
||||
}
|
||||
```
|
||||
|
||||
#### Repo Config Load Failed
|
||||
|
||||
This message `type` indicates that `git-next` wasn't able to load the configuration for the
|
||||
repo from the `git-next.toml` file in the repository.
|
||||
|
||||
**Action Required**: Review the `reason` provided.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"reason": "File not found: .git-next.toml",
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio"
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "config.load.failed"
|
||||
}
|
||||
```
|
||||
|
||||
#### Webhook Registration Failed
|
||||
|
||||
This message `type` indicates that `git-next` wasn't able to register it's webhook with the
|
||||
forge repository, so will not receive updates when the branches in the repo are updated.
|
||||
|
||||
**Action Required**: Review the `reason` provided.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"reason": "repo config not loaded",
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio"
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "webhook.registration.failed"
|
||||
}
|
||||
```
|
||||
|
||||
## Behaviour
|
||||
|
||||
The branch names are configurable, but we will talk about `main`, `next` and `dev`.
|
||||
|
||||
Development happens on the `dev` branch, where each commit is expected to
|
||||
be able to pass the CI checks.
|
||||
|
||||
(Note: in the diagrams, mermaid isn't capable of showing `main` and `next` on
|
||||
the same commit, so we show `next` as empty)
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
|
||||
branch dev
|
||||
commit
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
When the `git-next` server sees that the `dev` branch is ahead of the `next`
|
||||
branch, it will push the `next` branch fast-forward one commit along the `dev`
|
||||
branch.
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
commit
|
||||
|
||||
branch dev
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
It will then wait for the CI checks to pass for the newly updated `next` branch.
|
||||
When the CI checks for the `next` branch pass, it will push the `main` branch
|
||||
fast-forward to the `next` branch. We return to the top and start again.
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
|
||||
branch dev
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
If the CI checks should fail for the `next` branch, the developer should
|
||||
**amend** that commit **in the history of their `dev` branch**.
|
||||
They should then force-push their rebased `dev` branch.
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
commit
|
||||
|
||||
checkout main
|
||||
|
||||
branch dev
|
||||
commit
|
||||
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
`git-next` will then detect that the `next` branch is no longer part of the
|
||||
`dev` branch ancestory, and will reset `next` back to `main`.
|
||||
We then return to the top, where `git-next` sees that `dev` is ahead of `next`.
|
||||
|
||||
When the `dev` branch is on the same commit as the `main` branch, then there
|
||||
are no pending commits and `git-next` will wait until it receives a webhook
|
||||
indicating that there has been a push to one of the branches. At which point
|
||||
it will start at the top again.
|
||||
|
||||
### Important
|
||||
|
||||
The `dev` branch _should_ have the `next` branch as an ancestor.
|
||||
|
||||
However, when the commit on tip of the `next` branch has failed CI and is
|
||||
amended, this will not be the case. When this happens `git-next` will
|
||||
**force-push** the `next` branch back to the same commit as the `main` branch.
|
||||
|
||||
This is the only time a force-push will happen in `git-next`.
|
||||
|
||||
In short, the `next` branch **belongs** to `git-next`. Don't try to update it
|
||||
yourself. `git-next` will update the `next` it as it sees fit.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To use `git-next` for trunk-based development, follow these steps:
|
||||
|
||||
### Initialise the repo (optional)
|
||||
|
||||
You need to specify which branches you are using. You can do this in the repo,
|
||||
or in the server configuration.
|
||||
|
||||
To create a default config file for the repo, run this command in the root of
|
||||
your repo:
|
||||
|
||||
```shell
|
||||
git next init
|
||||
```
|
||||
|
||||
This will create a `.git-next.toml` file. [Default](./crates/cli/default.toml)
|
||||
|
||||
By default the expected branches are `main`, `next` and `dev`. Each of these
|
||||
three branches _must_ exist in your repo.
|
||||
|
||||
### Initialise the server
|
||||
|
||||
The server uses the file `git-next-server.toml` for configuration. It expects
|
||||
to find this file the the current directory when executed.
|
||||
|
||||
The create the default config file, run this command:
|
||||
|
||||
```shell
|
||||
git next server init
|
||||
```
|
||||
|
||||
This will create a `git-next-server.toml` file. [Default](./crates/server/server-default.toml)
|
||||
|
||||
Edit this file to your needs. See the [Configuration](#configuration) section above.
|
||||
|
||||
### Run the server
|
||||
|
||||
In the directory with your `git-next-server.toml` file, run the command:
|
||||
|
||||
```shell
|
||||
git next server start
|
||||
```
|
||||
|
||||
### Forges
|
||||
|
||||
The following forges are supported:
|
||||
|
||||
- [ForgeJo](https://forgejo.org) (probably compatible with Gitea, but not tested)
|
||||
- [GitHub](https://github.com/)
|
||||
|
||||
Note: ForgeJo is a hard fork of Gitea, but currently they are largely compatible.
|
||||
For now using a `forge_type` of `ForgeJo` with a Gitea instance will probably work
|
||||
okay. The only API calls we make are around registering and unregistering webhooks.
|
||||
So, as long as those APIs remain the same, they should be compatible.
|
||||
|
||||
#### ForgeJo
|
||||
|
||||
Configure the forge in `git-next-server.toml` like:
|
||||
|
||||
```toml
|
||||
[forge.jo]
|
||||
forge_type = "ForgeJo"
|
||||
hostname = "git.myforgejo.com"
|
||||
user = "bob"
|
||||
token = "..."
|
||||
|
||||
[forge.jo.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
|
||||
```
|
||||
|
||||
The token is created on your ForgeJo instance at (for example)
|
||||
`https://git.myforgejo.com/user/settings/applications`
|
||||
and requires the `write:repository` permission.
|
||||
|
||||
#### GitHub
|
||||
|
||||
Configure the forge in `git-next-server.toml` like:
|
||||
|
||||
```toml
|
||||
[forge.gh]
|
||||
forge_type = "GitHub"
|
||||
hostname = "github.com" # required even for GitHub
|
||||
user = "bob"
|
||||
token = "..."
|
||||
|
||||
[forge.gh.repos]
|
||||
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://github.com/user/hello on the branch 'main'
|
||||
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
||||
```
|
||||
|
||||
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
||||
|
||||
## Docker
|
||||
|
||||
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
|
||||
|
||||
```shell
|
||||
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Here is an example `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
server:
|
||||
image: git.kemitix.net/kemitix/git-next:latest
|
||||
container_name: git-next-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RUST_LOG: "hyper=warn,info"
|
||||
ports:
|
||||
- 8080:8092
|
||||
volumes:
|
||||
- ./:/app/
|
||||
```
|
||||
|
||||
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
|
||||
`8092` and that you are using a reverse proxy to route traffic arriving at
|
||||
`listen.url` to port `8080`.
|
||||
|
||||
### Docker Run
|
||||
|
||||
This will run with the `server start` options:
|
||||
|
||||
```shell
|
||||
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
|
||||
```
|
||||
|
||||
To perform `server init`:
|
||||
|
||||
```shell
|
||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
|
||||
```
|
||||
|
||||
To perform repo `init`:
|
||||
|
||||
```shell
|
||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
|
||||
```
|
||||
|
||||
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
||||
request, please
|
||||
[create an issue](https://git.kemitix.net/kemitix/git-next/issues/new).
|
||||
If you'd like to contribute code, feel free to submit changes.
|
||||
|
||||
Before you start committing, run the `just install-hooks` command to setup the
|
||||
Git Hooks. ([Get Just](https://just.systems/man/en/chapter_3.html))
|
||||
|
||||
## Crate Dependency
|
||||
|
||||
The following diagram shows the dependency between the crates that make up `git-next`:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
|
||||
cli --> core
|
||||
cli --> forge_forgejo
|
||||
cli --> forge_github
|
||||
|
||||
forge_forgejo --> core
|
||||
|
||||
forge_github --> core
|
||||
```
|
||||
|
||||
## Actor Supervision Tree
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
Root
|
||||
Alerts
|
||||
FileWatcher
|
||||
Server
|
||||
Repo 1
|
||||
Repo 2
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
`git-next` is released under the [MIT License](./LICENSE).
|
||||
See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information.
|
||||
|
|
110
crates/cli/Cargo.toml
Normal file
110
crates/cli/Cargo.toml
Normal file
|
@ -0,0 +1,110 @@
|
|||
[package]
|
||||
name = "git-next"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
description = "git-next, the trunk-based development manager"
|
||||
authors = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
documentation = { workspace = true }
|
||||
keywords = { workspace = true }
|
||||
categories = { workspace = true }
|
||||
|
||||
[features]
|
||||
# default = ["forgejo", "github"]
|
||||
default = ["forgejo", "github", "tui"]
|
||||
forgejo = ["git-next-forge-forgejo"]
|
||||
github = ["git-next-forge-github"]
|
||||
tui = [
|
||||
"ratatui",
|
||||
"directories",
|
||||
"lazy_static",
|
||||
"tui-scrollview",
|
||||
"regex",
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
git-next-core = { workspace = true }
|
||||
git-next-forge-forgejo = { workspace = true, optional = true }
|
||||
git-next-forge-github = { workspace = true, optional = true }
|
||||
|
||||
native-tls = { workspace = true }
|
||||
|
||||
# TUI
|
||||
ratatui = { workspace = true, optional = true }
|
||||
directories = { workspace = true, optional = true }
|
||||
lazy_static = { workspace = true, optional = true }
|
||||
color-eyre = { workspace = true }
|
||||
tui-scrollview = { workspace = true, optional = true }
|
||||
regex = { workspace = true, optional = true }
|
||||
chrono = { workspace = true, optional = true }
|
||||
|
||||
# CLI parsing
|
||||
clap = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# logging
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-error.workspace = true
|
||||
|
||||
# Conventional Commit check
|
||||
git-conventional = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
toml = { workspace = true }
|
||||
|
||||
# Actors
|
||||
kameo = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
bon = { workspace = true }
|
||||
derive_more = { workspace = true }
|
||||
derive-with = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Webhooks
|
||||
serde_json = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
time = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
standardwebhooks = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
warp = { workspace = true }
|
||||
|
||||
# file watcher (linux)
|
||||
notify = { workspace = true }
|
||||
|
||||
# email
|
||||
lettre = { workspace = true }
|
||||
sendmail = { workspace = true }
|
||||
|
||||
# desktop notifications
|
||||
notifica = { workspace = true }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["native-tls"]
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
assert2 = { workspace = true }
|
||||
test-log = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
mockall = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
672
crates/cli/README.md
Normal file
672
crates/cli/README.md
Normal file
|
@ -0,0 +1,672 @@
|
|||
# git-next
|
||||
|
||||
## Trunk-based developement manager.
|
||||
|
||||
> A source-control branching model, where developers collaborate on code in a single branch
|
||||
> called ‘trunk’, resist any pressure to create other long-lived development branches by
|
||||
> employing documented techniques. They therefore avoid merge hell, do not break the build,
|
||||
> and live happily ever after. - [source](https://trunkbaseddevelopment.com)
|
||||
|
||||
- Status: **BETA** - dog-fooding
|
||||
|
||||
`git-next` is a combined server and command-line tool that enables trunk-based
|
||||
development workflows where each commit must pass CI before being included in
|
||||
the main branch.
|
||||
|
||||
## Features
|
||||
|
||||
- Allows enforcing the requirement for each commit to pass the CI pipeline before being
|
||||
included in the main branch
|
||||
- Provides a server component that manages the trunk-based development process
|
||||
- Ensure a consistent, high-quality codebase by preventing untested changes
|
||||
from being added to main
|
||||
- Requires each commit uses conventional commit format.
|
||||
|
||||
See [Behaviour](#behaviour) to learn how we do this.
|
||||
|
||||
## Prerequisits
|
||||
|
||||
- Rust 1.76.0 or later - https://www.rust-lang.org
|
||||
- pgk-config
|
||||
- libssl-dev
|
||||
- libdbus-1-dev (ubuntu/debian)
|
||||
- dbus-devel (fedora)
|
||||
|
||||
See `.cargo/config.toml` for how they are configured.
|
||||
|
||||
## Installation
|
||||
|
||||
You can install `git-next` from <https://crates.io/>:
|
||||
|
||||
```shell
|
||||
cargo install git-next
|
||||
```
|
||||
|
||||
If you use [mise](https://mise.jdx.dev):
|
||||
|
||||
```shell
|
||||
mise use -g cargo:git-next
|
||||
```
|
||||
|
||||
Or you can install `git-next` from source after cloning:
|
||||
|
||||
```shell
|
||||
cargo install --path crates/cli
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] cli
|
||||
- [x] server
|
||||
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
||||
- [x] tui overview
|
||||
- [ ] webui overview
|
||||
|
||||
## Branch Names
|
||||
|
||||
`git-next` uses three branches, `main`, `next` and `dev`, although they do not
|
||||
need to have those names. In the documentation we will use those names, but
|
||||
each repo must specify the names of the branches to use for each, even if they
|
||||
happen to have those same names.
|
||||
|
||||
## Configuration
|
||||
|
||||
- The branches to use for `main`, `next` and `dev` must be specified in either
|
||||
the `.git-next.toml` in the repo itself, or in the server configuration file,
|
||||
`git-next-server.toml`. See below for details.
|
||||
- CI checks should be configured to run when the `next` branch is `pushed`.
|
||||
- The `dev` branch _must_ have the `main` branch as an ancestor.
|
||||
- The `next` branch _must_ have the `main` branch as an ancestor.
|
||||
|
||||
### Server
|
||||
|
||||
The server is configured by the `git-next-server.toml` file.
|
||||
|
||||
#### listen
|
||||
|
||||
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,
|
||||
(e.g. github.com). You can do this via any method that suits your environment,
|
||||
e.g. ngrok or a reverse proxy from a web server that itself can route traffic
|
||||
to the machine you are running the git-next server on.
|
||||
|
||||
Specify the address and port the server should listen to for incoming webhooks.
|
||||
This is the address and port that your reverse proxy should route traffic to.
|
||||
|
||||
- **addr** - the IP address the server should bind to
|
||||
- **port** - the IP port the server should bind to
|
||||
|
||||
##### url
|
||||
|
||||
The HTTPS URL for forges to send webhooks to.
|
||||
|
||||
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
|
||||
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
|
||||
forge is running on.
|
||||
|
||||
#### shout
|
||||
|
||||
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
|
||||
|
||||
```toml
|
||||
[storage]
|
||||
path = "./data"
|
||||
```
|
||||
|
||||
`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
|
||||
does not need to be backed up, as any missing information will be cloned when
|
||||
the server starts up.
|
||||
|
||||
- **path** - directory to store local copies of monitored repos
|
||||
|
||||
#### forge
|
||||
|
||||
Within the forge tree, specify each forge you want to monitor repos on.
|
||||
|
||||
Give your forge an alias, e.g. `default`, `gh`, `github`.
|
||||
|
||||
e.g.
|
||||
|
||||
```toml
|
||||
[forge.github]
|
||||
forge_type = "GitHub"
|
||||
hostname = "github.com"
|
||||
user = "username"
|
||||
token = "api-key"
|
||||
max_dev_commits = 25
|
||||
```
|
||||
|
||||
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
||||
- **hostname** - the hostname for the forge.
|
||||
- **user** - the user to authenticate as
|
||||
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
|
||||
- **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
|
||||
|
||||
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
||||
to `next`.
|
||||
|
||||
#### repos
|
||||
|
||||
For each forge, you need to specify which repos on the forge you want to
|
||||
monitor. They do not need to be owned by the `user`, but they `user` must have
|
||||
the `push` and `force-push` permissions as mentioned above for each of the
|
||||
repositories.
|
||||
|
||||
e.g.
|
||||
|
||||
```toml
|
||||
[forge.github.repos]
|
||||
my-repo = { repo = "owner/repo", branch = "main", gitdir = "/home/pcampbell/project/my-repo" }
|
||||
|
||||
[forge.github.repos.other-repo]
|
||||
repo = "user/other"
|
||||
branch = "master"
|
||||
main = "master"
|
||||
next = "ci-testing"
|
||||
dev = "trunk"
|
||||
```
|
||||
|
||||
Note that toml allows specifying the values on one line, or across multiple
|
||||
lines. Both are equivalent. What is not equivalent between `my-repo` and
|
||||
`other-repo`, is that one will require a configuration file within the repo
|
||||
itself. `other-repo` specifies the `main`, `next` and `dev` branches to be
|
||||
used, but `my-repo` doesn't.
|
||||
|
||||
A sample `.git-next-toml` file that would need to exist in `my-repo`'s `owner/repo`
|
||||
repo, on the `main` branch:
|
||||
|
||||
```toml
|
||||
[branches]
|
||||
main = "main"
|
||||
next = "next"
|
||||
dev = "dev"
|
||||
```
|
||||
|
||||
- **repo** - the owner and name of the repo to be monitored
|
||||
- **branch** - the branch to look for a `.git-next.toml` file if needed
|
||||
- **gitdir** - (optional) you can use a local copy of the repo
|
||||
- **main** - the branch to use as `main`
|
||||
- **next** - the branch to use as `next`
|
||||
- **dev** - the branch to use as `dev`
|
||||
|
||||
##### gitdir
|
||||
|
||||
Additional notes on using `gitdir`:
|
||||
|
||||
When you specify the `gitdir` value, the repo cloned in that directory will
|
||||
be used for perform the equivalent of `git fetch`, `git push` and `git push
|
||||
--force-with-lease`.
|
||||
|
||||
These commands will not affect the contents of your working tree, nor will
|
||||
it change any local branches. Only the details about branches on the remote
|
||||
forge will be updated.
|
||||
|
||||
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
|
||||
untested and undefined.
|
||||
|
||||
## Webhook Notifications
|
||||
|
||||
When sending a Webhook Notification to a user they are sent using the
|
||||
Standard Webhooks format. That means all POST messages have the
|
||||
following headers:
|
||||
|
||||
- `Webhook-Id`
|
||||
- `Webhook-Signature`
|
||||
- `Webhook-Timestamp`
|
||||
|
||||
### Events
|
||||
|
||||
#### Dev Not Based on Main
|
||||
|
||||
This message `type` indicates that the `dev` branch is not based on `main`.
|
||||
|
||||
**Action Required**: Rebase the `dev` branch onto the `main` branch.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"branches": {
|
||||
"dev": "dev",
|
||||
"main": "main"
|
||||
},
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio",
|
||||
"log": [
|
||||
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
|
||||
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
|
||||
"|/",
|
||||
"* 8c19680 refactor: macros use a more common syntax"
|
||||
]
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "branch.dev.not-on-main"
|
||||
}
|
||||
```
|
||||
|
||||
#### CI Check Failed
|
||||
|
||||
This message `type` indicates that the commit on the tip of the `next` branch has failed the
|
||||
configured CI checks.
|
||||
|
||||
**Action Required**: Either update the commit to correct the issue CI raised, or, if the issue
|
||||
is transient (e.g. a network issue), re-run/re-start the job in your CI.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"commit": {
|
||||
"sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
|
||||
"message": "feat: add log graph to notifications (1 of 2)"
|
||||
},
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio",
|
||||
"log": [
|
||||
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
|
||||
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
|
||||
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
|
||||
]
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "cicheck.failed"
|
||||
}
|
||||
```
|
||||
|
||||
#### Repo Config Load Failed
|
||||
|
||||
This message `type` indicates that `git-next` wasn't able to load the configuration for the
|
||||
repo from the `git-next.toml` file in the repository.
|
||||
|
||||
**Action Required**: Review the `reason` provided.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"reason": "File not found: .git-next.toml",
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio"
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "config.load.failed"
|
||||
}
|
||||
```
|
||||
|
||||
#### Webhook Registration Failed
|
||||
|
||||
This message `type` indicates that `git-next` wasn't able to register it's webhook with the
|
||||
forge repository, so will not receive updates when the branches in the repo are updated.
|
||||
|
||||
**Action Required**: Review the `reason` provided.
|
||||
|
||||
Sample payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"reason": "repo config not loaded",
|
||||
"forge_alias": "jo",
|
||||
"repo_alias": "kxio"
|
||||
},
|
||||
"timestamp": "1721760933",
|
||||
"type": "webhook.registration.failed"
|
||||
}
|
||||
```
|
||||
|
||||
## Behaviour
|
||||
|
||||
The branch names are configurable, but we will talk about `main`, `next` and `dev`.
|
||||
|
||||
Development happens on the `dev` branch, where each commit is expected to
|
||||
be able to pass the CI checks.
|
||||
|
||||
(Note: in the diagrams, mermaid isn't capable of showing `main` and `next` on
|
||||
the same commit, so we show `next` as empty)
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
|
||||
branch dev
|
||||
commit
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
When the `git-next` server sees that the `dev` branch is ahead of the `next`
|
||||
branch, it will push the `next` branch fast-forward one commit along the `dev`
|
||||
branch.
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
commit
|
||||
|
||||
branch dev
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
It will then wait for the CI checks to pass for the newly updated `next` branch.
|
||||
When the CI checks for the `next` branch pass, it will push the `main` branch
|
||||
fast-forward to the `next` branch. We return to the top and start again.
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
|
||||
branch dev
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
If the CI checks should fail for the `next` branch, the developer should
|
||||
**amend** that commit **in the history of their `dev` branch**.
|
||||
They should then force-push their rebased `dev` branch.
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit
|
||||
commit
|
||||
|
||||
branch next
|
||||
commit
|
||||
|
||||
checkout main
|
||||
|
||||
branch dev
|
||||
commit
|
||||
|
||||
commit
|
||||
commit
|
||||
```
|
||||
|
||||
`git-next` will then detect that the `next` branch is no longer part of the
|
||||
`dev` branch ancestory, and will reset `next` back to `main`.
|
||||
We then return to the top, where `git-next` sees that `dev` is ahead of `next`.
|
||||
|
||||
When the `dev` branch is on the same commit as the `main` branch, then there
|
||||
are no pending commits and `git-next` will wait until it receives a webhook
|
||||
indicating that there has been a push to one of the branches. At which point
|
||||
it will start at the top again.
|
||||
|
||||
### Important
|
||||
|
||||
The `dev` branch _should_ have the `next` branch as an ancestor.
|
||||
|
||||
However, when the commit on tip of the `next` branch has failed CI and is
|
||||
amended, this will not be the case. When this happens `git-next` will
|
||||
**force-push** the `next` branch back to the same commit as the `main` branch.
|
||||
|
||||
This is the only time a force-push will happen in `git-next`.
|
||||
|
||||
In short, the `next` branch **belongs** to `git-next`. Don't try to update it
|
||||
yourself. `git-next` will update the `next` it as it sees fit.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To use `git-next` for trunk-based development, follow these steps:
|
||||
|
||||
### Initialise the repo (optional)
|
||||
|
||||
You need to specify which branches you are using. You can do this in the repo,
|
||||
or in the server configuration.
|
||||
|
||||
To create a default config file for the repo, run this command in the root of
|
||||
your repo:
|
||||
|
||||
```shell
|
||||
git next init
|
||||
```
|
||||
|
||||
This will create a `.git-next.toml` file. [Default](./crates/cli/default.toml)
|
||||
|
||||
By default the expected branches are `main`, `next` and `dev`. Each of these
|
||||
three branches _must_ exist in your repo.
|
||||
|
||||
### Initialise the server
|
||||
|
||||
The server uses the file `git-next-server.toml` for configuration. It expects
|
||||
to find this file the the current directory when executed.
|
||||
|
||||
The create the default config file, run this command:
|
||||
|
||||
```shell
|
||||
git next server init
|
||||
```
|
||||
|
||||
This will create a `git-next-server.toml` file. [Default](./crates/server/server-default.toml)
|
||||
|
||||
Edit this file to your needs. See the [Configuration](#configuration) section above.
|
||||
|
||||
### Run the server
|
||||
|
||||
In the directory with your `git-next-server.toml` file, run the command:
|
||||
|
||||
```shell
|
||||
git next server start
|
||||
```
|
||||
|
||||
### Forges
|
||||
|
||||
The following forges are supported:
|
||||
|
||||
- [ForgeJo](https://forgejo.org) (probably compatible with Gitea, but not tested)
|
||||
- [GitHub](https://github.com/)
|
||||
|
||||
Note: ForgeJo is a hard fork of Gitea, but currently they are largely compatible.
|
||||
For now using a `forge_type` of `ForgeJo` with a Gitea instance will probably work
|
||||
okay. The only API calls we make are around registering and unregistering webhooks.
|
||||
So, as long as those APIs remain the same, they should be compatible.
|
||||
|
||||
#### ForgeJo
|
||||
|
||||
Configure the forge in `git-next-server.toml` like:
|
||||
|
||||
```toml
|
||||
[forge.jo]
|
||||
forge_type = "ForgeJo"
|
||||
hostname = "git.myforgejo.com"
|
||||
user = "bob"
|
||||
token = "..."
|
||||
|
||||
[forge.jo.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
|
||||
```
|
||||
|
||||
The token is created on your ForgeJo instance at (for example)
|
||||
`https://git.myforgejo.com/user/settings/applications`
|
||||
and requires the `write:repository` permission.
|
||||
|
||||
#### GitHub
|
||||
|
||||
Configure the forge in `git-next-server.toml` like:
|
||||
|
||||
```toml
|
||||
[forge.gh]
|
||||
forge_type = "GitHub"
|
||||
hostname = "github.com" # required even for GitHub
|
||||
user = "bob"
|
||||
token = "..."
|
||||
|
||||
[forge.gh.repos]
|
||||
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://github.com/user/hello on the branch 'main'
|
||||
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
||||
```
|
||||
|
||||
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
||||
|
||||
## Docker
|
||||
|
||||
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
|
||||
|
||||
```shell
|
||||
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Here is an example `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
server:
|
||||
image: git.kemitix.net/kemitix/git-next:latest
|
||||
container_name: git-next-server
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
RUST_LOG: "hyper=warn,info"
|
||||
ports:
|
||||
- 8080:8092
|
||||
volumes:
|
||||
- ./:/app/
|
||||
```
|
||||
|
||||
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
|
||||
`8092` and that you are using a reverse proxy to route traffic arriving at
|
||||
`listen.url` to port `8080`.
|
||||
|
||||
### Docker Run
|
||||
|
||||
This will run with the `server start` options:
|
||||
|
||||
```shell
|
||||
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
|
||||
```
|
||||
|
||||
To perform `server init`:
|
||||
|
||||
```shell
|
||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
|
||||
```
|
||||
|
||||
To perform repo `init`:
|
||||
|
||||
```shell
|
||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
|
||||
```
|
||||
|
||||
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
||||
request, please
|
||||
[create an issue](https://git.kemitix.net/kemitix/git-next/issues/new).
|
||||
If you'd like to contribute code, feel free to submit changes.
|
||||
|
||||
Before you start committing, run the `just install-hooks` command to setup the
|
||||
Git Hooks. ([Get Just](https://just.systems/man/en/chapter_3.html))
|
||||
|
||||
## Crate Dependency
|
||||
|
||||
The following diagram shows the dependency between the crates that make up `git-next`:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
|
||||
cli --> core
|
||||
cli --> forge_forgejo
|
||||
cli --> forge_github
|
||||
|
||||
forge_forgejo --> core
|
||||
|
||||
forge_github --> core
|
||||
```
|
||||
|
||||
## Actor Supervision Tree
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
Root
|
||||
Alerts
|
||||
FileWatcher
|
||||
Server
|
||||
Repo 1
|
||||
Repo 2
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
`git-next` is released under the [MIT License](./LICENSE).
|
|
@ -1,6 +1,6 @@
|
|||
//
|
||||
use crate::alerts::short_message;
|
||||
use crate::core::git::UserNotification;
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
pub(super) fn send_desktop_notification(user_notification: &UserNotification) {
|
||||
let message = short_message(user_notification);
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::UserNotification,
|
||||
server::{EmailConfig, SmtpConfig},
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate::core::git::UserNotification;
|
||||
use git_next_core::git::UserNotification;
|
||||
use tracing::info;
|
||||
|
||||
use std::{
|
|
@ -1,6 +1,5 @@
|
|||
//
|
||||
use crate::core::{git::UserNotification, server::Shout};
|
||||
use crate::message;
|
||||
use git_next_core::{git::UserNotification, message, server::Shout};
|
||||
|
||||
message!(UpdateShout, Shout, "Updated Shout configuration");
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use derive_more::derive::Constructor;
|
||||
|
||||
use crate::core::{git::UserNotification, server::Shout};
|
||||
use git_next_core::{git::UserNotification, server::Shout};
|
||||
|
||||
pub use history::History;
|
||||
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
|
|
@ -1,7 +1,7 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::core::git::UserNotification;
|
||||
use assert2::let_assert;
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
use crate::{alerts::History, repo::tests::given};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate::core::{git::UserNotification, server::OutboundWebhook};
|
||||
use git_next_core::{git::UserNotification, server::OutboundWebhook};
|
||||
use secrecy::ExposeSecret as _;
|
||||
use standardwebhooks::Webhook;
|
||||
|
|
@ -6,7 +6,7 @@ use kameo::{mailbox::unbounded::UnboundedMailbox, message::Message, Actor};
|
|||
use notify::{event::ModifyKind, RecommendedWatcher, Watcher};
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::message;
|
||||
use git_next_core::message;
|
||||
|
||||
use crate::{
|
||||
default_on_actor_link_died, default_on_actor_panic, default_on_actor_stop, on_actor_start,
|
|
@ -1,11 +1,11 @@
|
|||
//
|
||||
use crate::core::git::{ForgeLike, RepoDetails};
|
||||
use git_next_core::git::{ForgeLike, RepoDetails};
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
use crate::forges::forgejo::ForgeJo;
|
||||
use git_next_forge_forgejo::ForgeJo;
|
||||
|
||||
#[cfg(feature = "github")]
|
||||
use crate::forges::github::Github;
|
||||
use git_next_forge_github::Github;
|
||||
|
||||
use kxio::net::Net;
|
||||
|
||||
|
@ -16,9 +16,9 @@ impl Forge {
|
|||
pub fn create(repo_details: RepoDetails, net: Net) -> Box<dyn ForgeLike> {
|
||||
match repo_details.forge.forge_type() {
|
||||
#[cfg(feature = "forgejo")]
|
||||
crate::core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
|
||||
git_next_core::ForgeType::ForgeJo => Box::new(ForgeJo::new(repo_details, net)),
|
||||
#[cfg(feature = "github")]
|
||||
crate::core::ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
|
||||
git_next_core::ForgeType::GitHub => Box::new(Github::new(repo_details, net)),
|
||||
_ => {
|
||||
drop(repo_details);
|
||||
drop(net);
|
|
@ -2,7 +2,7 @@
|
|||
#[cfg(any(feature = "forgejo", feature = "github"))]
|
||||
use super::*;
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
self as core,
|
||||
git::{self, RepoDetails},
|
||||
GitDir, RepoConfigSource, StoragePathType,
|
||||
|
@ -11,25 +11,23 @@ use crate::core::{
|
|||
#[cfg(feature = "forgejo")]
|
||||
#[test]
|
||||
fn test_forgejo_name() {
|
||||
let mock_net = kxio::net::mock();
|
||||
let repo_details = given_repo_details(crate::core::ForgeType::ForgeJo);
|
||||
let forge = Forge::create(repo_details, mock_net.clone().into());
|
||||
let net = kxio::net::mock();
|
||||
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
|
||||
let forge = Forge::create(repo_details, net.into());
|
||||
assert_eq!(forge.name(), "forgejo");
|
||||
mock_net.assert_no_unused_plans();
|
||||
}
|
||||
|
||||
#[cfg(feature = "github")]
|
||||
#[test]
|
||||
fn test_github_name() {
|
||||
let mock_net = kxio::net::mock();
|
||||
let repo_details = given_repo_details(crate::core::ForgeType::GitHub);
|
||||
let forge = Forge::create(repo_details, mock_net.clone().into());
|
||||
let net = kxio::net::mock();
|
||||
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
|
||||
let forge = Forge::create(repo_details, net.into());
|
||||
assert_eq!(forge.name(), "github");
|
||||
mock_net.assert_no_unused_plans();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn given_repo_details(forge_type: crate::core::ForgeType) -> RepoDetails {
|
||||
fn given_repo_details(forge_type: git_next_core::ForgeType) -> RepoDetails {
|
||||
let fs = kxio::fs::temp().unwrap_or_else(|e| {
|
||||
println!("{e}");
|
||||
panic!("fs")
|
|
@ -1,15 +1,12 @@
|
|||
//
|
||||
#[macro_export]
|
||||
macro_rules! tell {
|
||||
($actor_ref:expr, $message:expr) => {{
|
||||
tracing::info!(msg = stringify!($message), "about to send");
|
||||
($actor_ref:expr, $message:expr) => {
|
||||
tell!(stringify!($actor_ref), $actor_ref, $message)
|
||||
}};
|
||||
};
|
||||
($actor_name:expr, $actor_ref:expr, $message:expr) => {{
|
||||
tracing::info!(actor = $actor_name, msg = stringify!($message), "send");
|
||||
let response = $actor_ref.tell($message).await;
|
||||
tracing::info!(actor = $actor_name, msg = stringify!($message), "sent");
|
||||
response
|
||||
tracing::debug!(actor = $actor_name, msg = stringify!($message), "send");
|
||||
$actor_ref.tell($message).await
|
||||
}};
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
mod alerts;
|
||||
mod base_actor;
|
||||
mod core;
|
||||
mod file_watcher;
|
||||
mod forge;
|
||||
mod init;
|
||||
|
@ -12,8 +11,6 @@ mod repo;
|
|||
mod root;
|
||||
mod server;
|
||||
|
||||
mod forges;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
mod tui;
|
||||
|
||||
|
@ -22,16 +19,13 @@ mod tests;
|
|||
|
||||
mod webhook;
|
||||
|
||||
use crate::core::git;
|
||||
use git_next_core::git;
|
||||
use kameo::actor::{pubsub::PubSub, ActorRef};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
pub use color_eyre::eyre::eyre as err;
|
||||
pub use color_eyre::Result;
|
||||
|
||||
use color_eyre::Result;
|
||||
use kxio::{fs, net};
|
||||
|
||||
pub type MessageBus<T> = ActorRef<PubSub<T>>;
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use crate::repo::messages::MessageToken;
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{
|
||||
commit::Message,
|
||||
push::{reset, Force},
|
||||
|
@ -10,35 +10,22 @@ use crate::core::{
|
|||
},
|
||||
RepoConfig,
|
||||
};
|
||||
use crate::Result;
|
||||
|
||||
use derive_more::Display;
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, derive_more::Display)]
|
||||
pub enum AdvanceError {
|
||||
/// The next commit message is invalid (non-conventional)
|
||||
UnconventionalCommitMessage { reason: String },
|
||||
/// The next commit has a 'WIP:' commit message prefix
|
||||
NextCommitIsWIP,
|
||||
/// The next commit is already on dev
|
||||
#[cfg(test)]
|
||||
NextIsDev,
|
||||
/// Unexpected error
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
// advance next to the next commit towards the head of the dev branch
|
||||
#[instrument(fields(next), skip_all)]
|
||||
pub fn advance_next(
|
||||
commit: Commit,
|
||||
force: crate::core::git::push::Force,
|
||||
commit: Option<Commit>,
|
||||
force: git_next_core::git::push::Force,
|
||||
repo_details: &RepoDetails,
|
||||
repo_config: &RepoConfig,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
message_token: MessageToken,
|
||||
) -> Result<MessageToken, AdvanceError> {
|
||||
) -> Result<MessageToken> {
|
||||
let commit = commit.ok_or_else(|| Error::NextAtDev)?;
|
||||
validate_commit_message(commit.message())?;
|
||||
|
||||
info!("Advancing next to commit '{}'", commit);
|
||||
reset(
|
||||
open_repository,
|
||||
|
@ -46,17 +33,15 @@ pub fn advance_next(
|
|||
&repo_config.branches().next(),
|
||||
&commit.into(),
|
||||
&force,
|
||||
)
|
||||
.map_err(|err| err.to_string())
|
||||
.map_err(AdvanceError::Unexpected)?;
|
||||
)?;
|
||||
Ok(message_token)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn validate_commit_message(message: &Message) -> Result<(), AdvanceError> {
|
||||
fn validate_commit_message(message: &Message) -> Result<()> {
|
||||
let message = &message.to_string();
|
||||
if message.to_ascii_lowercase().starts_with("wip") {
|
||||
return Err(AdvanceError::NextCommitIsWIP);
|
||||
return Err(Error::IsWorkInProgress);
|
||||
}
|
||||
match ::git_conventional::Commit::parse(message) {
|
||||
Ok(commit) => {
|
||||
|
@ -65,7 +50,7 @@ fn validate_commit_message(message: &Message) -> Result<(), AdvanceError> {
|
|||
}
|
||||
Err(err) => {
|
||||
warn!(?err, "Fail");
|
||||
Err(AdvanceError::UnconventionalCommitMessage {
|
||||
Err(Error::InvalidCommitMessage {
|
||||
reason: err.kind().to_string(),
|
||||
})
|
||||
}
|
||||
|
@ -85,7 +70,6 @@ pub fn find_next_commit_on_dev(
|
|||
};
|
||||
if commit == main {
|
||||
force = Force::From(GitRef::from(next.sha().clone()));
|
||||
// next_commit.replace(commit);
|
||||
break;
|
||||
};
|
||||
next_commit.replace(commit);
|
||||
|
@ -111,3 +95,20 @@ pub fn advance_main(
|
|||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error, Display)]
|
||||
pub enum Error {
|
||||
#[display("push: {}", 0)]
|
||||
Push(#[from] crate::git::push::Error),
|
||||
|
||||
#[display("no commits to advance next to")]
|
||||
NextAtDev,
|
||||
|
||||
#[display("commit is a Work-in-progress")]
|
||||
IsWorkInProgress,
|
||||
|
||||
#[display("commit message is not in conventional commit format: {reason}")]
|
||||
InvalidCommitMessage { reason: String },
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate::core::{git, RepoConfigSource};
|
||||
use git_next_core::{git, RepoConfigSource};
|
||||
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
|
@ -1,11 +1,10 @@
|
|||
//
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{instrument, warn};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::core::git;
|
||||
use git_next_core::git;
|
||||
|
||||
use crate::repo::logger;
|
||||
use crate::{
|
||||
repo::{
|
||||
branch::{advance_next, find_next_commit_on_dev},
|
||||
|
@ -19,43 +18,31 @@ use crate::{
|
|||
impl Message<AdvanceNext> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
||||
#[instrument(skip(self, ctx))]
|
||||
async fn handle(
|
||||
&mut self,
|
||||
msg: AdvanceNext,
|
||||
ctx: Context<'_, Self, Self::Reply>,
|
||||
) -> Self::Reply {
|
||||
tracing::debug!("start");
|
||||
logger(self.log.as_ref(), "start: AdvanceNext").await;
|
||||
let Some(repo_config) = &self.repo_details.repo_config else {
|
||||
tracing::debug!("repo details.repo_config is unset");
|
||||
return Ok(());
|
||||
};
|
||||
let Some(open_repository) = &self.open_repository else {
|
||||
tracing::debug!("no open repository");
|
||||
return Ok(());
|
||||
};
|
||||
tracing::debug!("prerequisites okay");
|
||||
|
||||
let AdvanceNextPayload {
|
||||
next,
|
||||
main,
|
||||
dev_commit_history,
|
||||
} = msg.peel();
|
||||
tracing::debug!(?next, ?main, ?dev_commit_history, "can we advance?");
|
||||
let (commit, force) = find_next_commit_on_dev(&next, &main, &dev_commit_history);
|
||||
tracing::debug!(?commit, ?force, "advance?");
|
||||
let Some(commit) = commit else {
|
||||
tracing::debug!("next is on dev - not advancing next");
|
||||
self.alert_tui("Next is on Dev").await?;
|
||||
return Ok(());
|
||||
if let Some(commit) = &commit {
|
||||
self.update_tui(RepoUpdate::AdvancingNext {
|
||||
commit: commit.clone(),
|
||||
force: force.clone(),
|
||||
})
|
||||
.await?;
|
||||
};
|
||||
tracing::debug!(?commit, "we have a commit to advance to");
|
||||
self.update_tui(RepoUpdate::AdvancingNext {
|
||||
commit: commit.clone(),
|
||||
force: force.clone(),
|
||||
})
|
||||
.await?;
|
||||
match advance_next(
|
||||
commit,
|
||||
force,
|
||||
|
@ -74,9 +61,7 @@ impl Message<AdvanceNext> for RepoActor {
|
|||
Err(err) => self.alert_tui(format!("fetching: {err}")).await?,
|
||||
}
|
||||
// INFO: pause to allow any CI checks to be started
|
||||
tracing::debug!(duration_ms = %self.sleep_duration.as_millis(), "sleeping to allow CI to start");
|
||||
tokio::time::sleep(self.sleep_duration).await;
|
||||
tracing::debug!("sleeping for CI finished");
|
||||
Ok(do_send(
|
||||
&ctx.actor_ref(),
|
||||
ValidateRepo::new(message_token),
|
|
@ -3,7 +3,7 @@ use color_eyre::Result;
|
|||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, instrument, warn};
|
||||
|
||||
use crate::core::git;
|
||||
use git_next_core::git;
|
||||
|
||||
use crate::{
|
||||
repo::{
|
|
@ -3,7 +3,7 @@ use color_eyre::Result;
|
|||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
use crate::core::git::UserNotification;
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
use crate::{
|
||||
repo::{
|
|
@ -3,7 +3,7 @@ use color_eyre::Result;
|
|||
use kameo::message::{Context, Message};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::core::git::{forge::commit::Status, graph, UserNotification};
|
||||
use git_next_core::git::{forge::commit::Status, graph, UserNotification};
|
||||
|
||||
use crate::{
|
||||
repo::{
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
|
||||
use crate::core::git::UserNotification;
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
impl Message<RegisterWebhook> for RepoActor {
|
||||
type Reply = Result<()>;
|
|
@ -1,16 +1,10 @@
|
|||
//
|
||||
use std::collections::HashMap;
|
||||
|
||||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use crate::core::git::{
|
||||
push::Force,
|
||||
validation::positions::{validate, Positions, PositionsError},
|
||||
UserNotification,
|
||||
};
|
||||
use crate::s;
|
||||
use crate::Result;
|
||||
use crate::{
|
||||
repo::{
|
||||
do_send, logger,
|
||||
|
@ -19,6 +13,12 @@ use crate::{
|
|||
},
|
||||
server::actor::messages::RepoUpdate,
|
||||
};
|
||||
use git_next_core::git::{
|
||||
push::Force,
|
||||
validation::positions::{validate, Error, Positions},
|
||||
UserNotification,
|
||||
};
|
||||
use git_next_core::s;
|
||||
|
||||
impl Message<ValidateRepo> for RepoActor {
|
||||
type Reply = Result<()>;
|
||||
|
@ -69,7 +69,6 @@ impl Message<ValidateRepo> for RepoActor {
|
|||
return Ok(());
|
||||
};
|
||||
logger(self.log.as_ref(), "have repo config").await;
|
||||
info!("validating positions");
|
||||
match validate(&**open_repository, &self.repo_details, &repo_config) {
|
||||
Ok((
|
||||
Positions {
|
||||
|
@ -81,7 +80,6 @@ impl Message<ValidateRepo> for RepoActor {
|
|||
},
|
||||
git_log,
|
||||
)) => {
|
||||
info!("positions - ok");
|
||||
let mut positions = HashMap::new();
|
||||
positions
|
||||
.entry(s!(s!(main.sha())[0..7]))
|
||||
|
@ -122,14 +120,13 @@ impl Message<ValidateRepo> for RepoActor {
|
|||
self.log.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
info!("advance next sent");
|
||||
} else {
|
||||
info!("do nothing");
|
||||
self.update_tui(RepoUpdate::Okay { main, next, dev })
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(PositionsError::Retryable(message)) => {
|
||||
Err(Error::Retryable(message)) => {
|
||||
warn!(?message, "Retryable");
|
||||
self.alert_tui(format!("retryable: {message}")).await?;
|
||||
logger(self.log.as_ref(), message).await;
|
||||
|
@ -142,7 +139,7 @@ impl Message<ValidateRepo> for RepoActor {
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
Err(PositionsError::UserIntervention(user_notification)) => {
|
||||
Err(Error::UserIntervention(user_notification)) => {
|
||||
warn!(?user_notification, "User Intervention");
|
||||
self.alert_tui(format!("USER INTERVENTION: {user_notification}"))
|
||||
.await?;
|
||||
|
@ -158,7 +155,7 @@ impl Message<ValidateRepo> for RepoActor {
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
Err(PositionsError::NonRetryable(message)) => {
|
||||
Err(Error::NonRetryable(message)) => {
|
||||
warn!(?message, "NonRetryable");
|
||||
self.alert_tui(format!("Error: {message}")).await?;
|
||||
logger(self.log.as_ref(), message).await;
|
|
@ -3,7 +3,7 @@ use color_eyre::Result;
|
|||
use kameo::message::{Context, Message};
|
||||
use tracing::{info, instrument, warn};
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{Commit, ForgeLike},
|
||||
webhook::{push::Branch, Push},
|
||||
BranchName, WebhookAuth,
|
|
@ -1,14 +1,12 @@
|
|||
//
|
||||
use crate::{
|
||||
core::{
|
||||
git::{repository::open::OpenRepositoryLike, RepoDetails},
|
||||
BranchName, RepoConfig,
|
||||
},
|
||||
err, Result,
|
||||
use git_next_core::{
|
||||
git::{repository::open::OpenRepositoryLike, RepoDetails},
|
||||
BranchName, RepoConfig,
|
||||
};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use derive_more::Display;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
/// Loads the [RepoConfig] from the `.git-next.toml` file in the repository
|
||||
|
@ -32,6 +30,26 @@ fn required_branch(branch_name: &BranchName, branches: &[BranchName]) -> Result<
|
|||
branches
|
||||
.iter()
|
||||
.find(|branch| *branch == branch_name)
|
||||
.ok_or_else(|| err!(branch_name.clone()))?;
|
||||
.ok_or_else(|| Error::BranchNotFound(branch_name.clone()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error, Display)]
|
||||
pub enum Error {
|
||||
#[display("file")]
|
||||
File(#[from] crate::git::file::Error),
|
||||
|
||||
#[display("config")]
|
||||
Config(#[from] git_next_core::server::Error),
|
||||
|
||||
#[display("toml")]
|
||||
Toml(#[from] toml::de::Error),
|
||||
|
||||
#[display("push")]
|
||||
Push(#[from] crate::git::push::Error),
|
||||
|
||||
#[display("branch not found: {}", 0)]
|
||||
BranchNotFound(BranchName),
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
//
|
||||
use derive_more::Display;
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{forge::commit::Status, Commit, UserNotification},
|
||||
ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId,
|
||||
message, newtype, ForgeNotification, RegisteredWebhook, RepoConfig, WebhookAuth, WebhookId,
|
||||
};
|
||||
use crate::message;
|
||||
use crate::newtype;
|
||||
|
||||
message!(
|
||||
LoadConfigFromRepo,
|
|
@ -8,7 +8,7 @@ use kxio::net::Net;
|
|||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, info, instrument, warn};
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{
|
||||
self,
|
||||
repository::{factory::RepositoryFactory, open::OpenRepositoryLike},
|
90
crates/cli/src/repo/notifications.rs
Normal file
90
crates/cli/src/repo/notifications.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
//
|
||||
|
||||
use crate::repo::messages::NotifyUser;
|
||||
|
||||
use git_next_core::git::UserNotification;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
impl NotifyUser {
|
||||
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
|
||||
let timestamp = timestamp.unix_timestamp().to_string();
|
||||
match &**self {
|
||||
UserNotification::CICheckFailed {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
commit,
|
||||
log,
|
||||
} => json!({
|
||||
"type": "cicheck.failed",
|
||||
"timestamp": timestamp,
|
||||
"data": {
|
||||
"forge_alias": forge_alias,
|
||||
"repo_alias": repo_alias,
|
||||
"commit": {
|
||||
"sha": commit.sha(),
|
||||
"message": commit.message()
|
||||
},
|
||||
"log": **log
|
||||
}
|
||||
}),
|
||||
UserNotification::RepoConfigLoadFailure {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
reason,
|
||||
} => json!({
|
||||
"type": "config.load.failed",
|
||||
"timestamp": timestamp,
|
||||
"data": {
|
||||
"forge_alias": forge_alias,
|
||||
"repo_alias": repo_alias,
|
||||
"reason": reason
|
||||
}
|
||||
}),
|
||||
UserNotification::WebhookRegistration {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
reason,
|
||||
} => json!({
|
||||
"type": "webhook.registration.failed",
|
||||
"timestamp": timestamp,
|
||||
"data": {
|
||||
"forge_alias": forge_alias,
|
||||
"repo_alias": repo_alias,
|
||||
"reason": reason
|
||||
}
|
||||
}),
|
||||
UserNotification::DevNotBasedOnMain {
|
||||
forge_alias,
|
||||
repo_alias,
|
||||
dev_branch,
|
||||
main_branch,
|
||||
dev_commit,
|
||||
main_commit,
|
||||
log,
|
||||
} => json!({
|
||||
"type": "branch.dev.not-on-main",
|
||||
"timestamp": timestamp,
|
||||
"data": {
|
||||
"forge_alias": forge_alias,
|
||||
"repo_alias": repo_alias,
|
||||
"branches": {
|
||||
"dev": dev_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,6 +1,7 @@
|
|||
use crate::git;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
use crate::err;
|
||||
|
||||
#[test]
|
||||
fn push_is_error_should_error() {
|
||||
|
@ -9,11 +10,14 @@ fn push_is_error_should_error() {
|
|||
let repo_config = given::a_repo_config();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
expect::fetch_ok(&mut open_repository);
|
||||
expect::push(&mut open_repository, Err(err!("on-push")));
|
||||
expect::push(&mut open_repository, Err(git::push::Error::Lock));
|
||||
let_assert!(
|
||||
Err(err) = branch::advance_main(commit, &repo_details, &repo_config, &open_repository)
|
||||
);
|
||||
assert_eq!(err.to_string(), "on-push");
|
||||
assert!(matches!(
|
||||
err,
|
||||
branch::Error::Push(crate::git::push::Error::Lock)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
|
@ -1,6 +1,5 @@
|
|||
//
|
||||
use crate::repo::branch::{find_next_commit_on_dev, AdvanceError};
|
||||
use crate::Result;
|
||||
use crate::repo::branch::find_next_commit_on_dev;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -12,12 +11,9 @@ fn advance_next_sut(
|
|||
repo_config: &RepoConfig,
|
||||
open_repository: &dyn OpenRepositoryLike,
|
||||
message_token: MessageToken,
|
||||
) -> Result<MessageToken, AdvanceError> {
|
||||
) -> branch::Result<MessageToken> {
|
||||
let (commit, force) = find_next_commit_on_dev(next, main, dev_commit_history);
|
||||
let Some(commit) = commit else {
|
||||
return Err(AdvanceError::NextIsDev);
|
||||
};
|
||||
crate::repo::branch::advance_next(
|
||||
branch::advance_next(
|
||||
commit,
|
||||
force,
|
||||
repo_details,
|
||||
|
@ -53,7 +49,7 @@ mod when_at_dev {
|
|||
)
|
||||
);
|
||||
tracing::debug!("Got: {err}");
|
||||
assert_eq!(err, AdvanceError::NextIsDev);
|
||||
assert!(matches!(err, branch::Error::NextAtDev));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,28 +84,24 @@ mod can_advance {
|
|||
)
|
||||
);
|
||||
tracing::debug!("Got: {err}");
|
||||
assert_eq!(err, AdvanceError::NextCommitIsWIP);
|
||||
assert!(matches!(err, branch::Error::IsWorkInProgress));
|
||||
}
|
||||
}
|
||||
|
||||
mod to_invalid_commit {
|
||||
use crate::err;
|
||||
|
||||
// commit on dev is either invalid message or a WIP
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn should_not_push_on_error() {
|
||||
fn should_not_push_and_error() {
|
||||
let next = given::a_commit();
|
||||
let main = &next;
|
||||
let dev = given::a_commit();
|
||||
let dev_commit_history = &[dev, next.clone()];
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.return_once(|| Err(err!("test")));
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
// no on_push defined - so any call to push will cause an error
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Err(err) = advance_next_sut(
|
||||
|
@ -123,7 +115,11 @@ mod can_advance {
|
|||
)
|
||||
);
|
||||
tracing::debug!("Got: {err}");
|
||||
assert_eq!(err.to_string(), "test");
|
||||
assert!(matches!(
|
||||
err,
|
||||
branch::Error::InvalidCommitMessage{reason}
|
||||
if reason == "Missing type in the commit summary, expected `type: description`"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,8 +128,6 @@ mod can_advance {
|
|||
use super::*;
|
||||
|
||||
mod push_is_err {
|
||||
use crate::{err, s};
|
||||
|
||||
// the git push command fails
|
||||
use super::*;
|
||||
|
||||
|
@ -147,7 +141,7 @@ mod can_advance {
|
|||
let repo_config = given::a_repo_config();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
expect::fetch_ok(&mut open_repository);
|
||||
expect::push(&mut open_repository, Err(err!("on-push")));
|
||||
expect::push(&mut open_repository, Err(git::push::Error::Lock));
|
||||
let message_token = given::a_message_token();
|
||||
let_assert!(
|
||||
Err(err) = advance_next_sut(
|
||||
|
@ -161,7 +155,7 @@ mod can_advance {
|
|||
)
|
||||
);
|
||||
tracing::debug!("Got: {err:?}");
|
||||
assert_eq!(err, AdvanceError::Unexpected(s!("on-push")));
|
||||
assert!(matches!(err, branch::Error::Push(git::push::Error::Lock)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::core::git::push::Force;
|
||||
use crate::core::git::GitRef;
|
||||
use git_next_core::git::push::Force;
|
||||
use git_next_core::git::GitRef;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
@ -7,6 +7,7 @@ use super::*;
|
|||
mod advance_main;
|
||||
mod advance_next;
|
||||
|
||||
use crate::git;
|
||||
use crate::repo::branch;
|
||||
|
||||
#[tokio::test]
|
|
@ -1,4 +1,4 @@
|
|||
use crate::Result;
|
||||
use git_next_core::git::fetch;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
@ -7,7 +7,7 @@ pub fn fetch_ok(open_repository: &mut MockOpenRepositoryLike) {
|
|||
expect::fetch(open_repository, Ok(()));
|
||||
}
|
||||
|
||||
pub fn fetch(open_repository: &mut MockOpenRepositoryLike, result: Result<()>) {
|
||||
pub fn fetch(open_repository: &mut MockOpenRepositoryLike, result: Result<(), fetch::Error>) {
|
||||
open_repository
|
||||
.expect_fetch()
|
||||
.times(1)
|
||||
|
@ -18,7 +18,10 @@ pub fn push_ok(open_repository: &mut MockOpenRepositoryLike) {
|
|||
expect::push(open_repository, Ok(()));
|
||||
}
|
||||
|
||||
pub fn push(open_repository: &mut MockOpenRepositoryLike, result: Result<()>) {
|
||||
pub fn push(
|
||||
open_repository: &mut MockOpenRepositoryLike,
|
||||
result: Result<(), crate::git::push::Error>,
|
||||
) {
|
||||
open_repository
|
||||
.expect_push()
|
||||
.times(1)
|
|
@ -1,16 +1,15 @@
|
|||
use crate::{
|
||||
alerts::{AlertsActor, History},
|
||||
server::{actor::messages::ServerUpdate, ServerActor},
|
||||
};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
use git::forge::MockForgeLike;
|
||||
use git_next_core::server::ListenUrl;
|
||||
use kameo::actor::{pubsub::PubSub, ActorRef};
|
||||
use kxio::{fs::FileSystem, net::Net};
|
||||
|
||||
use crate::{
|
||||
alerts::{AlertsActor, History},
|
||||
core::server::ListenUrl,
|
||||
server::{actor::messages::ServerUpdate, ServerActor},
|
||||
};
|
||||
|
||||
pub fn has_all_valid_remote_defaults(
|
||||
open_repository: &mut MockOpenRepositoryLike,
|
||||
repo_details: &RepoDetails,
|
||||
|
@ -149,7 +148,7 @@ pub fn a_commit_with_message(message: impl Into<crate::git::commit::Message>) ->
|
|||
}
|
||||
|
||||
pub fn a_commit_message() -> crate::git::commit::Message {
|
||||
crate::git::commit::Message::new(format!("test: {}", a_name()))
|
||||
crate::git::commit::Message::new(a_name())
|
||||
}
|
||||
|
||||
pub fn a_named_commit_sha(name: impl Into<String>) -> Sha {
|
|
@ -1,10 +1,10 @@
|
|||
use crate::{repo::messages::AdvanceMain, tell, Result};
|
||||
use crate::{repo::messages::AdvanceMain, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_repo_config_should_fetch_then_push_then_revalidate() -> Result<()> {
|
||||
async fn when_repo_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -49,7 +49,7 @@ async fn when_repo_config_should_fetch_then_push_then_revalidate() -> Result<()>
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_app_config_should_fetch_then_push_then_revalidate() -> Result<()> {
|
||||
async fn when_app_config_should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
|
@ -2,14 +2,14 @@ use std::time::Duration;
|
|||
|
||||
use crate::{
|
||||
repo::messages::{AdvanceNext, AdvanceNextPayload},
|
||||
tell, Result,
|
||||
tell,
|
||||
};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_fetch_then_push_then_revalidate() -> Result<()> {
|
||||
async fn should_fetch_then_push_then_revalidate() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
|
@ -1,17 +1,15 @@
|
|||
use git::forge::MockForgeLike;
|
||||
|
||||
use crate::{repo::messages::CheckCIStatus, tell, Result};
|
||||
use crate::{repo::messages::CheckCIStatus, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_passthrough_to_receive_ci_status() -> Result<()> {
|
||||
async fn should_passthrough_to_receive_ci_status() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
let next_commit = given::a_named_commit("next");
|
||||
let mut forge = MockForgeLike::new();
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge
|
||||
.expect_commit_status()
|
||||
.with(mockall::predicate::eq(next_commit.clone()))
|
|
@ -1,12 +1,12 @@
|
|||
use kxio::net::Net;
|
||||
|
||||
use crate::{err, tell, Result};
|
||||
use crate::tell;
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_clone() -> Result<()> {
|
||||
async fn should_clone() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, /* mut */ repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -35,22 +35,21 @@ async fn should_clone() -> Result<()> {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
//then
|
||||
tick(1).await;
|
||||
cloned
|
||||
.read()
|
||||
.map_err(|e| err!(e.to_string()))
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|o| assert_eq!(o.len(), 1))?;
|
||||
net.assert_no_unused_plans();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_open() -> Result<()> {
|
||||
async fn should_open() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -79,7 +78,7 @@ async fn should_open() -> Result<()> {
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
|
||||
|
@ -87,9 +86,8 @@ async fn should_open() -> Result<()> {
|
|||
tick(1).await;
|
||||
opened
|
||||
.read()
|
||||
.map_err(|e| err!(e.to_string()))
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|o| assert_eq!(o.len(), 1))?;
|
||||
net.assert_no_unused_plans();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -98,7 +96,7 @@ async fn should_open() -> Result<()> {
|
|||
/// branches. When it doesn't we should load the `.git-next.yaml` from from the
|
||||
/// repo and get the branch names from there by sending a [LoadConfigFromRepo] message.
|
||||
#[tokio::test]
|
||||
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> Result<()> {
|
||||
async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, mut repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -122,14 +120,13 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> Result<(
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
|
||||
//then
|
||||
log.require_message_containing("send: LoadConfigFromRepo")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -137,7 +134,7 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> Result<(
|
|||
/// The server config can optionally include the names of the main, next and dev
|
||||
/// branches. When it does we should register the webhook by sending [RegisterWebhook] message.
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_server_has_repo_config_should_send_register_webhook() -> Result<()> {
|
||||
async fn when_server_has_repo_config_should_send_register_webhook() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -159,16 +156,15 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> Result<()
|
|||
repo_details,
|
||||
given::a_forge(),
|
||||
fs.as_real(),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
tell!(addr, CloneRepo::new())?;
|
||||
|
||||
//then
|
||||
tick(1).await;
|
||||
tracing::debug!(?log, "");
|
||||
debug!(?log, "");
|
||||
log.require_message_containing("send: RegisterWebhook")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
use crate::{err, repo::messages::LoadConfigFromRepo, tell, Result};
|
||||
use crate::{repo::messages::LoadConfigFromRepo, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_read_file_ok_should_send_config_loaded() -> Result<()> {
|
||||
async fn when_read_file_ok_should_send_config_loaded() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -46,13 +46,13 @@ async fn when_read_file_ok_should_send_config_loaded() -> Result<()> {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_read_file_err_should_notify_user() -> Result<()> {
|
||||
async fn when_read_file_err_should_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_read_file()
|
||||
.return_once(move |_, _| Err(err!("FileNotFound")));
|
||||
.return_once(move |_, _| Err(git::file::Error::FileNotFound));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
|
@ -1,10 +1,10 @@
|
|||
//
|
||||
use crate::{err, repo::messages::ReceiveRepoConfig, tell, Result};
|
||||
use crate::{repo::messages::ReceiveRepoConfig, tell};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_store_repo_config_in_actor() -> Result<()> {
|
||||
async fn should_store_repo_config_in_actor() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -25,7 +25,7 @@ async fn should_store_repo_config_in_actor() -> Result<()> {
|
|||
let reo_actor_view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| err!("examine actor: {e:?}"))?;
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(
|
||||
reo_actor_view.repo_details.repo_config,
|
||||
Some(new_repo_config)
|
||||
|
@ -34,7 +34,7 @@ async fn should_store_repo_config_in_actor() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_register_webhook() -> Result<()> {
|
||||
async fn should_register_webhook() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
|
@ -2,13 +2,13 @@ use std::time::Duration;
|
|||
|
||||
use git::forge::commit::Status;
|
||||
|
||||
use crate::{repo::messages::ReceiveCIStatus, tell, Result};
|
||||
use crate::{repo::messages::ReceiveCIStatus, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_pass_should_advance_main_to_next() -> Result<()> {
|
||||
async fn when_pass_should_advance_main_to_next() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -33,7 +33,7 @@ async fn when_pass_should_advance_main_to_next() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_pending_should_recheck_ci_status() -> Result<()> {
|
||||
async fn when_pending_should_recheck_ci_status() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -58,7 +58,7 @@ async fn when_pending_should_recheck_ci_status() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_fail_should_recheck_after_delay() -> Result<()> {
|
||||
async fn when_fail_should_recheck_after_delay() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -82,7 +82,7 @@ async fn when_fail_should_recheck_after_delay() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn when_fail_should_notify_user() -> Result<()> {
|
||||
async fn when_fail_should_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
|
@ -1,18 +1,16 @@
|
|||
use git::forge::MockForgeLike;
|
||||
|
||||
use crate::{repo::messages::RegisterWebhook, tell, Result};
|
||||
use crate::{repo::messages::RegisterWebhook, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_registered_ok_should_send_webhook_registered() -> Result<()> {
|
||||
async fn when_registered_ok_should_send_webhook_registered() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
||||
let registered_webhook = given::a_registered_webhook();
|
||||
let mut forge = MockForgeLike::new();
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge
|
||||
.expect_register_webhook()
|
||||
.return_once(move |_| Ok(registered_webhook));
|
||||
|
@ -32,12 +30,12 @@ async fn when_registered_ok_should_send_webhook_registered() -> Result<()> {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_registered_error_should_send_notify_user() -> Result<()> {
|
||||
async fn when_registered_error_should_send_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
||||
let mut forge = MockForgeLike::new();
|
||||
let mut forge = git::MockForgeLike::new();
|
||||
forge.expect_register_webhook().return_once(move |_| {
|
||||
Err(git::forge::webhook::Error::FailedToRegister(
|
||||
"foo".to_string(),
|
|
@ -2,16 +2,15 @@
|
|||
use kxio::net::Net;
|
||||
|
||||
use crate::{
|
||||
err,
|
||||
repo::messages::{AdvanceNext, AdvanceNextPayload, ValidateRepo},
|
||||
tell, Result,
|
||||
tell,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_to_main(
|
||||
) -> Result<()> {
|
||||
) -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -22,8 +21,7 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
|
|||
// commit_log main
|
||||
let main_commit = expect::main_commit_log(&mut open_repository, branches.main());
|
||||
// next - based on main
|
||||
let next_commit = given::a_commit();
|
||||
let next_branch_log = vec![next_commit.clone(), main_commit.clone()];
|
||||
let next_branch_log = vec![given::a_commit(), main_commit.clone()];
|
||||
// dev - based on main, but not on next
|
||||
let dev_branch_log = vec![main_commit.clone()];
|
||||
// commit_log next - based on main, but not a parent of dev
|
||||
|
@ -33,12 +31,11 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
|
|||
.with(eq(branches.next()), eq([main_commit.clone()]))
|
||||
.return_once(move |_, _| Ok(next_branch_log));
|
||||
// commit_log dev
|
||||
let dev_branch_log_clone = dev_branch_log.clone();
|
||||
open_repository
|
||||
.expect_commit_log()
|
||||
.times(1)
|
||||
.with(eq(branches.dev()), eq([main_commit.clone()]))
|
||||
.return_once(|_, _| Ok(dev_branch_log_clone));
|
||||
.with(eq(branches.dev()), eq([main_commit]))
|
||||
.return_once(|_, _| Ok(dev_branch_log));
|
||||
// expect to reset the branch
|
||||
expect::fetch_ok(&mut open_repository);
|
||||
expect::push_ok(&mut open_repository);
|
||||
|
@ -51,7 +48,7 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
|
|||
);
|
||||
tell!(addr, ValidateRepo::new(MessageToken::default()))?;
|
||||
|
||||
// then
|
||||
//then
|
||||
log.require_message_containing(format!("Branch {} has been reset", branches.next()))
|
||||
.await?;
|
||||
Ok(())
|
||||
|
@ -59,7 +56,7 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_on_main_should_be_reset_t
|
|||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_reset_to_dev(
|
||||
) -> Result<()> {
|
||||
) -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -114,7 +111,7 @@ async fn repo_with_next_not_an_ancestor_of_dev_and_dev_ahead_of_main_should_be_r
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_on_or_near_main_should_be_reset() -> Result<()> {
|
||||
async fn repo_with_next_not_on_or_near_main_should_be_reset() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -159,7 +156,7 @@ async fn repo_with_next_not_on_or_near_main_should_be_reset() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_not_based_on_main_should_be_reset() -> Result<()> {
|
||||
async fn repo_with_next_not_based_on_main_should_be_reset() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -204,7 +201,7 @@ async fn repo_with_next_not_based_on_main_should_be_reset() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> Result<()> {
|
||||
async fn repo_with_next_ahead_of_main_should_check_ci_status() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -248,7 +245,7 @@ async fn repo_with_next_ahead_of_main_should_check_ci_status() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_dev_and_next_on_main_should_do_nothing() -> Result<()> {
|
||||
async fn repo_with_dev_and_next_on_main_should_do_nothing() -> TestResult {
|
||||
// Do nothing, when the situation changes we will hear about it via a webhook
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
|
@ -292,7 +289,7 @@ async fn repo_with_dev_and_next_on_main_should_do_nothing() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_dev_ahead_of_next_should_advance_next() -> Result<()> {
|
||||
async fn repo_with_dev_ahead_of_next_should_advance_next() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -343,7 +340,7 @@ async fn repo_with_dev_ahead_of_next_should_advance_next() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> Result<()> {
|
||||
async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -388,7 +385,7 @@ async fn repo_with_dev_not_ahead_of_main_should_notify_user() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_accept_message_with_current_token() -> Result<()> {
|
||||
async fn should_accept_message_with_current_token() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -396,10 +393,10 @@ async fn should_accept_message_with_current_token() -> Result<()> {
|
|||
let server_actor_ref = given::a_server_actor(fs.as_real(), net.clone());
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(git::repository::factory::mock()),
|
||||
git::repository::factory::mock(),
|
||||
given::a_forge(),
|
||||
server_actor_ref,
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
||||
let addr = kameo::spawn(actor);
|
||||
|
@ -409,12 +406,11 @@ async fn should_accept_message_with_current_token() -> Result<()> {
|
|||
|
||||
//then
|
||||
log.require_message_containing("accepted token: 2").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_accept_message_with_new_token() -> Result<()> {
|
||||
async fn should_accept_message_with_new_token() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -423,10 +419,10 @@ async fn should_accept_message_with_new_token() -> Result<()> {
|
|||
//when
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(git::repository::factory::mock()),
|
||||
git::repository::factory::mock(),
|
||||
given::a_forge(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_message_token(MessageToken::new(2_u32));
|
||||
let addr = kameo::spawn(actor);
|
||||
|
@ -434,12 +430,11 @@ async fn should_accept_message_with_new_token() -> Result<()> {
|
|||
|
||||
//then
|
||||
log.require_message_containing("accepted token: 3").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_reject_message_with_expired_token() -> Result<()> {
|
||||
async fn should_reject_message_with_expired_token() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -448,10 +443,10 @@ async fn should_reject_message_with_expired_token() -> Result<()> {
|
|||
//when
|
||||
let (actor, log) = given::a_repo_actor(
|
||||
repo_details,
|
||||
Box::new(git::repository::factory::mock()),
|
||||
git::repository::factory::mock(),
|
||||
given::a_forge(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_message_token(MessageToken::new(4_u32));
|
||||
let addr = kameo::spawn(actor);
|
||||
|
@ -459,20 +454,19 @@ async fn should_reject_message_with_expired_token() -> Result<()> {
|
|||
|
||||
//then
|
||||
log.no_message_contains("accepted token").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
// NOTE: failed then passed on retry: count = 6
|
||||
async fn should_send_validate_repo_when_retryable_error() -> Result<()> {
|
||||
async fn should_send_validate_repo_when_retryable_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository.expect_fetch().return_once(|| Ok(()));
|
||||
open_repository
|
||||
.expect_commit_log()
|
||||
.return_once(|_, _| Err(err!("Lock")));
|
||||
.return_once(|_, _| Err(git::commit::log::Error::Lock));
|
||||
|
||||
//when
|
||||
let (addr, log) = when::start_actor_with_open_repository(
|
||||
|
@ -490,7 +484,7 @@ async fn should_send_validate_repo_when_retryable_error() -> Result<()> {
|
|||
}
|
||||
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn should_send_notify_user_when_non_retryable_error() -> Result<()> {
|
||||
async fn should_send_notify_user_when_non_retryable_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
|
@ -1,12 +1,12 @@
|
|||
use kxio::net::Net;
|
||||
|
||||
use crate::{err, repo::messages::WebhookNotification, tell, Result};
|
||||
use crate::{repo::messages::WebhookNotification, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_no_expected_auth_token_drop_notification() -> Result<()> {
|
||||
async fn when_no_expected_auth_token_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -22,7 +22,7 @@ async fn when_no_expected_auth_token_drop_notification() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
given::a_forge(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(None);
|
||||
|
||||
|
@ -36,12 +36,11 @@ async fn when_no_expected_auth_token_drop_notification() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("server has no auth token")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_no_repo_config_drop_notification() -> Result<()> {
|
||||
async fn when_no_repo_config_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs).with_repo_config(None);
|
||||
|
@ -57,7 +56,7 @@ async fn when_no_repo_config_drop_notification() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
given::a_forge(),
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
|
@ -71,12 +70,11 @@ async fn when_no_repo_config_drop_notification() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("server has no repo config")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_auth_is_invalid_drop_notification() -> Result<()> {
|
||||
async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -96,7 +94,7 @@ async fn when_message_auth_is_invalid_drop_notification() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
|
@ -110,12 +108,11 @@ async fn when_message_auth_is_invalid_drop_notification() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("message authorisation is invalid")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_ignorable_drop_notification() -> Result<()> {
|
||||
async fn when_message_is_ignorable_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -139,7 +136,7 @@ async fn when_message_is_ignorable_drop_notification() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
|
@ -153,12 +150,11 @@ async fn when_message_is_ignorable_drop_notification() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("forge sent ignorable message")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_not_a_push_drop_notification() -> Result<()> {
|
||||
async fn when_message_is_not_a_push_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_details = given::repo_details(&fs);
|
||||
|
@ -182,7 +178,7 @@ async fn when_message_is_not_a_push_drop_notification() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
|
||||
|
||||
|
@ -196,12 +192,11 @@ async fn when_message_is_not_a_push_drop_notification() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("message parse error - not a push")
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_on_unknown_branch_drop_notification() -> Result<()> {
|
||||
async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -229,7 +224,7 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> Result<()
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -244,12 +239,11 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> Result<()
|
|||
//then
|
||||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing("unknown branch").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_already_seen_commit_to_main() -> Result<()> {
|
||||
async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -278,7 +272,7 @@ async fn when_message_is_push_already_seen_commit_to_main() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -294,12 +288,11 @@ async fn when_message_is_push_already_seen_commit_to_main() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing(format!("not a new commit on {main}"))
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_already_seen_commit_to_next() -> Result<()> {
|
||||
async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -328,7 +321,7 @@ async fn when_message_is_push_already_seen_commit_to_next() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -344,12 +337,11 @@ async fn when_message_is_push_already_seen_commit_to_next() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing(format!("not a new commit on {next_branch}"))
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_already_seen_commit_to_dev() -> Result<()> {
|
||||
async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -378,7 +370,7 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> Result<()> {
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -394,12 +386,11 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> Result<()> {
|
|||
log.no_message_contains("send").await?;
|
||||
log.require_message_containing(format!("not a new commit on {dev}"))
|
||||
.await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> Result<()> {
|
||||
async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -427,7 +418,7 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -441,15 +432,14 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
|
|||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| err!("examine actor: {e:?}"))?;
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.last_main_commit, Some(push_commit));
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> Result<()> {
|
||||
async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -477,7 +467,7 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -491,15 +481,14 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
|
|||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| err!("examine actor: {e:?}"))?;
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.last_next_commit, Some(push_commit));
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> Result<()> {
|
||||
async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let repo_config = given::a_repo_config();
|
||||
|
@ -527,7 +516,7 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
|
|||
Box::new(repository_factory),
|
||||
forge,
|
||||
given::a_server_actor(fs.as_real(), net.clone()),
|
||||
net.clone(),
|
||||
net,
|
||||
);
|
||||
let actor = actor
|
||||
.with_webhook_auth(Some(given::a_webhook_auth()))
|
||||
|
@ -541,9 +530,8 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
|
|||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| err!("examine actor: {e:?}"))?;
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.last_dev_commit, Some(push_commit));
|
||||
log.require_message_containing("send: ValidateRepo").await?;
|
||||
net.assert_no_unused_plans();
|
||||
Ok(())
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
use crate::{err, repo::messages::WebhookRegistered, tell, Result};
|
||||
use crate::{repo::messages::WebhookRegistered, tell};
|
||||
|
||||
//
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_store_webhook_details() -> Result<()> {
|
||||
async fn should_store_webhook_details() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -27,14 +27,14 @@ async fn should_store_webhook_details() -> Result<()> {
|
|||
let view = addr
|
||||
.ask(ExamineActor)
|
||||
.await
|
||||
.map_err(|e| err!("examine actor: {e:?}"))?;
|
||||
.map_err(|e| format!("examine actor: {e:?}"))?;
|
||||
assert_eq!(view.webhook_id, Some(webhook_id));
|
||||
assert_eq!(view.webhook_auth, Some(webhook_auth));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_send_validate_repo_message() -> Result<()> {
|
||||
async fn should_send_validate_repo_message() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (open_repository, repo_details) = given::an_open_repository(&fs);
|
|
@ -1,26 +1,29 @@
|
|||
//
|
||||
use super::*;
|
||||
|
||||
use crate::git::file;
|
||||
use crate::repo::load;
|
||||
use crate::{err, s, Result};
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_file_not_found_should_error() -> Result<()> {
|
||||
async fn when_file_not_found_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
open_repository
|
||||
.expect_read_file()
|
||||
.returning(|_, _| Err(err!("FileNotFound")));
|
||||
.returning(|_, _| Err(file::Error::FileNotFound));
|
||||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
tracing::debug!("Got: {err:?}");
|
||||
assert_eq!(err.to_string(), s!("FileNotFound"));
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(
|
||||
err,
|
||||
load::Error::File(crate::git::file::Error::FileNotFound)
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn when_file_format_invalid_should_error() -> Result<()> {
|
||||
async fn when_file_format_invalid_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -31,13 +34,13 @@ async fn when_file_format_invalid_should_error() -> Result<()> {
|
|||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
tracing::debug!("Got: {err:?}");
|
||||
assert!(err.to_string().starts_with("TOML parse error at"));
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::Toml(_)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_main_branch_is_missing_should_error() -> Result<()> {
|
||||
async fn when_main_branch_is_missing_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -63,13 +66,13 @@ async fn when_main_branch_is_missing_should_error() -> Result<()> {
|
|||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
tracing::debug!("Got: {err:?}");
|
||||
assert_eq!(err.to_string(), main.to_string());
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == main));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_next_branch_is_missing_should_error() -> Result<()> {
|
||||
async fn when_next_branch_is_missing_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -95,13 +98,13 @@ async fn when_next_branch_is_missing_should_error() -> Result<()> {
|
|||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
tracing::debug!("Got: {err:?}");
|
||||
assert_eq!(err.to_string(), next.to_string());
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == next));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_dev_branch_is_missing_should_error() -> Result<()> {
|
||||
async fn when_dev_branch_is_missing_should_error() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -127,13 +130,13 @@ async fn when_dev_branch_is_missing_should_error() -> Result<()> {
|
|||
//when
|
||||
let_assert!(Err(err) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
tracing::debug!("Got: {err:?}");
|
||||
assert_eq!(err.to_string(), dev.to_string());
|
||||
debug!("Got: {err:?}");
|
||||
assert!(matches!(err, load::Error::BranchNotFound(branch) if branch == dev));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn when_valid_file_should_return_repo_config() -> Result<()> {
|
||||
async fn when_valid_file_should_return_repo_config() -> TestResult {
|
||||
//given
|
||||
let fs = given::a_filesystem();
|
||||
let (mut open_repository, repo_details) = given::an_open_repository(&fs);
|
||||
|
@ -160,7 +163,7 @@ async fn when_valid_file_should_return_repo_config() -> Result<()> {
|
|||
//when
|
||||
let_assert!(Ok(result) = load::config_from_repository(&repo_details, &open_repository).await);
|
||||
//then
|
||||
tracing::debug!("Got: {result:?}");
|
||||
debug!("Got: {result:?}");
|
||||
assert_eq!(result, repo_config);
|
||||
Ok(())
|
||||
}
|
|
@ -5,10 +5,9 @@ use crate::{
|
|||
messages::{CloneRepo, MessageToken},
|
||||
ActorLog, RepoActor,
|
||||
},
|
||||
Result,
|
||||
};
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{
|
||||
commit::Sha,
|
||||
repository::{
|
||||
|
@ -16,14 +15,14 @@ use crate::core::{
|
|||
open::{MockOpenRepositoryLike, OpenRepositoryLike},
|
||||
Direction,
|
||||
},
|
||||
Commit, ForgeLike, Generation, RepoDetails,
|
||||
Commit, ForgeLike, Generation, MockForgeLike, RepoDetails,
|
||||
},
|
||||
message,
|
||||
webhook::{forge_notification::Body, Push},
|
||||
BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, Hostname,
|
||||
RegisteredWebhook, RemoteUrl, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource,
|
||||
ServerRepoConfig, StoragePathType, WebhookAuth, WebhookId,
|
||||
};
|
||||
use crate::message;
|
||||
|
||||
use assert2::let_assert;
|
||||
use kameo::{
|
||||
|
@ -31,6 +30,7 @@ use kameo::{
|
|||
Reply,
|
||||
};
|
||||
use mockall::predicate::eq;
|
||||
use tracing::{debug, error};
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
|
@ -38,6 +38,8 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
mod branch;
|
||||
mod expect;
|
||||
pub mod given;
|
||||
|
@ -53,9 +55,9 @@ impl ActorLog {
|
|||
pub async fn no_message_contains(
|
||||
&self,
|
||||
needle: impl AsRef<str> + Send + std::fmt::Display,
|
||||
) -> Result<()> {
|
||||
) -> TestResult {
|
||||
if self.find_in_messages(needle.as_ref()).await? {
|
||||
tracing::error!(?self, "");
|
||||
error!(?self, "");
|
||||
panic!("found unexpected message: {needle}");
|
||||
}
|
||||
Ok(())
|
||||
|
@ -64,24 +66,26 @@ impl ActorLog {
|
|||
pub async fn require_message_containing(
|
||||
&self,
|
||||
needle: impl AsRef<str> + Send + std::fmt::Display,
|
||||
) -> Result<()> {
|
||||
) -> TestResult {
|
||||
if !self.find_in_messages(needle.as_ref()).await? {
|
||||
tracing::error!(?self, "");
|
||||
error!(?self, "");
|
||||
panic!("expected message not found: {needle}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn find_in_messages(&self, needle: impl AsRef<str> + Send) -> Result<bool> {
|
||||
async fn find_in_messages(
|
||||
&self,
|
||||
needle: impl AsRef<str> + Send,
|
||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
// Very short sleep to allow tests to get a chance to tick
|
||||
// This should be enough for most tests.
|
||||
tracing::debug!(" ! sleeping...");
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
tracing::debug!(" ! {}", needle.as_ref());
|
||||
let found = self.read().await.iter().any(|message| {
|
||||
tracing::debug!(" ? {message}");
|
||||
message.contains(needle.as_ref())
|
||||
});
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
let found = self
|
||||
.read()
|
||||
.await
|
||||
.iter()
|
||||
.any(|message| message.contains(needle.as_ref()));
|
||||
Ok(found)
|
||||
}
|
||||
}
|
|
@ -31,8 +31,7 @@ pub fn start_actor_with_open_repository(
|
|||
let fs = given::a_filesystem();
|
||||
let net: Net = given::a_network().into();
|
||||
let server_actor_ref: ActorRef<ServerActor> = given::a_server_actor(fs.as_real(), net.clone());
|
||||
let (actor, log) =
|
||||
given::a_repo_actor(repo_details, Box::new(mock()), forge, server_actor_ref, net);
|
||||
let (actor, log) = given::a_repo_actor(repo_details, mock(), forge, server_actor_ref, net);
|
||||
let actor = actor.with_open_repository(Some(open_repository));
|
||||
(kameo::spawn(actor), log)
|
||||
}
|
|
@ -4,10 +4,9 @@
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::core::git::RepositoryFactory;
|
||||
use crate::s;
|
||||
use color_eyre::Result;
|
||||
use derive_more::derive::Constructor;
|
||||
use git_next_core::{git::RepositoryFactory, s};
|
||||
use kameo::{
|
||||
actor::{pubsub::PubSub, ActorRef},
|
||||
mailbox::unbounded::UnboundedMailbox,
|
|
@ -2,7 +2,7 @@
|
|||
use color_eyre::Result;
|
||||
use kameo::message::{Context, Message};
|
||||
|
||||
use crate::core::server::AppConfig;
|
||||
use git_next_core::server::AppConfig;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
|
@ -5,7 +5,7 @@ use kameo::{
|
|||
message::{Context, Message},
|
||||
};
|
||||
|
||||
use crate::core::{ForgeAlias, RepoAlias};
|
||||
use git_next_core::{ForgeAlias, RepoAlias};
|
||||
|
||||
use crate::{
|
||||
alerts::messages::UpdateShout,
|
|
@ -4,13 +4,13 @@ use std::net::SocketAddr;
|
|||
use derive_more::Constructor;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{self, forge::commit::Status, graph::Log, Commit},
|
||||
message,
|
||||
server::{AppConfig, Storage},
|
||||
webhook::{push::Branch, Push},
|
||||
ForgeAlias, RepoAlias, RepoBranches, RepoConfig,
|
||||
};
|
||||
use crate::message;
|
||||
|
||||
// receive server config
|
||||
message!(
|
|
@ -10,13 +10,11 @@ use kxio::{fs::FileSystem, net::Net};
|
|||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::{error, instrument, warn};
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
||||
server::{AppConfig, ListenUrl, Storage},
|
||||
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
||||
},
|
||||
err, Result,
|
||||
use git_next_core::{
|
||||
git::{repository::factory::RepositoryFactory, Generation, RepoDetails},
|
||||
s,
|
||||
server::{self, AppConfig, ListenUrl, Storage},
|
||||
ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig, StoragePathType,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -24,7 +22,7 @@ use crate::{
|
|||
forge::Forge,
|
||||
on_actor_link_died, on_actor_panic, on_actor_start, on_actor_stop,
|
||||
repo::RepoActor,
|
||||
s, spawn, tell,
|
||||
spawn, tell,
|
||||
webhook::{router::WebhookRouterActor, WebhookActor},
|
||||
MessageBus,
|
||||
};
|
||||
|
@ -37,6 +35,26 @@ mod tests;
|
|||
mod handlers;
|
||||
pub mod messages;
|
||||
|
||||
#[derive(Debug, derive_more::Display, derive_more::From)]
|
||||
pub enum Error {
|
||||
#[display("Failed to create data directories")]
|
||||
FailedToCreateDataDirectory(kxio::fs::Error),
|
||||
|
||||
#[display("The forge data path is not a directory: {path:?}")]
|
||||
ForgeDirIsNotDirectory {
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
Config(server::Error),
|
||||
|
||||
Io(std::io::Error),
|
||||
|
||||
General(String),
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
#[derive(derive_with::With)]
|
||||
#[with(message_log)]
|
||||
|
@ -139,7 +157,7 @@ impl ServerActor {
|
|||
let path_handle = self.fs.path(&path);
|
||||
if path_handle.exists()? {
|
||||
if !path_handle.is_dir()? {
|
||||
return Err(err!("ForgeDirIsNotDirectory {}", path.display()));
|
||||
return Err(Error::ForgeDirIsNotDirectory { path });
|
||||
}
|
||||
} else {
|
||||
tracing::info!(%forge_name, ?path_handle, "creating storage");
|
||||
|
@ -267,7 +285,7 @@ impl ServerActor {
|
|||
if let Some(t) = self.shutdown_trigger.take() {
|
||||
t.send(message)
|
||||
.await
|
||||
.map_err(|e| err!("failed sending shutdown trigger: {e:?}"))?;
|
||||
.map_err(|e| format!("failed sending shutdown trigger: {e:?}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -291,7 +309,7 @@ impl ServerActor {
|
|||
}
|
||||
if cfg!(not(test)) {
|
||||
tell!(ctx.actor_ref(), msg)
|
||||
.map_err(|e| err!("failed sending: {log_message}: {e:?}"))?;
|
||||
.map_err(|e| format!("failed sending: {log_message}: {e:?}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
|
||||
use kameo::actor::pubsub::PubSub;
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git,
|
||||
server::{AppConfig, Http, Listen, ListenUrl, Shout, Storage},
|
||||
};
|
||||
|
@ -23,8 +23,8 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
|
|||
//given
|
||||
// parameters
|
||||
let fs = given::a_filesystem();
|
||||
let mock_net = given::a_network();
|
||||
let alerts = given::an_alerts_actor(mock_net.clone().into());
|
||||
let net = given::a_network();
|
||||
let alerts = given::an_alerts_actor(net.clone().into());
|
||||
let repo = git::repository::factory::mock();
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
|
||||
|
@ -34,10 +34,10 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
|
|||
let server = ServerActor::new(
|
||||
false, // ui
|
||||
fs.as_real(),
|
||||
mock_net.clone().into(),
|
||||
net.into(),
|
||||
alerts,
|
||||
file_update_subs,
|
||||
Box::new(repo),
|
||||
repo,
|
||||
duration,
|
||||
);
|
||||
|
||||
|
@ -67,7 +67,6 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
|
|||
assert!(message_log.read().iter().any(|log| !log
|
||||
.iter()
|
||||
.any(|line| line == "send: ReceiveValidServerConfig")));
|
||||
mock_net.assert_no_unused_plans();
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -6,7 +6,7 @@ use kxio::{fs::FileSystem, net::Net};
|
|||
use tokio::sync::RwLock;
|
||||
use tracing::info;
|
||||
|
||||
use crate::core::git::RepositoryFactory;
|
||||
use git_next_core::git::RepositoryFactory;
|
||||
|
||||
use crate::{root::RootActor, tell};
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
//
|
||||
use crate::{
|
||||
core::{
|
||||
self as core,
|
||||
git::{self, repository::Direction, validation::remotes::validate_default_remotes},
|
||||
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource,
|
||||
RepoPath, StoragePathType, User,
|
||||
},
|
||||
Result,
|
||||
};
|
||||
use assert2::let_assert;
|
||||
use git_next_core::{
|
||||
self as core,
|
||||
git::{self, repository::Direction, validation::remotes::validate_default_remotes},
|
||||
ApiToken, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
||||
StoragePathType, User,
|
||||
};
|
||||
use secrecy::SecretString;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[test]
|
||||
fn test_repo_config_load() -> Result<()> {
|
||||
let toml = r#"[branches]
|
|
@ -1,9 +1,9 @@
|
|||
//
|
||||
mod init {
|
||||
use crate::Result;
|
||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
mod init {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn should_not_update_file_if_it_exists() -> Result<()> {
|
||||
fn should_not_update_file_if_it_exists() -> TestResult {
|
||||
let fs = kxio::fs::temp()?;
|
||||
let file = fs.base().join(".git-next.toml");
|
||||
fs.file(&file).write("contents")?;
|
||||
|
@ -20,7 +20,7 @@ mod init {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn should_create_default_file_if_not_exists() -> Result<()> {
|
||||
fn should_create_default_file_if_not_exists() -> TestResult {
|
||||
let fs = kxio::fs::temp()?;
|
||||
|
||||
crate::init::run(&fs)?;
|
||||
|
@ -38,28 +38,3 @@ mod init {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod errors {
|
||||
// all errors should be Send
|
||||
const fn is_send<T: Send>() {}
|
||||
|
||||
#[test]
|
||||
const fn core_git_file_error_is_send() {
|
||||
is_send::<crate::core::git::file::Error>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
const fn core_git_repository_error_is_send() {
|
||||
is_send::<crate::core::git::repository::Error>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
const fn core_git_forge_webhook_error_is_send() {
|
||||
is_send::<crate::core::git::forge::webhook::Error>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
const fn core_git_validation_remotes_error_is_send() {
|
||||
is_send::<crate::core::git::validation::remotes::Error>();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
//
|
||||
use crate::message;
|
||||
use git_next_core::message;
|
||||
|
||||
message!(Tick => std::io::Result<()>, "Update the TUI");
|
|
@ -23,7 +23,7 @@ pub use model::*;
|
|||
|
||||
use crate::tell;
|
||||
|
||||
use super::{components::key_focus::KeyFocus, Tick};
|
||||
use super::Tick;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Tui {
|
||||
|
@ -92,42 +92,25 @@ impl Tui {
|
|||
if key.kind != KeyEventKind::Press {
|
||||
return Ok(());
|
||||
}
|
||||
match self.state.key_focus {
|
||||
KeyFocus::None => match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
actor_tui.kill();
|
||||
}
|
||||
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();
|
||||
}
|
||||
KeyCode::Char('/') => {
|
||||
self.state.key_focus = KeyFocus::Filter;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
KeyFocus::Filter => match key.code {
|
||||
KeyCode::Char(char) => self.state.filter.push(char),
|
||||
KeyCode::Backspace => {
|
||||
self.state.filter.pop();
|
||||
}
|
||||
KeyCode::Tab | KeyCode::Enter => self.state.key_focus = KeyFocus::None,
|
||||
KeyCode::Esc => {
|
||||
self.state.filter = UIRepoFilter::default();
|
||||
self.state.key_focus = KeyFocus::None;
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
actor_tui.kill();
|
||||
}
|
||||
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,11 +1,6 @@
|
|||
//
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt::{Debug, Display},
|
||||
time::Instant,
|
||||
};
|
||||
use std::{collections::BTreeMap, fmt::Display, time::Instant};
|
||||
|
||||
use derive_more::derive::{DerefMut, Display};
|
||||
use ratatui::{
|
||||
layout::Alignment,
|
||||
prelude::{Buffer, Rect},
|
||||
|
@ -17,24 +12,18 @@ use ratatui::{
|
|||
use tracing::info;
|
||||
use tui_scrollview::ScrollViewState;
|
||||
|
||||
use crate::core::{
|
||||
use git_next_core::{
|
||||
git::{self, graph::Log, Commit},
|
||||
ForgeAlias, RepoAlias, RepoBranches,
|
||||
};
|
||||
use crate::{newtype, s};
|
||||
|
||||
use crate::{
|
||||
server::actor::messages::ValidAppConfig,
|
||||
tui::components::{key_focus::KeyFocus, ConfiguredAppWidget},
|
||||
};
|
||||
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,
|
||||
pub filter: UIRepoFilter,
|
||||
pub key_focus: KeyFocus,
|
||||
}
|
||||
impl State {
|
||||
pub fn initial() -> Self {
|
||||
|
@ -42,8 +31,6 @@ impl State {
|
|||
last_update: Instant::now(),
|
||||
started: Instant::now(),
|
||||
mode: ServerState::Initial { tick: 0 },
|
||||
filter: UIRepoFilter::default(),
|
||||
key_focus: KeyFocus::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,15 +55,6 @@ fn time() -> String {
|
|||
chrono::Local::now().format("%H:%M").to_string()
|
||||
}
|
||||
|
||||
newtype!(
|
||||
UIRepoFilter,
|
||||
String,
|
||||
Default,
|
||||
DerefMut,
|
||||
Display,
|
||||
"Filter to limit repos shown"
|
||||
);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ServerState {
|
||||
/// UI has started but has no information on the state of the server
|
||||
|
@ -254,14 +232,6 @@ pub enum RepoState {
|
|||
},
|
||||
}
|
||||
impl RepoState {
|
||||
pub const fn repo_alias(&self) -> &RepoAlias {
|
||||
match self {
|
||||
Self::Identified { repo_alias, .. }
|
||||
| Self::Configured { repo_alias, .. }
|
||||
| Self::Ready { repo_alias, .. } => repo_alias,
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub fn update_branches(&mut self, branches: RepoBranches) {
|
||||
match self {
|
||||
|
@ -389,36 +359,13 @@ impl StatefulWidget for &State {
|
|||
Self: Sized,
|
||||
{
|
||||
let block = Block::bordered()
|
||||
.title_top(
|
||||
Line::from(
|
||||
if self.filter.is_empty() && self.key_focus == KeyFocus::None {
|
||||
s!("")
|
||||
} else {
|
||||
format!(" Filter: {} ", self.filter)
|
||||
},
|
||||
)
|
||||
.alignment(Alignment::Left)
|
||||
.style(if self.key_focus == KeyFocus::Filter {
|
||||
Color::Red
|
||||
} else {
|
||||
Color::Green
|
||||
}),
|
||||
)
|
||||
.title_top(
|
||||
Line::from(format!(" Git-Next v{} ", clap::crate_version!()).bold())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.title_bottom(
|
||||
Line::from(if self.key_focus == KeyFocus::Filter {
|
||||
s!(" [esc] clear [tab/enter] finish ")
|
||||
} else {
|
||||
s!("")
|
||||
})
|
||||
.alignment(Alignment::Left),
|
||||
)
|
||||
.title_bottom(
|
||||
Line::from(vec![
|
||||
" [q]uit [/] filter ".into(),
|
||||
" [q]uit ".into(),
|
||||
self.beating_heart().into(),
|
||||
" ".into(),
|
||||
])
|
||||
|
@ -433,11 +380,7 @@ impl StatefulWidget for &State {
|
|||
.centered()
|
||||
.render(interior, buf),
|
||||
ServerState::Configured { forges } => {
|
||||
ConfiguredAppWidget {
|
||||
forges,
|
||||
filter: &self.filter,
|
||||
}
|
||||
.render(interior, buf, state);
|
||||
ConfiguredAppWidget { forges }.render(interior, buf, state);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
mod model {
|
||||
mod repo_state {
|
||||
use crate::core::{git::graph::Log, RepoBranches};
|
||||
use git_next_core::{git::graph::Log, RepoBranches};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::{
|
|
@ -1,6 +1,6 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::core::ForgeAlias;
|
||||
use git_next_core::ForgeAlias;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Direction, Layout, Rect, Size},
|
||||
|
@ -8,13 +8,12 @@ use ratatui::{
|
|||
};
|
||||
use tui_scrollview::{ScrollView, ScrollViewState};
|
||||
|
||||
use crate::tui::actor::{ForgeState, UIRepoFilter};
|
||||
use crate::tui::actor::ForgeState;
|
||||
|
||||
use super::{forge::ForgeWidget, HeightContraintLength};
|
||||
|
||||
pub struct ConfiguredAppWidget<'a> {
|
||||
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
|
||||
pub filter: &'a UIRepoFilter,
|
||||
}
|
||||
impl HeightContraintLength for ConfiguredAppWidget<'_> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
|
@ -63,7 +62,6 @@ impl<'a> ConfiguredAppWidget<'a> {
|
|||
forge_alias,
|
||||
repos: &state.repos,
|
||||
view_state: state.view_state,
|
||||
filter: self.filter,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate::core::ForgeAlias;
|
||||
use git_next_core::ForgeAlias;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
|
||||
|
||||
use crate::tui::components::HeightContraintLength;
|
|
@ -1,7 +1,7 @@
|
|||
//
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::core::{ForgeAlias, RepoAlias};
|
||||
use git_next_core::{ForgeAlias, RepoAlias};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Direction, Layout, Rect},
|
||||
|
@ -10,14 +10,13 @@ use ratatui::{
|
|||
};
|
||||
|
||||
use crate::tui::{
|
||||
actor::{RepoState, UIRepoFilter},
|
||||
actor::RepoState,
|
||||
components::{repo::RepoWidget, HeightContraintLength},
|
||||
};
|
||||
|
||||
pub struct ExpandedForgeWidget<'a> {
|
||||
pub forge_alias: &'a ForgeAlias,
|
||||
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
|
||||
pub filter: &'a UIRepoFilter,
|
||||
}
|
||||
impl HeightContraintLength for ExpandedForgeWidget<'_> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
|
@ -56,15 +55,6 @@ impl<'a> ExpandedForgeWidget<'a> {
|
|||
fn children(&self) -> Vec<RepoWidget<'a>> {
|
||||
self.repos
|
||||
.values()
|
||||
.filter(|repo_state| {
|
||||
if self.filter.is_empty() {
|
||||
true
|
||||
} else {
|
||||
let repo_alias = repo_state.repo_alias();
|
||||
// eprintln!("--> {} ? {}", self.filter.as_str(), repo_alias);
|
||||
repo_alias.contains(self.filter.as_str())
|
||||
}
|
||||
})
|
||||
.map(|repo_state| RepoWidget { repo_state })
|
||||
.collect::<Vec<_>>()
|
||||
}
|
|
@ -4,12 +4,12 @@ mod expanded;
|
|||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::core::{ForgeAlias, RepoAlias};
|
||||
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, UIRepoFilter, ViewState};
|
||||
use crate::tui::actor::{RepoState, ViewState};
|
||||
|
||||
use super::HeightContraintLength;
|
||||
|
||||
|
@ -17,7 +17,6 @@ pub struct ForgeWidget<'a> {
|
|||
pub forge_alias: &'a ForgeAlias,
|
||||
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
|
||||
pub view_state: ViewState,
|
||||
pub filter: &'a UIRepoFilter,
|
||||
}
|
||||
impl HeightContraintLength for ForgeWidget<'_> {
|
||||
fn height_constraint_length(&self) -> u16 {
|
||||
|
@ -29,7 +28,6 @@ impl HeightContraintLength for ForgeWidget<'_> {
|
|||
ViewState::Expanded => ExpandedForgeWidget {
|
||||
forge_alias: self.forge_alias,
|
||||
repos: self.repos,
|
||||
filter: self.filter,
|
||||
}
|
||||
.height_constraint_length(),
|
||||
}
|
||||
|
@ -48,7 +46,6 @@ impl Widget for ForgeWidget<'_> {
|
|||
ViewState::Expanded => ExpandedForgeWidget {
|
||||
forge_alias: self.forge_alias,
|
||||
repos: self.repos,
|
||||
filter: self.filter,
|
||||
}
|
||||
.render(area, buf),
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue