Compare commits

..

No commits in common. "main" and "v0.14.1" have entirely different histories.

209 changed files with 2412 additions and 2032 deletions

View file

@ -1 +0,0 @@
target/

View file

@ -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

718
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,41 +1,39 @@
[package]
name = "git-next"
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.package]
version = "0.14.1"
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"
@ -63,7 +61,6 @@ git-url-parse = "0.4"
# fs/network
kxio = "5.1"
# kxio = { path = "../kxio/" }
# TOML parsing
serde = { version = "1.0", features = ["derive"] }
@ -115,21 +112,10 @@ 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.24", features = ["async-timeout"] }

View file

@ -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
View file

@ -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.

105
crates/cli/Cargo.toml Normal file
View file

@ -0,0 +1,105 @@
[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 }
# 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 }
[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
View 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).

View file

@ -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);

View file

@ -1,5 +1,5 @@
//
use crate::core::{
use git_next_core::{
git::UserNotification,
server::{EmailConfig, SmtpConfig},
};

View file

@ -1,5 +1,5 @@
//
use crate::core::git::UserNotification;
use git_next_core::git::UserNotification;
use tracing::info;
use std::{

View file

@ -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");

View file

@ -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};

View file

@ -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};

View file

@ -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;

View file

@ -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,

View file

@ -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);

View file

@ -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,
@ -12,7 +12,7 @@ use crate::core::{
#[test]
fn test_forgejo_name() {
let mock_net = kxio::net::mock();
let repo_details = given_repo_details(crate::core::ForgeType::ForgeJo);
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
let forge = Forge::create(repo_details, mock_net.clone().into());
assert_eq!(forge.name(), "forgejo");
mock_net.assert_no_unused_plans();
@ -22,14 +22,14 @@ fn test_forgejo_name() {
#[test]
fn test_github_name() {
let mock_net = kxio::net::mock();
let repo_details = given_repo_details(crate::core::ForgeType::GitHub);
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
let forge = Forge::create(repo_details, mock_net.clone().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")

View file

@ -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
}};
}

View file

@ -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>>;

View file

@ -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 },
}

View file

@ -1,5 +1,5 @@
//
use crate::core::{git, RepoConfigSource};
use git_next_core::{git, RepoConfigSource};
use color_eyre::Result;
use kameo::message::{Context, Message};

View file

@ -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),

View file

@ -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::{

View file

@ -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::{

View file

@ -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::{

View file

@ -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<()>;

View file

@ -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;

View file

@ -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,

View file

@ -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),
}

View file

@ -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,

View file

@ -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},

View 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
}
}),
}
}
}

View file

@ -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]

View file

@ -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)));
}
}

View file

@ -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]

View file

@ -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)

View file

@ -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 {

View file

@ -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);

View file

@ -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);

View file

@ -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()))

View file

@ -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);
@ -42,7 +42,7 @@ async fn should_clone() -> Result<()> {
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();
@ -50,7 +50,7 @@ async fn should_clone() -> Result<()> {
}
#[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);
@ -87,7 +87,7 @@ 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();
@ -98,7 +98,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);
@ -137,7 +137,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);
@ -165,7 +165,7 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> Result<()
//then
tick(1).await;
tracing::debug!(?log, "");
debug!(?log, "");
log.require_message_containing("send: RegisterWebhook")
.await?;
net.assert_no_unused_plans();

View file

@ -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(

View file

@ -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);

View file

@ -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);

View file

@ -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(),

View file

@ -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,7 +393,7 @@ 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(),
@ -414,7 +411,7 @@ async fn should_accept_message_with_current_token() -> Result<()> {
}
#[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,7 +420,7 @@ 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(),
@ -439,7 +436,7 @@ async fn should_accept_message_with_new_token() -> Result<()> {
}
#[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,7 +445,7 @@ 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(),
@ -465,14 +462,14 @@ async fn should_reject_message_with_expired_token() -> Result<()> {
#[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 +487,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);

View file

@ -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);
@ -41,7 +41,7 @@ async fn when_no_expected_auth_token_drop_notification() -> Result<()> {
}
#[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);
@ -76,7 +76,7 @@ async fn when_no_repo_config_drop_notification() -> Result<()> {
}
#[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);
@ -115,7 +115,7 @@ async fn when_message_auth_is_invalid_drop_notification() -> Result<()> {
}
#[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);
@ -158,7 +158,7 @@ async fn when_message_is_ignorable_drop_notification() -> Result<()> {
}
#[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);
@ -201,7 +201,7 @@ async fn when_message_is_not_a_push_drop_notification() -> Result<()> {
}
#[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();
@ -249,7 +249,7 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> Result<()
}
#[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();
@ -299,7 +299,7 @@ async fn when_message_is_push_already_seen_commit_to_main() -> Result<()> {
}
#[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();
@ -349,7 +349,7 @@ async fn when_message_is_push_already_seen_commit_to_next() -> Result<()> {
}
#[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();
@ -399,7 +399,7 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> Result<()> {
}
#[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();
@ -441,7 +441,7 @@ 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();
@ -449,7 +449,7 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
}
#[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();
@ -491,7 +491,7 @@ 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();
@ -499,7 +499,7 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
}
#[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();
@ -541,7 +541,7 @@ 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();

View file

@ -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);

View file

@ -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(())
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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,

View file

@ -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::{

View file

@ -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,

View file

@ -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!(

View file

@ -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(())
}

View file

@ -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},
};
@ -37,7 +37,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
mock_net.clone().into(),
alerts,
file_update_subs,
Box::new(repo),
repo,
duration,
);

View file

@ -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};

View file

@ -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]

View file

@ -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>();
}
}

View file

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

View file

@ -17,11 +17,10 @@ use ratatui::{
use tracing::info;
use tui_scrollview::ScrollViewState;
use crate::core::{
use git_next_core::{
git::{self, graph::Log, Commit},
ForgeAlias, RepoAlias, RepoBranches,
newtype, s, ForgeAlias, RepoAlias, RepoBranches,
};
use crate::{newtype, s};
use crate::{
server::actor::messages::ValidAppConfig,
@ -254,11 +253,11 @@ pub enum RepoState {
},
}
impl RepoState {
pub const fn repo_alias(&self) -> &RepoAlias {
pub fn repo_alias(&self) -> &RepoAlias {
match self {
Self::Identified { repo_alias, .. }
| Self::Configured { repo_alias, .. }
| Self::Ready { repo_alias, .. } => repo_alias,
RepoState::Identified { repo_alias, .. }
| RepoState::Configured { repo_alias, .. }
| RepoState::Ready { repo_alias, .. } => repo_alias,
}
}

View file

@ -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::{

View file

@ -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},

View file

@ -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;

View file

@ -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},

View file

@ -4,9 +4,9 @@ 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};

View file

@ -1,5 +1,5 @@
//
use crate::core::git::graph::Log;
use git_next_core::git::graph::Log;
use ratatui::{
style::{Color, Style},
text::{Line, Span, Text},

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