Compare commits

..

22 commits

Author SHA1 Message Date
17031f4d23 fix: another test should create valid urls
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 9m39s
Test / build (map[name:stable]) (push) Successful in 11m15s
Release Please / Release-plz (push) Successful in 16s
Release Please / Docker image (push) Successful in 2m25s
2025-01-26 19:53:16 +00:00
1ef76dd7be fix: tests should create valid urls
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 9m35s
Test / build (map[name:stable]) (push) Successful in 11m14s
Release Please / Release-plz (push) Successful in 15s
Release Please / Docker image (push) Successful in 2m26s
2025-01-26 19:20:27 +00:00
b3b8c0ec90 build: adds a daily build against the latest nightly version of rust
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 9m39s
Test / build (map[name:stable]) (push) Successful in 11m12s
Release Please / Release-plz (push) Successful in 14s
Release Please / Docker image (push) Successful in 2m24s
2025-01-26 14:03:29 +00:00
1197408c5c fix: docker build
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 9m40s
Test / build (map[name:stable]) (push) Successful in 11m21s
Release Please / Release-plz (push) Successful in 15s
Release Please / Docker image (push) Successful in 2m25s
2025-01-26 13:36:21 +00:00
7387d7c871 refactor: flatten into single crate
Some checks failed
Test / build (map[name:nightly]) (push) Successful in 9m38s
Test / build (map[name:stable]) (push) Successful in 11m14s
Release Please / Docker image (push) Failing after 4s
Release Please / Release-plz (push) Successful in 14s
2025-01-26 08:24:32 +00:00
e5a5e508ff refactor: merge core crate into cli crate
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 11m12s
Test / build (map[name:stable]) (push) Successful in 11m19s
Release Please / Release-plz (push) Successful in 15s
Release Please / Docker image (push) Successful in 2m21s
2025-01-26 06:56:52 +00:00
c8cc45ca7f refactor: merge github crate into cli crate
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 10m4s
Test / build (map[name:stable]) (push) Successful in 12m12s
Release Please / Release-plz (push) Successful in 1m10s
Release Please / Docker image (push) Successful in 5m56s
2025-01-25 20:35:05 +00:00
52de3ef86e refactor: merge forgejo crate into cli crate
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 14m5s
Test / build (map[name:stable]) (push) Successful in 17m12s
Release Please / Release-plz (push) Successful in 1m43s
Release Please / Docker image (push) Successful in 6m22s
2025-01-23 13:36:02 +00:00
f71e28512d build(justfile): run-*-in-docker builds the image first
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 14m3s
Test / build (map[name:stable]) (push) Successful in 19m3s
Release Please / Release-plz (push) Successful in 1m21s
Release Please / Docker image (push) Successful in 5m25s
2025-01-23 13:34:47 +00:00
a605c3499a fix: clippy fixes
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 12m15s
Test / build (map[name:stable]) (push) Successful in 12m37s
Release Please / Release-plz (push) Successful in 1m26s
Release Please / Docker image (push) Successful in 9m26s
2025-01-23 08:35:55 +00:00
030129a746 chore: remove stray file mise.linux.toml
All checks were successful
Test / build (map[name:stable]) (push) Successful in 12m20s
Test / build (map[name:nightly]) (push) Successful in 12m9s
Release Please / Release-plz (push) Successful in 1m6s
Release Please / Docker image (push) Successful in 7m7s
2025-01-20 18:48:01 +00:00
92c24eafd1 fix: include proposed data directory in create error
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 12m5s
Test / build (map[name:stable]) (push) Successful in 15m42s
Release Please / Release-plz (push) Successful in 1m9s
Release Please / Docker image (push) Successful in 5m43s
2025-01-20 10:11:45 +00:00
1694347f00 build: add run{,-ui}-in-docker justfile recipes
All checks were successful
Test / build (map[name:stable]) (push) Successful in 11m14s
Test / build (map[name:nightly]) (push) Successful in 13m17s
Release Please / Release-plz (push) Successful in 49s
Release Please / Docker image (push) Successful in 4m17s
2025-01-20 10:10:05 +00:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
5ea4fb8d6f chore: release v0.14.1
All checks were successful
Test / build (map[name:nightly]) (pull_request) Successful in 12m7s
Test / build (map[name:stable]) (pull_request) Successful in 14m52s
Test / build (map[name:stable]) (push) Successful in 10m43s
Test / build (map[name:nightly]) (push) Successful in 12m6s
Release Please / Docker image (push) Successful in 5m4s
Release Please / Release-plz (push) Successful in 2m25s
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2025-01-19 20:16:57 +00:00
Renovate Bot
cb76f2c7bd chore(deps): update rust crate gix to 0.70
All checks were successful
Test / build (map[name:stable]) (push) Successful in 11m13s
Test / build (map[name:nightly]) (push) Successful in 12m1s
Release Please / Release-plz (push) Successful in 1m11s
Release Please / Docker image (push) Successful in 7m18s
2025-01-19 20:01:30 +00:00
3833ba86db feat(ui): filter repos
All checks were successful
Test / build (map[name:stable]) (push) Successful in 12m46s
Test / build (map[name:nightly]) (push) Successful in 12m0s
Release Please / Release-plz (push) Successful in 1m12s
Release Please / Docker image (push) Successful in 7m59s
Closes kemitix/git-next#206
2025-01-19 19:07:33 +00:00
90779527cf feat: remove openssl transitive dependency
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 10m18s
Test / build (map[name:stable]) (push) Successful in 12m5s
Release Please / Release-plz (push) Successful in 1m6s
Release Please / Docker image (push) Successful in 7m22s
2025-01-18 20:31:41 +00:00
Renovate Bot
7b4e679c65 chore(deps): update rust crate gix to 0.69
All checks were successful
Test / build (map[name:stable]) (push) Successful in 11m28s
Test / build (map[name:nightly]) (push) Successful in 12m42s
Release Please / Release-plz (push) Successful in 1m7s
Release Please / Docker image (push) Successful in 8m1s
2025-01-17 20:42:07 +00:00
Renovate Bot
a8298d943a chore(deps): update rust crate kxio to v5
All checks were successful
Test / build (map[name:stable]) (push) Successful in 11m21s
Test / build (map[name:nightly]) (push) Successful in 12m40s
Release Please / Release-plz (push) Successful in 1m8s
Release Please / Docker image (push) Successful in 10m26s
2025-01-17 20:06:04 +00:00
Renovate Bot
f8625a59f8 chore(deps): update rust crate rstest to 0.24
All checks were successful
Release Please / Release-plz (push) Successful in 1m22s
Release Please / Docker image (push) Successful in 11m59s
Test / build (map[name:stable]) (push) Successful in 11m15s
Test / build (map[name:nightly]) (push) Successful in 12m29s
2025-01-17 08:44:25 +00:00
Renovate Bot
6d66ccf391 chore(deps): update rust crate kameo to 0.14
All checks were successful
Test / build (map[name:stable]) (push) Successful in 10m36s
Test / build (map[name:nightly]) (push) Successful in 13m8s
Release Please / Release-plz (push) Successful in 1m17s
Release Please / Docker image (push) Successful in 13m13s
2025-01-17 08:44:25 +00:00
Renovate Bot
de6875347d chore(deps): update rust crate notify to v8
Some checks failed
Release Please / Release-plz (push) Successful in 1m25s
Release Please / Docker image (push) Successful in 13m29s
Test / build (map[name:stable]) (push) Has been cancelled
Test / build (map[name:nightly]) (push) Has been cancelled
2025-01-16 21:41:25 +00:00
209 changed files with 2623 additions and 2924 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
target/

View 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

View file

@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file.
## `git-next-forge-github` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-github-v0.14.0...git-next-forge-github-v0.14.1) - 2025-01-19
### Other
- *(deps)* update rust crate kxio to v5
## `git-next-forge-forgejo` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-forge-forgejo-v0.14.0...git-next-forge-forgejo-v0.14.1) - 2025-01-19
### Other
- *(deps)* update rust crate kxio to v5
## `git-next-core` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/git-next-core-v0.14.0...git-next-core-v0.14.1) - 2025-01-19
### Added
- remove openssl transitive dependency
### Other
- *(deps)* update rust crate gix to 0.69
## `git-next` - [0.14.1](https://git.kemitix.net/kemitix/git-next/compare/v0.14.0...v0.14.1) - 2025-01-19
### Added
- *(ui)* filter repos
- remove openssl transitive dependency
### Other
- *(deps)* update rust crate kxio to v5
## [0.14.0] - 2025-01-16
### Bug Fixes

1257
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,39 +1,41 @@
[workspace]
resolver = "2"
members = ["crates/*"]
[workspace.package]
version = "0.14.0"
[package]
name = "git-next"
version = "0.14.1"
edition = "2021"
license = "MIT"
repository = "https://git.kemitix.net/kemitix/git-next"
description = "trunk-based development manager"
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
rust-version = "1.76"
description = "trunk-based development manager"
documentation = "https://git.kemitix.net/kemitix/git-next/src/branch/main/README.md"
keywords = ["git", "cli", "server", "tool"]
categories = ["development-tools"]
# [workspace.lints.clippy]
# pedantic = { level = "warn", priority = -1 }
# nursery = { level = "warn", priority = -1 }
# unwrap_used = "warn"
# expect_used = "warn"
[features]
# default = ["forgejo", "github"]
default = ["forgejo", "github", "tui"]
forgejo = []
github = []
tui = [
"ratatui",
"directories",
"lazy_static",
"tui-scrollview",
"regex",
"chrono",
]
[workspace.dependencies]
git-next-core = { path = "crates/core", version = "0.14" }
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.14" }
git-next-forge-github = { path = "crates/forge-github", version = "0.14" }
[dependencies]
color-eyre = "0.6"
# TUI
ratatui = "0.29"
directories = "6.0"
lazy_static = "1.5"
color-eyre = "0.6"
tui-scrollview = "0.5"
regex = "1.10"
chrono = "0.4"
ratatui = { version = "0.29", optional = true }
directories = { version = "6.0", optional = true }
lazy_static = { version = "1.5", optional = true }
tui-scrollview = { version = "0.5", optional = true }
regex = { version = "1.10", optional = true }
chrono = { version = "0.4", optional = true }
# CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] }
@ -44,7 +46,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-error = "0.2.0"
# base64 decoding
base64 = "0.22"
# base64 = { version = "0.22", optional = true }
# sha256 encoding (e.g. verify github webhooks)
hmac = "0.12"
@ -52,7 +54,7 @@ sha2 = "0.10"
hex = "0.4"
# git
gix = { version = "0.68", features = [
gix = { version = "0.70", features = [
"dirwalk",
"blocking-http-transport-reqwest-rust-tls",
] }
@ -60,9 +62,8 @@ async-trait = "0.1"
git-url-parse = "0.4"
# fs/network
kxio = "4.0"
native-tls = { version = "0.2", features = ["vendored"] }
kxio = "5.1"
# kxio = { path = "../kxio/" }
# TOML parsing
serde = { version = "1.0", features = ["derive"] }
@ -84,11 +85,12 @@ standardwebhooks = "1.0"
# boilerplate
bon = "3.0"
derive_more = { version = "1.0.0-beta", features = [
derive_more = { version = "1.0.0", features = [
"as_ref",
"constructor",
"display",
"deref",
"deref_mut",
"from",
] }
derive-with = "0.5"
@ -100,23 +102,34 @@ pike = "0.1"
take-until = "0.2"
# file watcher
notify = "7.0"
notify = "8.0"
# Actors
kameo = "0.13"
kameo = "0.14"
tokio = { version = "1.37", features = ["full"] }
# email
lettre = "0.11"
lettre = { version = "0.11", default-features = false, features = ["builder", "rustls-tls", "smtp-transport"] }
sendmail = "2.0"
# desktop notifications
notifica = "3.0"
# Testing
mockall = "0.13"
[dev-dependencies]
assert2 = "0.3"
pretty_assertions = "1.4"
rand = "0.8"
mockall = "0.13"
rstest = { version = "0.24", features = ["async-timeout"] }
test-log = "0.2"
rstest = { version = "0.23", features = ["async-timeout"] }
[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)'] }

View file

@ -6,7 +6,7 @@ RUN apk add --no-cache dbus-dev=1.14.10-r4 && rm -rf /vra/cache/apk/*
FROM chef AS planner
COPY Cargo.toml ./
COPY crates crates
COPY src src
RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder

662
README.md
View file

@ -2,11 +2,671 @@
## Trunk-based developement manager.
> A source-control branching model, where developers collaborate on code in a single branch
> called trunk, resist any pressure to create other long-lived development branches by
> employing documented techniques. They therefore avoid merge hell, do not break the build,
> and live happily ever after. - [source](https://trunkbaseddevelopment.com)
`git-next` is a combined server and command-line tool that enables trunk-based
development workflows where each commit must pass CI before being included in
the main branch.
## Features
- Allows enforcing the requirement for each commit to pass the CI pipeline before being
included in the main branch
- Provides a server component that manages the trunk-based development process
- Ensure a consistent, high-quality codebase by preventing untested changes
from being added to main
- Requires each commit uses conventional commit format.
See [Behaviour](#behaviour) to learn how we do this.
![Demo](./demo.gif)
## Prerequisits
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).

View file

@ -1,110 +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 }
native-tls = { workspace = true }
# TUI
ratatui = { workspace = true, optional = true }
directories = { workspace = true, optional = true }
lazy_static = { workspace = true, optional = true }
color-eyre = { workspace = true }
tui-scrollview = { workspace = true, optional = true }
regex = { workspace = true, optional = true }
chrono = { workspace = true, optional = true }
# CLI parsing
clap = { workspace = true }
# fs/network
kxio = { workspace = true }
# logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-error.workspace = true
# Conventional Commit check
git-conventional = { workspace = true }
# TOML parsing
toml = { workspace = true }
# Actors
kameo = { workspace = true }
tokio = { workspace = true }
# boilerplate
bon = { workspace = true }
derive_more = { workspace = true }
derive-with = { workspace = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
# Webhooks
serde_json = { workspace = true }
ulid = { workspace = true }
time = { workspace = true }
secrecy = { workspace = true }
standardwebhooks = { workspace = true }
bytes = { workspace = true }
warp = { workspace = true }
# file watcher (linux)
notify = { workspace = true }
# email
lettre = { workspace = true }
sendmail = { workspace = true }
# desktop notifications
notifica = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["native-tls"]
[dev-dependencies]
# Testing
assert2 = { workspace = true }
test-log = { workspace = true }
rand = { workspace = true }
pretty_assertions = { workspace = true }
mockall = { workspace = true }
rstest = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
unwrap_used = "warn"
expect_used = "warn"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }

View file

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

View file

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

View file

@ -1,70 +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]
native-tls = { workspace = true }
# 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 }
[package.metadata.cargo-machete]
ignored = ["native-tls"]
[dev-dependencies]
# Testing
assert2 = { workspace = true }
rand = { workspace = true }
test-log = { workspace = true }
pretty_assertions = { workspace = true }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ build:
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 clippy
cargo hack build
@ -11,11 +12,36 @@ build:
# cargo test --example get
# 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:
docker run --rm -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:
docker run --rm -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:
@echo "Installing git hooks"

View file

@ -1,6 +1,6 @@
//
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) {
let message = short_message(user_notification);

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
//
use derive_more::derive::Constructor;
use git_next_core::{git::UserNotification, server::Shout};
use crate::core::{git::UserNotification, server::Shout};
pub use history::History;
use kameo::{mailbox::unbounded::UnboundedMailbox, Actor};

View file

@ -1,7 +1,7 @@
use std::time::Duration;
use crate::core::git::UserNotification;
use assert2::let_assert;
use git_next_core::git::UserNotification;
use crate::{alerts::History, repo::tests::given};

View file

@ -1,5 +1,5 @@
//
use git_next_core::{git::UserNotification, server::OutboundWebhook};
use crate::core::{git::UserNotification, server::OutboundWebhook};
use secrecy::ExposeSecret as _;
use standardwebhooks::Webhook;

View file

@ -1,4 +1,4 @@
use crate::config::{
use crate::core::config::{
ApiToken, BranchName, ForgeAlias, ForgeDetails, ForgeType, Hostname, RepoAlias, RepoBranches,
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())
}
pub(crate) fn user(n: u32) -> User {
pub fn user(n: u32) -> User {
User::new(format!("user-{n}"))
}
pub(crate) fn hostname(n: u32) -> Hostname {
pub fn hostname(n: u32) -> Hostname {
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}"))
}
pub(crate) fn branch_name(n: u32) -> BranchName {
pub fn branch_name(n: u32) -> BranchName {
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}"))
}
pub(crate) fn repo_alias(n: u32) -> RepoAlias {
pub fn repo_alias(n: u32) -> RepoAlias {
RepoAlias::new(format!("repo-alias-{n}"))
}

View file

@ -1,9 +1,7 @@
use std::collections::BTreeMap;
use crate::{
config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User},
s,
};
use crate::core::config::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User};
use crate::s;
use super::CommitCount;

View file

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

View file

@ -2,7 +2,10 @@
mod api_token;
mod branch_name;
mod commit_count;
#[cfg(test)]
pub mod common;
mod forge_alias;
mod forge_config;
mod forge_details;
@ -53,4 +56,4 @@ pub use webhook::forge_notification::ForgeNotification;
pub use webhook::id::WebhookId;
// re-export
pub use pike::{pike, pike_opt, pike_res};
pub use pike::pike;

View file

@ -1,5 +1,5 @@
//
use crate::config::{WebhookAuth, WebhookId};
use crate::core::config::{WebhookAuth, WebhookId};
#[derive(Debug, derive_more::Constructor)]
pub struct RegisteredWebhook {

View file

@ -1,4 +1,4 @@
use crate::config::BranchName;
use crate::core::config::BranchName;
/// Mapped from `.git-next.toml` file at `branches`
#[derive(

View file

@ -1,4 +1,4 @@
use crate::config::{RepoBranches, RepoConfigSource};
use crate::core::config::{RepoBranches, RepoConfigSource};
/// Mapped from `.git-next.toml` file in target repo
/// Is also derived from the optional parameters in `git-next-server.toml` at

View file

@ -15,24 +15,10 @@ use serde::{Deserialize, Serialize};
use tracing::info;
use crate::{
config::{ForgeAlias, ForgeConfig, RepoAlias},
newtype, s,
core::config::{ForgeAlias, ForgeConfig, RepoAlias},
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
#[derive(
Clone,

View file

@ -2,7 +2,7 @@
use std::path::PathBuf;
use crate::{
config::{
core::config::{
git_dir::StoragePathType, BranchName, GitDir, RepoBranches, RepoConfig, RepoConfigSource,
RepoPath,
},

View file

@ -7,16 +7,15 @@ use std::collections::BTreeMap;
use std::path::PathBuf;
use crate::{
core::{
server::{AppConfig, Http, Storage},
webhook::push::Branch,
},
s,
server::{AppConfig, Http, Storage},
webhook::push::Branch,
};
mod url;
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
type TestResult = Result<()>;
mod server_repo_config {
use super::*;
@ -100,10 +99,12 @@ mod server_repo_config {
}
}
mod repo_config {
use crate::Result;
use super::*;
#[test]
fn should_parse_toml() -> TestResult {
fn should_parse_toml() -> Result<()> {
let main = given::a_name();
let next = given::a_name();
let dev = given::a_name();
@ -517,10 +518,12 @@ mod server {
use super::*;
mod load {
use crate::{err, Result};
use super::*;
#[test]
fn load_should_parse_app_config() -> TestResult {
fn load_should_parse_app_config() -> Result<()> {
let app_config = given::an_app_config();
let fs = kxio::fs::temp()?;
let_assert!(Ok(()) = write_app_config(&app_config, &fs), "write");
@ -530,7 +533,7 @@ mod server {
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_addr = http.ip();
let http_port = app_config.listen_socket_addr()?.port();
@ -553,11 +556,11 @@ mod server {
.forges()
.next()
.map(|(fa, _)| fa)
.ok_or("forge missing")?;
.ok_or_else(|| err!("forge missing"))?;
let forge_default = app_config
.forge
.get(forge_alias.as_ref())
.ok_or("forge missing")?;
.ok_or_else(|| err!("forge missing"))?;
let forge_type = forge_default.forge_type();
let forge_hostname = forge_default.hostname();
let forge_user = forge_default.user();
@ -716,7 +719,7 @@ mod push {
let message = given::a_name();
let push_event = {
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));
}
@ -727,7 +730,7 @@ mod push {
let message = given::a_name();
let push_event = {
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));
}
@ -738,7 +741,7 @@ mod push {
let message = given::a_name();
let push_event = {
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));
}
@ -749,7 +752,7 @@ mod push {
let message = given::a_name();
let push_event = {
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);
}
@ -761,7 +764,7 @@ mod push {
let push_event = {
let branch_name = repo_branches.main();
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);
}
@ -773,14 +776,14 @@ mod push {
let push_event = {
let branch_name = repo_branches.main();
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);
}
}
mod given {
use crate::server::{EmailConfig, Listen, ListenUrl, OutboundWebhook, Shout, SmtpConfig};
use crate::core::server::{EmailConfig, Listen, ListenUrl, OutboundWebhook, Shout, SmtpConfig};
use super::*;
use rand::Rng as _;
@ -911,8 +914,8 @@ mod given {
RepoAlias::new(a_name())
}
pub fn a_webhook_message_body() -> crate::webhook::forge_notification::Body {
crate::webhook::forge_notification::Body::new(a_name())
pub fn a_webhook_message_body() -> crate::core::webhook::forge_notification::Body {
crate::core::webhook::forge_notification::Body::new(a_name())
}
pub fn some_repo_branches() -> RepoBranches {

View file

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
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.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, derive_more::Constructor)]

View file

@ -1,5 +1,5 @@
//
use crate::config::{BranchName, RepoBranches};
use crate::core::config::{BranchName, RepoBranches};
use derive_more::Constructor;
#[derive(Clone, Debug, Constructor, PartialEq, Eq, derive_with::With)]

View file

@ -1,5 +1,6 @@
mod auth {
use crate::{s, WebhookAuth};
use crate::core::WebhookAuth;
use crate::s;
#[test]
fn bytes() -> Result<(), Box<dyn std::error::Error>> {

View file

@ -1,5 +1,6 @@
//
use crate::{newtype, webhook};
use crate::core::webhook;
use crate::newtype;
use derive_more::Display;
use serde::Serialize;
@ -68,18 +69,3 @@ pub struct Histories {
pub next: 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,
}
}

View file

@ -1,38 +1,36 @@
//
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("lock")]
Lock,
// #[error("lock")]
// Lock,
#[error("File not found: {}", 0)]
NotFound(String),
// #[error("File not found: {}", 0)]
// NotFound(String),
#[error("Unable to parse file contents")]
ParseContent,
// #[error("Unable to parse file contents")]
// ParseContent,
#[error("Unable to decode from base64")]
DecodeFromBase64,
// #[error("Unable to decode from base64")]
// DecodeFromBase64,
#[error("Unable to decode from UTF-8")]
DecodeFromUtf8,
// #[error("Unable to decode from UTF-8")]
// DecodeFromUtf8,
#[error("Unknown file encoding: {}", 0)]
UnknownEncoding(String),
// #[error("Unknown file encoding: {}", 0)]
// UnknownEncoding(String),
#[error("Not a file: {}", 0)]
NotFile(String),
// #[error("Not a file: {}", 0)]
// NotFile(String),
#[error("Unknown error (status: {})", 0)]
Unknown(String),
// #[error("Unknown error (status: {})", 0)]
// Unknown(String),
#[error("commit log: {0}")]
CommitLog(#[from] crate::git::commit::log::Error),
#[error("commit not found")]
CommitNotFound,
// #[error("commit log: {0}")]
// CommitLog(#[from] crate::git::commit::log::Error),
// #[error("commit not found")]
// CommitNotFound,
#[error("no tree in commit")]
NoTreeInCommit(String),

View file

@ -1,7 +1,9 @@
pub mod commit;
pub(super) mod r#trait;
pub mod r#trait;
pub mod webhook;
#[allow(clippy::module_name_repetitions)]
pub use r#trait::ForgeLike;
#[cfg(test)]
pub use r#trait::MockForgeLike;

View file

@ -1,5 +1,5 @@
//
use crate::{
use crate::core::{
git, server::RepoListenUrl, webhook, ForgeNotification, RegisteredWebhook, WebhookAuth,
WebhookId,
};
@ -8,6 +8,8 @@ use crate::{
#[async_trait::async_trait]
pub trait ForgeLike: std::fmt::Debug + Send + Sync {
fn duplicate(&self) -> Box<dyn ForgeLike>;
#[cfg(test)]
fn name(&self) -> String;
/// 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>;
// Lists all the webhooks
#[cfg(test)]
async fn list_webhooks(
&self,
repo_listen_url: &RepoListenUrl,

View file

@ -1,7 +1,7 @@
//
use derive_more::{Constructor, Display};
use crate::{Hostname, RepoPath};
use crate::core::{Hostname, RepoPath};
#[derive(Clone, Debug, PartialEq, Eq, Constructor, Display)]
#[display("{}:{}", host, repo_path)]

View file

@ -3,7 +3,8 @@ use std::borrow::ToOwned;
use take_until::TakeUntilExt;
use crate::{newtype, GitDir, RepoBranches};
use crate::core::{GitDir, RepoBranches};
use crate::newtype;
use super::RepoDetails;

View file

@ -1,6 +1,5 @@
//
pub mod commit;
pub mod fetch;
pub mod file;
pub mod forge;
mod generation;
@ -18,38 +17,30 @@ mod tests;
pub use commit::Commit;
pub use forge::ForgeLike;
pub use forge::MockForgeLike;
pub use generation::Generation;
#[allow(clippy::module_name_repetitions)]
pub use git_ref::GitRef;
#[allow(clippy::module_name_repetitions)]
pub use git_remote::GitRemote;
pub use repo_details::RepoDetails;
pub use repository::Repository;
pub use repository::RepositoryFactory;
pub use user_notification::UserNotification;
use crate::common::branch_name;
use crate::common::repo_alias;
use crate::common::repo_path;
use crate::ForgeDetails;
use crate::GitDir;
use crate::RepoConfig;
#[cfg(test)]
#[must_use]
pub fn repo_details(
n: u32,
generation: Generation,
forge: ForgeDetails,
repo_config: Option<RepoConfig>,
gitdir: GitDir,
forge: super::ForgeDetails,
repo_config: Option<super::RepoConfig>,
gitdir: super::GitDir,
) -> RepoDetails {
RepoDetails {
generation,
repo_alias: repo_alias(n),
repo_path: repo_path(n),
repo_alias: super::common::repo_alias(n),
repo_path: super::common::repo_path(n),
gitdir,
branch: branch_name(n),
branch: super::common::branch_name(n),
forge,
repo_config,
}

View file

@ -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)]
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
///
/// Performs a 'git fetch' first to ensure we have up-to-date branch positions before

View file

@ -1,12 +1,16 @@
//
use crate::core::pike;
use crate::s;
use crate::{
core::{
BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, RemoteUrl, RepoAlias,
RepoConfig, RepoPath, ServerRepoConfig, StoragePathType,
},
git::{
self,
repository::open::{oreal::RealOpenRepository, OpenRepositoryLike},
Generation,
},
pike, s, BranchName, ForgeAlias, ForgeConfig, ForgeDetails, GitDir, Hostname, RemoteUrl,
RepoAlias, RepoConfig, RepoPath, ServerRepoConfig, StoragePathType,
};
use std::sync::{Arc, RwLock};
@ -70,8 +74,9 @@ impl RepoDetails {
&self.gitdir
}
#[cfg(test)]
#[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;
self.forge = forge.with_hostname(hostname);
self

View file

@ -41,9 +41,10 @@ pub fn real() -> Box<dyn RepositoryFactory> {
Box::new(RealRepositoryFactory)
}
#[cfg(test)]
#[must_use]
pub fn mock() -> Box<MockRepositoryFactory> {
Box::new(MockRepositoryFactory::new())
pub fn mock() -> MockRepositoryFactory {
MockRepositoryFactory::new()
}
#[derive(Debug, Clone)]

View file

@ -1,21 +1,18 @@
//
use crate::{
core::RemoteUrl,
git::{
self,
repository::{
open::{OpenRepository, OpenRepositoryLike},
test::TestRepository,
},
validation::remotes::validate_default_remotes,
repository::open::OpenRepositoryLike, validation::remotes::validate_default_remotes,
RepoDetails,
},
GitDir, RemoteUrl,
};
use tracing::info;
pub mod factory;
pub mod open;
#[cfg(test)]
mod test;
#[allow(clippy::module_name_repetitions)]
@ -24,19 +21,20 @@ pub use factory::RepositoryFactory;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum Repository {
Real,
Test(TestRepository),
}
// #[cfg(test)]
// #[derive(Clone, Debug)]
// #[allow(clippy::large_enum_variant)]
// pub enum Repository {
// Real,
// Test(test::TestRepository),
// }
#[cfg(test)]
pub(crate) const fn test(
pub const fn test(
fs: kxio::fs::FileSystem,
forge_details: crate::ForgeDetails,
) -> TestRepository {
TestRepository::new(fs, vec![], vec![], forge_details)
forge_details: crate::core::ForgeDetails,
) -> test::TestRepository {
test::TestRepository::new(fs, vec![], vec![], forge_details)
}
/// Opens a repository, cloning if necessary
@ -60,6 +58,7 @@ pub fn open(
Ok(open_repository)
}
#[cfg(test)]
#[allow(clippy::module_name_repetitions)]
pub trait RepositoryLike {
/// Opens the repository.
@ -67,15 +66,16 @@ pub trait RepositoryLike {
/// # Errors
///
/// 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.
///
/// # Errors
///
/// Will return an `Err` if there are any network connectivity issues
/// connecting with the server.
fn git_clone(&self, repo_details: &RepoDetails) -> Result<OpenRepository>;
// /// Clones the git repository from the remote server.
// ///
// /// # Errors
// ///
// /// Will return an `Err` if there are any network connectivity issues
// /// connecting with the server.
// #[cfg(test)]
// fn git_clone(&self, repo_details: &RepoDetails) -> Result<open::OpenRepository>;
}
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
@ -98,39 +98,35 @@ pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("invalid git dir: {0}")]
InvalidGitDir(GitDir),
// #[error("invalid git dir: {0}")]
// InvalidGitDir(GitDir),
#[error("kxiofs: {0}")]
KxioFs(#[from] kxio::fs::Error),
#[error("io: {0}")]
Io(std::io::Error),
// #[error("io: {0}")]
// Io(std::io::Error),
#[error("git exec wait: {0}")]
Wait(std::io::Error),
#[error("git exec spawn: {0}")]
Spawn(std::io::Error),
// #[error("git exec wait: {0}")]
// Wait(std::io::Error),
// #[error("git exec spawn: {0}")]
// Spawn(std::io::Error),
#[error("validation: {0}")]
Validation(String),
#[error("git clone: {0}")]
Clone(String),
#[error("git fetch: {0}")]
FetchError(#[from] git::fetch::Error),
// #[error("git fetch: {0}")]
// FetchError(#[from] git::fetch::Error),
#[error("open: {0}")]
Open(String),
#[error("git fetch: {0}")]
Fetch(String),
#[error("fake repository lock")]
FakeLock,
// #[error("fake repository lock")]
// FakeLock,
#[error("MismatchDefaultFetchRemote(found: {found:?}, expected: {expected:?})")]
MismatchDefaultFetchRemote {
found: Box<RemoteUrl>,

View file

@ -3,56 +3,55 @@
mod tests;
pub mod oreal;
#[cfg(test)]
pub mod otest;
use crate::{
git,
git::repository::{
open::{oreal::RealOpenRepository, otest::TestOpenRepository},
Direction,
},
BranchName, GitDir, RemoteUrl,
core::{BranchName, RemoteUrl},
git::{self, repository::Direction},
Result,
};
use std::{
path::Path,
sync::{Arc, RwLock},
};
use std::path::Path;
#[cfg(test)]
#[allow(clippy::module_name_repetitions)]
#[derive(Clone, Debug)]
pub enum OpenRepository {
/// A real git repository.
///
/// This variant is the normal implementation for use in production code.
Real(RealOpenRepository),
// /// A real git repository.
// ///
// /// This variant is the normal implementation for use in production code.
// #[cfg(test)]
// Real(oreal::RealOpenRepository),
/// A real git repository, but with preprogrammed responses to network access.
///
/// This variant is for use only in testing. Requests to methods
/// that would require network access, such as to the git remote
/// server will result in an error, unless a predefined change
/// has been scheduled for that request.
Test(TestOpenRepository),
Test(otest::TestOpenRepository),
}
#[cfg(not(tarpaulin_include))]
pub fn real(gix_repo: gix::Repository, forge_details: crate::ForgeDetails) -> OpenRepository {
OpenRepository::Real(oreal::RealOpenRepository::new(
Arc::new(RwLock::new(gix_repo.into())),
forge_details,
))
}
// #[cfg(test)]
// #[cfg(not(tarpaulin_include))]
// pub fn real(gix_repo: gix::Repository, forge_details: crate::core::ForgeDetails) -> OpenRepository {
// OpenRepository::Real(oreal::RealOpenRepository::new(
// std::sync::Arc::new(std::sync::RwLock::new(gix_repo.into())),
// forge_details,
// ))
// }
#[cfg(test)]
#[cfg(not(tarpaulin_include))] // don't test mocks
pub(crate) fn test(
gitdir: &GitDir,
pub fn test(
gitdir: &crate::core::GitDir,
fs: &kxio::fs::FileSystem,
on_fetch: Vec<otest::OnFetch>,
on_push: Vec<otest::OnPush>,
forge_details: crate::ForgeDetails,
forge_details: crate::core::ForgeDetails,
) -> OpenRepository {
OpenRepository::Test(TestOpenRepository::new(
OpenRepository::Test(otest::TestOpenRepository::new(
gitdir,
fs,
on_fetch,
@ -64,8 +63,9 @@ pub(crate) fn test(
#[allow(clippy::module_name_repetitions)]
#[mockall::automock]
pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
/// Creates a clone of the `OpenRepositoryLike`.
fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
// /// Creates a clone of the `OpenRepositoryLike`.
// #[cfg(test)]
// fn duplicate(&self) -> Box<dyn OpenRepositoryLike>;
/// 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
/// 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>;
/// 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
/// 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`
///
@ -96,7 +96,7 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
branch_name: &BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> git::push::Result<()>;
) -> Result<()>;
/// 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,
branch_name: &BranchName,
find_commits: &[git::Commit],
) -> git::commit::log::Result<Vec<git::Commit>>;
) -> Result<Vec<git::Commit>>;
/// Read the contents of a file as a string.
///
@ -117,20 +117,21 @@ pub trait OpenRepositoryLike: std::fmt::Debug + Sync + Send {
/// # Errors
///
/// 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)]
pub(crate) fn mock() -> Box<MockOpenRepositoryLike> {
pub fn mock() -> Box<MockOpenRepositoryLike> {
Box::new(MockOpenRepositoryLike::new())
}
#[cfg(test)]
impl std::ops::Deref for OpenRepository {
type Target = dyn OpenRepositoryLike;
fn deref(&self) -> &Self::Target {
match self {
Self::Real(real) => real,
// Self::Real(real) => real,
Self::Test(test) => test,
}
}

View file

@ -1,8 +1,9 @@
//
use crate::{
git::{self, repository::OpenRepositoryLike},
s, BranchName, ForgeDetails, Hostname, RemoteUrl, RepoPath,
core::{BranchName, ForgeDetails, Hostname, RemoteUrl, RepoPath},
err, git,
};
use crate::{s, Result};
use derive_more::Constructor;
use gix::bstr::BStr;
@ -11,7 +12,6 @@ use tracing::{info, warn};
use std::{
borrow::ToOwned,
path::Path,
result::Result,
sync::{Arc, RwLock},
};
@ -21,26 +21,29 @@ pub struct RealOpenRepository {
forge_details: ForgeDetails,
}
impl super::OpenRepositoryLike for RealOpenRepository {
fn remote_branches(&self) -> git::push::Result<Vec<BranchName>> {
fn remote_branches(&self) -> Result<Vec<BranchName>> {
let refs = self
.inner
.read()
.map_err(|_| git::push::Error::Lock)
.map_err(|_| err!("read"))
.and_then(|repo| {
Ok(repo.to_thread_local().references()?).and_then(|refs| {
Ok(refs.remote_branches().map(|rb| {
rb.filter_map(Result::ok)
.map(|r| r.name().to_owned())
.map(|n| s!(n))
.filter_map(|p| {
p.strip_prefix("refs/remotes/origin/")
.map(ToOwned::to_owned)
})
.filter(|b| b.as_str() != "HEAD")
.map(BranchName::new)
.collect::<Vec<_>>()
})?)
})
repo.to_thread_local()
.references()
.map_err(|_| err!("thread local references"))
.and_then(|refs| {
Ok(refs.remote_branches().map(|rb| {
rb.filter_map(Result::ok)
.map(|r| r.name().to_owned())
.map(|n| s!(n))
.filter_map(|p| {
p.strip_prefix("refs/remotes/origin/")
.map(ToOwned::to_owned)
})
.filter(|b| b.as_str() != "HEAD")
.map(BranchName::new)
.collect::<Vec<_>>()
})?)
})
})?;
Ok(refs)
}
@ -66,12 +69,12 @@ impl super::OpenRepositoryLike for RealOpenRepository {
#[tracing::instrument(skip_all)]
#[cfg(not(tarpaulin_include))] // would require writing to external service
fn fetch(&self) -> Result<(), git::fetch::Error> {
fn fetch(&self) -> Result<()> {
if self
.find_default_remote(git::repository::Direction::Fetch)
.is_none()
{
return Err(git::fetch::Error::NoFetchRemoteFound);
return Err(err!("No Fetch Remote Found"));
}
info!("Fetching");
gix::command::prepare("/usr/bin/git fetch --prune")
@ -79,12 +82,12 @@ impl super::OpenRepositoryLike for RealOpenRepository {
git_dir: Some(
self.inner
.read()
.map_err(|_| git::fetch::Error::Lock)
.map_err(|_| err!("Lock"))
.map(|r| r.git_dir().to_path_buf())?,
),
..Default::default()
})
.with_shell_allow_argument_splitting()
.with_shell_allow_manual_argument_splitting()
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?
@ -101,7 +104,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
branch_name: &BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> Result<(), git::push::Error> {
) -> Result<()> {
use secrecy::ExposeSecret as _;
let origin = repo_details.origin();
@ -120,7 +123,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
let git_dir = self
.inner
.read()
.map_err(|_| git::push::Error::Lock)
.map_err(|_| err!("Lock"))
.map(|r| r.git_dir().to_path_buf())?;
let ctx = gix::diff::command::Context {
git_dir: Some(git_dir),
@ -128,7 +131,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
};
gix::command::prepare(command.expose_secret())
.with_context(ctx)
.with_shell_allow_argument_splitting()
.with_shell_allow_manual_argument_splitting()
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?
@ -140,7 +143,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
&self,
branch_name: &BranchName,
find_commits: &[git::Commit],
) -> Result<Vec<git::Commit>, git::commit::log::Error> {
) -> Result<Vec<git::Commit>> {
let limit: usize = if find_commits.is_empty() {
1
} else {
@ -148,63 +151,60 @@ impl super::OpenRepositoryLike for RealOpenRepository {
.max_dev_commits()
.map_or(25, |commit_count| commit_count.clone().peel() as usize)
};
self.inner
.read()
.map_err(|_| git::commit::log::Error::Lock)
.map(|repo| {
let branch = format!("remotes/origin/{branch_name}");
let branch = BStr::new(&branch);
let thread_local = repo.to_thread_local();
let branch_head = thread_local
.rev_parse_single(branch)
self.inner.read().map_err(|_| err!("Lock")).map(|repo| {
let branch = format!("remotes/origin/{branch_name}");
let branch = BStr::new(&branch);
let thread_local = repo.to_thread_local();
let branch_head = thread_local
.rev_parse_single(branch)
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let object = branch_head
.object()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let commit = object
.try_into_commit()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let walk = thread_local
.rev_walk([commit.id])
.all()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let mut commits = vec![];
for item in walk.take(limit) {
let item = item
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let object = branch_head
let commit = item
.object()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let commit = object
.try_into_commit()
let id = commit.id();
let message = commit
.message_raw()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let walk = thread_local
.rev_walk([commit.id])
.all()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let mut commits = vec![];
for item in walk.take(limit) {
let item = item
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let commit = item
.object()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let id = commit.id();
let message = commit
.message_raw()
.map_err(|e| s!(e))
.map_err(as_gix_error(branch_name.clone()))?;
let commit = git::Commit::new(
git::commit::Sha::new(s!(id)),
git::commit::Message::new(s!(message)),
);
if find_commits.contains(&commit) {
commits.push(commit);
break;
}
let commit = git::Commit::new(
git::commit::Sha::new(s!(id)),
git::commit::Message::new(s!(message)),
);
if find_commits.contains(&commit) {
commits.push(commit);
break;
}
Ok(commits)
})?
commits.push(commit);
}
Ok(commits)
})?
}
#[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
.read()
.map_err(|_| git::file::Error::Lock)
.map_err(|_| err!("Lock"))
.and_then(|repo| {
let thread_local = repo.to_thread_local();
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> {
Box::new(self.clone())
}
// #[cfg(test)]
// fn duplicate(&self) -> Box<dyn super::OpenRepositoryLike> {
// Box::new(self.clone())
// }
}
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> git::commit::log::Error {
|error| git::commit::log::Error::Gix { branch, error }
fn as_gix_error(branch: BranchName) -> impl FnOnce(String) -> color_eyre::eyre::Error {
move |error| err!("gix: branch: {branch}: {error}")
}
impl From<&RemoteUrl> for git::GitRemote {

View file

@ -1,10 +1,11 @@
//
use crate::s;
use crate::{
git::{
self,
repository::open::{OpenRepositoryLike, RealOpenRepository},
core::{
git::{self, repository::open::oreal::RealOpenRepository},
BranchName, ForgeDetails, GitDir, RemoteUrl, RepoBranches,
},
s, BranchName, ForgeDetails, GitDir, RemoteUrl, RepoBranches,
err, Result,
};
use derive_more::Constructor;
@ -14,7 +15,7 @@ use std::{
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)]
pub struct OnFetch {
repo_branches: RepoBranches,
@ -29,7 +30,7 @@ impl OnFetch {
///
/// 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.
pub fn invoke(&self) -> git::fetch::Result<()> {
pub fn invoke(&self) -> Result<()> {
(self.action)(&self.repo_branches, &self.gitdir, &self.fs)
}
}
@ -42,7 +43,7 @@ pub type OnPushFn = fn(
&RepoBranches,
&GitDir,
&kxio::fs::FileSystem,
) -> git::push::Result<()>;
) -> Result<()>;
#[derive(Clone, Debug, Constructor)]
pub struct OnPush {
repo_branches: RepoBranches,
@ -63,7 +64,7 @@ impl OnPush {
branch_name: &BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> git::push::Result<()> {
) -> Result<()> {
(self.action)(
repo_details,
branch_name,
@ -86,7 +87,7 @@ pub struct TestOpenRepository {
}
#[cfg(not(tarpaulin_include))]
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()
}
@ -94,14 +95,11 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
self.real.find_default_remote(direction)
}
fn fetch(&self) -> Result<(), git::fetch::Error> {
let i: usize = *self
.fetch_counter
.read()
.map_err(|_| git::fetch::Error::Lock)?;
fn fetch(&self) -> Result<()> {
let i: usize = *self.fetch_counter.read().map_err(|_| err!("Lock"))?;
self.fetch_counter
.write()
.map_err(|_| git::fetch::Error::Lock)
.map_err(|_| err!("Lock"))
.map(|mut c| *c += 1)?;
#[allow(clippy::expect_used)]
self.on_fetch
@ -116,15 +114,12 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
branch_name: &BranchName,
to_commit: &git::GitRef,
force: &git::push::Force,
) -> git::push::Result<()> {
let i: usize = *self
.push_counter
.read()
.map_err(|_| git::fetch::Error::Lock)?;
) -> Result<()> {
let i: usize = *self.push_counter.read().map_err(|_| err!("Lock"))?;
println!("Push: {i}");
self.push_counter
.write()
.map_err(|_| git::fetch::Error::Lock)
.map_err(|_| err!("Lock"))
.map(|mut c| *c += 1)?;
#[allow(clippy::expect_used)]
self.on_push
@ -137,17 +132,17 @@ impl git::repository::OpenRepositoryLike for TestOpenRepository {
&self,
branch_name: &BranchName,
find_commits: &[git::Commit],
) -> git::commit::log::Result<Vec<git::Commit>> {
) -> Result<Vec<git::Commit>> {
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)
}
fn duplicate(&self) -> Box<dyn OpenRepositoryLike> {
Box::new(self.clone())
}
// fn duplicate(&self) -> Box<dyn super::OpenRepositoryLike> {
// Box::new(self.clone())
// }
}
impl TestOpenRepository {
pub(crate) fn new(

View file

@ -1,6 +1,6 @@
use std::ops::Deref as _;
use crate::CommitCount;
use crate::core::CommitCount;
//
use super::*;

View file

@ -1,12 +1,14 @@
//
use crate::{
core::{
BranchName, ForgeConfig, ForgeType, GitDir, Hostname, RepoAlias, RepoBranches, RepoConfig,
RepoConfigSource, RepoPath, ServerRepoConfig, StoragePathType, User,
},
git::{
self,
repository::RepositoryLike as _,
tests::{given, then},
},
BranchName, ForgeConfig, ForgeType, GitDir, Hostname, RepoAlias, RepoBranches, RepoConfig,
RepoConfigSource, RepoPath, ServerRepoConfig, StoragePathType, User,
};
use assert2::let_assert;
@ -14,7 +16,8 @@ use secrecy::ExposeSecret;
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 fetch;

View file

@ -0,0 +1 @@

View file

@ -2,6 +2,7 @@
use derive_more::Constructor;
use crate::{
core::{ForgeDetails, GitDir},
git::{
self,
repository::{
@ -11,9 +12,7 @@ use crate::{
},
RepositoryLike, Result,
},
RepoDetails,
},
ForgeDetails, GitDir,
};
#[allow(clippy::module_name_repetitions)]
@ -43,8 +42,4 @@ impl RepositoryLike for TestRepository {
self.forge_details.clone(),
))
}
fn git_clone(&self, _repo_details: &RepoDetails) -> Result<OpenRepository> {
todo!()
}
}

View file

@ -1,7 +1,7 @@
//
use crate::{
core::{ApiToken, GitDir, StoragePathType},
git::{self, tests::given},
ApiToken, GitDir, StoragePathType,
};
use assert2::let_assert;

View file

@ -1,9 +1,12 @@
//
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_dir::StoragePathType,
s, webhook, BranchName, ForgeAlias, ForgeConfig, ForgeType, GitDir, Hostname, RemoteUrl,
RepoAlias, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, ServerRepoConfig,
s, Result,
};
use assert2::let_assert;
@ -14,8 +17,6 @@ use std::{
path::{Path, PathBuf},
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
mod commit {
use super::*;
@ -180,7 +181,7 @@ mod repo_details {
}
}
pub mod given {
use crate::ForgeDetails;
use crate::core::ForgeDetails;
use super::*;
@ -200,9 +201,9 @@ pub mod given {
RepoAlias::new(a_name())
}
pub fn a_pathbuf() -> PathBuf {
PathBuf::from(given::a_name())
}
// pub fn a_pathbuf() -> PathBuf {
// PathBuf::from(given::a_name())
// }
pub fn a_name() -> String {
use rand::Rng;
@ -296,9 +297,12 @@ pub mod given {
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();
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 {
@ -354,37 +358,39 @@ pub mod given {
}
pub mod then {
use crate::err;
use super::*;
pub fn commit_named_file_to_branch(
file_name: &Path,
contents: &str,
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> TestResult {
// git checkout ${branch_name}
git_checkout_new_branch(branch_name, gitdir)?;
// echo ${word} > file-${word}
let pathbuf = PathBuf::from(gitdir);
let file = fs.base().join(pathbuf).join(file_name);
#[allow(clippy::expect_used)]
fs.file(&file).write(contents)?;
// git add ${file}
git_add_file(gitdir, &file)?;
// git commit -m"Added ${file}"
git_commit(gitdir, &file)?;
then::push_branch(fs, gitdir, branch_name)?;
Ok(())
}
// pub fn commit_named_file_to_branch(
// file_name: &Path,
// contents: &str,
// fs: &kxio::fs::FileSystem,
// gitdir: &GitDir,
// branch_name: &BranchName,
// ) -> Result<()> {
// // git checkout ${branch_name}
// git_checkout_new_branch(branch_name, gitdir)?;
// // echo ${word} > file-${word}
// let pathbuf = PathBuf::from(gitdir);
// let file = fs.base().join(pathbuf).join(file_name);
// #[allow(clippy::expect_used)]
// fs.file(&file).write(contents)?;
// // git add ${file}
// git_add_file(gitdir, &file)?;
// // git commit -m"Added ${file}"
// git_commit(gitdir, &file)?;
//
// then::push_branch(fs, gitdir, branch_name)?;
//
// Ok(())
// }
pub fn create_a_commit_on_branch(
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> TestResult {
) -> Result<()> {
// git checkout ${branch_name}
git_checkout_new_branch(branch_name, gitdir)?;
// echo ${word} > file-${word}
@ -406,7 +412,7 @@ pub mod then {
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> TestResult {
) -> Result<()> {
let gitrefs = fs
.base()
.join(gitdir.to_path_buf())
@ -421,7 +427,7 @@ pub mod then {
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(
&format!("git checkout -b {branch_name}"),
std::process::Command::new("/usr/bin/git")
@ -432,7 +438,7 @@ pub mod then {
Ok(())
}
pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> TestResult {
pub fn git_switch(branch_name: &BranchName, gitdir: &GitDir) -> Result<()> {
exec(
&format!("git switch {branch_name}"),
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}");
match output {
Ok(output) => {
@ -459,12 +465,12 @@ pub mod then {
}
Err(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(
&format!("git add {file:?}"),
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(
&format!(r#"git commit -m"Added {file:?}""#),
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(
"git log --all --oneline --decorate --graph",
std::process::Command::new("/usr/bin/git")
@ -498,7 +504,7 @@ pub mod then {
fs: &kxio::fs::FileSystem,
gitdir: &GitDir,
branch_name: &BranchName,
) -> Result<git::commit::Sha, Box<dyn std::error::Error>> {
) -> color_eyre::Result<git::commit::Sha> {
let main_ref = fs
.base()
.join(gitdir.to_path_buf())

View file

@ -1,7 +1,8 @@
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 super::graph::Log;

View file

@ -1,12 +1,11 @@
//
use crate::{
git::{self, repository::open::OpenRepositoryLike, RepoDetails, UserNotification},
s, BranchName, RepoConfig,
core::{git::RepoDetails, BranchName, RepoConfig},
git::{self, repository::open::OpenRepositoryLike, UserNotification},
Result,
};
use tracing::{debug, instrument};
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub struct Positions {
pub main: git::Commit,
@ -28,35 +27,32 @@ pub fn validate(
open_repository: &dyn OpenRepositoryLike,
repo_details: &git::RepoDetails,
repo_config: &RepoConfig,
) -> Result<(Positions, git::graph::Log)> {
) -> PositionsResult<(Positions, git::graph::Log)> {
let main_branch = repo_config.branches().main();
let next_branch = repo_config.branches().next();
let dev_branch = repo_config.branches().dev();
// Collect Commit Histories for `main`, `next` and `dev` branches
open_repository.fetch()?;
open_repository
.fetch()
.map_err(|e| PositionsError::Retryable(e.to_string()))?;
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
let main = commit_histories
.main
.first()
.cloned()
.ok_or_else(|| Error::NonRetryable(format!("Branch has no commits: {main_branch}")))?;
let next = commit_histories
.next
.first()
.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}")))?;
let main = commit_histories.main.first().cloned().ok_or_else(|| {
PositionsError::NonRetryable(format!("Branch has no commits: {main_branch}"))
})?;
let next = commit_histories.next.first().cloned().ok_or_else(|| {
PositionsError::NonRetryable(format!("Branch has no commits: {next_branch}"))
})?;
let dev = commit_histories.dev.first().cloned().ok_or_else(|| {
PositionsError::NonRetryable(format!("Branch has no commits: {dev_branch}"))
})?;
// Validations:
// Dev must be on main branch, else the USER must rebase it
if is_not_based_on(&commit_histories.dev, &main) {
return Err(Error::UserIntervention(
return Err(PositionsError::UserIntervention(
UserNotification::DevNotBasedOnMain {
forge_alias: repo_details.forge.forge_alias().clone(),
repo_alias: repo_details.repo_alias.clone(),
@ -121,7 +117,7 @@ fn reset_next_to_main(
main: &git::Commit,
next: &git::Commit,
next_branch: &BranchName,
) -> Error {
) -> PositionsError {
match git::push::reset(
open_repository,
repo_details,
@ -129,8 +125,8 @@ fn reset_next_to_main(
&main.clone().into(),
&git::push::Force::From(next.clone().into()),
) {
Ok(()) => Error::Retryable(format!("Branch {next_branch} has been reset")),
Err(err) => Error::NonRetryable(format!(
Ok(()) => PositionsError::Retryable(format!("Branch {next_branch} has been reset")),
Err(err) => PositionsError::NonRetryable(format!(
"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(
open_repository: &dyn OpenRepositoryLike,
repo_config: &RepoConfig,
) -> git::commit::log::Result<git::commit::Histories> {
) -> Result<git::commit::Histories> {
debug!("main...");
let main = (open_repository.commit_log(&repo_config.branches().main(), &[]))?;
let main_head = [main[0].clone()];
@ -166,8 +162,9 @@ pub fn get_commit_histories(
Ok(histories)
}
pub type PositionsResult<T> = std::result::Result<T, PositionsError>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
pub enum PositionsError {
#[error("{0} - will retry")]
Retryable(String),
@ -177,13 +174,3 @@ pub enum Error {
#[error("user intervention required")]
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))
}
}

View file

@ -1,7 +1,8 @@
//
use crate::{
core::RemoteUrl,
git::{self, repository::open::OpenRepositoryLike},
s, RemoteUrl,
s,
};
#[tracing::instrument(skip_all)]
@ -44,12 +45,11 @@ pub enum Error {
#[error("no default fetch remote")]
NoDefaultFetchRemote,
#[error("no url for default push remote")]
NoUrlForDefaultPushRemote,
#[error("no hostname for default push remote")]
NoHostnameForDefaultPushRemote,
// #[error("no url for default push remote")]
// NoUrlForDefaultPushRemote,
// #[error("no hostname for default push remote")]
// NoHostnameForDefaultPushRemote,
#[error("unable to open repo: {0}")]
UnableToOpenRepo(String),

View file

@ -1,5 +1,6 @@
//
use crate::{
core::{GitDir, StoragePathType},
git::{
self,
repository::{
@ -10,7 +11,6 @@ use crate::{
tests::{given, then},
validation::positions::validate,
},
s, GitDir, StoragePathType,
};
use assert2::let_assert;
@ -56,6 +56,10 @@ mod positions {
mod validate {
use git::validation::positions::PositionsError;
use crate::err;
use super::*;
#[test]
@ -66,7 +70,7 @@ mod positions {
let mut mock_open_repository = git::repository::open::mock();
mock_open_repository
.expect_fetch()
.return_once(|| Err(git::fetch::Error::TestFailureExpected));
.return_once(|| Err(err!("expected failure")));
let mut repository_factory = git::repository::factory::mock();
repository_factory
.expect_open()
@ -81,10 +85,7 @@ mod positions {
println!("{result:?}");
let_assert!(Err(err) = result, "validate");
assert!(matches!(
err,
git::validation::positions::Error::Retryable(_)
));
assert!(matches!(err, PositionsError::Retryable(_)));
}
#[test]
@ -100,10 +101,7 @@ mod positions {
.expect_commit_log()
.returning(move |branch_name, _| {
if branch_name == &main_branch {
Err(git::commit::log::Error::Gix {
branch: branch_name.clone(),
error: s!("foo"),
})
Err(err!("{branch_name}: foo"))
} else {
Ok(vec![])
}
@ -120,10 +118,7 @@ mod positions {
let result = validate(&*open_repository, &repo_details, &repo_config);
println!("{result:?}");
assert!(matches!(
result,
Err(git::validation::positions::Error::Retryable(_))
));
assert!(matches!(result, Err(PositionsError::Retryable(_))));
}
#[test]
@ -139,10 +134,7 @@ mod positions {
.expect_commit_log()
.returning(move |branch_name, _| {
if branch_name == &next_branch {
Err(git::commit::log::Error::Gix {
branch: branch_name.clone(),
error: s!("foo"),
})
Err(err!("{branch_name} foo"))
} else {
Ok(vec![given::a_commit()])
}
@ -159,10 +151,7 @@ mod positions {
let result = validate(&*open_repository, &repo_details, &repo_config);
println!("{result:?}");
assert!(matches!(
result,
Err(git::validation::positions::Error::Retryable(_))
));
assert!(matches!(result, Err(PositionsError::Retryable(_))));
}
#[test]
@ -178,10 +167,7 @@ mod positions {
.expect_commit_log()
.returning(move |branch_name, _| {
if branch_name == &dev_branch {
Err(git::commit::log::Error::Gix {
branch: branch_name.clone(),
error: s!("foo"),
})
Err(err!("{branch_name} foo"))
} else {
Ok(vec![given::a_commit()])
}
@ -198,10 +184,7 @@ mod positions {
let result = validate(&*open_repository, &repo_details, &repo_config);
println!("{result:?}");
assert!(matches!(
result,
Err(git::validation::positions::Error::Retryable(_))
));
assert!(matches!(result, Err(PositionsError::Retryable(_))));
}
#[test]
@ -232,7 +215,7 @@ mod positions {
// add a commit to next 1 -> 4
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
Ok(())
},
));
let_assert!(
@ -250,7 +233,7 @@ mod positions {
//then
assert!(matches!(
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::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
Ok(())
},
));
// second fetch as prep to push
@ -293,7 +276,7 @@ mod positions {
fs.deref().clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
Ok(())
},
));
test_repository.on_push(OnPush::new(
@ -318,7 +301,7 @@ mod positions {
&git::push::Force::From(sha_next.into()),
"should force push only if next is on expected sha"
);
git::push::Result::Ok(())
Ok(())
},
));
let_assert!(
@ -336,10 +319,7 @@ mod positions {
//then
println!("Got: {err:?}");
// NOTE: assertions for correct push are in on_push above
assert!(matches!(
err,
git::validation::positions::Error::Retryable(_)
));
assert!(matches!(err, PositionsError::Retryable(_)));
}
#[test]
@ -370,7 +350,7 @@ mod positions {
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
Ok(())
},
));
// second fetch as prep to push
@ -380,7 +360,7 @@ mod positions {
fs.deref().clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
Ok(())
},
));
test_repository.on_push(OnPush::new(
@ -388,7 +368,7 @@ mod positions {
gitdir.clone(),
fs.deref().clone(),
|_repo_details, _branch_name, _gitref, _force, _repo_branches, _gitdir, _fs| {
git::push::Result::Err(git::push::Error::Lock)
Err(err!("Lock"))
},
));
let_assert!(
@ -409,10 +389,7 @@ mod positions {
Ok(_) = then::get_sha_for_branch(&fs, &gitdir, &repo_config.branches().next()),
"load next branch sha"
);
assert!(matches!(
err,
git::validation::positions::Error::NonRetryable(_)
));
assert!(matches!(err, PositionsError::NonRetryable(_)));
}
#[test]
#[allow(clippy::expect_used)]
@ -441,7 +418,7 @@ mod positions {
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
Ok(())
},
));
// second fetch as prep to push
@ -451,7 +428,7 @@ mod positions {
fs.deref().clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
Ok(())
},
));
test_repository.on_push(OnPush::new(
@ -476,7 +453,7 @@ mod positions {
&git::push::Force::From(sha_next.into()),
"should force push only if next is on expected sha"
);
git::push::Result::Ok(())
Ok(())
},
));
let_assert!(
@ -494,10 +471,7 @@ mod positions {
//then
println!("Got: {err:?}");
// NOTE: assertions for correct push are in on_push above
assert!(matches!(
err,
git::validation::positions::Error::Retryable(_)
));
assert!(matches!(err, PositionsError::Retryable(_)));
}
#[test]
@ -528,7 +502,7 @@ mod positions {
then::create_a_commit_on_branch(fs, gitdir, &branches.next())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
Ok(())
},
));
// second fetch as prep to push
@ -538,7 +512,7 @@ mod positions {
fs.deref().clone(),
|_branches, _gitdir, _fs| {
// don't change anything
git::fetch::Result::Ok(())
Ok(())
},
));
test_repository.on_push(OnPush::new(
@ -563,7 +537,7 @@ mod positions {
&git::push::Force::From(sha_next.into()),
"should force push only if next is on expected sha"
);
git::push::Result::Ok(())
Ok(())
},
));
let_assert!(
@ -624,7 +598,7 @@ mod positions {
then::create_a_commit_on_branch(fs, gitdir, &branches.dev())?;
then::git_log_all(gitdir)?;
git::fetch::Result::Ok(())
Ok(())
},
));
let_assert!(

View file

@ -1,15 +1,15 @@
#[macro_export]
macro_rules! message {
($name:ident, $value:ty, $docs:literal) => {
git_next_core::newtype!($name, $value, $docs);
$crate::newtype!($name, $value, $docs);
};
($name:ident, $docs:literal) => {
git_next_core::newtype!($name, $docs);
$crate::newtype!($name, $docs);
};
($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) => {
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