Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
17031f4d23 | |||
1ef76dd7be | |||
b3b8c0ec90 | |||
1197408c5c | |||
7387d7c871 | |||
e5a5e508ff | |||
c8cc45ca7f | |||
52de3ef86e | |||
f71e28512d | |||
a605c3499a | |||
030129a746 | |||
92c24eafd1 | |||
1694347f00 |
209 changed files with 2032 additions and 2412 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
49
.forgejo/workflows/daily-nightly.yml
Normal file
49
.forgejo/workflows/daily-nightly.yml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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
718
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
68
Cargo.toml
68
Cargo.toml
|
@ -1,39 +1,41 @@
|
||||||
[workspace]
|
[package]
|
||||||
resolver = "2"
|
name = "git-next"
|
||||||
members = ["crates/*"]
|
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.kemitix.net/kemitix/git-next"
|
repository = "https://git.kemitix.net/kemitix/git-next"
|
||||||
|
description = "trunk-based development manager"
|
||||||
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
||||||
rust-version = "1.76"
|
rust-version = "1.76"
|
||||||
description = "trunk-based development manager"
|
|
||||||
documentation = "https://git.kemitix.net/kemitix/git-next/src/branch/main/README.md"
|
documentation = "https://git.kemitix.net/kemitix/git-next/src/branch/main/README.md"
|
||||||
keywords = ["git", "cli", "server", "tool"]
|
keywords = ["git", "cli", "server", "tool"]
|
||||||
categories = ["development-tools"]
|
categories = ["development-tools"]
|
||||||
|
|
||||||
# [workspace.lints.clippy]
|
[features]
|
||||||
# pedantic = { level = "warn", priority = -1 }
|
# default = ["forgejo", "github"]
|
||||||
# nursery = { level = "warn", priority = -1 }
|
default = ["forgejo", "github", "tui"]
|
||||||
# unwrap_used = "warn"
|
forgejo = []
|
||||||
# expect_used = "warn"
|
github = []
|
||||||
|
tui = [
|
||||||
|
"ratatui",
|
||||||
|
"directories",
|
||||||
|
"lazy_static",
|
||||||
|
"tui-scrollview",
|
||||||
|
"regex",
|
||||||
|
"chrono",
|
||||||
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[dependencies]
|
||||||
git-next-core = { path = "crates/core", version = "0.14" }
|
|
||||||
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.14" }
|
color-eyre = "0.6"
|
||||||
git-next-forge-github = { path = "crates/forge-github", version = "0.14" }
|
|
||||||
|
|
||||||
# TUI
|
# TUI
|
||||||
ratatui = "0.29"
|
ratatui = { version = "0.29", optional = true }
|
||||||
directories = "6.0"
|
directories = { version = "6.0", optional = true }
|
||||||
lazy_static = "1.5"
|
lazy_static = { version = "1.5", optional = true }
|
||||||
color-eyre = "0.6"
|
tui-scrollview = { version = "0.5", optional = true }
|
||||||
tui-scrollview = "0.5"
|
regex = { version = "1.10", optional = true }
|
||||||
regex = "1.10"
|
chrono = { version = "0.4", optional = true }
|
||||||
chrono = "0.4"
|
|
||||||
|
|
||||||
# CLI parsing
|
# CLI parsing
|
||||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||||
|
@ -44,7 +46,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
|
|
||||||
# base64 decoding
|
# base64 decoding
|
||||||
base64 = "0.22"
|
# base64 = { version = "0.22", optional = true }
|
||||||
|
|
||||||
# sha256 encoding (e.g. verify github webhooks)
|
# sha256 encoding (e.g. verify github webhooks)
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
|
@ -61,6 +63,7 @@ git-url-parse = "0.4"
|
||||||
|
|
||||||
# fs/network
|
# fs/network
|
||||||
kxio = "5.1"
|
kxio = "5.1"
|
||||||
|
# kxio = { path = "../kxio/" }
|
||||||
|
|
||||||
# TOML parsing
|
# TOML parsing
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -112,10 +115,21 @@ sendmail = "2.0"
|
||||||
# desktop notifications
|
# desktop notifications
|
||||||
notifica = "3.0"
|
notifica = "3.0"
|
||||||
|
|
||||||
# Testing
|
mockall = "0.13"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
assert2 = "0.3"
|
assert2 = "0.3"
|
||||||
|
|
||||||
pretty_assertions = "1.4"
|
pretty_assertions = "1.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
mockall = "0.13"
|
|
||||||
test-log = "0.2"
|
|
||||||
rstest = { version = "0.24", features = ["async-timeout"] }
|
rstest = { version = "0.24", features = ["async-timeout"] }
|
||||||
|
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)'] }
|
||||||
|
|
|
@ -6,7 +6,7 @@ RUN apk add --no-cache dbus-dev=1.14.10-r4 && rm -rf /vra/cache/apk/*
|
||||||
|
|
||||||
FROM chef AS planner
|
FROM chef AS planner
|
||||||
COPY Cargo.toml ./
|
COPY Cargo.toml ./
|
||||||
COPY crates crates
|
COPY src src
|
||||||
RUN cargo chef prepare --recipe-path recipe.json
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
FROM chef AS builder
|
FROM chef AS builder
|
||||||
|
|
662
README.md
662
README.md
|
@ -2,11 +2,671 @@
|
||||||
|
|
||||||
## Trunk-based developement manager.
|
## 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
|
`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
|
development workflows where each commit must pass CI before being included in
|
||||||
the main branch.
|
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)
|
![Demo](./demo.gif)
|
||||||
|
|
||||||
|
## Prerequisits
|
||||||
|
|
||||||
See [README.md](https://git.kemitix.net/kemitix/git-next/src/branch/main/crates/cli/README.md) for more information.
|
- Rust 1.76.0 or later - https://www.rust-lang.org
|
||||||
|
- pgk-config
|
||||||
|
- libssl-dev
|
||||||
|
- libdbus-1-dev (ubuntu/debian)
|
||||||
|
- dbus-devel (fedora)
|
||||||
|
|
||||||
|
See `.cargo/config.toml` for how they are configured.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You can install `git-next` from <https://crates.io/>:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install git-next
|
||||||
|
```
|
||||||
|
|
||||||
|
If you use [mise](https://mise.jdx.dev):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mise use -g cargo:git-next
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can install `git-next` from source after cloning:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install --path crates/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [x] cli
|
||||||
|
- [x] server
|
||||||
|
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
||||||
|
- [x] tui overview
|
||||||
|
- [ ] webui overview
|
||||||
|
|
||||||
|
## Branch Names
|
||||||
|
|
||||||
|
`git-next` uses three branches, `main`, `next` and `dev`, although they do not
|
||||||
|
need to have those names. In the documentation we will use those names, but
|
||||||
|
each repo must specify the names of the branches to use for each, even if they
|
||||||
|
happen to have those same names.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- The branches to use for `main`, `next` and `dev` must be specified in either
|
||||||
|
the `.git-next.toml` in the repo itself, or in the server configuration file,
|
||||||
|
`git-next-server.toml`. See below for details.
|
||||||
|
- CI checks should be configured to run when the `next` branch is `pushed`.
|
||||||
|
- The `dev` branch _must_ have the `main` branch as an ancestor.
|
||||||
|
- The `next` branch _must_ have the `main` branch as an ancestor.
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
The server is configured by the `git-next-server.toml` file.
|
||||||
|
|
||||||
|
#### listen
|
||||||
|
|
||||||
|
The server should listen for webhook notifications from each forge.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[listen]
|
||||||
|
http = { addr = "0.0.0.0", port = 8080 }
|
||||||
|
url = "https://localhost:8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### http
|
||||||
|
|
||||||
|
The server needs to be able to receive webhook notifications from your forge,
|
||||||
|
(e.g. github.com). You can do this via any method that suits your environment,
|
||||||
|
e.g. ngrok or a reverse proxy from a web server that itself can route traffic
|
||||||
|
to the machine you are running the git-next server on.
|
||||||
|
|
||||||
|
Specify the address and port the server should listen to for incoming webhooks.
|
||||||
|
This is the address and port that your reverse proxy should route traffic to.
|
||||||
|
|
||||||
|
- **addr** - the IP address the server should bind to
|
||||||
|
- **port** - the IP port the server should bind to
|
||||||
|
|
||||||
|
##### url
|
||||||
|
|
||||||
|
The HTTPS URL for forges to send webhooks to.
|
||||||
|
|
||||||
|
Your forges need to know where they should route webhooks to. This should be
|
||||||
|
an address this is accessible to the forge. So, for github.com, it would need
|
||||||
|
to be a publicly accessible HTTPS URL. For a self-hosted forge, e.g. ForgeJo,
|
||||||
|
on your own network, then it only needs to be accessible from the server your
|
||||||
|
forge is running on.
|
||||||
|
|
||||||
|
#### shout
|
||||||
|
|
||||||
|
The server should be able to notify the user when manual intervention is required.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[shout]
|
||||||
|
desktop = true
|
||||||
|
|
||||||
|
[shout.webhook]
|
||||||
|
url = "https//localhost:9090"
|
||||||
|
secret = "secret-password"
|
||||||
|
|
||||||
|
[shout.email]
|
||||||
|
from = "git-next@example.com"
|
||||||
|
to = "developer@example.com"
|
||||||
|
|
||||||
|
[shout.email.smtp]
|
||||||
|
hostname = "smtp.example.com"
|
||||||
|
username = "git-next@example.com"
|
||||||
|
password = "MySecretEmailPassword42"
|
||||||
|
```
|
||||||
|
|
||||||
|
##### desktop
|
||||||
|
|
||||||
|
When specified as `true`, desktop notifications will be sent for some events.
|
||||||
|
|
||||||
|
##### webhook
|
||||||
|
|
||||||
|
Will send a POST request for some events.
|
||||||
|
|
||||||
|
- **url** - the URL to POST the notification to and the
|
||||||
|
- **secret** - the sync key used to sign the webhook payload
|
||||||
|
|
||||||
|
See [Notifications](#notifications) for more details.
|
||||||
|
|
||||||
|
##### email
|
||||||
|
|
||||||
|
Will send an email for some events.
|
||||||
|
|
||||||
|
- **from** - the email address to send the email from
|
||||||
|
- **to** - the email address to send the email to
|
||||||
|
|
||||||
|
With just `from` and `to` specified, `git-next` will attempt to send emails
|
||||||
|
with `sendmail` if it is configured.
|
||||||
|
|
||||||
|
Alternativly, you can use an SMTP relay.
|
||||||
|
|
||||||
|
###### smtp
|
||||||
|
|
||||||
|
Will send emails using an SMTP relay.
|
||||||
|
|
||||||
|
- **hostname** - the SMTP relay server
|
||||||
|
- **username** - the account to authenticate as
|
||||||
|
- **password** - the password to authenticate with
|
||||||
|
|
||||||
|
#### storage
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[storage]
|
||||||
|
path = "./data"
|
||||||
|
```
|
||||||
|
|
||||||
|
`git-next` will create a bare clone of each repo that you configure it to
|
||||||
|
monitor. They will all be created in the directory specified here. This data
|
||||||
|
does not need to be backed up, as any missing information will be cloned when
|
||||||
|
the server starts up.
|
||||||
|
|
||||||
|
- **path** - directory to store local copies of monitored repos
|
||||||
|
|
||||||
|
#### forge
|
||||||
|
|
||||||
|
Within the forge tree, specify each forge you want to monitor repos on.
|
||||||
|
|
||||||
|
Give your forge an alias, e.g. `default`, `gh`, `github`.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[forge.github]
|
||||||
|
forge_type = "GitHub"
|
||||||
|
hostname = "github.com"
|
||||||
|
user = "username"
|
||||||
|
token = "api-key"
|
||||||
|
max_dev_commits = 25
|
||||||
|
```
|
||||||
|
|
||||||
|
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
||||||
|
- **hostname** - the hostname for the forge.
|
||||||
|
- **user** - the user to authenticate as
|
||||||
|
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
|
||||||
|
- **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
|
||||||
|
|
||||||
|
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
||||||
|
to `next`.
|
||||||
|
|
||||||
|
#### repos
|
||||||
|
|
||||||
|
For each forge, you need to specify which repos on the forge you want to
|
||||||
|
monitor. They do not need to be owned by the `user`, but they `user` must have
|
||||||
|
the `push` and `force-push` permissions as mentioned above for each of the
|
||||||
|
repositories.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[forge.github.repos]
|
||||||
|
my-repo = { repo = "owner/repo", branch = "main", gitdir = "/home/pcampbell/project/my-repo" }
|
||||||
|
|
||||||
|
[forge.github.repos.other-repo]
|
||||||
|
repo = "user/other"
|
||||||
|
branch = "master"
|
||||||
|
main = "master"
|
||||||
|
next = "ci-testing"
|
||||||
|
dev = "trunk"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that toml allows specifying the values on one line, or across multiple
|
||||||
|
lines. Both are equivalent. What is not equivalent between `my-repo` and
|
||||||
|
`other-repo`, is that one will require a configuration file within the repo
|
||||||
|
itself. `other-repo` specifies the `main`, `next` and `dev` branches to be
|
||||||
|
used, but `my-repo` doesn't.
|
||||||
|
|
||||||
|
A sample `.git-next-toml` file that would need to exist in `my-repo`'s `owner/repo`
|
||||||
|
repo, on the `main` branch:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[branches]
|
||||||
|
main = "main"
|
||||||
|
next = "next"
|
||||||
|
dev = "dev"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **repo** - the owner and name of the repo to be monitored
|
||||||
|
- **branch** - the branch to look for a `.git-next.toml` file if needed
|
||||||
|
- **gitdir** - (optional) you can use a local copy of the repo
|
||||||
|
- **main** - the branch to use as `main`
|
||||||
|
- **next** - the branch to use as `next`
|
||||||
|
- **dev** - the branch to use as `dev`
|
||||||
|
|
||||||
|
##### gitdir
|
||||||
|
|
||||||
|
Additional notes on using `gitdir`:
|
||||||
|
|
||||||
|
When you specify the `gitdir` value, the repo cloned in that directory will
|
||||||
|
be used for perform the equivalent of `git fetch`, `git push` and `git push
|
||||||
|
--force-with-lease`.
|
||||||
|
|
||||||
|
These commands will not affect the contents of your working tree, nor will
|
||||||
|
it change any local branches. Only the details about branches on the remote
|
||||||
|
forge will be updated.
|
||||||
|
|
||||||
|
Currently `git-next` can only use a `gitdir` if the forge and repo is the
|
||||||
|
same one specified as the `origin` remote. Otherwise the behaviour is
|
||||||
|
untested and undefined.
|
||||||
|
|
||||||
|
## Webhook Notifications
|
||||||
|
|
||||||
|
When sending a Webhook Notification to a user they are sent using the
|
||||||
|
Standard Webhooks format. That means all POST messages have the
|
||||||
|
following headers:
|
||||||
|
|
||||||
|
- `Webhook-Id`
|
||||||
|
- `Webhook-Signature`
|
||||||
|
- `Webhook-Timestamp`
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
#### Dev Not Based on Main
|
||||||
|
|
||||||
|
This message `type` indicates that the `dev` branch is not based on `main`.
|
||||||
|
|
||||||
|
**Action Required**: Rebase the `dev` branch onto the `main` branch.
|
||||||
|
|
||||||
|
Sample payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"branches": {
|
||||||
|
"dev": "dev",
|
||||||
|
"main": "main"
|
||||||
|
},
|
||||||
|
"forge_alias": "jo",
|
||||||
|
"repo_alias": "kxio",
|
||||||
|
"log": [
|
||||||
|
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
|
||||||
|
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
|
||||||
|
"|/",
|
||||||
|
"* 8c19680 refactor: macros use a more common syntax"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timestamp": "1721760933",
|
||||||
|
"type": "branch.dev.not-on-main"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CI Check Failed
|
||||||
|
|
||||||
|
This message `type` indicates that the commit on the tip of the `next` branch has failed the
|
||||||
|
configured CI checks.
|
||||||
|
|
||||||
|
**Action Required**: Either update the commit to correct the issue CI raised, or, if the issue
|
||||||
|
is transient (e.g. a network issue), re-run/re-start the job in your CI.
|
||||||
|
|
||||||
|
Sample payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"commit": {
|
||||||
|
"sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
|
||||||
|
"message": "feat: add log graph to notifications (1 of 2)"
|
||||||
|
},
|
||||||
|
"forge_alias": "jo",
|
||||||
|
"repo_alias": "kxio",
|
||||||
|
"log": [
|
||||||
|
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
|
||||||
|
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
|
||||||
|
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timestamp": "1721760933",
|
||||||
|
"type": "cicheck.failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Repo Config Load Failed
|
||||||
|
|
||||||
|
This message `type` indicates that `git-next` wasn't able to load the configuration for the
|
||||||
|
repo from the `git-next.toml` file in the repository.
|
||||||
|
|
||||||
|
**Action Required**: Review the `reason` provided.
|
||||||
|
|
||||||
|
Sample payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"reason": "File not found: .git-next.toml",
|
||||||
|
"forge_alias": "jo",
|
||||||
|
"repo_alias": "kxio"
|
||||||
|
},
|
||||||
|
"timestamp": "1721760933",
|
||||||
|
"type": "config.load.failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Webhook Registration Failed
|
||||||
|
|
||||||
|
This message `type` indicates that `git-next` wasn't able to register it's webhook with the
|
||||||
|
forge repository, so will not receive updates when the branches in the repo are updated.
|
||||||
|
|
||||||
|
**Action Required**: Review the `reason` provided.
|
||||||
|
|
||||||
|
Sample payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"reason": "repo config not loaded",
|
||||||
|
"forge_alias": "jo",
|
||||||
|
"repo_alias": "kxio"
|
||||||
|
},
|
||||||
|
"timestamp": "1721760933",
|
||||||
|
"type": "webhook.registration.failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behaviour
|
||||||
|
|
||||||
|
The branch names are configurable, but we will talk about `main`, `next` and `dev`.
|
||||||
|
|
||||||
|
Development happens on the `dev` branch, where each commit is expected to
|
||||||
|
be able to pass the CI checks.
|
||||||
|
|
||||||
|
(Note: in the diagrams, mermaid isn't capable of showing `main` and `next` on
|
||||||
|
the same commit, so we show `next` as empty)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gitGraph
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
|
||||||
|
branch next
|
||||||
|
|
||||||
|
branch dev
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
```
|
||||||
|
|
||||||
|
When the `git-next` server sees that the `dev` branch is ahead of the `next`
|
||||||
|
branch, it will push the `next` branch fast-forward one commit along the `dev`
|
||||||
|
branch.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gitGraph
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
|
||||||
|
branch next
|
||||||
|
commit
|
||||||
|
|
||||||
|
branch dev
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
```
|
||||||
|
|
||||||
|
It will then wait for the CI checks to pass for the newly updated `next` branch.
|
||||||
|
When the CI checks for the `next` branch pass, it will push the `main` branch
|
||||||
|
fast-forward to the `next` branch. We return to the top and start again.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gitGraph
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
|
||||||
|
branch next
|
||||||
|
|
||||||
|
branch dev
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
```
|
||||||
|
|
||||||
|
If the CI checks should fail for the `next` branch, the developer should
|
||||||
|
**amend** that commit **in the history of their `dev` branch**.
|
||||||
|
They should then force-push their rebased `dev` branch.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gitGraph
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
|
||||||
|
branch next
|
||||||
|
commit
|
||||||
|
|
||||||
|
checkout main
|
||||||
|
|
||||||
|
branch dev
|
||||||
|
commit
|
||||||
|
|
||||||
|
commit
|
||||||
|
commit
|
||||||
|
```
|
||||||
|
|
||||||
|
`git-next` will then detect that the `next` branch is no longer part of the
|
||||||
|
`dev` branch ancestory, and will reset `next` back to `main`.
|
||||||
|
We then return to the top, where `git-next` sees that `dev` is ahead of `next`.
|
||||||
|
|
||||||
|
When the `dev` branch is on the same commit as the `main` branch, then there
|
||||||
|
are no pending commits and `git-next` will wait until it receives a webhook
|
||||||
|
indicating that there has been a push to one of the branches. At which point
|
||||||
|
it will start at the top again.
|
||||||
|
|
||||||
|
### Important
|
||||||
|
|
||||||
|
The `dev` branch _should_ have the `next` branch as an ancestor.
|
||||||
|
|
||||||
|
However, when the commit on tip of the `next` branch has failed CI and is
|
||||||
|
amended, this will not be the case. When this happens `git-next` will
|
||||||
|
**force-push** the `next` branch back to the same commit as the `main` branch.
|
||||||
|
|
||||||
|
This is the only time a force-push will happen in `git-next`.
|
||||||
|
|
||||||
|
In short, the `next` branch **belongs** to `git-next`. Don't try to update it
|
||||||
|
yourself. `git-next` will update the `next` it as it sees fit.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To use `git-next` for trunk-based development, follow these steps:
|
||||||
|
|
||||||
|
### Initialise the repo (optional)
|
||||||
|
|
||||||
|
You need to specify which branches you are using. You can do this in the repo,
|
||||||
|
or in the server configuration.
|
||||||
|
|
||||||
|
To create a default config file for the repo, run this command in the root of
|
||||||
|
your repo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git next init
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a `.git-next.toml` file. [Default](./crates/cli/default.toml)
|
||||||
|
|
||||||
|
By default the expected branches are `main`, `next` and `dev`. Each of these
|
||||||
|
three branches _must_ exist in your repo.
|
||||||
|
|
||||||
|
### Initialise the server
|
||||||
|
|
||||||
|
The server uses the file `git-next-server.toml` for configuration. It expects
|
||||||
|
to find this file the the current directory when executed.
|
||||||
|
|
||||||
|
The create the default config file, run this command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git next server init
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a `git-next-server.toml` file. [Default](./crates/server/server-default.toml)
|
||||||
|
|
||||||
|
Edit this file to your needs. See the [Configuration](#configuration) section above.
|
||||||
|
|
||||||
|
### Run the server
|
||||||
|
|
||||||
|
In the directory with your `git-next-server.toml` file, run the command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git next server start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Forges
|
||||||
|
|
||||||
|
The following forges are supported:
|
||||||
|
|
||||||
|
- [ForgeJo](https://forgejo.org) (probably compatible with Gitea, but not tested)
|
||||||
|
- [GitHub](https://github.com/)
|
||||||
|
|
||||||
|
Note: ForgeJo is a hard fork of Gitea, but currently they are largely compatible.
|
||||||
|
For now using a `forge_type` of `ForgeJo` with a Gitea instance will probably work
|
||||||
|
okay. The only API calls we make are around registering and unregistering webhooks.
|
||||||
|
So, as long as those APIs remain the same, they should be compatible.
|
||||||
|
|
||||||
|
#### ForgeJo
|
||||||
|
|
||||||
|
Configure the forge in `git-next-server.toml` like:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[forge.jo]
|
||||||
|
forge_type = "ForgeJo"
|
||||||
|
hostname = "git.myforgejo.com"
|
||||||
|
user = "bob"
|
||||||
|
token = "..."
|
||||||
|
|
||||||
|
[forge.jo.repos]
|
||||||
|
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://git.example.net/user/hello on the branch 'main'
|
||||||
|
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
||||||
|
```
|
||||||
|
|
||||||
|
The token is created on your ForgeJo instance at (for example)
|
||||||
|
`https://git.myforgejo.com/user/settings/applications`
|
||||||
|
and requires the `write:repository` permission.
|
||||||
|
|
||||||
|
#### GitHub
|
||||||
|
|
||||||
|
Configure the forge in `git-next-server.toml` like:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[forge.gh]
|
||||||
|
forge_type = "GitHub"
|
||||||
|
hostname = "github.com" # required even for GitHub
|
||||||
|
user = "bob"
|
||||||
|
token = "..."
|
||||||
|
|
||||||
|
[forge.gh.repos]
|
||||||
|
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://github.com/user/hello on the branch 'main'
|
||||||
|
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
||||||
|
```
|
||||||
|
|
||||||
|
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
Here is an example `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: git.kemitix.net/kemitix/git-next:latest
|
||||||
|
container_name: git-next-server
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
RUST_LOG: "hyper=warn,info"
|
||||||
|
ports:
|
||||||
|
- 8080:8092
|
||||||
|
volumes:
|
||||||
|
- ./:/app/
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
|
||||||
|
`8092` and that you are using a reverse proxy to route traffic arriving at
|
||||||
|
`listen.url` to port `8080`.
|
||||||
|
|
||||||
|
### Docker Run
|
||||||
|
|
||||||
|
This will run with the `server start` options:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
To perform `server init`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
|
||||||
|
```
|
||||||
|
|
||||||
|
To perform repo `init`:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
|
||||||
|
```
|
||||||
|
|
||||||
|
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
||||||
|
request, please
|
||||||
|
[create an issue](https://git.kemitix.net/kemitix/git-next/issues/new).
|
||||||
|
If you'd like to contribute code, feel free to submit changes.
|
||||||
|
|
||||||
|
Before you start committing, run the `just install-hooks` command to setup the
|
||||||
|
Git Hooks. ([Get Just](https://just.systems/man/en/chapter_3.html))
|
||||||
|
|
||||||
|
## Crate Dependency
|
||||||
|
|
||||||
|
The following diagram shows the dependency between the crates that make up `git-next`:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
|
||||||
|
cli --> core
|
||||||
|
cli --> forge_forgejo
|
||||||
|
cli --> forge_github
|
||||||
|
|
||||||
|
forge_forgejo --> core
|
||||||
|
|
||||||
|
forge_github --> core
|
||||||
|
```
|
||||||
|
|
||||||
|
## Actor Supervision Tree
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
Root
|
||||||
|
Alerts
|
||||||
|
FileWatcher
|
||||||
|
Server
|
||||||
|
Repo 1
|
||||||
|
Repo 2
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
`git-next` is released under the [MIT License](./LICENSE).
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
[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)'] }
|
|
|
@ -1,672 +0,0 @@
|
||||||
# git-next
|
|
||||||
|
|
||||||
## Trunk-based developement manager.
|
|
||||||
|
|
||||||
> A source-control branching model, where developers collaborate on code in a single branch
|
|
||||||
> called ‘trunk’, resist any pressure to create other long-lived development branches by
|
|
||||||
> employing documented techniques. They therefore avoid merge hell, do not break the build,
|
|
||||||
> and live happily ever after. - [source](https://trunkbaseddevelopment.com)
|
|
||||||
|
|
||||||
- Status: **BETA** - dog-fooding
|
|
||||||
|
|
||||||
`git-next` is a combined server and command-line tool that enables trunk-based
|
|
||||||
development workflows where each commit must pass CI before being included in
|
|
||||||
the main branch.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Allows enforcing the requirement for each commit to pass the CI pipeline before being
|
|
||||||
included in the main branch
|
|
||||||
- Provides a server component that manages the trunk-based development process
|
|
||||||
- Ensure a consistent, high-quality codebase by preventing untested changes
|
|
||||||
from being added to main
|
|
||||||
- Requires each commit uses conventional commit format.
|
|
||||||
|
|
||||||
See [Behaviour](#behaviour) to learn how we do this.
|
|
||||||
|
|
||||||
## Prerequisits
|
|
||||||
|
|
||||||
- Rust 1.76.0 or later - https://www.rust-lang.org
|
|
||||||
- pgk-config
|
|
||||||
- libssl-dev
|
|
||||||
- libdbus-1-dev (ubuntu/debian)
|
|
||||||
- dbus-devel (fedora)
|
|
||||||
|
|
||||||
See `.cargo/config.toml` for how they are configured.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
You can install `git-next` from <https://crates.io/>:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cargo install git-next
|
|
||||||
```
|
|
||||||
|
|
||||||
If you use [mise](https://mise.jdx.dev):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
mise use -g cargo:git-next
|
|
||||||
```
|
|
||||||
|
|
||||||
Or you can install `git-next` from source after cloning:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
cargo install --path crates/cli
|
|
||||||
```
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
- [x] cli
|
|
||||||
- [x] server
|
|
||||||
- [x] notifications - notify user when intervention required (e.g. to rebase)
|
|
||||||
- [x] tui overview
|
|
||||||
- [ ] webui overview
|
|
||||||
|
|
||||||
## Branch Names
|
|
||||||
|
|
||||||
`git-next` uses three branches, `main`, `next` and `dev`, although they do not
|
|
||||||
need to have those names. In the documentation we will use those names, but
|
|
||||||
each repo must specify the names of the branches to use for each, even if they
|
|
||||||
happen to have those same names.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
- The branches to use for `main`, `next` and `dev` must be specified in either
|
|
||||||
the `.git-next.toml` in the repo itself, or in the server configuration file,
|
|
||||||
`git-next-server.toml`. See below for details.
|
|
||||||
- CI checks should be configured to run when the `next` branch is `pushed`.
|
|
||||||
- The `dev` branch _must_ have the `main` branch as an ancestor.
|
|
||||||
- The `next` branch _must_ have the `main` branch as an ancestor.
|
|
||||||
|
|
||||||
### Server
|
|
||||||
|
|
||||||
The server is configured by the `git-next-server.toml` file.
|
|
||||||
|
|
||||||
#### listen
|
|
||||||
|
|
||||||
The server should listen for webhook notifications from each forge.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[listen]
|
|
||||||
http = { addr = "0.0.0.0", port = 8080 }
|
|
||||||
url = "https://localhost:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
##### http
|
|
||||||
|
|
||||||
The server needs to be able to receive webhook notifications from your forge,
|
|
||||||
(e.g. github.com). You can do this via any method that suits your environment,
|
|
||||||
e.g. ngrok or a reverse proxy from a web server that itself can route traffic
|
|
||||||
to the machine you are running the git-next server on.
|
|
||||||
|
|
||||||
Specify the address and port the server should listen to for incoming webhooks.
|
|
||||||
This is the address and port that your reverse proxy should route traffic to.
|
|
||||||
|
|
||||||
- **addr** - the IP address the server should bind to
|
|
||||||
- **port** - the IP port the server should bind to
|
|
||||||
|
|
||||||
##### url
|
|
||||||
|
|
||||||
The HTTPS URL for forges to send webhooks to.
|
|
||||||
|
|
||||||
Your forges need to know where they should route webhooks to. This should be
|
|
||||||
an address this is accessible to the forge. So, for github.com, it would need
|
|
||||||
to be a publicly accessible HTTPS URL. For a self-hosted forge, e.g. ForgeJo,
|
|
||||||
on your own network, then it only needs to be accessible from the server your
|
|
||||||
forge is running on.
|
|
||||||
|
|
||||||
#### shout
|
|
||||||
|
|
||||||
The server should be able to notify the user when manual intervention is required.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[shout]
|
|
||||||
desktop = true
|
|
||||||
|
|
||||||
[shout.webhook]
|
|
||||||
url = "https//localhost:9090"
|
|
||||||
secret = "secret-password"
|
|
||||||
|
|
||||||
[shout.email]
|
|
||||||
from = "git-next@example.com"
|
|
||||||
to = "developer@example.com"
|
|
||||||
|
|
||||||
[shout.email.smtp]
|
|
||||||
hostname = "smtp.example.com"
|
|
||||||
username = "git-next@example.com"
|
|
||||||
password = "MySecretEmailPassword42"
|
|
||||||
```
|
|
||||||
|
|
||||||
##### desktop
|
|
||||||
|
|
||||||
When specified as `true`, desktop notifications will be sent for some events.
|
|
||||||
|
|
||||||
##### webhook
|
|
||||||
|
|
||||||
Will send a POST request for some events.
|
|
||||||
|
|
||||||
- **url** - the URL to POST the notification to and the
|
|
||||||
- **secret** - the sync key used to sign the webhook payload
|
|
||||||
|
|
||||||
See [Notifications](#notifications) for more details.
|
|
||||||
|
|
||||||
##### email
|
|
||||||
|
|
||||||
Will send an email for some events.
|
|
||||||
|
|
||||||
- **from** - the email address to send the email from
|
|
||||||
- **to** - the email address to send the email to
|
|
||||||
|
|
||||||
With just `from` and `to` specified, `git-next` will attempt to send emails
|
|
||||||
with `sendmail` if it is configured.
|
|
||||||
|
|
||||||
Alternativly, you can use an SMTP relay.
|
|
||||||
|
|
||||||
###### smtp
|
|
||||||
|
|
||||||
Will send emails using an SMTP relay.
|
|
||||||
|
|
||||||
- **hostname** - the SMTP relay server
|
|
||||||
- **username** - the account to authenticate as
|
|
||||||
- **password** - the password to authenticate with
|
|
||||||
|
|
||||||
#### storage
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[storage]
|
|
||||||
path = "./data"
|
|
||||||
```
|
|
||||||
|
|
||||||
`git-next` will create a bare clone of each repo that you configure it to
|
|
||||||
monitor. They will all be created in the directory specified here. This data
|
|
||||||
does not need to be backed up, as any missing information will be cloned when
|
|
||||||
the server starts up.
|
|
||||||
|
|
||||||
- **path** - directory to store local copies of monitored repos
|
|
||||||
|
|
||||||
#### forge
|
|
||||||
|
|
||||||
Within the forge tree, specify each forge you want to monitor repos on.
|
|
||||||
|
|
||||||
Give your forge an alias, e.g. `default`, `gh`, `github`.
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[forge.github]
|
|
||||||
forge_type = "GitHub"
|
|
||||||
hostname = "github.com"
|
|
||||||
user = "username"
|
|
||||||
token = "api-key"
|
|
||||||
max_dev_commits = 25
|
|
||||||
```
|
|
||||||
|
|
||||||
- **forge_type** - one of: `ForgeJo` or `GitHub`
|
|
||||||
- **hostname** - the hostname for the forge.
|
|
||||||
- **user** - the user to authenticate as
|
|
||||||
- **token** - application token for the user. See [Forges](#forges) below for the permissions required for each forge.
|
|
||||||
- **max_dev_commits** - [optional] the maximum number of commits allowed between `dev` and `main`. Defaults to 25.
|
|
||||||
|
|
||||||
Generally, the `user` will need to be able to push to `main` and to _force-push_
|
|
||||||
to `next`.
|
|
||||||
|
|
||||||
#### repos
|
|
||||||
|
|
||||||
For each forge, you need to specify which repos on the forge you want to
|
|
||||||
monitor. They do not need to be owned by the `user`, but they `user` must have
|
|
||||||
the `push` and `force-push` permissions as mentioned above for each of the
|
|
||||||
repositories.
|
|
||||||
|
|
||||||
e.g.
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[forge.github.repos]
|
|
||||||
my-repo = { repo = "owner/repo", branch = "main", gitdir = "/home/pcampbell/project/my-repo" }
|
|
||||||
|
|
||||||
[forge.github.repos.other-repo]
|
|
||||||
repo = "user/other"
|
|
||||||
branch = "master"
|
|
||||||
main = "master"
|
|
||||||
next = "ci-testing"
|
|
||||||
dev = "trunk"
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that toml allows specifying the values on one line, or across multiple
|
|
||||||
lines. Both are equivalent. What is not equivalent between `my-repo` and
|
|
||||||
`other-repo`, is that one will require a configuration file within the repo
|
|
||||||
itself. `other-repo` specifies the `main`, `next` and `dev` branches to be
|
|
||||||
used, but `my-repo` doesn't.
|
|
||||||
|
|
||||||
A sample `.git-next-toml` file that would need to exist in `my-repo`'s `owner/repo`
|
|
||||||
repo, on the `main` branch:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[branches]
|
|
||||||
main = "main"
|
|
||||||
next = "next"
|
|
||||||
dev = "dev"
|
|
||||||
```
|
|
||||||
|
|
||||||
- **repo** - the owner and name of the repo to be monitored
|
|
||||||
- **branch** - the branch to look for a `.git-next.toml` file if needed
|
|
||||||
- **gitdir** - (optional) you can use a local copy of the repo
|
|
||||||
- **main** - the branch to use as `main`
|
|
||||||
- **next** - the branch to use as `next`
|
|
||||||
- **dev** - the branch to use as `dev`
|
|
||||||
|
|
||||||
##### gitdir
|
|
||||||
|
|
||||||
Additional notes on using `gitdir`:
|
|
||||||
|
|
||||||
When you specify the `gitdir` value, the repo cloned in that directory will
|
|
||||||
be used for perform the equivalent of `git fetch`, `git push` and `git push
|
|
||||||
--force-with-lease`.
|
|
||||||
|
|
||||||
These commands will not affect the contents of your working tree, nor will
|
|
||||||
it change any local branches. Only the details about branches on the remote
|
|
||||||
forge will be updated.
|
|
||||||
|
|
||||||
Currently `git-next` can only use a `gitdir` if the forge and repo is the
|
|
||||||
same one specified as the `origin` remote. Otherwise the behaviour is
|
|
||||||
untested and undefined.
|
|
||||||
|
|
||||||
## Webhook Notifications
|
|
||||||
|
|
||||||
When sending a Webhook Notification to a user they are sent using the
|
|
||||||
Standard Webhooks format. That means all POST messages have the
|
|
||||||
following headers:
|
|
||||||
|
|
||||||
- `Webhook-Id`
|
|
||||||
- `Webhook-Signature`
|
|
||||||
- `Webhook-Timestamp`
|
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
#### Dev Not Based on Main
|
|
||||||
|
|
||||||
This message `type` indicates that the `dev` branch is not based on `main`.
|
|
||||||
|
|
||||||
**Action Required**: Rebase the `dev` branch onto the `main` branch.
|
|
||||||
|
|
||||||
Sample payload:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"branches": {
|
|
||||||
"dev": "dev",
|
|
||||||
"main": "main"
|
|
||||||
},
|
|
||||||
"forge_alias": "jo",
|
|
||||||
"repo_alias": "kxio",
|
|
||||||
"log": [
|
|
||||||
"* 9bfce91 (origin/dev) fix: add log graph to notifications",
|
|
||||||
"| * c37bd2c (origin/next, origin/main) feat: add log graph to notifications",
|
|
||||||
"|/",
|
|
||||||
"* 8c19680 refactor: macros use a more common syntax"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timestamp": "1721760933",
|
|
||||||
"type": "branch.dev.not-on-main"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CI Check Failed
|
|
||||||
|
|
||||||
This message `type` indicates that the commit on the tip of the `next` branch has failed the
|
|
||||||
configured CI checks.
|
|
||||||
|
|
||||||
**Action Required**: Either update the commit to correct the issue CI raised, or, if the issue
|
|
||||||
is transient (e.g. a network issue), re-run/re-start the job in your CI.
|
|
||||||
|
|
||||||
Sample payload:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"commit": {
|
|
||||||
"sha": "c37bd2caf6825f9770d725a681e5cfc09d7fd4f2",
|
|
||||||
"message": "feat: add log graph to notifications (1 of 2)"
|
|
||||||
},
|
|
||||||
"forge_alias": "jo",
|
|
||||||
"repo_alias": "kxio",
|
|
||||||
"log": [
|
|
||||||
"* 9bfce91 (origin/dev) feat: add log graph to notifications (2 of 2)",
|
|
||||||
"* c37bd2c (origin/next) feat: add log graph to notifications (1 of 2)",
|
|
||||||
"* 8c19680 (origin/main) refactor: macros use a more common syntax"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timestamp": "1721760933",
|
|
||||||
"type": "cicheck.failed"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Repo Config Load Failed
|
|
||||||
|
|
||||||
This message `type` indicates that `git-next` wasn't able to load the configuration for the
|
|
||||||
repo from the `git-next.toml` file in the repository.
|
|
||||||
|
|
||||||
**Action Required**: Review the `reason` provided.
|
|
||||||
|
|
||||||
Sample payload:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"reason": "File not found: .git-next.toml",
|
|
||||||
"forge_alias": "jo",
|
|
||||||
"repo_alias": "kxio"
|
|
||||||
},
|
|
||||||
"timestamp": "1721760933",
|
|
||||||
"type": "config.load.failed"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Webhook Registration Failed
|
|
||||||
|
|
||||||
This message `type` indicates that `git-next` wasn't able to register it's webhook with the
|
|
||||||
forge repository, so will not receive updates when the branches in the repo are updated.
|
|
||||||
|
|
||||||
**Action Required**: Review the `reason` provided.
|
|
||||||
|
|
||||||
Sample payload:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": {
|
|
||||||
"reason": "repo config not loaded",
|
|
||||||
"forge_alias": "jo",
|
|
||||||
"repo_alias": "kxio"
|
|
||||||
},
|
|
||||||
"timestamp": "1721760933",
|
|
||||||
"type": "webhook.registration.failed"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Behaviour
|
|
||||||
|
|
||||||
The branch names are configurable, but we will talk about `main`, `next` and `dev`.
|
|
||||||
|
|
||||||
Development happens on the `dev` branch, where each commit is expected to
|
|
||||||
be able to pass the CI checks.
|
|
||||||
|
|
||||||
(Note: in the diagrams, mermaid isn't capable of showing `main` and `next` on
|
|
||||||
the same commit, so we show `next` as empty)
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gitGraph
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
|
|
||||||
branch next
|
|
||||||
|
|
||||||
branch dev
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
```
|
|
||||||
|
|
||||||
When the `git-next` server sees that the `dev` branch is ahead of the `next`
|
|
||||||
branch, it will push the `next` branch fast-forward one commit along the `dev`
|
|
||||||
branch.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gitGraph
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
|
|
||||||
branch next
|
|
||||||
commit
|
|
||||||
|
|
||||||
branch dev
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
```
|
|
||||||
|
|
||||||
It will then wait for the CI checks to pass for the newly updated `next` branch.
|
|
||||||
When the CI checks for the `next` branch pass, it will push the `main` branch
|
|
||||||
fast-forward to the `next` branch. We return to the top and start again.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gitGraph
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
|
|
||||||
branch next
|
|
||||||
|
|
||||||
branch dev
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
```
|
|
||||||
|
|
||||||
If the CI checks should fail for the `next` branch, the developer should
|
|
||||||
**amend** that commit **in the history of their `dev` branch**.
|
|
||||||
They should then force-push their rebased `dev` branch.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
gitGraph
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
|
|
||||||
branch next
|
|
||||||
commit
|
|
||||||
|
|
||||||
checkout main
|
|
||||||
|
|
||||||
branch dev
|
|
||||||
commit
|
|
||||||
|
|
||||||
commit
|
|
||||||
commit
|
|
||||||
```
|
|
||||||
|
|
||||||
`git-next` will then detect that the `next` branch is no longer part of the
|
|
||||||
`dev` branch ancestory, and will reset `next` back to `main`.
|
|
||||||
We then return to the top, where `git-next` sees that `dev` is ahead of `next`.
|
|
||||||
|
|
||||||
When the `dev` branch is on the same commit as the `main` branch, then there
|
|
||||||
are no pending commits and `git-next` will wait until it receives a webhook
|
|
||||||
indicating that there has been a push to one of the branches. At which point
|
|
||||||
it will start at the top again.
|
|
||||||
|
|
||||||
### Important
|
|
||||||
|
|
||||||
The `dev` branch _should_ have the `next` branch as an ancestor.
|
|
||||||
|
|
||||||
However, when the commit on tip of the `next` branch has failed CI and is
|
|
||||||
amended, this will not be the case. When this happens `git-next` will
|
|
||||||
**force-push** the `next` branch back to the same commit as the `main` branch.
|
|
||||||
|
|
||||||
This is the only time a force-push will happen in `git-next`.
|
|
||||||
|
|
||||||
In short, the `next` branch **belongs** to `git-next`. Don't try to update it
|
|
||||||
yourself. `git-next` will update the `next` it as it sees fit.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
To use `git-next` for trunk-based development, follow these steps:
|
|
||||||
|
|
||||||
### Initialise the repo (optional)
|
|
||||||
|
|
||||||
You need to specify which branches you are using. You can do this in the repo,
|
|
||||||
or in the server configuration.
|
|
||||||
|
|
||||||
To create a default config file for the repo, run this command in the root of
|
|
||||||
your repo:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git next init
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create a `.git-next.toml` file. [Default](./crates/cli/default.toml)
|
|
||||||
|
|
||||||
By default the expected branches are `main`, `next` and `dev`. Each of these
|
|
||||||
three branches _must_ exist in your repo.
|
|
||||||
|
|
||||||
### Initialise the server
|
|
||||||
|
|
||||||
The server uses the file `git-next-server.toml` for configuration. It expects
|
|
||||||
to find this file the the current directory when executed.
|
|
||||||
|
|
||||||
The create the default config file, run this command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git next server init
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create a `git-next-server.toml` file. [Default](./crates/server/server-default.toml)
|
|
||||||
|
|
||||||
Edit this file to your needs. See the [Configuration](#configuration) section above.
|
|
||||||
|
|
||||||
### Run the server
|
|
||||||
|
|
||||||
In the directory with your `git-next-server.toml` file, run the command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git next server start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Forges
|
|
||||||
|
|
||||||
The following forges are supported:
|
|
||||||
|
|
||||||
- [ForgeJo](https://forgejo.org) (probably compatible with Gitea, but not tested)
|
|
||||||
- [GitHub](https://github.com/)
|
|
||||||
|
|
||||||
Note: ForgeJo is a hard fork of Gitea, but currently they are largely compatible.
|
|
||||||
For now using a `forge_type` of `ForgeJo` with a Gitea instance will probably work
|
|
||||||
okay. The only API calls we make are around registering and unregistering webhooks.
|
|
||||||
So, as long as those APIs remain the same, they should be compatible.
|
|
||||||
|
|
||||||
#### ForgeJo
|
|
||||||
|
|
||||||
Configure the forge in `git-next-server.toml` like:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[forge.jo]
|
|
||||||
forge_type = "ForgeJo"
|
|
||||||
hostname = "git.myforgejo.com"
|
|
||||||
user = "bob"
|
|
||||||
token = "..."
|
|
||||||
|
|
||||||
[forge.jo.repos]
|
|
||||||
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://git.example.net/user/hello on the branch 'main'
|
|
||||||
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
|
||||||
```
|
|
||||||
|
|
||||||
The token is created on your ForgeJo instance at (for example)
|
|
||||||
`https://git.myforgejo.com/user/settings/applications`
|
|
||||||
and requires the `write:repository` permission.
|
|
||||||
|
|
||||||
#### GitHub
|
|
||||||
|
|
||||||
Configure the forge in `git-next-server.toml` like:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[forge.gh]
|
|
||||||
forge_type = "GitHub"
|
|
||||||
hostname = "github.com" # required even for GitHub
|
|
||||||
user = "bob"
|
|
||||||
token = "..."
|
|
||||||
|
|
||||||
[forge.gh.repos]
|
|
||||||
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://github.com/user/hello on the branch 'main'
|
|
||||||
world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch
|
|
||||||
```
|
|
||||||
|
|
||||||
The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions.
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
`git-next` is available as a [Docker image](https://git.kemitix.net/kemitix/-/packages/container/git-next/).
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker pull docker pull git.kemitix.net/kemitix/git-next:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
Here is an example `docker-compose.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
server:
|
|
||||||
image: git.kemitix.net/kemitix/git-next:latest
|
|
||||||
container_name: git-next-server
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
RUST_LOG: "hyper=warn,info"
|
|
||||||
ports:
|
|
||||||
- 8080:8092
|
|
||||||
volumes:
|
|
||||||
- ./:/app/
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: this assumes the `git-next-server.toml` has a `listen.http.port` of
|
|
||||||
`8092` and that you are using a reverse proxy to route traffic arriving at
|
|
||||||
`listen.url` to port `8080`.
|
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
This will run with the `server start` options:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -it -p "8080:8092" -v .:/app/ git.kemitix.net/kemitix/git-next:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
To perform `server init`:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest server init
|
|
||||||
```
|
|
||||||
|
|
||||||
To perform repo `init`:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run -it -v .:/app/ git.kemitix.net/kemitix/git-next:latest init
|
|
||||||
```
|
|
||||||
|
|
||||||
TUI support is not available in the docker container. See [kemitix/git-next#154](https://git.kemitix.net/kemitix/git-next/issues/154).
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions to `git-next` are welcome! If you find a bug or have a feature
|
|
||||||
request, please
|
|
||||||
[create an issue](https://git.kemitix.net/kemitix/git-next/issues/new).
|
|
||||||
If you'd like to contribute code, feel free to submit changes.
|
|
||||||
|
|
||||||
Before you start committing, run the `just install-hooks` command to setup the
|
|
||||||
Git Hooks. ([Get Just](https://just.systems/man/en/chapter_3.html))
|
|
||||||
|
|
||||||
## Crate Dependency
|
|
||||||
|
|
||||||
The following diagram shows the dependency between the crates that make up `git-next`:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
stateDiagram-v2
|
|
||||||
|
|
||||||
cli --> core
|
|
||||||
cli --> forge_forgejo
|
|
||||||
cli --> forge_github
|
|
||||||
|
|
||||||
forge_forgejo --> core
|
|
||||||
|
|
||||||
forge_github --> core
|
|
||||||
```
|
|
||||||
|
|
||||||
## Actor Supervision Tree
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
mindmap
|
|
||||||
Root
|
|
||||||
Alerts
|
|
||||||
FileWatcher
|
|
||||||
Server
|
|
||||||
Repo 1
|
|
||||||
Repo 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
`git-next` is released under the [MIT License](./LICENSE).
|
|
|
@ -1,90 +0,0 @@
|
||||||
//
|
|
||||||
|
|
||||||
use crate::repo::messages::NotifyUser;
|
|
||||||
|
|
||||||
use git_next_core::git::UserNotification;
|
|
||||||
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
impl NotifyUser {
|
|
||||||
pub fn as_json(&self, timestamp: time::OffsetDateTime) -> serde_json::Value {
|
|
||||||
let timestamp = timestamp.unix_timestamp().to_string();
|
|
||||||
match &**self {
|
|
||||||
UserNotification::CICheckFailed {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
commit,
|
|
||||||
log,
|
|
||||||
} => json!({
|
|
||||||
"type": "cicheck.failed",
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"data": {
|
|
||||||
"forge_alias": forge_alias,
|
|
||||||
"repo_alias": repo_alias,
|
|
||||||
"commit": {
|
|
||||||
"sha": commit.sha(),
|
|
||||||
"message": commit.message()
|
|
||||||
},
|
|
||||||
"log": **log
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
UserNotification::RepoConfigLoadFailure {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
reason,
|
|
||||||
} => json!({
|
|
||||||
"type": "config.load.failed",
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"data": {
|
|
||||||
"forge_alias": forge_alias,
|
|
||||||
"repo_alias": repo_alias,
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
UserNotification::WebhookRegistration {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
reason,
|
|
||||||
} => json!({
|
|
||||||
"type": "webhook.registration.failed",
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"data": {
|
|
||||||
"forge_alias": forge_alias,
|
|
||||||
"repo_alias": repo_alias,
|
|
||||||
"reason": reason
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
UserNotification::DevNotBasedOnMain {
|
|
||||||
forge_alias,
|
|
||||||
repo_alias,
|
|
||||||
dev_branch,
|
|
||||||
main_branch,
|
|
||||||
dev_commit,
|
|
||||||
main_commit,
|
|
||||||
log,
|
|
||||||
} => json!({
|
|
||||||
"type": "branch.dev.not-on-main",
|
|
||||||
"timestamp": timestamp,
|
|
||||||
"data": {
|
|
||||||
"forge_alias": forge_alias,
|
|
||||||
"repo_alias": repo_alias,
|
|
||||||
"branches": {
|
|
||||||
"dev": dev_branch,
|
|
||||||
"main": main_branch
|
|
||||||
},
|
|
||||||
"commits": {
|
|
||||||
"dev": {
|
|
||||||
"sha": dev_commit.sha(),
|
|
||||||
"message": dev_commit.message()
|
|
||||||
},
|
|
||||||
"main": {
|
|
||||||
"sha": main_commit.sha(),
|
|
||||||
"message": main_commit.message()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log": **log
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "git-next-core"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
repository = { workspace = true }
|
|
||||||
description = "core for git-next, the trunk-based development manager"
|
|
||||||
|
|
||||||
[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)'] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["forgejo", "github"]
|
|
||||||
forgejo = []
|
|
||||||
github = []
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# logging
|
|
||||||
tracing = { workspace = true }
|
|
||||||
|
|
||||||
# fs/network
|
|
||||||
kxio = { workspace = true }
|
|
||||||
|
|
||||||
# TOML parsing
|
|
||||||
serde = { workspace = true }
|
|
||||||
toml = { workspace = true }
|
|
||||||
|
|
||||||
# Secrets and Password
|
|
||||||
secrecy = { workspace = true }
|
|
||||||
|
|
||||||
# Git
|
|
||||||
gix = { workspace = true }
|
|
||||||
git-url-parse = { workspace = true }
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
|
|
||||||
# Webhooks
|
|
||||||
ulid = { workspace = true }
|
|
||||||
time = { workspace = true }
|
|
||||||
|
|
||||||
# boilerplate
|
|
||||||
derive_more = { workspace = true }
|
|
||||||
derive-with = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
pike = { workspace = true }
|
|
||||||
|
|
||||||
# TOML parsing
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
|
|
||||||
mockall = { workspace = true }
|
|
||||||
|
|
||||||
#iters
|
|
||||||
take-until = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
# Testing
|
|
||||||
assert2 = { workspace = true }
|
|
||||||
rand = { workspace = true }
|
|
||||||
test-log = { workspace = true }
|
|
||||||
pretty_assertions = { workspace = true }
|
|
|
@ -1,9 +0,0 @@
|
||||||
# git-next
|
|
||||||
|
|
||||||
## Trunk-based developement manager.
|
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|
||||||
See [git-next](https://crates.io/crates/git-next) for more information.
|
|
|
@ -1,34 +0,0 @@
|
||||||
//
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("io")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("unable to open repo: {0}")]
|
|
||||||
UnableToOpenRepo(String),
|
|
||||||
|
|
||||||
#[error("no remote found")]
|
|
||||||
NoFetchRemoteFound,
|
|
||||||
|
|
||||||
#[error("remote connect: {0}")]
|
|
||||||
Connect(String),
|
|
||||||
|
|
||||||
#[error("prepare: {0}")]
|
|
||||||
Prepare(String),
|
|
||||||
|
|
||||||
#[error("receive: {0}")]
|
|
||||||
Receive(String),
|
|
||||||
|
|
||||||
#[error("lock")]
|
|
||||||
Lock,
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[error("expected failure in test")]
|
|
||||||
TestFailureExpected,
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[error("test")]
|
|
||||||
TestResult(#[from] Box<dyn std::error::Error>),
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
use std::ops::Deref as _;
|
|
||||||
|
|
||||||
//
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// assumes running in the git-next repo which should have main, next and dev as remote branches
|
|
||||||
fn should_return_file() -> TestResult {
|
|
||||||
let_assert!(Ok(fs) = kxio::fs::temp());
|
|
||||||
let repo_config = given::a_repo_config();
|
|
||||||
let file_name = given::a_pathbuf();
|
|
||||||
let contents = given::a_name();
|
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
|
||||||
let forge_details = given::forge_details();
|
|
||||||
|
|
||||||
let test_repository = git::repository::test(fs.deref().clone(), forge_details);
|
|
||||||
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
|
|
||||||
then::commit_named_file_to_branch(
|
|
||||||
&file_name,
|
|
||||||
&contents,
|
|
||||||
&fs,
|
|
||||||
&gitdir,
|
|
||||||
&repo_config.branches().main(),
|
|
||||||
)?;
|
|
||||||
// then::create_a_commit_on_branch(&fs, &gitdir, &repo_config.branches().main())?;
|
|
||||||
let_assert!(
|
|
||||||
Ok(result) = open_repository.read_file(&repo_config.branches().main(), &file_name),
|
|
||||||
"read file"
|
|
||||||
);
|
|
||||||
assert_eq!(result, contents);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
// assumes running in the git-next repo which should have main, next and dev as remote branches
|
|
||||||
fn should_error_on_missing_file() -> TestResult {
|
|
||||||
let_assert!(Ok(fs) = kxio::fs::temp());
|
|
||||||
let gitdir = GitDir::new(fs.base().to_path_buf(), StoragePathType::Internal);
|
|
||||||
let forge_details = given::forge_details();
|
|
||||||
let test_repository = git::repository::test(fs.deref().clone(), forge_details);
|
|
||||||
let_assert!(Ok(open_repository) = test_repository.open(&gitdir));
|
|
||||||
let repo_config = &given::a_repo_config();
|
|
||||||
let branches = repo_config.branches();
|
|
||||||
then::create_a_commit_on_branch(&fs, &gitdir, &branches.dev())?;
|
|
||||||
let_assert!(
|
|
||||||
Err(err) = open_repository.read_file(&branches.dev(), &given::a_pathbuf()),
|
|
||||||
"read file"
|
|
||||||
);
|
|
||||||
eprintln!("err: {err:#?}");
|
|
||||||
assert!(matches!(err, git::file::Error::FileNotFound));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "git-next-forge-forgejo"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
repository = { workspace = true }
|
|
||||||
description = "Forgejo support for git-next, the trunk-based development manager"
|
|
||||||
|
|
||||||
[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)'] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
git-next-core = { workspace = true }
|
|
||||||
|
|
||||||
# logging
|
|
||||||
tracing = { workspace = true }
|
|
||||||
|
|
||||||
# git
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
|
|
||||||
# fs/network
|
|
||||||
kxio = { workspace = true }
|
|
||||||
|
|
||||||
# TOML parsing
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
|
|
||||||
# Secrets and Password
|
|
||||||
secrecy = { workspace = true }
|
|
||||||
|
|
||||||
# # Actors
|
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
# Testing
|
|
||||||
assert2 = { workspace = true }
|
|
||||||
rand = { workspace = true }
|
|
|
@ -1,9 +0,0 @@
|
||||||
# git-next
|
|
||||||
|
|
||||||
## Trunk-based developement manager.
|
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|
||||||
See [git-next](https://crates.io/crates/git-next) for more information.
|
|
|
@ -1,54 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "git-next-forge-github"
|
|
||||||
version = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
repository = { workspace = true }
|
|
||||||
description = "GitHub support for git-next, the trunk-based development manager"
|
|
||||||
|
|
||||||
[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)'] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
git-next-core = { workspace = true }
|
|
||||||
|
|
||||||
# own version for UserAgent requests to github.com
|
|
||||||
clap = { workspace = true }
|
|
||||||
|
|
||||||
# logging
|
|
||||||
tracing = { workspace = true }
|
|
||||||
|
|
||||||
# sha256 encoding (e.g. verify github webhooks)
|
|
||||||
hmac = { workspace = true }
|
|
||||||
sha2 = { workspace = true }
|
|
||||||
hex = { workspace = true }
|
|
||||||
|
|
||||||
# git
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
|
|
||||||
# fs/network
|
|
||||||
kxio = { workspace = true }
|
|
||||||
|
|
||||||
# TOML parsing
|
|
||||||
serde = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
|
|
||||||
# Secrets and Password
|
|
||||||
secrecy = { workspace = true }
|
|
||||||
|
|
||||||
# boilerplate
|
|
||||||
derive_more = { workspace = true }
|
|
||||||
|
|
||||||
# # Actors
|
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
# Testing
|
|
||||||
assert2 = { workspace = true }
|
|
||||||
rand = { workspace = true }
|
|
|
@ -1,9 +0,0 @@
|
||||||
# git-next
|
|
||||||
|
|
||||||
## Trunk-based developement manager.
|
|
||||||
|
|
||||||
`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.
|
|
||||||
|
|
||||||
See [git-next](https://crates.io/crates/git-next) for more information.
|
|
26
justfile
26
justfile
|
@ -3,6 +3,7 @@ build:
|
||||||
set -e
|
set -e
|
||||||
cargo fmt
|
cargo fmt
|
||||||
cargo fmt --check
|
cargo fmt --check
|
||||||
|
forgejo-todo-checker --workspace $PWD --site https://git.kemitix.net --repo kemitix/git-next
|
||||||
cargo machete
|
cargo machete
|
||||||
cargo hack clippy
|
cargo hack clippy
|
||||||
cargo hack build
|
cargo hack build
|
||||||
|
@ -11,12 +12,37 @@ build:
|
||||||
# cargo test --example get
|
# cargo test --example get
|
||||||
# cargo mutants --jobs 4
|
# cargo mutants --jobs 4
|
||||||
|
|
||||||
|
ci:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cargo fmt
|
||||||
|
cargo fmt --check
|
||||||
|
forgejo-todo-checker --workspace $PWD --site https://git.kemitix.net --repo kemitix/git-next
|
||||||
|
cargo machete
|
||||||
|
cargo hack --feature-powerset clippy
|
||||||
|
cargo hack --feature-powerset build
|
||||||
|
cargo hack --feature-powerset test
|
||||||
|
cargo doc
|
||||||
|
# cargo test --example get
|
||||||
|
# cargo mutants --jobs 4
|
||||||
|
|
||||||
test-in-docker:
|
test-in-docker:
|
||||||
docker run --rm -u $(id -u):$(id -g) -v ${PWD}:/app/ git.kemitix.net/kemitix/rust:latest cargo test
|
docker run --rm -u $(id -u):$(id -g) -v ${PWD}:/app/ git.kemitix.net/kemitix/rust:latest cargo test
|
||||||
|
|
||||||
shell-in-docker:
|
shell-in-docker:
|
||||||
docker run --rm -u $(id -u):$(id -g) -it -v ${PWD}:/app/ git.kemitix.net/kemitix/rust:latest bash
|
docker run --rm -u $(id -u):$(id -g) -it -v ${PWD}:/app/ git.kemitix.net/kemitix/rust:latest bash
|
||||||
|
|
||||||
|
docker-test-image := "git.kemitix.net/kemitix/git-next:test"
|
||||||
|
|
||||||
|
build-docker:
|
||||||
|
docker build . -t {{ docker-test-image }}
|
||||||
|
|
||||||
|
run-in-docker: build-docker
|
||||||
|
docker run --rm -u $(id -u):$(id -g) -v ${PWD}:/app/ {{ docker-test-image }} server start
|
||||||
|
|
||||||
|
run-ui-in-docker: build-docker
|
||||||
|
docker run --rm -u $(id -u):$(id -g) -it -v ${PWD}:/app/ {{ docker-test-image }} server start --ui
|
||||||
|
|
||||||
install-hooks:
|
install-hooks:
|
||||||
@echo "Installing git hooks"
|
@echo "Installing git hooks"
|
||||||
cargo install cc-cli
|
cargo install cc-cli
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[tools]
|
|
||||||
"cargo:cargo-hack" = "latest"
|
|
|
@ -1,6 +1,6 @@
|
||||||
//
|
//
|
||||||
use crate::alerts::short_message;
|
use crate::alerts::short_message;
|
||||||
use git_next_core::git::UserNotification;
|
use crate::core::git::UserNotification;
|
||||||
|
|
||||||
pub(super) fn send_desktop_notification(user_notification: &UserNotification) {
|
pub(super) fn send_desktop_notification(user_notification: &UserNotification) {
|
||||||
let message = short_message(user_notification);
|
let message = short_message(user_notification);
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
use git_next_core::{
|
use crate::core::{
|
||||||
git::UserNotification,
|
git::UserNotification,
|
||||||
server::{EmailConfig, SmtpConfig},
|
server::{EmailConfig, SmtpConfig},
|
||||||
};
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
use git_next_core::git::UserNotification;
|
use crate::core::git::UserNotification;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
|
@ -1,5 +1,6 @@
|
||||||
//
|
//
|
||||||
use git_next_core::{git::UserNotification, message, server::Shout};
|
use crate::core::{git::UserNotification, server::Shout};
|
||||||
|
use crate::message;
|
||||||
|
|
||||||
message!(UpdateShout, Shout, "Updated Shout configuration");
|
message!(UpdateShout, Shout, "Updated Shout configuration");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use derive_more::derive::Constructor;
|
use derive_more::derive::Constructor;
|
||||||
|
|
||||||
use git_next_core::{git::UserNotification, server::Shout};
|
use crate::core::{git::UserNotification, server::Shout};
|
||||||
|
|
||||||
pub use history::History;
|
pub use history::History;
|
||||||
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
|
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};
|
|
@ -1,7 +1,7 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::core::git::UserNotification;
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
use git_next_core::git::UserNotification;
|
|
||||||
|
|
||||||
use crate::{alerts::History, repo::tests::given};
|
use crate::{alerts::History, repo::tests::given};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
use git_next_core::{git::UserNotification, server::OutboundWebhook};
|
use crate::core::{git::UserNotification, server::OutboundWebhook};
|
||||||
use secrecy::ExposeSecret as _;
|
use secrecy::ExposeSecret as _;
|
||||||
use standardwebhooks::Webhook;
|
use standardwebhooks::Webhook;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::{
|
use crate::core::config::{
|
||||||
ApiToken, BranchName, ForgeAlias, ForgeDetails, ForgeType, Hostname, RepoAlias, RepoBranches,
|
ApiToken, BranchName, ForgeAlias, ForgeDetails, ForgeType, Hostname, RepoAlias, RepoBranches,
|
||||||
RepoConfig, RepoConfigSource, RepoPath, User,
|
RepoConfig, RepoConfigSource, RepoPath, User,
|
||||||
};
|
};
|
||||||
|
@ -15,31 +15,31 @@ pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn api_token(n: u32) -> ApiToken {
|
pub fn api_token(n: u32) -> ApiToken {
|
||||||
ApiToken::new(format!("api-{n}").into())
|
ApiToken::new(format!("api-{n}").into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn user(n: u32) -> User {
|
pub fn user(n: u32) -> User {
|
||||||
User::new(format!("user-{n}"))
|
User::new(format!("user-{n}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hostname(n: u32) -> Hostname {
|
pub fn hostname(n: u32) -> Hostname {
|
||||||
Hostname::new(format!("hostname-{n}"))
|
Hostname::new(format!("hostname-{n}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn forge_name(n: u32) -> ForgeAlias {
|
pub fn forge_name(n: u32) -> ForgeAlias {
|
||||||
ForgeAlias::new(format!("forge-name-{n}"))
|
ForgeAlias::new(format!("forge-name-{n}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn branch_name(n: u32) -> BranchName {
|
pub fn branch_name(n: u32) -> BranchName {
|
||||||
BranchName::new(format!("branch-name-{n}"))
|
BranchName::new(format!("branch-name-{n}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn repo_path(n: u32) -> RepoPath {
|
pub fn repo_path(n: u32) -> RepoPath {
|
||||||
RepoPath::new(format!("repo-path-{n}"))
|
RepoPath::new(format!("repo-path-{n}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn repo_alias(n: u32) -> RepoAlias {
|
pub fn repo_alias(n: u32) -> RepoAlias {
|
||||||
RepoAlias::new(format!("repo-alias-{n}"))
|
RepoAlias::new(format!("repo-alias-{n}"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::core::config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User};
|
||||||
config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User},
|
use crate::s;
|
||||||
s,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::CommitCount;
|
use super::CommitCount;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::{ApiToken, ForgeAlias, ForgeConfig, ForgeType, Hostname, User};
|
use crate::core::config::{ApiToken, ForgeAlias, ForgeConfig, ForgeType, Hostname, User};
|
||||||
|
|
||||||
use super::CommitCount;
|
use super::CommitCount;
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
mod api_token;
|
mod api_token;
|
||||||
mod branch_name;
|
mod branch_name;
|
||||||
mod commit_count;
|
mod commit_count;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
||||||
mod forge_alias;
|
mod forge_alias;
|
||||||
mod forge_config;
|
mod forge_config;
|
||||||
mod forge_details;
|
mod forge_details;
|
||||||
|
@ -53,4 +56,4 @@ pub use webhook::forge_notification::ForgeNotification;
|
||||||
pub use webhook::id::WebhookId;
|
pub use webhook::id::WebhookId;
|
||||||
|
|
||||||
// re-export
|
// re-export
|
||||||
pub use pike::{pike, pike_opt, pike_res};
|
pub use pike::pike;
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
use crate::config::{WebhookAuth, WebhookId};
|
use crate::core::config::{WebhookAuth, WebhookId};
|
||||||
|
|
||||||
#[derive(Debug, derive_more::Constructor)]
|
#[derive(Debug, derive_more::Constructor)]
|
||||||
pub struct RegisteredWebhook {
|
pub struct RegisteredWebhook {
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::BranchName;
|
use crate::core::config::BranchName;
|
||||||
|
|
||||||
/// Mapped from `.git-next.toml` file at `branches`
|
/// Mapped from `.git-next.toml` file at `branches`
|
||||||
#[derive(
|
#[derive(
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::config::{RepoBranches, RepoConfigSource};
|
use crate::core::config::{RepoBranches, RepoConfigSource};
|
||||||
|
|
||||||
/// Mapped from `.git-next.toml` file in target repo
|
/// Mapped from `.git-next.toml` file in target repo
|
||||||
/// Is also derived from the optional parameters in `git-next-server.toml` at
|
/// Is also derived from the optional parameters in `git-next-server.toml` at
|
|
@ -15,24 +15,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{ForgeAlias, ForgeConfig, RepoAlias},
|
core::config::{ForgeAlias, ForgeConfig, RepoAlias},
|
||||||
newtype, s,
|
newtype, s, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("fs: {0}")]
|
|
||||||
KxioFs(#[from] kxio::fs::Error),
|
|
||||||
|
|
||||||
#[error("deserialise toml: {0}")]
|
|
||||||
TomlDe(#[from] toml::de::Error),
|
|
||||||
|
|
||||||
#[error("parse IP addres/port: {0}")]
|
|
||||||
AddressParse(#[from] std::net::AddrParseError),
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
/// Mapped from the `git-next-server.toml` file
|
/// Mapped from the `git-next-server.toml` file
|
||||||
#[derive(
|
#[derive(
|
||||||
Clone,
|
Clone,
|
|
@ -2,7 +2,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{
|
core::config::{
|
||||||
git_dir::StoragePathType, BranchName, GitDir, RepoBranches, RepoConfig, RepoConfigSource,
|
git_dir::StoragePathType, BranchName, GitDir, RepoBranches, RepoConfig, RepoConfigSource,
|
||||||
RepoPath,
|
RepoPath,
|
||||||
},
|
},
|
|
@ -7,16 +7,15 @@ use std::collections::BTreeMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
s,
|
core::{
|
||||||
server::{AppConfig, Http, Storage},
|
server::{AppConfig, Http, Storage},
|
||||||
webhook::push::Branch,
|
webhook::push::Branch,
|
||||||
|
},
|
||||||
|
s,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod url;
|
mod url;
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
|
|
||||||
type TestResult = Result<()>;
|
|
||||||
|
|
||||||
mod server_repo_config {
|
mod server_repo_config {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -100,10 +99,12 @@ mod server_repo_config {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mod repo_config {
|
mod repo_config {
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn should_parse_toml() -> TestResult {
|
fn should_parse_toml() -> Result<()> {
|
||||||
let main = given::a_name();
|
let main = given::a_name();
|
||||||
let next = given::a_name();
|
let next = given::a_name();
|
||||||
let dev = given::a_name();
|
let dev = given::a_name();
|
||||||
|
@ -517,10 +518,12 @@ mod server {
|
||||||
use super::*;
|
use super::*;
|
||||||
mod load {
|
mod load {
|
||||||
|
|
||||||
|
use crate::{err, Result};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_should_parse_app_config() -> TestResult {
|
fn load_should_parse_app_config() -> Result<()> {
|
||||||
let app_config = given::an_app_config();
|
let app_config = given::an_app_config();
|
||||||
let fs = kxio::fs::temp()?;
|
let fs = kxio::fs::temp()?;
|
||||||
let_assert!(Ok(()) = write_app_config(&app_config, &fs), "write");
|
let_assert!(Ok(()) = write_app_config(&app_config, &fs), "write");
|
||||||
|
@ -530,7 +533,7 @@ mod server {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_app_config(app_config: &AppConfig, fs: &kxio::fs::FileSystem) -> TestResult {
|
fn write_app_config(app_config: &AppConfig, fs: &kxio::fs::FileSystem) -> Result<()> {
|
||||||
let http = &app_config.listen_socket_addr()?;
|
let http = &app_config.listen_socket_addr()?;
|
||||||
let http_addr = http.ip();
|
let http_addr = http.ip();
|
||||||
let http_port = app_config.listen_socket_addr()?.port();
|
let http_port = app_config.listen_socket_addr()?.port();
|
||||||
|
@ -553,11 +556,11 @@ mod server {
|
||||||
.forges()
|
.forges()
|
||||||
.next()
|
.next()
|
||||||
.map(|(fa, _)| fa)
|
.map(|(fa, _)| fa)
|
||||||
.ok_or("forge missing")?;
|
.ok_or_else(|| err!("forge missing"))?;
|
||||||
let forge_default = app_config
|
let forge_default = app_config
|
||||||
.forge
|
.forge
|
||||||
.get(forge_alias.as_ref())
|
.get(forge_alias.as_ref())
|
||||||
.ok_or("forge missing")?;
|
.ok_or_else(|| err!("forge missing"))?;
|
||||||
let forge_type = forge_default.forge_type();
|
let forge_type = forge_default.forge_type();
|
||||||
let forge_hostname = forge_default.hostname();
|
let forge_hostname = forge_default.hostname();
|
||||||
let forge_user = forge_default.user();
|
let forge_user = forge_default.user();
|
||||||
|
@ -716,7 +719,7 @@ mod push {
|
||||||
let message = given::a_name();
|
let message = given::a_name();
|
||||||
let push_event = {
|
let push_event = {
|
||||||
let branch_name = repo_branches.main();
|
let branch_name = repo_branches.main();
|
||||||
crate::webhook::Push::new(branch_name, sha, message)
|
crate::core::webhook::Push::new(branch_name, sha, message)
|
||||||
};
|
};
|
||||||
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Main));
|
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Main));
|
||||||
}
|
}
|
||||||
|
@ -727,7 +730,7 @@ mod push {
|
||||||
let message = given::a_name();
|
let message = given::a_name();
|
||||||
let push_event = {
|
let push_event = {
|
||||||
let branch_name = repo_branches.next();
|
let branch_name = repo_branches.next();
|
||||||
crate::webhook::Push::new(branch_name, sha, message)
|
crate::core::webhook::Push::new(branch_name, sha, message)
|
||||||
};
|
};
|
||||||
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Next));
|
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Next));
|
||||||
}
|
}
|
||||||
|
@ -738,7 +741,7 @@ mod push {
|
||||||
let message = given::a_name();
|
let message = given::a_name();
|
||||||
let push_event = {
|
let push_event = {
|
||||||
let branch_name = repo_branches.dev();
|
let branch_name = repo_branches.dev();
|
||||||
crate::webhook::Push::new(branch_name, sha, message)
|
crate::core::webhook::Push::new(branch_name, sha, message)
|
||||||
};
|
};
|
||||||
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Dev));
|
assert_eq!(push_event.branch(&repo_branches), Some(Branch::Dev));
|
||||||
}
|
}
|
||||||
|
@ -749,7 +752,7 @@ mod push {
|
||||||
let message = given::a_name();
|
let message = given::a_name();
|
||||||
let push_event = {
|
let push_event = {
|
||||||
let branch_name = BranchName::new(given::a_name());
|
let branch_name = BranchName::new(given::a_name());
|
||||||
crate::webhook::Push::new(branch_name, sha, message)
|
crate::core::webhook::Push::new(branch_name, sha, message)
|
||||||
};
|
};
|
||||||
assert_eq!(push_event.branch(&repo_branches), None);
|
assert_eq!(push_event.branch(&repo_branches), None);
|
||||||
}
|
}
|
||||||
|
@ -761,7 +764,7 @@ mod push {
|
||||||
let push_event = {
|
let push_event = {
|
||||||
let branch_name = repo_branches.main();
|
let branch_name = repo_branches.main();
|
||||||
let sha = sha.clone();
|
let sha = sha.clone();
|
||||||
crate::webhook::Push::new(branch_name, sha, message)
|
crate::core::webhook::Push::new(branch_name, sha, message)
|
||||||
};
|
};
|
||||||
assert_eq!(push_event.sha(), sha);
|
assert_eq!(push_event.sha(), sha);
|
||||||
}
|
}
|
||||||
|
@ -773,14 +776,14 @@ mod push {
|
||||||
let push_event = {
|
let push_event = {
|
||||||
let branch_name = repo_branches.main();
|
let branch_name = repo_branches.main();
|
||||||
let message = message.clone();
|
let message = message.clone();
|
||||||
crate::webhook::Push::new(branch_name, sha, message)
|
crate::core::webhook::Push::new(branch_name, sha, message)
|
||||||
};
|
};
|
||||||
assert_eq!(push_event.message(), message);
|
assert_eq!(push_event.message(), message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mod given {
|
mod given {
|
||||||
|
|
||||||
use crate::server::{EmailConfig, Listen, ListenUrl, OutboundWebhook, Shout, SmtpConfig};
|
use crate::core::server::{EmailConfig, Listen, ListenUrl, OutboundWebhook, Shout, SmtpConfig};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rand::Rng as _;
|
use rand::Rng as _;
|
||||||
|
@ -911,8 +914,8 @@ mod given {
|
||||||
RepoAlias::new(a_name())
|
RepoAlias::new(a_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_webhook_message_body() -> crate::webhook::forge_notification::Body {
|
pub fn a_webhook_message_body() -> crate::core::webhook::forge_notification::Body {
|
||||||
crate::webhook::forge_notification::Body::new(a_name())
|
crate::core::webhook::forge_notification::Body::new(a_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn some_repo_branches() -> RepoBranches {
|
pub fn some_repo_branches() -> RepoBranches {
|
|
@ -3,7 +3,7 @@ use std::collections::BTreeMap;
|
||||||
|
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
|
||||||
use crate::config::{ForgeAlias, RepoAlias};
|
use crate::core::config::{ForgeAlias, RepoAlias};
|
||||||
|
|
||||||
/// A notification receive from a Forge, typically via a Webhook.
|
/// A notification receive from a Forge, typically via a Webhook.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Constructor)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Constructor)]
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
use crate::config::{BranchName, RepoBranches};
|
use crate::core::config::{BranchName, RepoBranches};
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Constructor, PartialEq, Eq, derive_with::With)]
|
#[derive(Clone, Debug, Constructor, PartialEq, Eq, derive_with::With)]
|
|
@ -1,5 +1,6 @@
|
||||||
mod auth {
|
mod auth {
|
||||||
use crate::{s, WebhookAuth};
|
use crate::core::WebhookAuth;
|
||||||
|
use crate::s;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bytes() -> Result<(), Box<dyn std::error::Error>> {
|
fn bytes() -> Result<(), Box<dyn std::error::Error>> {
|
|
@ -1,5 +1,6 @@
|
||||||
//
|
//
|
||||||
use crate::{newtype, webhook};
|
use crate::core::webhook;
|
||||||
|
use crate::newtype;
|
||||||
|
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
@ -68,18 +69,3 @@ pub struct Histories {
|
||||||
pub next: Vec<Commit>,
|
pub next: Vec<Commit>,
|
||||||
pub dev: Vec<Commit>,
|
pub dev: Vec<Commit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod log {
|
|
||||||
use crate::BranchName;
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("branch: {branch}, error: {error}")]
|
|
||||||
Gix { branch: BranchName, error: String },
|
|
||||||
|
|
||||||
#[error("lock")]
|
|
||||||
Lock,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +1,36 @@
|
||||||
//
|
//
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("lock")]
|
// #[error("lock")]
|
||||||
Lock,
|
// Lock,
|
||||||
|
|
||||||
#[error("File not found: {}", 0)]
|
// #[error("File not found: {}", 0)]
|
||||||
NotFound(String),
|
// NotFound(String),
|
||||||
|
|
||||||
#[error("Unable to parse file contents")]
|
// #[error("Unable to parse file contents")]
|
||||||
ParseContent,
|
// ParseContent,
|
||||||
|
|
||||||
#[error("Unable to decode from base64")]
|
// #[error("Unable to decode from base64")]
|
||||||
DecodeFromBase64,
|
// DecodeFromBase64,
|
||||||
|
|
||||||
#[error("Unable to decode from UTF-8")]
|
// #[error("Unable to decode from UTF-8")]
|
||||||
DecodeFromUtf8,
|
// DecodeFromUtf8,
|
||||||
|
|
||||||
#[error("Unknown file encoding: {}", 0)]
|
// #[error("Unknown file encoding: {}", 0)]
|
||||||
UnknownEncoding(String),
|
// UnknownEncoding(String),
|
||||||
|
|
||||||
#[error("Not a file: {}", 0)]
|
// #[error("Not a file: {}", 0)]
|
||||||
NotFile(String),
|
// NotFile(String),
|
||||||
|
|
||||||
#[error("Unknown error (status: {})", 0)]
|
// #[error("Unknown error (status: {})", 0)]
|
||||||
Unknown(String),
|
// Unknown(String),
|
||||||
|
|
||||||
#[error("commit log: {0}")]
|
// #[error("commit log: {0}")]
|
||||||
CommitLog(#[from] crate::git::commit::log::Error),
|
// CommitLog(#[from] crate::git::commit::log::Error),
|
||||||
|
|
||||||
#[error("commit not found")]
|
|
||||||
CommitNotFound,
|
|
||||||
|
|
||||||
|
// #[error("commit not found")]
|
||||||
|
// CommitNotFound,
|
||||||
#[error("no tree in commit")]
|
#[error("no tree in commit")]
|
||||||
NoTreeInCommit(String),
|
NoTreeInCommit(String),
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
pub mod commit;
|
pub mod commit;
|
||||||
pub(super) mod r#trait;
|
pub mod r#trait;
|
||||||
pub mod webhook;
|
pub mod webhook;
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub use r#trait::ForgeLike;
|
pub use r#trait::ForgeLike;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub use r#trait::MockForgeLike;
|
pub use r#trait::MockForgeLike;
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::core::{
|
||||||
git, server::RepoListenUrl, webhook, ForgeNotification, RegisteredWebhook, WebhookAuth,
|
git, server::RepoListenUrl, webhook, ForgeNotification, RegisteredWebhook, WebhookAuth,
|
||||||
WebhookId,
|
WebhookId,
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,8 @@ use crate::{
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
pub trait ForgeLike: std::fmt::Debug + Send + Sync {
|
pub trait ForgeLike: std::fmt::Debug + Send + Sync {
|
||||||
fn duplicate(&self) -> Box<dyn ForgeLike>;
|
fn duplicate(&self) -> Box<dyn ForgeLike>;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
|
|
||||||
/// Checks that the message has a valid authorisation.
|
/// Checks that the message has a valid authorisation.
|
||||||
|
@ -38,6 +40,7 @@ pub trait ForgeLike: std::fmt::Debug + Send + Sync {
|
||||||
) -> git::forge::webhook::Result<git::forge::commit::Status>;
|
) -> git::forge::webhook::Result<git::forge::commit::Status>;
|
||||||
|
|
||||||
// Lists all the webhooks
|
// Lists all the webhooks
|
||||||
|
#[cfg(test)]
|
||||||
async fn list_webhooks(
|
async fn list_webhooks(
|
||||||
&self,
|
&self,
|
||||||
repo_listen_url: &RepoListenUrl,
|
repo_listen_url: &RepoListenUrl,
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use derive_more::{Constructor, Display};
|
use derive_more::{Constructor, Display};
|
||||||
|
|
||||||
use crate::{Hostname, RepoPath};
|
use crate::core::{Hostname, RepoPath};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Constructor, Display)]
|
#[derive(Clone, Debug, PartialEq, Eq, Constructor, Display)]
|
||||||
#[display("{}:{}", host, repo_path)]
|
#[display("{}:{}", host, repo_path)]
|
|
@ -3,7 +3,8 @@ use std::borrow::ToOwned;
|
||||||
|
|
||||||
use take_until::TakeUntilExt;
|
use take_until::TakeUntilExt;
|
||||||
|
|
||||||
use crate::{newtype, GitDir, RepoBranches};
|
use crate::core::{GitDir, RepoBranches};
|
||||||
|
use crate::newtype;
|
||||||
|
|
||||||
use super::RepoDetails;
|
use super::RepoDetails;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
//
|
//
|
||||||
pub mod commit;
|
pub mod commit;
|
||||||
pub mod fetch;
|
|
||||||
pub mod file;
|
pub mod file;
|
||||||
pub mod forge;
|
pub mod forge;
|
||||||
mod generation;
|
mod generation;
|
||||||
|
@ -18,38 +17,30 @@ mod tests;
|
||||||
|
|
||||||
pub use commit::Commit;
|
pub use commit::Commit;
|
||||||
pub use forge::ForgeLike;
|
pub use forge::ForgeLike;
|
||||||
pub use forge::MockForgeLike;
|
|
||||||
pub use generation::Generation;
|
pub use generation::Generation;
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub use git_ref::GitRef;
|
pub use git_ref::GitRef;
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub use git_remote::GitRemote;
|
pub use git_remote::GitRemote;
|
||||||
pub use repo_details::RepoDetails;
|
pub use repo_details::RepoDetails;
|
||||||
pub use repository::Repository;
|
|
||||||
pub use repository::RepositoryFactory;
|
pub use repository::RepositoryFactory;
|
||||||
pub use user_notification::UserNotification;
|
pub use user_notification::UserNotification;
|
||||||
|
|
||||||
use crate::common::branch_name;
|
#[cfg(test)]
|
||||||
use crate::common::repo_alias;
|
|
||||||
use crate::common::repo_path;
|
|
||||||
use crate::ForgeDetails;
|
|
||||||
use crate::GitDir;
|
|
||||||
use crate::RepoConfig;
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn repo_details(
|
pub fn repo_details(
|
||||||
n: u32,
|
n: u32,
|
||||||
generation: Generation,
|
generation: Generation,
|
||||||
forge: ForgeDetails,
|
forge: super::ForgeDetails,
|
||||||
repo_config: Option<RepoConfig>,
|
repo_config: Option<super::RepoConfig>,
|
||||||
gitdir: GitDir,
|
gitdir: super::GitDir,
|
||||||
) -> RepoDetails {
|
) -> RepoDetails {
|
||||||
RepoDetails {
|
RepoDetails {
|
||||||
generation,
|
generation,
|
||||||
repo_alias: repo_alias(n),
|
repo_alias: super::common::repo_alias(n),
|
||||||
repo_path: repo_path(n),
|
repo_path: super::common::repo_path(n),
|
||||||
gitdir,
|
gitdir,
|
||||||
branch: branch_name(n),
|
branch: super::common::branch_name(n),
|
||||||
forge,
|
forge,
|
||||||
repo_config,
|
repo_config,
|
||||||
}
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
//
|
//
|
||||||
use crate::{git, git::repository::open::OpenRepositoryLike, BranchName};
|
use crate::{
|
||||||
|
core::BranchName,
|
||||||
|
git::{self, repository::open::OpenRepositoryLike},
|
||||||
|
Result,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum Force {
|
pub enum Force {
|
||||||
|
@ -15,36 +19,6 @@ impl std::fmt::Display for Force {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("io")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("network: {0}")]
|
|
||||||
Network(#[from] kxio::net::Error),
|
|
||||||
|
|
||||||
#[error("fetch: {0}")]
|
|
||||||
Fetch(#[from] git::fetch::Error),
|
|
||||||
|
|
||||||
#[error("lock")]
|
|
||||||
Lock,
|
|
||||||
|
|
||||||
#[error("gix open: {0}")]
|
|
||||||
Open(#[from] Box<gix::open::Error>),
|
|
||||||
|
|
||||||
#[error("gix iter: {0}")]
|
|
||||||
GixIter(#[from] gix::reference::iter::Error),
|
|
||||||
|
|
||||||
#[error("gix iter init: {0}")]
|
|
||||||
GixIterInit(#[from] gix::reference::iter::init::Error),
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[error("test")]
|
|
||||||
TestResult(#[from] Box<dyn std::error::Error>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the position of a branch in the remote repo
|
/// Resets the position of a branch in the remote repo
|
||||||
///
|
///
|
||||||
/// Performs a 'git fetch' first to ensure we have up-to-date branch positions before
|
/// Performs a 'git fetch' first to ensure we have up-to-date branch positions before
|
|
@ -1,12 +1,16 @@
|
||||||
//
|
//
|
||||||
|
use crate::core::pike;
|
||||||
|
use crate::s;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::{
|
||||||
|
BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, RemoteUrl, RepoAlias,
|
||||||
|
RepoConfig, RepoPath, ServerRepoConfig, StoragePathType,
|
||||||
|
},
|
||||||
git::{
|
git::{
|
||||||
self,
|
self,
|
||||||
repository::open::{oreal::RealOpenRepository, OpenRepositoryLike},
|
repository::open::{oreal::RealOpenRepository, OpenRepositoryLike},
|
||||||
Generation,
|
Generation,
|
||||||
},
|
},
|
||||||
pike, s, BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, Hostname, RemoteUrl,
|
|
||||||
RepoAlias, RepoConfig, RepoPath, ServerRepoConfig, StoragePathType,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
@ -70,8 +74,9 @@ impl RepoDetails {
|
||||||
&self.gitdir
|
&self.gitdir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_hostname(mut self, hostname: Hostname) -> Self {
|
pub fn with_hostname(mut self, hostname: crate::core::Hostname) -> Self {
|
||||||
let forge = self.forge;
|
let forge = self.forge;
|
||||||
self.forge = forge.with_hostname(hostname);
|
self.forge = forge.with_hostname(hostname);
|
||||||
self
|
self
|
|
@ -41,9 +41,10 @@ pub fn real() -> Box<dyn RepositoryFactory> {
|
||||||
Box::new(RealRepositoryFactory)
|
Box::new(RealRepositoryFactory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn mock() -> Box<MockRepositoryFactory> {
|
pub fn mock() -> MockRepositoryFactory {
|
||||||
Box::new(MockRepositoryFactory::new())
|
MockRepositoryFactory::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
|
@ -1,21 +1,18 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::RemoteUrl,
|
||||||
git::{
|
git::{
|
||||||
self,
|
repository::open::OpenRepositoryLike, validation::remotes::validate_default_remotes,
|
||||||
repository::{
|
|
||||||
open::{OpenRepository, OpenRepositoryLike},
|
|
||||||
test::TestRepository,
|
|
||||||
},
|
|
||||||
validation::remotes::validate_default_remotes,
|
|
||||||
RepoDetails,
|
RepoDetails,
|
||||||
},
|
},
|
||||||
GitDir, RemoteUrl,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
pub mod factory;
|
pub mod factory;
|
||||||
pub mod open;
|
pub mod open;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
|
@ -24,19 +21,20 @@ pub use factory::RepositoryFactory;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
// #[cfg(test)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
// #[derive(Clone, Debug)]
|
||||||
pub enum Repository {
|
// #[allow(clippy::large_enum_variant)]
|
||||||
Real,
|
// pub enum Repository {
|
||||||
Test(TestRepository),
|
// Real,
|
||||||
}
|
// Test(test::TestRepository),
|
||||||
|
// }
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) const fn test(
|
pub const fn test(
|
||||||
fs: kxio::fs::FileSystem,
|
fs: kxio::fs::FileSystem,
|
||||||
forge_details: crate::ForgeDetails,
|
forge_details: crate::core::ForgeDetails,
|
||||||
) -> TestRepository {
|
) -> test::TestRepository {
|
||||||
TestRepository::new(fs, vec![], vec![], forge_details)
|
test::TestRepository::new(fs, vec![], vec![], forge_details)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a repository, cloning if necessary
|
/// Opens a repository, cloning if necessary
|
||||||
|
@ -60,6 +58,7 @@ pub fn open(
|
||||||
Ok(open_repository)
|
Ok(open_repository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
pub trait RepositoryLike {
|
pub trait RepositoryLike {
|
||||||
/// Opens the repository.
|
/// Opens the repository.
|
||||||
|
@ -67,15 +66,16 @@ pub trait RepositoryLike {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Will return an `Err` if the repository can't be opened.
|
/// Will return an `Err` if the repository can't be opened.
|
||||||
fn open(&self, gitdir: &GitDir) -> Result<OpenRepository>;
|
fn open(&self, gitdir: &crate::core::GitDir) -> Result<open::OpenRepository>;
|
||||||
|
|
||||||
/// Clones the git repository from the remote server.
|
// /// Clones the git repository from the remote server.
|
||||||
///
|
// ///
|
||||||
/// # Errors
|
// /// # Errors
|
||||||
///
|
// ///
|
||||||
/// Will return an `Err` if there are any network connectivity issues
|
// /// Will return an `Err` if there are any network connectivity issues
|
||||||
/// connecting with the server.
|
// /// connecting with the server.
|
||||||
fn git_clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository>;
|
// #[cfg(test)]
|
||||||
|
// fn git_clone(&self, repo_details: &RepoDetails) -> Result<open::OpenRepository>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||||
|
@ -98,39 +98,35 @@ pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("invalid git dir: {0}")]
|
// #[error("invalid git dir: {0}")]
|
||||||
InvalidGitDir(GitDir),
|
// InvalidGitDir(GitDir),
|
||||||
|
|
||||||
#[error("kxiofs: {0}")]
|
#[error("kxiofs: {0}")]
|
||||||
KxioFs(#[from] kxio::fs::Error),
|
KxioFs(#[from] kxio::fs::Error),
|
||||||
|
|
||||||
#[error("io: {0}")]
|
// #[error("io: {0}")]
|
||||||
Io(std::io::Error),
|
// Io(std::io::Error),
|
||||||
|
|
||||||
#[error("git exec wait: {0}")]
|
// #[error("git exec wait: {0}")]
|
||||||
Wait(std::io::Error),
|
// Wait(std::io::Error),
|
||||||
|
|
||||||
#[error("git exec spawn: {0}")]
|
|
||||||
Spawn(std::io::Error),
|
|
||||||
|
|
||||||
|
// #[error("git exec spawn: {0}")]
|
||||||
|
// Spawn(std::io::Error),
|
||||||
#[error("validation: {0}")]
|
#[error("validation: {0}")]
|
||||||
Validation(String),
|
Validation(String),
|
||||||
|
|
||||||
#[error("git clone: {0}")]
|
#[error("git clone: {0}")]
|
||||||
Clone(String),
|
Clone(String),
|
||||||
|
|
||||||
#[error("git fetch: {0}")]
|
// #[error("git fetch: {0}")]
|
||||||
FetchError(#[from] git::fetch::Error),
|
// FetchError(#[from] git::fetch::Error),
|
||||||
|
|
||||||
#[error("open: {0}")]
|
#[error("open: {0}")]
|
||||||
Open(String),
|
Open(String),
|
||||||
|
|
||||||
#[error("git fetch: {0}")]
|
#[error("git fetch: {0}")]
|
||||||
Fetch(String),
|
Fetch(String),
|
||||||
|
|
||||||
#[error("fake repository lock")]
|
// #[error("fake repository lock")]
|
||||||
FakeLock,
|
// FakeLock,
|
||||||
|
|
||||||
#[error("MismatchDefaultFetchRemote(found: {found:?}, expected: {expected:?})")]
|
#[error("MismatchDefaultFetchRemote(found: {found:?}, expected: {expected:?})")]
|
||||||
MismatchDefaultFetchRemote {
|
MismatchDefaultFetchRemote {
|
||||||
found: Box<RemoteUrl>,
|
found: Box<RemoteUrl>,
|
|
@ -3,56 +3,55 @@
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
pub mod oreal;
|
pub mod oreal;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub mod otest;
|
pub mod otest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
git,
|
core::{BranchName, RemoteUrl},
|
||||||
git::repository::{
|
git::{self, repository::Direction},
|
||||||
open::{oreal::RealOpenRepository, otest::TestOpenRepository},
|
Result,
|
||||||
Direction,
|
|
||||||
},
|
|
||||||
BranchName, GitDir, RemoteUrl,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::path::Path;
|
||||||
path::Path,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum OpenRepository {
|
pub enum OpenRepository {
|
||||||
/// A real git repository.
|
// /// A real git repository.
|
||||||
///
|
// ///
|
||||||
/// This variant is the normal implementation for use in production code.
|
// /// This variant is the normal implementation for use in production code.
|
||||||
Real(RealOpenRepository),
|
// #[cfg(test)]
|
||||||
|
// Real(oreal::RealOpenRepository),
|
||||||
/// A real git repository, but with preprogrammed responses to network access.
|
/// A real git repository, but with preprogrammed responses to network access.
|
||||||
///
|
///
|
||||||
/// This variant is for use only in testing. Requests to methods
|
/// This variant is for use only in testing. Requests to methods
|
||||||
/// that would require network access, such as to the git remote
|
/// that would require network access, such as to the git remote
|
||||||
/// server will result in an error, unless a predefined change
|
/// server will result in an error, unless a predefined change
|
||||||
/// has been scheduled for that request.
|
/// has been scheduled for that request.
|
||||||
Test(TestOpenRepository),
|
Test(otest::TestOpenRepository),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(tarpaulin_include))]
|
// #[cfg(test)]
|
||||||
pub fn real(gix_repo: gix::Repository, forge_details: crate::ForgeDetails) -> OpenRepository {
|
// #[cfg(not(tarpaulin_include))]
|
||||||
OpenRepository::Real(oreal::RealOpenRepository::new(
|
// pub fn real(gix_repo: gix::Repository, forge_details: crate::core::ForgeDetails) -> OpenRepository {
|
||||||
Arc::new(RwLock::new(gix_repo.into())),
|
// OpenRepository::Real(oreal::RealOpenRepository::new(
|
||||||
forge_details,
|
// std::sync::Arc::new(std::sync::RwLock::new(gix_repo.into())),
|
||||||
))
|
// forge_details,
|
||||||
}
|
// ))
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
#[cfg(not(tarpaulin_include))] // don't test mocks
|
#[cfg(not(tarpaulin_include))] // don't test mocks
|
||||||
pub(crate) fn test(
|
pub fn test(
|
||||||
gitdir: &GitDir,
|
gitdir: &crate::core::GitDir,
|
||||||
fs: &kxio::fs::FileSystem,
|
fs: &kxio::fs::FileSystem,
|
||||||
on_fetch: Vec<otest::OnFetch>,
|
on_fetch: Vec<otest::OnFetch>,
|
||||||
on_push: Vec<otest::OnPush>,
|
on_push: Vec<otest::OnPush>,
|
||||||
forge_details: crate::ForgeDetails,
|
forge_details: crate::core::ForgeDetails,
|
||||||
) -> OpenRepository {
|
) -> OpenRepository {
|
||||||
OpenRepository::Test(TestOpenRepository::new(
|
OpenRepository::Test(otest::TestOpenRepository::new(
|
||||||
gitdir,
|
gitdir,
|
||||||
fs,
|
fs,
|
||||||
on_fetch,
|
on_fetch,
|
||||||
|
@ -64,8 +63,9 @@ pub(crate) fn test(
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
/// Creates a clone of the `OpenRepositoryLike`.
|
// /// Creates a clone of the `OpenRepositoryLike`.
|
||||||
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
// #[cfg(test)]
|
||||||
|
// fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
|
||||||
|
|
||||||
/// Returns a `Vec` of all the branches in the remote repo.
|
/// Returns a `Vec` of all the branches in the remote repo.
|
||||||
///
|
///
|
||||||
|
@ -73,7 +73,7 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
///
|
///
|
||||||
/// Will return `Err` if there are any network connectivity issues with
|
/// Will return `Err` if there are any network connectivity issues with
|
||||||
/// the remote server.
|
/// the remote server.
|
||||||
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>>;
|
fn remote_branches(&self) -> Result<Vec<BranchName>>;
|
||||||
fn find_default_remote(&self, direction: Direction) -> Option<RemoteUrl>;
|
fn find_default_remote(&self, direction: Direction) -> Option<RemoteUrl>;
|
||||||
|
|
||||||
/// Performs a `git fetch`
|
/// Performs a `git fetch`
|
||||||
|
@ -82,7 +82,7 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
///
|
///
|
||||||
/// Will return an `Err` if their is no remote fetch defined in .git/config, or
|
/// Will return an `Err` if their is no remote fetch defined in .git/config, or
|
||||||
/// if there are any network connectivity issues with the remote server.
|
/// if there are any network connectivity issues with the remote server.
|
||||||
fn fetch(&self) -> Result<(), git::fetch::Error>;
|
fn fetch(&self) -> Result<()>;
|
||||||
|
|
||||||
/// Performs a `git push`
|
/// Performs a `git push`
|
||||||
///
|
///
|
||||||
|
@ -96,7 +96,7 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
to_commit: &git::GitRef,
|
to_commit: &git::GitRef,
|
||||||
force: &git::push::Force,
|
force: &git::push::Force,
|
||||||
) -> git::push::Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
/// List of commits in a branch, optionally up-to any specified commit.
|
/// List of commits in a branch, optionally up-to any specified commit.
|
||||||
///
|
///
|
||||||
|
@ -108,7 +108,7 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
&self,
|
&self,
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
find_commits: &[git::Commit],
|
find_commits: &[git::Commit],
|
||||||
) -> git::commit::log::Result<Vec<git::Commit>>;
|
) -> Result<Vec<git::Commit>>;
|
||||||
|
|
||||||
/// Read the contents of a file as a string.
|
/// Read the contents of a file as a string.
|
||||||
///
|
///
|
||||||
|
@ -117,20 +117,21 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Will return `Err` if the file does not exists on the specified branch.
|
/// Will return `Err` if the file does not exists on the specified branch.
|
||||||
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String>;
|
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> Result<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn mock() -> Box<MockOpenRepositoryLike> {
|
pub fn mock() -> Box<MockOpenRepositoryLike> {
|
||||||
Box::new(MockOpenRepositoryLike::new())
|
Box::new(MockOpenRepositoryLike::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
impl std::ops::Deref for OpenRepository {
|
impl std::ops::Deref for OpenRepository {
|
||||||
type Target = dyn OpenRepositoryLike;
|
type Target = dyn OpenRepositoryLike;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
match self {
|
match self {
|
||||||
Self::Real(real) => real,
|
// Self::Real(real) => real,
|
||||||
Self::Test(test) => test,
|
Self::Test(test) => test,
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
git::{self, repository::OpenRepositoryLike},
|
core::{BranchName, ForgeDetails, Hostname, RemoteUrl, RepoPath},
|
||||||
s, BranchName, ForgeDetails, Hostname, RemoteUrl, RepoPath,
|
err, git,
|
||||||
};
|
};
|
||||||
|
use crate::{s, Result};
|
||||||
|
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
use gix::bstr::BStr;
|
use gix::bstr::BStr;
|
||||||
|
@ -11,7 +12,6 @@ use tracing::{info, warn};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::ToOwned,
|
borrow::ToOwned,
|
||||||
path::Path,
|
path::Path,
|
||||||
result::Result,
|
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,13 +21,16 @@ pub struct RealOpenRepository {
|
||||||
forge_details: ForgeDetails,
|
forge_details: ForgeDetails,
|
||||||
}
|
}
|
||||||
impl super::OpenRepositoryLike for RealOpenRepository {
|
impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>> {
|
fn remote_branches(&self) -> Result<Vec<BranchName>> {
|
||||||
let refs = self
|
let refs = self
|
||||||
.inner
|
.inner
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| git::push::Error::Lock)
|
.map_err(|_| err!("read"))
|
||||||
.and_then(|repo| {
|
.and_then(|repo| {
|
||||||
Ok(repo.to_thread_local().references()?).and_then(|refs| {
|
repo.to_thread_local()
|
||||||
|
.references()
|
||||||
|
.map_err(|_| err!("thread local references"))
|
||||||
|
.and_then(|refs| {
|
||||||
Ok(refs.remote_branches().map(|rb| {
|
Ok(refs.remote_branches().map(|rb| {
|
||||||
rb.filter_map(Result::ok)
|
rb.filter_map(Result::ok)
|
||||||
.map(|r| r.name().to_owned())
|
.map(|r| r.name().to_owned())
|
||||||
|
@ -66,12 +69,12 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
#[cfg(not(tarpaulin_include))] // would require writing to external service
|
#[cfg(not(tarpaulin_include))] // would require writing to external service
|
||||||
fn fetch(&self) -> Result<(), git::fetch::Error> {
|
fn fetch(&self) -> Result<()> {
|
||||||
if self
|
if self
|
||||||
.find_default_remote(git::repository::Direction::Fetch)
|
.find_default_remote(git::repository::Direction::Fetch)
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
return Err(git::fetch::Error::NoFetchRemoteFound);
|
return Err(err!("No Fetch Remote Found"));
|
||||||
}
|
}
|
||||||
info!("Fetching");
|
info!("Fetching");
|
||||||
gix::command::prepare("/usr/bin/git fetch --prune")
|
gix::command::prepare("/usr/bin/git fetch --prune")
|
||||||
|
@ -79,7 +82,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
git_dir: Some(
|
git_dir: Some(
|
||||||
self.inner
|
self.inner
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| git::fetch::Error::Lock)
|
.map_err(|_| err!("Lock"))
|
||||||
.map(|r| r.git_dir().to_path_buf())?,
|
.map(|r| r.git_dir().to_path_buf())?,
|
||||||
),
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -101,7 +104,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
to_commit: &git::GitRef,
|
to_commit: &git::GitRef,
|
||||||
force: &git::push::Force,
|
force: &git::push::Force,
|
||||||
) -> Result<(), git::push::Error> {
|
) -> Result<()> {
|
||||||
use secrecy::ExposeSecret as _;
|
use secrecy::ExposeSecret as _;
|
||||||
|
|
||||||
let origin = repo_details.origin();
|
let origin = repo_details.origin();
|
||||||
|
@ -120,7 +123,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
let git_dir = self
|
let git_dir = self
|
||||||
.inner
|
.inner
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| git::push::Error::Lock)
|
.map_err(|_| err!("Lock"))
|
||||||
.map(|r| r.git_dir().to_path_buf())?;
|
.map(|r| r.git_dir().to_path_buf())?;
|
||||||
let ctx = gix::diff::command::Context {
|
let ctx = gix::diff::command::Context {
|
||||||
git_dir: Some(git_dir),
|
git_dir: Some(git_dir),
|
||||||
|
@ -140,7 +143,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
&self,
|
&self,
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
find_commits: &[git::Commit],
|
find_commits: &[git::Commit],
|
||||||
) -> Result<Vec<git::Commit>, git::commit::log::Error> {
|
) -> Result<Vec<git::Commit>> {
|
||||||
let limit: usize = if find_commits.is_empty() {
|
let limit: usize = if find_commits.is_empty() {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
|
@ -148,10 +151,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
.max_dev_commits()
|
.max_dev_commits()
|
||||||
.map_or(25, |commit_count| commit_count.clone().peel() as usize)
|
.map_or(25, |commit_count| commit_count.clone().peel() as usize)
|
||||||
};
|
};
|
||||||
self.inner
|
self.inner.read().map_err(|_| err!("Lock")).map(|repo| {
|
||||||
.read()
|
|
||||||
.map_err(|_| git::commit::log::Error::Lock)
|
|
||||||
.map(|repo| {
|
|
||||||
let branch = format!("remotes/origin/{branch_name}");
|
let branch = format!("remotes/origin/{branch_name}");
|
||||||
let branch = BStr::new(&branch);
|
let branch = BStr::new(&branch);
|
||||||
let thread_local = repo.to_thread_local();
|
let thread_local = repo.to_thread_local();
|
||||||
|
@ -201,10 +201,10 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all, fields(%branch_name, ?file_name))]
|
#[tracing::instrument(skip_all, fields(%branch_name, ?file_name))]
|
||||||
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String> {
|
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> Result<String> {
|
||||||
self.inner
|
self.inner
|
||||||
.read()
|
.read()
|
||||||
.map_err(|_| git::file::Error::Lock)
|
.map_err(|_| err!("Lock"))
|
||||||
.and_then(|repo| {
|
.and_then(|repo| {
|
||||||
let thread_local = repo.to_thread_local();
|
let thread_local = repo.to_thread_local();
|
||||||
let fref = thread_local.find_reference(format!("origin/{branch_name}").as_str())?;
|
let fref = thread_local.find_reference(format!("origin/{branch_name}").as_str())?;
|
||||||
|
@ -223,13 +223,14 @@ impl super::OpenRepositoryLike for RealOpenRepository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
|
// #[cfg(test)]
|
||||||
Box::new(self.clone())
|
// fn duplicate(&self) -> Box<dyn super::OpenRepositoryLike> {
|
||||||
}
|
// Box::new(self.clone())
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::Error {
|
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> color_eyre::eyre::Error {
|
||||||
|error| git::commit::log::Error::Gix { branch, error }
|
move |error| err!("gix: branch: {branch}: {error}")
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&RemoteUrl> for git::GitRemote {
|
impl From<&RemoteUrl> for git::GitRemote {
|
|
@ -1,10 +1,11 @@
|
||||||
//
|
//
|
||||||
|
use crate::s;
|
||||||
use crate::{
|
use crate::{
|
||||||
git::{
|
core::{
|
||||||
self,
|
git::{self, repository::open::oreal::RealOpenRepository},
|
||||||
repository::open::{OpenRepositoryLike, RealOpenRepository},
|
BranchName, ForgeDetails, GitDir, RemoteUrl, RepoBranches,
|
||||||
},
|
},
|
||||||
s, BranchName, ForgeDetails, GitDir, RemoteUrl, RepoBranches,
|
err, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
@ -14,7 +15,7 @@ use std::{
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type OnFetchFn = fn(&RepoBranches, &GitDir, &kxio::fs::FileSystem) -> git::fetch::Result<()>;
|
pub type OnFetchFn = fn(&RepoBranches, &GitDir, &kxio::fs::FileSystem) -> Result<()>;
|
||||||
#[derive(Clone, Debug, Constructor)]
|
#[derive(Clone, Debug, Constructor)]
|
||||||
pub struct OnFetch {
|
pub struct OnFetch {
|
||||||
repo_branches: RepoBranches,
|
repo_branches: RepoBranches,
|
||||||
|
@ -29,7 +30,7 @@ impl OnFetch {
|
||||||
///
|
///
|
||||||
/// Will return any `Err` if there is no fetch remote defined in .git/config
|
/// Will return any `Err` if there is no fetch remote defined in .git/config
|
||||||
/// of if there are any network connectivity issues with the remote server.
|
/// of if there are any network connectivity issues with the remote server.
|
||||||
pub fn invoke(&self) -> git::fetch::Result<()> {
|
pub fn invoke(&self) -> Result<()> {
|
||||||
(self.action)(&self.repo_branches, &self.gitdir, &self.fs)
|
(self.action)(&self.repo_branches, &self.gitdir, &self.fs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +43,7 @@ pub type OnPushFn = fn(
|
||||||
&RepoBranches,
|
&RepoBranches,
|
||||||
&GitDir,
|
&GitDir,
|
||||||
&kxio::fs::FileSystem,
|
&kxio::fs::FileSystem,
|
||||||
) -> git::push::Result<()>;
|
) -> Result<()>;
|
||||||
#[derive(Clone, Debug, Constructor)]
|
#[derive(Clone, Debug, Constructor)]
|
||||||
pub struct OnPush {
|
pub struct OnPush {
|
||||||
repo_branches: RepoBranches,
|
repo_branches: RepoBranches,
|
||||||
|
@ -63,7 +64,7 @@ impl OnPush {
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
to_commit: &git::GitRef,
|
to_commit: &git::GitRef,
|
||||||
force: &git::push::Force,
|
force: &git::push::Force,
|
||||||
) -> git::push::Result<()> {
|
) -> Result<()> {
|
||||||
(self.action)(
|
(self.action)(
|
||||||
repo_details,
|
repo_details,
|
||||||
branch_name,
|
branch_name,
|
||||||
|
@ -86,7 +87,7 @@ pub struct TestOpenRepository {
|
||||||
}
|
}
|
||||||
#[cfg(not(tarpaulin_include))]
|
#[cfg(not(tarpaulin_include))]
|
||||||
impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
||||||
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>> {
|
fn remote_branches(&self) -> Result<Vec<BranchName>> {
|
||||||
self.real.remote_branches()
|
self.real.remote_branches()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,14 +95,11 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
||||||
self.real.find_default_remote(direction)
|
self.real.find_default_remote(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fetch(&self) -> Result<(), git::fetch::Error> {
|
fn fetch(&self) -> Result<()> {
|
||||||
let i: usize = *self
|
let i: usize = *self.fetch_counter.read().map_err(|_| err!("Lock"))?;
|
||||||
.fetch_counter
|
|
||||||
.read()
|
|
||||||
.map_err(|_| git::fetch::Error::Lock)?;
|
|
||||||
self.fetch_counter
|
self.fetch_counter
|
||||||
.write()
|
.write()
|
||||||
.map_err(|_| git::fetch::Error::Lock)
|
.map_err(|_| err!("Lock"))
|
||||||
.map(|mut c| *c += 1)?;
|
.map(|mut c| *c += 1)?;
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
self.on_fetch
|
self.on_fetch
|
||||||
|
@ -116,15 +114,12 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
to_commit: &git::GitRef,
|
to_commit: &git::GitRef,
|
||||||
force: &git::push::Force,
|
force: &git::push::Force,
|
||||||
) -> git::push::Result<()> {
|
) -> Result<()> {
|
||||||
let i: usize = *self
|
let i: usize = *self.push_counter.read().map_err(|_| err!("Lock"))?;
|
||||||
.push_counter
|
|
||||||
.read()
|
|
||||||
.map_err(|_| git::fetch::Error::Lock)?;
|
|
||||||
println!("Push: {i}");
|
println!("Push: {i}");
|
||||||
self.push_counter
|
self.push_counter
|
||||||
.write()
|
.write()
|
||||||
.map_err(|_| git::fetch::Error::Lock)
|
.map_err(|_| err!("Lock"))
|
||||||
.map(|mut c| *c += 1)?;
|
.map(|mut c| *c += 1)?;
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
self.on_push
|
self.on_push
|
||||||
|
@ -137,17 +132,17 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
|
||||||
&self,
|
&self,
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
find_commits: &[git::Commit],
|
find_commits: &[git::Commit],
|
||||||
) -> git::commit::log::Result<Vec<git::Commit>> {
|
) -> Result<Vec<git::Commit>> {
|
||||||
self.real.commit_log(branch_name, find_commits)
|
self.real.commit_log(branch_name, find_commits)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> git::file::Result<String> {
|
fn read_file(&self, branch_name: &BranchName, file_name: &Path) -> Result<String> {
|
||||||
self.real.read_file(branch_name, file_name)
|
self.real.read_file(branch_name, file_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
|
// fn duplicate(&self) -> Box<dyn super::OpenRepositoryLike> {
|
||||||
Box::new(self.clone())
|
// Box::new(self.clone())
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
impl TestOpenRepository {
|
impl TestOpenRepository {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
|
@ -1,6 +1,6 @@
|
||||||
use std::ops::Deref as _;
|
use std::ops::Deref as _;
|
||||||
|
|
||||||
use crate::CommitCount;
|
use crate::core::CommitCount;
|
||||||
|
|
||||||
//
|
//
|
||||||
use super::*;
|
use super::*;
|
|
@ -1,12 +1,14 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::{
|
||||||
|
BranchName, ForgeConfig, ForgeType, GitDir, Hostname, RepoAlias, RepoBranches, RepoConfig,
|
||||||
|
RepoConfigSource, RepoPath, ServerRepoConfig, StoragePathType, User,
|
||||||
|
},
|
||||||
git::{
|
git::{
|
||||||
self,
|
self,
|
||||||
repository::RepositoryLike as _,
|
repository::RepositoryLike as _,
|
||||||
tests::{given, then},
|
tests::{given, then},
|
||||||
},
|
},
|
||||||
BranchName, ForgeConfig, ForgeType, GitDir, Hostname, RepoAlias, RepoBranches, RepoConfig,
|
|
||||||
RepoConfigSource, RepoPath, ServerRepoConfig, StoragePathType, User,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
|
@ -14,7 +16,8 @@ use secrecy::ExposeSecret;
|
||||||
|
|
||||||
use std::{collections::BTreeMap, path::PathBuf};
|
use std::{collections::BTreeMap, path::PathBuf};
|
||||||
|
|
||||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
// type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||||
|
type TestResult = color_eyre::Result<()>;
|
||||||
|
|
||||||
mod commit_log;
|
mod commit_log;
|
||||||
mod fetch;
|
mod fetch;
|
1
src/core/git/repository/open/tests/read_file.rs
Normal file
1
src/core/git/repository/open/tests/read_file.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
use derive_more::Constructor;
|
use derive_more::Constructor;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::{ForgeDetails, GitDir},
|
||||||
git::{
|
git::{
|
||||||
self,
|
self,
|
||||||
repository::{
|
repository::{
|
||||||
|
@ -11,9 +12,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
RepositoryLike, Result,
|
RepositoryLike, Result,
|
||||||
},
|
},
|
||||||
RepoDetails,
|
|
||||||
},
|
},
|
||||||
ForgeDetails, GitDir,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
#[allow(clippy::module_name_repetitions)]
|
||||||
|
@ -43,8 +42,4 @@ impl RepositoryLike for TestRepository {
|
||||||
self.forge_details.clone(),
|
self.forge_details.clone(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_clone(&self, _repo_details: &RepoDetails) -> Result<OpenRepository> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::{ApiToken, GitDir, StoragePathType},
|
||||||
git::{self, tests::given},
|
git::{self, tests::given},
|
||||||
ApiToken, GitDir, StoragePathType,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
|
@ -1,9 +1,12 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::{
|
||||||
|
git_dir::StoragePathType, BranchName, ForgeAlias, ForgeConfig, ForgeType, GitDir, Hostname,
|
||||||
|
RemoteUrl, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
||||||
|
ServerRepoConfig,
|
||||||
|
},
|
||||||
git::{self, Generation, GitRef, GitRemote, RepoDetails},
|
git::{self, Generation, GitRef, GitRemote, RepoDetails},
|
||||||
git_dir::StoragePathType,
|
s, Result,
|
||||||
s, webhook, BranchName, ForgeAlias, ForgeConfig, ForgeType, GitDir, Hostname, RemoteUrl,
|
|
||||||
RepoAlias, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, ServerRepoConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
|
@ -14,8 +17,6 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
|
||||||
|
|
||||||
mod commit {
|
mod commit {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -180,7 +181,7 @@ mod repo_details {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub mod given {
|
pub mod given {
|
||||||
use crate::ForgeDetails;
|
use crate::core::ForgeDetails;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -200,9 +201,9 @@ pub mod given {
|
||||||
RepoAlias::new(a_name())
|
RepoAlias::new(a_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_pathbuf() -> PathBuf {
|
// pub fn a_pathbuf() -> PathBuf {
|
||||||
PathBuf::from(given::a_name())
|
// PathBuf::from(given::a_name())
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn a_name() -> String {
|
pub fn a_name() -> String {
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
@ -296,9 +297,12 @@ pub mod given {
|
||||||
git::commit::Sha::new(a_name())
|
git::commit::Sha::new(a_name())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_webhook_push(sha: &git::commit::Sha, message: &git::commit::Message) -> webhook::Push {
|
pub fn a_webhook_push(
|
||||||
|
sha: &git::commit::Sha,
|
||||||
|
message: &git::commit::Message,
|
||||||
|
) -> crate::core::webhook::Push {
|
||||||
let branch = a_branch_name();
|
let branch = a_branch_name();
|
||||||
webhook::Push::new(branch, s!(sha), s!(message))
|
crate::core::webhook::Push::new(branch, s!(sha), s!(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
|
pub fn a_filesystem() -> kxio::fs::TempFileSystem {
|
||||||
|
@ -354,37 +358,39 @@ pub mod given {
|
||||||
}
|
}
|
||||||
pub mod then {
|
pub mod then {
|
||||||
|
|
||||||
|
use crate::err;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn commit_named_file_to_branch(
|
// pub fn commit_named_file_to_branch(
|
||||||
file_name: &Path,
|
// file_name: &Path,
|
||||||
contents: &str,
|
// contents: &str,
|
||||||
fs: &kxio::fs::FileSystem,
|
// fs: &kxio::fs::FileSystem,
|
||||||
gitdir: &GitDir,
|
// gitdir: &GitDir,
|
||||||
branch_name: &BranchName,
|
// branch_name: &BranchName,
|
||||||
) -> TestResult {
|
// ) -> Result<()> {
|
||||||
// git checkout ${branch_name}
|
// // git checkout ${branch_name}
|
||||||
git_checkout_new_branch(branch_name, gitdir)?;
|
// git_checkout_new_branch(branch_name, gitdir)?;
|
||||||
// echo ${word} > file-${word}
|
// // echo ${word} > file-${word}
|
||||||
let pathbuf = PathBuf::from(gitdir);
|
// let pathbuf = PathBuf::from(gitdir);
|
||||||
let file = fs.base().join(pathbuf).join(file_name);
|
// let file = fs.base().join(pathbuf).join(file_name);
|
||||||
#[allow(clippy::expect_used)]
|
// #[allow(clippy::expect_used)]
|
||||||
fs.file(&file).write(contents)?;
|
// fs.file(&file).write(contents)?;
|
||||||
// git add ${file}
|
// // git add ${file}
|
||||||
git_add_file(gitdir, &file)?;
|
// git_add_file(gitdir, &file)?;
|
||||||
// git commit -m"Added ${file}"
|
// // git commit -m"Added ${file}"
|
||||||
git_commit(gitdir, &file)?;
|
// git_commit(gitdir, &file)?;
|
||||||
|
//
|
||||||
then::push_branch(fs, gitdir, branch_name)?;
|
// then::push_branch(fs, gitdir, branch_name)?;
|
||||||
|
//
|
||||||
Ok(())
|
// Ok(())
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn create_a_commit_on_branch(
|
pub fn create_a_commit_on_branch(
|
||||||
fs: &kxio::fs::FileSystem,
|
fs: &kxio::fs::FileSystem,
|
||||||
gitdir: &GitDir,
|
gitdir: &GitDir,
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
) -> TestResult {
|
) -> Result<()> {
|
||||||
// git checkout ${branch_name}
|
// git checkout ${branch_name}
|
||||||
git_checkout_new_branch(branch_name, gitdir)?;
|
git_checkout_new_branch(branch_name, gitdir)?;
|
||||||
// echo ${word} > file-${word}
|
// echo ${word} > file-${word}
|
||||||
|
@ -406,7 +412,7 @@ pub mod then {
|
||||||
fs: &kxio::fs::FileSystem,
|
fs: &kxio::fs::FileSystem,
|
||||||
gitdir: &GitDir,
|
gitdir: &GitDir,
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
) -> TestResult {
|
) -> Result<()> {
|
||||||
let gitrefs = fs
|
let gitrefs = fs
|
||||||
.base()
|
.base()
|
||||||
.join(gitdir.to_path_buf())
|
.join(gitdir.to_path_buf())
|
||||||
|
@ -421,7 +427,7 @@ pub mod then {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn git_checkout_new_branch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
|
pub fn git_checkout_new_branch(branch_name: &BranchName, gitdir: &GitDir) -> Result<()> {
|
||||||
exec(
|
exec(
|
||||||
&format!("git checkout -b {branch_name}"),
|
&format!("git checkout -b {branch_name}"),
|
||||||
std::process::Command::new("/usr/bin/git")
|
std::process::Command::new("/usr/bin/git")
|
||||||
|
@ -432,7 +438,7 @@ pub mod then {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
|
pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> Result<()> {
|
||||||
exec(
|
exec(
|
||||||
&format!("git switch {branch_name}"),
|
&format!("git switch {branch_name}"),
|
||||||
std::process::Command::new("/usr/bin/git")
|
std::process::Command::new("/usr/bin/git")
|
||||||
|
@ -442,7 +448,7 @@ pub mod then {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exec(label: &str, output: Result<std::process::Output, std::io::Error>) -> TestResult {
|
fn exec(label: &str, output: Result<std::process::Output, std::io::Error>) -> Result<()> {
|
||||||
println!("== {label}");
|
println!("== {label}");
|
||||||
match output {
|
match output {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
|
@ -459,12 +465,12 @@ pub mod then {
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
println!("ERROR: {err:#?}");
|
println!("ERROR: {err:#?}");
|
||||||
Ok(Err(err)?)
|
Err(err!(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_add_file(gitdir: &GitDir, file: &Path) -> TestResult {
|
fn git_add_file(gitdir: &GitDir, file: &Path) -> Result<()> {
|
||||||
exec(
|
exec(
|
||||||
&format!("git add {file:?}"),
|
&format!("git add {file:?}"),
|
||||||
std::process::Command::new("/usr/bin/git")
|
std::process::Command::new("/usr/bin/git")
|
||||||
|
@ -474,7 +480,7 @@ pub mod then {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn git_commit(gitdir: &GitDir, file: &Path) -> TestResult {
|
fn git_commit(gitdir: &GitDir, file: &Path) -> Result<()> {
|
||||||
exec(
|
exec(
|
||||||
&format!(r#"git commit -m"Added {file:?}""#),
|
&format!(r#"git commit -m"Added {file:?}""#),
|
||||||
std::process::Command::new("/usr/bin/git")
|
std::process::Command::new("/usr/bin/git")
|
||||||
|
@ -484,7 +490,7 @@ pub mod then {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn git_log_all(gitdir: &GitDir) -> TestResult {
|
pub fn git_log_all(gitdir: &GitDir) -> Result<()> {
|
||||||
exec(
|
exec(
|
||||||
"git log --all --oneline --decorate --graph",
|
"git log --all --oneline --decorate --graph",
|
||||||
std::process::Command::new("/usr/bin/git")
|
std::process::Command::new("/usr/bin/git")
|
||||||
|
@ -498,7 +504,7 @@ pub mod then {
|
||||||
fs: &kxio::fs::FileSystem,
|
fs: &kxio::fs::FileSystem,
|
||||||
gitdir: &GitDir,
|
gitdir: &GitDir,
|
||||||
branch_name: &BranchName,
|
branch_name: &BranchName,
|
||||||
) -> Result<git::commit::Sha, Box<dyn std::error::Error>> {
|
) -> color_eyre::Result<git::commit::Sha> {
|
||||||
let main_ref = fs
|
let main_ref = fs
|
||||||
.base()
|
.base()
|
||||||
.join(gitdir.to_path_buf())
|
.join(gitdir.to_path_buf())
|
|
@ -1,7 +1,8 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
//
|
//
|
||||||
use crate::{git::Commit, BranchName, ForgeAlias, RepoAlias};
|
use crate::core::{BranchName, ForgeAlias, RepoAlias};
|
||||||
|
use crate::git::Commit;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::graph::Log;
|
use super::graph::Log;
|
|
@ -1,12 +1,11 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
|
core::{git::RepoDetails, BranchName, RepoConfig},
|
||||||
s, BranchName, RepoConfig,
|
git::{self, repository::open::OpenRepositoryLike, UserNotification},
|
||||||
|
Result,
|
||||||
};
|
};
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Positions {
|
pub struct Positions {
|
||||||
pub main: git::Commit,
|
pub main: git::Commit,
|
||||||
|
@ -28,35 +27,32 @@ pub fn validate(
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
repo_details: &git::RepoDetails,
|
repo_details: &git::RepoDetails,
|
||||||
repo_config: &RepoConfig,
|
repo_config: &RepoConfig,
|
||||||
) -> Result<(Positions, git::graph::Log)> {
|
) -> PositionsResult<(Positions, git::graph::Log)> {
|
||||||
let main_branch = repo_config.branches().main();
|
let main_branch = repo_config.branches().main();
|
||||||
let next_branch = repo_config.branches().next();
|
let next_branch = repo_config.branches().next();
|
||||||
let dev_branch = repo_config.branches().dev();
|
let dev_branch = repo_config.branches().dev();
|
||||||
// Collect Commit Histories for `main`, `next` and `dev` branches
|
// Collect Commit Histories for `main`, `next` and `dev` branches
|
||||||
open_repository.fetch()?;
|
open_repository
|
||||||
|
.fetch()
|
||||||
|
.map_err(|e| PositionsError::Retryable(e.to_string()))?;
|
||||||
let git_log = git::graph::log(repo_details);
|
let git_log = git::graph::log(repo_details);
|
||||||
|
|
||||||
let commit_histories = get_commit_histories(open_repository, repo_config)?;
|
let commit_histories = get_commit_histories(open_repository, repo_config)
|
||||||
|
.map_err(|e| PositionsError::Retryable(e.to_string()))?;
|
||||||
// branch tips
|
// branch tips
|
||||||
let main = commit_histories
|
let main = commit_histories.main.first().cloned().ok_or_else(|| {
|
||||||
.main
|
PositionsError::NonRetryable(format!("Branch has no commits: {main_branch}"))
|
||||||
.first()
|
})?;
|
||||||
.cloned()
|
let next = commit_histories.next.first().cloned().ok_or_else(|| {
|
||||||
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {main_branch}")))?;
|
PositionsError::NonRetryable(format!("Branch has no commits: {next_branch}"))
|
||||||
let next = commit_histories
|
})?;
|
||||||
.next
|
let dev = commit_histories.dev.first().cloned().ok_or_else(|| {
|
||||||
.first()
|
PositionsError::NonRetryable(format!("Branch has no commits: {dev_branch}"))
|
||||||
.cloned()
|
})?;
|
||||||
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {next_branch}")))?;
|
|
||||||
let dev = commit_histories
|
|
||||||
.dev
|
|
||||||
.first()
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {dev_branch}")))?;
|
|
||||||
// Validations:
|
// Validations:
|
||||||
// Dev must be on main branch, else the USER must rebase it
|
// Dev must be on main branch, else the USER must rebase it
|
||||||
if is_not_based_on(&commit_histories.dev, &main) {
|
if is_not_based_on(&commit_histories.dev, &main) {
|
||||||
return Err(Error::UserIntervention(
|
return Err(PositionsError::UserIntervention(
|
||||||
UserNotification::DevNotBasedOnMain {
|
UserNotification::DevNotBasedOnMain {
|
||||||
forge_alias: repo_details.forge.forge_alias().clone(),
|
forge_alias: repo_details.forge.forge_alias().clone(),
|
||||||
repo_alias: repo_details.repo_alias.clone(),
|
repo_alias: repo_details.repo_alias.clone(),
|
||||||
|
@ -121,7 +117,7 @@ fn reset_next_to_main(
|
||||||
main: &git::Commit,
|
main: &git::Commit,
|
||||||
next: &git::Commit,
|
next: &git::Commit,
|
||||||
next_branch: &BranchName,
|
next_branch: &BranchName,
|
||||||
) -> Error {
|
) -> PositionsError {
|
||||||
match git::push::reset(
|
match git::push::reset(
|
||||||
open_repository,
|
open_repository,
|
||||||
repo_details,
|
repo_details,
|
||||||
|
@ -129,8 +125,8 @@ fn reset_next_to_main(
|
||||||
&main.clone().into(),
|
&main.clone().into(),
|
||||||
&git::push::Force::From(next.clone().into()),
|
&git::push::Force::From(next.clone().into()),
|
||||||
) {
|
) {
|
||||||
Ok(()) => Error::Retryable(format!("Branch {next_branch} has been reset")),
|
Ok(()) => PositionsError::Retryable(format!("Branch {next_branch} has been reset")),
|
||||||
Err(err) => Error::NonRetryable(format!(
|
Err(err) => PositionsError::NonRetryable(format!(
|
||||||
"Failed to reset branch '{next_branch}' to commit '{next}': {err}"
|
"Failed to reset branch '{next_branch}' to commit '{next}': {err}"
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
|
@ -154,7 +150,7 @@ fn is_based_on(commits: &[git::commit::Commit], needle: &git::Commit) -> bool {
|
||||||
pub fn get_commit_histories(
|
pub fn get_commit_histories(
|
||||||
open_repository: &dyn OpenRepositoryLike,
|
open_repository: &dyn OpenRepositoryLike,
|
||||||
repo_config: &RepoConfig,
|
repo_config: &RepoConfig,
|
||||||
) -> git::commit::log::Result<git::commit::Histories> {
|
) -> Result<git::commit::Histories> {
|
||||||
debug!("main...");
|
debug!("main...");
|
||||||
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
|
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
|
||||||
let main_head = [main[0].clone()];
|
let main_head = [main[0].clone()];
|
||||||
|
@ -166,8 +162,9 @@ pub fn get_commit_histories(
|
||||||
Ok(histories)
|
Ok(histories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type PositionsResult<T> = std::result::Result<T, PositionsError>;
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum PositionsError {
|
||||||
#[error("{0} - will retry")]
|
#[error("{0} - will retry")]
|
||||||
Retryable(String),
|
Retryable(String),
|
||||||
|
|
||||||
|
@ -177,13 +174,3 @@ pub enum Error {
|
||||||
#[error("user intervention required")]
|
#[error("user intervention required")]
|
||||||
UserIntervention(UserNotification),
|
UserIntervention(UserNotification),
|
||||||
}
|
}
|
||||||
impl From<git::fetch::Error> for Error {
|
|
||||||
fn from(value: git::fetch::Error) -> Self {
|
|
||||||
Self::Retryable(s!(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<git::commit::log::Error> for Error {
|
|
||||||
fn from(value: git::commit::log::Error) -> Self {
|
|
||||||
Self::Retryable(s!(value))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,8 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::RemoteUrl,
|
||||||
git::{self, repository::open::OpenRepositoryLike},
|
git::{self, repository::open::OpenRepositoryLike},
|
||||||
s, RemoteUrl,
|
s,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -44,12 +45,11 @@ pub enum Error {
|
||||||
#[error("no default fetch remote")]
|
#[error("no default fetch remote")]
|
||||||
NoDefaultFetchRemote,
|
NoDefaultFetchRemote,
|
||||||
|
|
||||||
#[error("no url for default push remote")]
|
// #[error("no url for default push remote")]
|
||||||
NoUrlForDefaultPushRemote,
|
// NoUrlForDefaultPushRemote,
|
||||||
|
|
||||||
#[error("no hostname for default push remote")]
|
|
||||||
NoHostnameForDefaultPushRemote,
|
|
||||||
|
|
||||||
|
// #[error("no hostname for default push remote")]
|
||||||
|
// NoHostnameForDefaultPushRemote,
|
||||||
#[error("unable to open repo: {0}")]
|
#[error("unable to open repo: {0}")]
|
||||||
UnableToOpenRepo(String),
|
UnableToOpenRepo(String),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
//
|
//
|
||||||
use crate::{
|
use crate::{
|
||||||
|
core::{GitDir, StoragePathType},
|
||||||
git::{
|
git::{
|
||||||
self,
|
self,
|
||||||
repository::{
|
repository::{
|
||||||
|
@ -10,7 +11,6 @@ use crate::{
|
||||||
tests::{given, then},
|
tests::{given, then},
|
||||||
validation::positions::validate,
|
validation::positions::validate,
|
||||||
},
|
},
|
||||||
s, GitDir, StoragePathType,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
|
@ -56,6 +56,10 @@ mod positions {
|
||||||
|
|
||||||
mod validate {
|
mod validate {
|
||||||
|
|
||||||
|
use git::validation::positions::PositionsError;
|
||||||
|
|
||||||
|
use crate::err;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -66,7 +70,7 @@ mod positions {
|
||||||
let mut mock_open_repository = git::repository::open::mock();
|
let mut mock_open_repository = git::repository::open::mock();
|
||||||
mock_open_repository
|
mock_open_repository
|
||||||
.expect_fetch()
|
.expect_fetch()
|
||||||
.return_once(|| Err(git::fetch::Error::TestFailureExpected));
|
.return_once(|| Err(err!("expected failure")));
|
||||||
let mut repository_factory = git::repository::factory::mock();
|
let mut repository_factory = git::repository::factory::mock();
|
||||||
repository_factory
|
repository_factory
|
||||||
.expect_open()
|
.expect_open()
|
||||||
|
@ -81,10 +85,7 @@ mod positions {
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
let_assert!(Err(err) = result, "validate");
|
let_assert!(Err(err) = result, "validate");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(err, PositionsError::Retryable(_)));
|
||||||
err,
|
|
||||||
git::validation::positions::Error::Retryable(_)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -100,10 +101,7 @@ mod positions {
|
||||||
.expect_commit_log()
|
.expect_commit_log()
|
||||||
.returning(move |branch_name, _| {
|
.returning(move |branch_name, _| {
|
||||||
if branch_name == &main_branch {
|
if branch_name == &main_branch {
|
||||||
Err(git::commit::log::Error::Gix {
|
Err(err!("{branch_name}: foo"))
|
||||||
branch: branch_name.clone(),
|
|
||||||
error: s!("foo"),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Ok(vec![])
|
Ok(vec![])
|
||||||
}
|
}
|
||||||
|
@ -120,10 +118,7 @@ mod positions {
|
||||||
let result = validate(&*open_repository, &repo_details, &repo_config);
|
let result = validate(&*open_repository, &repo_details, &repo_config);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(result, Err(PositionsError::Retryable(_))));
|
||||||
result,
|
|
||||||
Err(git::validation::positions::Error::Retryable(_))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -139,10 +134,7 @@ mod positions {
|
||||||
.expect_commit_log()
|
.expect_commit_log()
|
||||||
.returning(move |branch_name, _| {
|
.returning(move |branch_name, _| {
|
||||||
if branch_name == &next_branch {
|
if branch_name == &next_branch {
|
||||||
Err(git::commit::log::Error::Gix {
|
Err(err!("{branch_name} foo"))
|
||||||
branch: branch_name.clone(),
|
|
||||||
error: s!("foo"),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Ok(vec![given::a_commit()])
|
Ok(vec![given::a_commit()])
|
||||||
}
|
}
|
||||||
|
@ -159,10 +151,7 @@ mod positions {
|
||||||
let result = validate(&*open_repository, &repo_details, &repo_config);
|
let result = validate(&*open_repository, &repo_details, &repo_config);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(result, Err(PositionsError::Retryable(_))));
|
||||||
result,
|
|
||||||
Err(git::validation::positions::Error::Retryable(_))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -178,10 +167,7 @@ mod positions {
|
||||||
.expect_commit_log()
|
.expect_commit_log()
|
||||||
.returning(move |branch_name, _| {
|
.returning(move |branch_name, _| {
|
||||||
if branch_name == &dev_branch {
|
if branch_name == &dev_branch {
|
||||||
Err(git::commit::log::Error::Gix {
|
Err(err!("{branch_name} foo"))
|
||||||
branch: branch_name.clone(),
|
|
||||||
error: s!("foo"),
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
Ok(vec![given::a_commit()])
|
Ok(vec![given::a_commit()])
|
||||||
}
|
}
|
||||||
|
@ -198,10 +184,7 @@ mod positions {
|
||||||
let result = validate(&*open_repository, &repo_details, &repo_config);
|
let result = validate(&*open_repository, &repo_details, &repo_config);
|
||||||
println!("{result:?}");
|
println!("{result:?}");
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(result, Err(PositionsError::Retryable(_))));
|
||||||
result,
|
|
||||||
Err(git::validation::positions::Error::Retryable(_))
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -232,7 +215,7 @@ mod positions {
|
||||||
// add a commit to next 1 -> 4
|
// add a commit to next 1 -> 4
|
||||||
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
|
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
|
||||||
then::git_log_all(gitdir)?;
|
then::git_log_all(gitdir)?;
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
|
@ -250,7 +233,7 @@ mod positions {
|
||||||
//then
|
//then
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
err,
|
err,
|
||||||
git::validation::positions::Error::UserIntervention(_)
|
git::validation::positions::PositionsError::UserIntervention(_)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,7 +266,7 @@ mod positions {
|
||||||
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
|
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
|
||||||
|
|
||||||
then::git_log_all(gitdir)?;
|
then::git_log_all(gitdir)?;
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
// second fetch as prep to push
|
// second fetch as prep to push
|
||||||
|
@ -293,7 +276,7 @@ mod positions {
|
||||||
fs.deref().clone(),
|
fs.deref().clone(),
|
||||||
|_branches, _gitdir, _fs| {
|
|_branches, _gitdir, _fs| {
|
||||||
// don't change anything
|
// don't change anything
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
test_repository.on_push(OnPush::new(
|
test_repository.on_push(OnPush::new(
|
||||||
|
@ -318,7 +301,7 @@ mod positions {
|
||||||
&git::push::Force::From(sha_next.into()),
|
&git::push::Force::From(sha_next.into()),
|
||||||
"should force push only if next is on expected sha"
|
"should force push only if next is on expected sha"
|
||||||
);
|
);
|
||||||
git::push::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
|
@ -336,10 +319,7 @@ mod positions {
|
||||||
//then
|
//then
|
||||||
println!("Got: {err:?}");
|
println!("Got: {err:?}");
|
||||||
// NOTE: assertions for correct push are in on_push above
|
// NOTE: assertions for correct push are in on_push above
|
||||||
assert!(matches!(
|
assert!(matches!(err, PositionsError::Retryable(_)));
|
||||||
err,
|
|
||||||
git::validation::positions::Error::Retryable(_)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -370,7 +350,7 @@ mod positions {
|
||||||
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
|
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
|
||||||
|
|
||||||
then::git_log_all(gitdir)?;
|
then::git_log_all(gitdir)?;
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
// second fetch as prep to push
|
// second fetch as prep to push
|
||||||
|
@ -380,7 +360,7 @@ mod positions {
|
||||||
fs.deref().clone(),
|
fs.deref().clone(),
|
||||||
|_branches, _gitdir, _fs| {
|
|_branches, _gitdir, _fs| {
|
||||||
// don't change anything
|
// don't change anything
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
test_repository.on_push(OnPush::new(
|
test_repository.on_push(OnPush::new(
|
||||||
|
@ -388,7 +368,7 @@ mod positions {
|
||||||
gitdir.clone(),
|
gitdir.clone(),
|
||||||
fs.deref().clone(),
|
fs.deref().clone(),
|
||||||
|_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| {
|
|_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| {
|
||||||
git::push::Result::Err(git::push::Error::Lock)
|
Err(err!("Lock"))
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
|
@ -409,10 +389,7 @@ mod positions {
|
||||||
Ok(_) = then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().next()),
|
Ok(_) = then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().next()),
|
||||||
"load next branch sha"
|
"load next branch sha"
|
||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(err, PositionsError::NonRetryable(_)));
|
||||||
err,
|
|
||||||
git::validation::positions::Error::NonRetryable(_)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
#[test]
|
#[test]
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
|
@ -441,7 +418,7 @@ mod positions {
|
||||||
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
|
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
|
||||||
|
|
||||||
then::git_log_all(gitdir)?;
|
then::git_log_all(gitdir)?;
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
// second fetch as prep to push
|
// second fetch as prep to push
|
||||||
|
@ -451,7 +428,7 @@ mod positions {
|
||||||
fs.deref().clone(),
|
fs.deref().clone(),
|
||||||
|_branches, _gitdir, _fs| {
|
|_branches, _gitdir, _fs| {
|
||||||
// don't change anything
|
// don't change anything
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
test_repository.on_push(OnPush::new(
|
test_repository.on_push(OnPush::new(
|
||||||
|
@ -476,7 +453,7 @@ mod positions {
|
||||||
&git::push::Force::From(sha_next.into()),
|
&git::push::Force::From(sha_next.into()),
|
||||||
"should force push only if next is on expected sha"
|
"should force push only if next is on expected sha"
|
||||||
);
|
);
|
||||||
git::push::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
|
@ -494,10 +471,7 @@ mod positions {
|
||||||
//then
|
//then
|
||||||
println!("Got: {err:?}");
|
println!("Got: {err:?}");
|
||||||
// NOTE: assertions for correct push are in on_push above
|
// NOTE: assertions for correct push are in on_push above
|
||||||
assert!(matches!(
|
assert!(matches!(err, PositionsError::Retryable(_)));
|
||||||
err,
|
|
||||||
git::validation::positions::Error::Retryable(_)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -528,7 +502,7 @@ mod positions {
|
||||||
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
|
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
|
||||||
|
|
||||||
then::git_log_all(gitdir)?;
|
then::git_log_all(gitdir)?;
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
// second fetch as prep to push
|
// second fetch as prep to push
|
||||||
|
@ -538,7 +512,7 @@ mod positions {
|
||||||
fs.deref().clone(),
|
fs.deref().clone(),
|
||||||
|_branches, _gitdir, _fs| {
|
|_branches, _gitdir, _fs| {
|
||||||
// don't change anything
|
// don't change anything
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
test_repository.on_push(OnPush::new(
|
test_repository.on_push(OnPush::new(
|
||||||
|
@ -563,7 +537,7 @@ mod positions {
|
||||||
&git::push::Force::From(sha_next.into()),
|
&git::push::Force::From(sha_next.into()),
|
||||||
"should force push only if next is on expected sha"
|
"should force push only if next is on expected sha"
|
||||||
);
|
);
|
||||||
git::push::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let_assert!(
|
let_assert!(
|
||||||
|
@ -624,7 +598,7 @@ mod positions {
|
||||||
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
|
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
|
||||||
|
|
||||||
then::git_log_all(gitdir)?;
|
then::git_log_all(gitdir)?;
|
||||||
git::fetch::Result::Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
let_assert!(
|
let_assert!(
|
|
@ -1,15 +1,15 @@
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! message {
|
macro_rules! message {
|
||||||
($name:ident, $value:ty, $docs:literal) => {
|
($name:ident, $value:ty, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $value, $docs);
|
$crate::newtype!($name, $value, $docs);
|
||||||
};
|
};
|
||||||
($name:ident, $docs:literal) => {
|
($name:ident, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $docs);
|
$crate::newtype!($name, $docs);
|
||||||
};
|
};
|
||||||
($name:ident, $value:ty => $result:ty, $docs:literal) => {
|
($name:ident, $value:ty => $result:ty, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $value, $docs);
|
$crate::newtype!($name, $value, $docs);
|
||||||
};
|
};
|
||||||
($name:ident => $result:ty, $docs:literal) => {
|
($name:ident => $result:ty, $docs:literal) => {
|
||||||
git_next_core::newtype!($name, $docs);
|
$crate::newtype!($name, $docs);
|
||||||
};
|
};
|
||||||
}
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue