Compare commits

...

16 commits

Author SHA1 Message Date
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
44 changed files with 1000 additions and 1013 deletions

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

1229
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/*"]
[workspace.package]
version = "0.14.0"
version = "0.14.1"
edition = "2021"
license = "MIT"
@ -23,8 +23,6 @@ categories = ["development-tools"]
[workspace.dependencies]
git-next-core = { path = "crates/core", version = "0.14" }
git-next-forge-forgejo = { path = "crates/forge-forgejo", version = "0.14" }
git-next-forge-github = { path = "crates/forge-github", version = "0.14" }
# TUI
ratatui = "0.29"
@ -52,7 +50,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 +58,7 @@ async-trait = "0.1"
git-url-parse = "0.4"
# fs/network
kxio = "4.0"
native-tls = { version = "0.2", features = ["vendored"] }
kxio = "5.1"
# TOML parsing
serde = { version = "1.0", features = ["derive"] }
@ -84,11 +80,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,14 +97,14 @@ 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
@ -119,4 +116,4 @@ pretty_assertions = "1.4"
rand = "0.8"
mockall = "0.13"
test-log = "0.2"
rstest = { version = "0.23", features = ["async-timeout"] }
rstest = { version = "0.24", features = ["async-timeout"] }

View file

@ -14,8 +14,8 @@ categories = { workspace = true }
[features]
# default = ["forgejo", "github"]
default = ["forgejo", "github", "tui"]
forgejo = ["git-next-forge-forgejo"]
github = ["git-next-forge-github"]
forgejo = []
github = []
tui = [
"ratatui",
"directories",
@ -27,10 +27,6 @@ tui = [
[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 }
@ -57,6 +53,7 @@ git-conventional = { workspace = true }
# TOML parsing
toml = { workspace = true }
serde = { workspace = true }
# Actors
kameo = { workspace = true }
@ -88,8 +85,13 @@ sendmail = { workspace = true }
# desktop notifications
notifica = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["native-tls"]
# git
async-trait = { workspace = true }
# sha256 encoding (e.g. verify github webhooks)
hmac = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
[dev-dependencies]
# Testing

View file

@ -2,10 +2,10 @@
use git_next_core::git::{ForgeLike, RepoDetails};
#[cfg(feature = "forgejo")]
use git_next_forge_forgejo::ForgeJo;
use crate::forges::forgejo::ForgeJo;
#[cfg(feature = "github")]
use git_next_forge_github::Github;
use crate::forges::github::Github;
use kxio::net::Net;

View file

@ -11,19 +11,21 @@ use git_next_core::{
#[cfg(feature = "forgejo")]
#[test]
fn test_forgejo_name() {
let net = kxio::net::mock();
let mock_net = kxio::net::mock();
let repo_details = given_repo_details(git_next_core::ForgeType::ForgeJo);
let forge = Forge::create(repo_details, net.into());
let forge = Forge::create(repo_details, mock_net.clone().into());
assert_eq!(forge.name(), "forgejo");
mock_net.assert_no_unused_plans();
}
#[cfg(feature = "github")]
#[test]
fn test_github_name() {
let net = kxio::net::mock();
let mock_net = kxio::net::mock();
let repo_details = given_repo_details(git_next_core::ForgeType::GitHub);
let forge = Forge::create(repo_details, net.into());
let forge = Forge::create(repo_details, mock_net.clone().into());
assert_eq!(forge.name(), "github");
mock_net.assert_no_unused_plans();
}
#[allow(dead_code)]

View file

@ -1,7 +1,8 @@
//
#![allow(clippy::expect_used)] // used with mock net
use crate::ForgeJo;
use crate::forges::forgejo::ForgeJo;
use git_next_core::{
git::{self, forge::commit::Status, ForgeLike as _},
server::{ListenUrl, RepoListenUrl},
@ -23,8 +24,10 @@ mod forgejo {
#[test]
fn should_return_name() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
assert_eq!(forge.name(), "forgejo");
mock_net.assert_no_unused_plans();
}
mod is_message_authorised {
@ -34,42 +37,52 @@ mod forgejo {
#[test]
fn should_return_true_with_valid_header() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Valid(auth.clone()));
assert!(forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
#[test]
fn should_return_false_with_missing_header() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Missing);
assert!(!forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
#[test]
fn should_return_false_with_non_basic_prefix() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::NonBasic);
assert!(!forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
#[test]
fn should_return_false_with_non_ulid_value() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::NonUlid);
assert!(!forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
#[test]
fn should_return_false_with_wrong_ulid_value() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::WrongUlid);
assert!(!forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
}
@ -80,7 +93,8 @@ mod forgejo {
#[test]
fn should_parse_valid_body() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let repo_branches = given::repo_branches();
let next = repo_branches.next();
let sha = given::a_name();
@ -96,16 +110,19 @@ mod forgejo {
push.branch(&repo_branches),
Some(git_next_core::webhook::push::Branch::Next)
);
mock_net.assert_no_unused_plans();
}
#[test]
fn should_error_invalid_body() {
let fs = given::a_filesystem();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), given::a_network());
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&given::repo_details(&fs), mock_net.clone());
let body = git_next_core::webhook::forge_notification::Body::new(
r#"{"type":"invalid"}"#.to_string(),
);
let_assert!(Err(_) = forge.parse_webhook_body(&body));
mock_net.assert_no_unused_plans();
}
}
@ -118,73 +135,79 @@ mod forgejo {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let net = given::a_network();
given::a_commit_state("success", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
let mock_net = given::a_network();
given::a_commit_state("success", &mock_net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pass
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_pending_for_pending() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let net = given::a_network();
given::a_commit_state("pending", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
let mock_net = given::a_network();
given::a_commit_state("pending", &mock_net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_fail_for_failure() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let net = given::a_network();
given::a_commit_state("failure", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
let mock_net = given::a_network();
given::a_commit_state("failure", &mock_net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Fail
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_fail_for_error() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let net = given::a_network();
given::a_commit_state("error", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
let mock_net = given::a_network();
given::a_commit_state("error", &mock_net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Fail
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_pending_for_blank() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let net = given::a_network();
given::a_commit_state("", &net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, net);
let mock_net = given::a_network();
given::a_commit_state("", &mock_net, &repo_details, &commit);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_pending_for_no_statuses() {
let fs = given::a_filesystem();
let repo_details = given::repo_details(&fs);
let commit = given::a_commit();
let net = given::a_network();
net.on()
let mock_net = given::a_network();
mock_net
.on()
.get(given::a_commit_status_url(&repo_details, &commit))
.respond(StatusCode::OK)
.body(
@ -194,11 +217,12 @@ mod forgejo {
.to_string(),
)
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_pending_for_network_error() {
@ -212,11 +236,12 @@ mod forgejo {
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("book today")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, mock_net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
mock_net.assert_no_unused_plans();
}
}
@ -235,10 +260,10 @@ mod forgejo {
let hook_id_1 = given::a_forgejo_webhook_id();
let hook_id_2 = given::a_forgejo_webhook_id();
let hook_id_3 = given::a_forgejo_webhook_id();
let mut net = given::a_network();
let mock_net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
mock_net: &mock_net,
hostname,
repo_path,
token,
@ -256,7 +281,7 @@ mod forgejo {
// page 2 with no items - stops pagination
with::get_webhooks_by_page(2, &[], &mut args);
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Ok(result) = forge.list_webhooks(&repo_listen_url).await);
assert_eq!(
@ -266,6 +291,7 @@ mod forgejo {
WebhookId::new(format!("{hook_id_2}"))
]
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -276,9 +302,10 @@ mod forgejo {
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let net = given::a_network();
let mock_net = given::a_network();
net.on()
mock_net
.on()
.get(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?page=1&token={token}"
))
@ -286,9 +313,10 @@ mod forgejo {
.body("error_message")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Err(_) = forge.list_webhooks(&repo_listen_url).await);
mock_net.assert_no_unused_plans();
}
}
@ -303,9 +331,10 @@ mod forgejo {
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let webhook_id = given::a_webhook_id();
let net = given::a_network();
let mock_net = given::a_network();
net.on()
mock_net
.on()
.delete(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
))
@ -313,9 +342,10 @@ mod forgejo {
.body("")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Ok(()) = forge.unregister_webhook(&webhook_id).await);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -326,9 +356,10 @@ mod forgejo {
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let webhook_id = given::a_webhook_id();
let net = given::a_network();
let mock_net = given::a_network();
net.on()
mock_net
.on()
.delete(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
))
@ -336,13 +367,14 @@ mod forgejo {
.body("error")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.unregister_webhook(&webhook_id).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToUnregister(_)),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
}
@ -362,9 +394,9 @@ mod forgejo {
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mock_net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
mock_net: &mock_net,
hostname,
repo_path,
token,
@ -374,7 +406,8 @@ mod forgejo {
with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will succeed
let webhook_id = given::a_forgejo_webhook_id();
net.on()
mock_net
.on()
.post(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
))
@ -382,13 +415,14 @@ mod forgejo {
.body(json!({"id": webhook_id, "config":{}}).to_string())
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -401,13 +435,14 @@ mod forgejo {
"repo_details needs to NOT have repo_config for this test"
);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let net = given::a_network();
let forge = given::a_forgejo_forge(&repo_details, net);
let mock_net = given::a_network();
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!(
matches!(err, git::forge::webhook::Error::NoRepoConfig),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -423,9 +458,9 @@ mod forgejo {
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mock_net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
mock_net: &mock_net,
hostname,
repo_path,
token,
@ -448,7 +483,8 @@ mod forgejo {
with::unregister_webhook(&hooks[1], &mut args);
// register the webhook will succeed
let webhook_id = given::a_forgejo_webhook_id();
net.on()
mock_net
.on()
.post(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
))
@ -456,13 +492,14 @@ mod forgejo {
.body(json!({"id": webhook_id, "config":{}}).to_string())
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -478,9 +515,9 @@ mod forgejo {
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mock_net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
mock_net: &mock_net,
hostname,
repo_path,
token,
@ -489,7 +526,8 @@ mod forgejo {
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will return empty response
net.on()
mock_net
.on()
.post(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
))
@ -497,13 +535,14 @@ mod forgejo {
.body(json!({}).to_string()) // empty response)
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!(
matches!(err, git::forge::webhook::Error::NetworkResponseEmpty),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -519,9 +558,9 @@ mod forgejo {
let repo_path = &repo_details.repo_path;
let token = repo_details.forge.token().expose_secret();
let mut net = given::a_network();
let mock_net = given::a_network();
let mut args = with::WebhookArgs {
net: &mut net,
mock_net: &mock_net,
hostname,
repo_path,
token,
@ -530,7 +569,8 @@ mod forgejo {
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &mut args);
// register the webhook will return empty response
net.on()
mock_net
.on()
.post(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?token={token}"
))
@ -538,13 +578,14 @@ mod forgejo {
.body("error")
.expect("mock");
let forge = given::a_forgejo_forge(&repo_details, net);
let forge = given::a_forgejo_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
}
mod with {
@ -559,7 +600,7 @@ mod forgejo {
let hostname = args.hostname;
let repo_path = args.repo_path;
let token = args.token;
args.net
args.mock_net
.on()
.get(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks?page={page}&token={token}"
@ -573,7 +614,7 @@ mod forgejo {
let hostname = args.hostname;
let repo_path = args.repo_path;
let token = args.token;
args.net
args.mock_net
.on()
.delete(format!(
"https://{hostname}/api/v1/repos/{repo_path}/hooks/{webhook_id}?token={token}"
@ -583,7 +624,7 @@ mod forgejo {
.expect("mock");
}
pub struct WebhookArgs<'a> {
pub net: &'a MockNet,
pub mock_net: &'a MockNet,
pub hostname: &'a Hostname,
pub repo_path: &'a RepoPath,
pub token: &'a str,

View file

@ -2,7 +2,7 @@
use git_next_core::{git, server::RepoListenUrl, WebhookId};
use kxio::net::Net;
use crate::webhook::Hook;
use crate::forges::forgejo::webhook::Hook;
pub async fn list(
repo_details: &git::RepoDetails,

View file

@ -1,5 +1,5 @@
//
use crate as forgejo;
use crate::forges::forgejo;
use git_next_core::{git, webhook};

View file

@ -1,14 +1,17 @@
use git_next_core::git::forge::webhook::Error;
//
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use git_next_core::{
git::{self, forge::webhook::Error},
server::RepoListenUrl,
RegisteredWebhook, WebhookAuth, WebhookId,
};
use kxio::net::Net;
use secrecy::ExposeSecret as _;
use serde_json::json;
use tracing::{info, instrument, warn};
use crate::webhook;
use crate::webhook::Hook;
use crate::forges::forgejo::webhook;
use crate::forges::forgejo::webhook::Hook;
#[instrument(skip_all, fields(forge = %repo_details.forge.forge_alias(), repo = %repo_details.repo_alias))]
pub async fn register(

View file

@ -1,5 +1,5 @@
//
use crate::{self as github, GithubState};
use crate::forges::github::{self as github, GithubState};
use git_next_core::git::{self, forge::commit::Status};
use github::GithubStatus;

View file

@ -3,9 +3,9 @@
mod tests;
mod commit;
mod webhook;
pub mod webhook;
use crate as github;
use crate::forges::github;
use git_next_core::{
self as core, git,
server::{self, RepoListenUrl},
@ -79,12 +79,12 @@ impl git::ForgeLike for Github {
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct GithubStatus {
pub struct GithubStatus {
pub state: GithubState,
// other fields that we ignore
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
enum GithubState {
pub enum GithubState {
#[serde(rename = "success")]
Success,
#[serde(rename = "pending")]

View file

@ -1,7 +1,7 @@
//
#![allow(clippy::expect_used)]
use crate::{Github, GithubState, GithubStatus};
use crate::forges::github::{Github, GithubState, GithubStatus};
use git_next_core::{
git::{self, forge::commit::Status, ForgeLike},
server::ListenUrl,
@ -23,8 +23,10 @@ mod github {
#[test]
fn should_return_name() {
let forge = given::a_github_forge(&given::repo_details(), given::a_network());
let mock_net = given::a_network();
let forge = given::a_github_forge(&given::repo_details(), mock_net.clone());
assert_eq!(forge.name(), "github");
mock_net.assert_no_unused_plans();
}
mod is_message_authorised {
@ -33,27 +35,33 @@ mod github {
#[test]
fn should_return_true_with_valid_header() {
let forge = given::a_github_forge(&given::repo_details(), given::a_network());
let mock_net = given::a_network();
let forge = given::a_github_forge(&given::repo_details(), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Valid(
auth.clone(),
given::a_webhook_message_body(),
));
assert!(forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
#[test]
fn should_return_false_with_missing_header() {
let forge = given::a_github_forge(&given::repo_details(), given::a_network());
let mock_net = given::a_network();
let forge = given::a_github_forge(&given::repo_details(), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Missing);
assert!(!forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
#[test]
fn should_return_false_with_invalid_header() {
let forge = given::a_github_forge(&given::repo_details(), given::a_network());
let mock_net = given::a_network();
let forge = given::a_github_forge(&given::repo_details(), mock_net.clone());
let auth = given::a_webhook_auth();
let message = given::a_webhook_message(given::Header::Invalid);
assert!(!forge.is_message_authorised(&message, &auth));
mock_net.assert_no_unused_plans();
}
}
@ -63,7 +71,8 @@ mod github {
#[test]
fn should_parse_valid_body() {
let forge = given::a_github_forge(&given::repo_details(), given::a_network());
let mock_net = given::a_network();
let forge = given::a_github_forge(&given::repo_details(), mock_net.clone());
let repo_branches = given::repo_branches();
let next = repo_branches.next();
let sha = given::a_name();
@ -79,13 +88,16 @@ mod github {
push.branch(&repo_branches),
Some(webhook::push::Branch::Next)
);
mock_net.assert_no_unused_plans();
}
#[test]
fn should_error_invalid_body() {
let forge = given::a_github_forge(&given::repo_details(), given::a_network());
let mock_net = given::a_network();
let forge = given::a_github_forge(&given::repo_details(), mock_net.clone());
let body = Body::new(r#"{"type":"invalid"}"#.to_string());
let_assert!(Err(_) = forge.parse_webhook_body(&body));
mock_net.assert_no_unused_plans();
}
}
@ -101,10 +113,11 @@ mod github {
let (states, expected) = $value;
let repo_details = given::repo_details();
let commit = given::a_commit();
let net = given::a_network();
given::commit_states(&states, &net, &repo_details, &commit);
let forge = given::a_github_forge(&repo_details, net);
let mock_net = given::a_network();
given::commit_states(&states, &mock_net, &repo_details, &commit);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
assert_eq!(forge.commit_status(&commit).await.expect("status"), expected);
mock_net.assert_no_unused_plans();
}
)*
}
@ -130,34 +143,38 @@ mod github {
async fn should_return_pass_for_no_statuses() {
let repo_details = given::repo_details();
let commit = given::a_commit();
let net = given::a_network();
net.on()
let mock_net = given::a_network();
mock_net
.on()
.get(given::a_commit_status_url(&repo_details, &commit))
.respond(StatusCode::OK)
.body(json!([]).to_string())
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pass // no CI checks configured
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_pending_for_network_error() {
let repo_details = given::repo_details();
let commit = given::a_commit();
let net = given::a_network();
net.on()
let mock_net = given::a_network();
mock_net
.on()
.get(given::a_commit_status_url(&repo_details, &commit))
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("boom today")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
assert_eq!(
forge.commit_status(&commit).await.expect("status"),
Status::Pending
);
mock_net.assert_no_unused_plans();
}
}
@ -174,9 +191,9 @@ mod github {
let hook_id_1 = given::a_github_webhook_id();
let hook_id_2 = given::a_github_webhook_id();
let hook_id_3 = given::a_github_webhook_id();
let net = given::a_network();
let mock_net = given::a_network();
let args = with::WebhookArgs {
net: &net,
mock_net: &mock_net,
hostname,
repo_path,
};
@ -193,7 +210,7 @@ mod github {
// page 2 with no items - stops pagination
with::get_webhooks_by_page(2, &[], &args);
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Ok(result) = forge.list_webhooks(&repo_listen_url).await);
assert_eq!(
@ -203,6 +220,7 @@ mod github {
WebhookId::new(format!("{hook_id_2}"))
]
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -211,8 +229,9 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let net = given::a_network();
net.on()
let mock_net = given::a_network();
mock_net
.on()
.get(format!(
"https://api.{hostname}/repos/{repo_path}/hooks?page=1"
))
@ -220,9 +239,10 @@ mod github {
.body("error_message")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Err(_) = forge.list_webhooks(&repo_listen_url).await);
mock_net.assert_no_unused_plans();
}
}
@ -235,8 +255,9 @@ mod github {
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let webhook_id = given::a_webhook_id();
let net = given::a_network();
net.on()
let mock_net = given::a_network();
mock_net
.on()
.delete(format!(
"https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}"
))
@ -244,9 +265,10 @@ mod github {
.body("")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Ok(()) = forge.unregister_webhook(&webhook_id).await);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
async fn should_return_error_on_network_error() {
@ -257,10 +279,11 @@ mod github {
);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let net = given::a_network();
let mock_net = given::a_network();
// unregister the webhook will return empty response
let webhook_id = given::a_webhook_id();
net.on()
mock_net
.on()
.delete(format!(
"https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}"
))
@ -268,13 +291,14 @@ mod github {
.mock()
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.unregister_webhook(&webhook_id).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
}
@ -292,9 +316,9 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let net = given::a_network();
let mock_net = given::a_network();
let args = with::WebhookArgs {
net: &net,
mock_net: &mock_net,
hostname,
repo_path,
};
@ -302,7 +326,8 @@ mod github {
with::get_webhooks_by_page(1, &[], &args);
// register the webhook will succeed
let webhook_id = given::a_github_webhook_id();
net.on()
mock_net
.on()
.post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
.respond(StatusCode::OK)
.body(
@ -311,13 +336,14 @@ mod github {
)
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -329,13 +355,14 @@ mod github {
"repo_details needs to NOT have repo_config for this test"
);
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let net = given::a_network();
let forge = given::a_github_forge(&repo_details, net);
let mock_net = given::a_network();
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!(
matches!(err, git::forge::webhook::Error::NoRepoConfig),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -348,9 +375,9 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let net = given::a_network();
let mock_net = given::a_network();
let args = with::WebhookArgs {
net: &net,
mock_net: &mock_net,
hostname,
repo_path,
};
@ -367,7 +394,8 @@ mod github {
with::unregister_webhook(&hooks[1], &args);
// register the webhook will succeed
let webhook_id = given::a_github_webhook_id();
net.on()
mock_net
.on()
.post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
.respond(StatusCode::OK)
.body(
@ -376,13 +404,14 @@ mod github {
)
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Ok(registered_webhook) = forge.register_webhook(&repo_listen_url).await);
assert_eq!(
registered_webhook.id(),
&WebhookId::new(format!("{webhook_id}"))
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -395,28 +424,30 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let net = given::a_network();
let mock_net = given::a_network();
let args = with::WebhookArgs {
net: &net,
mock_net: &mock_net,
hostname,
repo_path,
};
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &args);
// register the webhook will return empty response
net.on()
mock_net
.on()
.post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
.respond(StatusCode::OK)
.body(json!({}).to_string())
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!(
matches!(err, git::forge::webhook::Error::NetworkResponseEmpty),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
#[tokio::test]
@ -429,28 +460,30 @@ mod github {
let repo_listen_url = given::a_repo_listen_url(&repo_details);
let hostname = repo_details.forge.hostname();
let repo_path = &repo_details.repo_path;
let net = given::a_network();
let mock_net = given::a_network();
let args = with::WebhookArgs {
net: &net,
mock_net: &mock_net,
hostname,
repo_path,
};
// there are no existing matching webhooks
with::get_webhooks_by_page(1, &[], &args);
// register the webhook will return empty response
net.on()
mock_net
.on()
.post(format!("https://api.{hostname}/repos/{repo_path}/hooks"))
.respond(StatusCode::INTERNAL_SERVER_ERROR)
.body("error")
.expect("mock");
let forge = given::a_github_forge(&repo_details, net);
let forge = given::a_github_forge(&repo_details, mock_net.clone());
let_assert!(Err(err) = forge.register_webhook(&repo_listen_url).await);
assert!(
matches!(err, git::forge::webhook::Error::FailedToRegister(_)),
"{err:?}"
);
mock_net.assert_no_unused_plans();
}
}
@ -462,7 +495,7 @@ mod github {
pub fn get_webhooks_by_page(page: u8, response: &[ReturnedWebhook], args: &WebhookArgs) {
let hostname = args.hostname;
let repo_path = args.repo_path;
args.net
args.mock_net
.on()
.get(format!(
"https://api.{hostname}/repos/{repo_path}/hooks?page={page}"
@ -476,7 +509,7 @@ mod github {
let webhook_id = hook1.id;
let hostname = args.hostname;
let repo_path = args.repo_path;
args.net
args.mock_net
.on()
.delete(format!(
"https://api.{hostname}/repos/{repo_path}/hooks/{webhook_id}"
@ -486,7 +519,7 @@ mod github {
.expect("mock");
}
pub struct WebhookArgs<'a> {
pub net: &'a kxio::net::MockNet,
pub mock_net: &'a kxio::net::MockNet,
pub hostname: &'a Hostname,
pub repo_path: &'a RepoPath,
}
@ -573,7 +606,9 @@ mod github {
let mut headers = BTreeMap::new();
match header {
Header::Valid(auth, body) => {
if let Some(sig) = crate::webhook::sign_body(&auth, &body) {
if let Some(sig) =
crate::forges::github::webhook::authorisation::sign_body(&auth, &body)
{
headers.insert("x-hub-signature-256".to_string(), format!("sha256={sig}"));
}
}

View file

@ -1,5 +1,5 @@
//
use crate as github;
use crate::forges::github;
use git_next_core::{git, server::RepoListenUrl, WebhookId};
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#list-repository-webhooks

View file

@ -3,7 +3,7 @@ use std::collections::HashMap;
//
use git_next_core::{git, webhook, ApiToken, BranchName};
mod authorisation;
pub mod authorisation;
mod list;
mod parser;
mod register;
@ -15,9 +15,6 @@ pub use parser::parse_body;
pub use register::register;
pub use unregister::unregister;
#[cfg(test)]
pub use authorisation::sign_body;
pub fn headers(token: &ApiToken) -> HashMap<String, String> {
use secrecy::ExposeSecret;

View file

@ -1,5 +1,5 @@
//
use crate as github;
use crate::forges::github;
use git_next_core::{git, webhook};

View file

@ -1,5 +1,5 @@
//
use crate::{self as github, webhook};
use crate::forges::github::{self as github, webhook};
use git_next_core::{git, server::RepoListenUrl, RegisteredWebhook, WebhookAuth, WebhookId};
use serde_json::json;

View file

@ -1,5 +1,5 @@
//
use crate as github;
use crate::forges::github;
use git_next_core::{git, WebhookId};
// https://docs.github.com/en/rest/repos/webhooks?apiVersion=2022-11-28#delete-a-repository-webhook

View file

@ -0,0 +1,6 @@
//
#[cfg(feature = "forgejo")]
pub mod forgejo;
#[cfg(feature = "github")]
pub mod github;

View file

@ -11,6 +11,8 @@ mod repo;
mod root;
mod server;
mod forges;
#[cfg(feature = "tui")]
mod tui;

View file

@ -35,7 +35,7 @@ async fn should_clone() -> TestResult {
repo_details,
given::a_forge(),
fs.as_real(),
net,
net.clone(),
);
tell!(addr, CloneRepo::new())?;
//then
@ -44,6 +44,7 @@ async fn should_clone() -> TestResult {
.read()
.map_err(|e| e.to_string())
.map(|o| assert_eq!(o.len(), 1))?;
net.assert_no_unused_plans();
Ok(())
}
@ -78,7 +79,7 @@ async fn should_open() -> TestResult {
repo_details,
given::a_forge(),
fs.as_real(),
net,
net.clone(),
);
tell!(addr, CloneRepo::new())?;
@ -88,6 +89,7 @@ async fn should_open() -> TestResult {
.read()
.map_err(|e| e.to_string())
.map(|o| assert_eq!(o.len(), 1))?;
net.assert_no_unused_plans();
Ok(())
}
@ -120,13 +122,14 @@ async fn when_server_has_no_repo_config_should_send_load_from_repo() -> TestResu
repo_details,
given::a_forge(),
fs.as_real(),
net,
net.clone(),
);
tell!(addr, CloneRepo::new())?;
//then
log.require_message_containing("send: LoadConfigFromRepo")
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -156,7 +159,7 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
repo_details,
given::a_forge(),
fs.as_real(),
net,
net.clone(),
);
tell!(addr, CloneRepo::new())?;
@ -165,6 +168,7 @@ async fn when_server_has_repo_config_should_send_register_webhook() -> TestResul
debug!(?log, "");
log.require_message_containing("send: RegisterWebhook")
.await?;
net.assert_no_unused_plans();
Ok(())
}

View file

@ -396,7 +396,7 @@ async fn should_accept_message_with_current_token() -> TestResult {
git::repository::factory::mock(),
given::a_forge(),
server_actor_ref,
net,
net.clone(),
);
let actor = actor.with_message_token(MessageToken::new(2_u32));
let addr = kameo::spawn(actor);
@ -406,6 +406,7 @@ async fn should_accept_message_with_current_token() -> TestResult {
//then
log.require_message_containing("accepted token: 2").await?;
net.assert_no_unused_plans();
Ok(())
}
@ -422,7 +423,7 @@ async fn should_accept_message_with_new_token() -> TestResult {
git::repository::factory::mock(),
given::a_forge(),
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_message_token(MessageToken::new(2_u32));
let addr = kameo::spawn(actor);
@ -430,6 +431,7 @@ async fn should_accept_message_with_new_token() -> TestResult {
//then
log.require_message_containing("accepted token: 3").await?;
net.assert_no_unused_plans();
Ok(())
}
@ -446,7 +448,7 @@ async fn should_reject_message_with_expired_token() -> TestResult {
git::repository::factory::mock(),
given::a_forge(),
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_message_token(MessageToken::new(4_u32));
let addr = kameo::spawn(actor);
@ -454,6 +456,7 @@ async fn should_reject_message_with_expired_token() -> TestResult {
//then
log.no_message_contains("accepted token").await?;
net.assert_no_unused_plans();
Ok(())
}

View file

@ -22,7 +22,7 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
Box::new(repository_factory),
given::a_forge(),
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_webhook_auth(None);
@ -36,6 +36,7 @@ async fn when_no_expected_auth_token_drop_notification() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing("server has no auth token")
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -56,7 +57,7 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
Box::new(repository_factory),
given::a_forge(),
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
@ -70,6 +71,7 @@ async fn when_no_repo_config_drop_notification() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing("server has no repo config")
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -94,7 +96,7 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
@ -108,6 +110,7 @@ async fn when_message_auth_is_invalid_drop_notification() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing("message authorisation is invalid")
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -136,7 +139,7 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
@ -150,6 +153,7 @@ async fn when_message_is_ignorable_drop_notification() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing("forge sent ignorable message")
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -178,7 +182,7 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor.with_webhook_auth(Some(given::a_webhook_auth()));
@ -192,6 +196,7 @@ async fn when_message_is_not_a_push_drop_notification() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing("message parse error - not a push")
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -224,7 +229,7 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -239,6 +244,7 @@ async fn when_message_is_push_on_unknown_branch_drop_notification() -> TestResul
//then
log.no_message_contains("send").await?;
log.require_message_containing("unknown branch").await?;
net.assert_no_unused_plans();
Ok(())
}
@ -272,7 +278,7 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -288,6 +294,7 @@ async fn when_message_is_push_already_seen_commit_to_main() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {main}"))
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -321,7 +328,7 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -337,6 +344,7 @@ async fn when_message_is_push_already_seen_commit_to_next() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {next_branch}"))
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -370,7 +378,7 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -386,6 +394,7 @@ async fn when_message_is_push_already_seen_commit_to_dev() -> TestResult {
log.no_message_contains("send").await?;
log.require_message_containing(format!("not a new commit on {dev}"))
.await?;
net.assert_no_unused_plans();
Ok(())
}
@ -418,7 +427,7 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -435,6 +444,7 @@ async fn when_message_is_push_new_commit_to_main_should_stash_and_validate_repo(
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.last_main_commit, Some(push_commit));
log.require_message_containing("send: ValidateRepo").await?;
net.assert_no_unused_plans();
Ok(())
}
@ -467,7 +477,7 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -484,6 +494,7 @@ async fn when_message_is_push_new_commit_to_next_should_stash_and_validate_repo(
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.last_next_commit, Some(push_commit));
log.require_message_containing("send: ValidateRepo").await?;
net.assert_no_unused_plans();
Ok(())
}
@ -516,7 +527,7 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
Box::new(repository_factory),
forge,
given::a_server_actor(fs.as_real(), net.clone()),
net,
net.clone(),
);
let actor = actor
.with_webhook_auth(Some(given::a_webhook_auth()))
@ -533,5 +544,6 @@ async fn when_message_is_push_new_commit_to_dev_should_stash_and_validate_repo()
.map_err(|e| format!("examine actor: {e:?}"))?;
assert_eq!(view.last_dev_commit, Some(push_commit));
log.require_message_containing("send: ValidateRepo").await?;
net.assert_no_unused_plans();
Ok(())
}

View file

@ -23,8 +23,8 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
//given
// parameters
let fs = given::a_filesystem();
let net = given::a_network();
let alerts = given::an_alerts_actor(net.clone().into());
let mock_net = given::a_network();
let alerts = given::an_alerts_actor(mock_net.clone().into());
let repo = git::repository::factory::mock();
let duration = std::time::Duration::from_millis(1);
@ -34,7 +34,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
let server = ServerActor::new(
false, // ui
fs.as_real(),
net.into(),
mock_net.clone().into(),
alerts,
file_update_subs,
repo,
@ -67,6 +67,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() -> TestResult {
assert!(message_log.read().iter().any(|log| !log
.iter()
.any(|line| line == "send: ReceiveValidServerConfig")));
mock_net.assert_no_unused_plans();
Ok(())
}

View file

@ -23,7 +23,7 @@ pub use model::*;
use crate::tell;
use super::Tick;
use super::{components::key_focus::KeyFocus, Tick};
#[derive(Debug)]
pub struct Tui {
@ -92,25 +92,42 @@ impl Tui {
if key.kind != KeyEventKind::Press {
return Ok(());
}
match key.code {
KeyCode::Char('q') => {
actor_tui.kill();
}
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
KeyCode::Char('f') | KeyCode::PageDown => {
self.scroll_view_state.scroll_page_down();
}
KeyCode::Char('b') | KeyCode::PageUp => {
self.scroll_view_state.scroll_page_up();
}
KeyCode::Char('g') | KeyCode::Home => {
self.scroll_view_state.scroll_to_top();
}
KeyCode::Char('G') | KeyCode::End => {
self.scroll_view_state.scroll_to_bottom();
}
_ => (),
match self.state.key_focus {
KeyFocus::None => match key.code {
KeyCode::Char('q') => {
actor_tui.kill();
}
KeyCode::Char('j') | KeyCode::Down => self.scroll_view_state.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => self.scroll_view_state.scroll_up(),
KeyCode::Char('f') | KeyCode::PageDown => {
self.scroll_view_state.scroll_page_down();
}
KeyCode::Char('b') | KeyCode::PageUp => {
self.scroll_view_state.scroll_page_up();
}
KeyCode::Char('g') | KeyCode::Home => {
self.scroll_view_state.scroll_to_top();
}
KeyCode::Char('G') | KeyCode::End => {
self.scroll_view_state.scroll_to_bottom();
}
KeyCode::Char('/') => {
self.state.key_focus = KeyFocus::Filter;
}
_ => (),
},
KeyFocus::Filter => match key.code {
KeyCode::Char(char) => self.state.filter.push(char),
KeyCode::Backspace => {
self.state.filter.pop();
}
KeyCode::Tab | KeyCode::Enter => self.state.key_focus = KeyFocus::None,
KeyCode::Esc => {
self.state.filter = UIRepoFilter::default();
self.state.key_focus = KeyFocus::None;
}
_ => (),
},
}
}
Ok(())

View file

@ -1,6 +1,11 @@
//
use std::{collections::BTreeMap, fmt::Display, time::Instant};
use std::{
collections::BTreeMap,
fmt::{Debug, Display},
time::Instant,
};
use derive_more::derive::{DerefMut, Display};
use ratatui::{
layout::Alignment,
prelude::{Buffer, Rect},
@ -14,16 +19,21 @@ use tui_scrollview::ScrollViewState;
use git_next_core::{
git::{self, graph::Log, Commit},
ForgeAlias, RepoAlias, RepoBranches,
newtype, s, ForgeAlias, RepoAlias, RepoBranches,
};
use crate::{server::actor::messages::ValidAppConfig, tui::components::ConfiguredAppWidget};
use crate::{
server::actor::messages::ValidAppConfig,
tui::components::{key_focus::KeyFocus, ConfiguredAppWidget},
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct State {
last_update: Instant,
started: Instant,
pub mode: ServerState,
pub filter: UIRepoFilter,
pub key_focus: KeyFocus,
}
impl State {
pub fn initial() -> Self {
@ -31,6 +41,8 @@ impl State {
last_update: Instant::now(),
started: Instant::now(),
mode: ServerState::Initial { tick: 0 },
filter: UIRepoFilter::default(),
key_focus: KeyFocus::default(),
}
}
@ -55,6 +67,15 @@ fn time() -> String {
chrono::Local::now().format("%H:%M").to_string()
}
newtype!(
UIRepoFilter,
String,
Default,
DerefMut,
Display,
"Filter to limit repos shown"
);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerState {
/// UI has started but has no information on the state of the server
@ -232,6 +253,14 @@ pub enum RepoState {
},
}
impl RepoState {
pub const fn repo_alias(&self) -> &RepoAlias {
match self {
Self::Identified { repo_alias, .. }
| Self::Configured { repo_alias, .. }
| Self::Ready { repo_alias, .. } => repo_alias,
}
}
#[tracing::instrument]
pub fn update_branches(&mut self, branches: RepoBranches) {
match self {
@ -359,13 +388,36 @@ impl StatefulWidget for &State {
Self: Sized,
{
let block = Block::bordered()
.title_top(
Line::from(
if self.filter.is_empty() && self.key_focus == KeyFocus::None {
s!("")
} else {
format!(" Filter: {} ", self.filter)
},
)
.alignment(Alignment::Left)
.style(if self.key_focus == KeyFocus::Filter {
Color::Red
} else {
Color::Green
}),
)
.title_top(
Line::from(format!(" Git-Next v{} ", clap::crate_version!()).bold())
.alignment(Alignment::Center),
)
.title_bottom(
Line::from(if self.key_focus == KeyFocus::Filter {
s!(" [esc] clear [tab/enter] finish ")
} else {
s!("")
})
.alignment(Alignment::Left),
)
.title_bottom(
Line::from(vec![
" [q]uit ".into(),
" [q]uit [/] filter ".into(),
self.beating_heart().into(),
" ".into(),
])
@ -380,7 +432,11 @@ impl StatefulWidget for &State {
.centered()
.render(interior, buf),
ServerState::Configured { forges } => {
ConfiguredAppWidget { forges }.render(interior, buf, state);
ConfiguredAppWidget {
forges,
filter: &self.filter,
}
.render(interior, buf, state);
}
}
}

View file

@ -8,12 +8,13 @@ use ratatui::{
};
use tui_scrollview::{ScrollView, ScrollViewState};
use crate::tui::actor::ForgeState;
use crate::tui::actor::{ForgeState, UIRepoFilter};
use super::{forge::ForgeWidget, HeightContraintLength};
pub struct ConfiguredAppWidget<'a> {
pub forges: &'a BTreeMap<ForgeAlias, ForgeState>,
pub filter: &'a UIRepoFilter,
}
impl HeightContraintLength for ConfiguredAppWidget<'_> {
fn height_constraint_length(&self) -> u16 {
@ -62,6 +63,7 @@ impl<'a> ConfiguredAppWidget<'a> {
forge_alias,
repos: &state.repos,
view_state: state.view_state,
filter: self.filter,
})
.collect::<Vec<_>>()
}

View file

@ -10,13 +10,14 @@ use ratatui::{
};
use crate::tui::{
actor::RepoState,
actor::{RepoState, UIRepoFilter},
components::{repo::RepoWidget, HeightContraintLength},
};
pub struct ExpandedForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
pub filter: &'a UIRepoFilter,
}
impl HeightContraintLength for ExpandedForgeWidget<'_> {
fn height_constraint_length(&self) -> u16 {
@ -55,6 +56,15 @@ impl<'a> ExpandedForgeWidget<'a> {
fn children(&self) -> Vec<RepoWidget<'a>> {
self.repos
.values()
.filter(|repo_state| {
if self.filter.is_empty() {
true
} else {
let repo_alias = repo_state.repo_alias();
// eprintln!("--> {} ? {}", self.filter.as_str(), repo_alias);
repo_alias.contains(self.filter.as_str())
}
})
.map(|repo_state| RepoWidget { repo_state })
.collect::<Vec<_>>()
}

View file

@ -9,7 +9,7 @@ use expanded::ExpandedForgeWidget;
use git_next_core::{ForgeAlias, RepoAlias};
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
use crate::tui::actor::{RepoState, ViewState};
use crate::tui::actor::{RepoState, UIRepoFilter, ViewState};
use super::HeightContraintLength;
@ -17,6 +17,7 @@ pub struct ForgeWidget<'a> {
pub forge_alias: &'a ForgeAlias,
pub repos: &'a BTreeMap<RepoAlias, RepoState>,
pub view_state: ViewState,
pub filter: &'a UIRepoFilter,
}
impl HeightContraintLength for ForgeWidget<'_> {
fn height_constraint_length(&self) -> u16 {
@ -28,6 +29,7 @@ impl HeightContraintLength for ForgeWidget<'_> {
ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias,
repos: self.repos,
filter: self.filter,
}
.height_constraint_length(),
}
@ -46,6 +48,7 @@ impl Widget for ForgeWidget<'_> {
ViewState::Expanded => ExpandedForgeWidget {
forge_alias: self.forge_alias,
repos: self.repos,
filter: self.filter,
}
.render(area, buf),
}

View file

@ -0,0 +1,8 @@
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum KeyFocus {
/// Keyboard is not focused on any input
#[default]
None,
/// Keyboard is focused on editing the filter
Filter,
}

View file

@ -2,6 +2,7 @@
mod configured_app;
mod forge;
mod history;
pub mod key_focus;
mod repo;
pub use configured_app::ConfiguredAppWidget;

View file

@ -1,7 +1,7 @@
//
use std::path::PathBuf;
use color_eyre::eyre::Result;
use color_eyre::eyre::{Context, Result};
use directories::ProjectDirs;
use lazy_static::lazy_static;
use tracing_error::ErrorLayer;
@ -30,7 +30,7 @@ fn project_directory() -> Option<ProjectDirs> {
}
pub fn initialize_logging() -> Result<()> {
std::fs::create_dir_all(DATA_FOLDER.clone())?;
std::fs::create_dir_all(DATA_FOLDER.clone()).context(DATA_FOLDER.display())?;
let log_path = DATA_FOLDER.join(LOG_FILE.clone());
let log_file = std::fs::File::create(log_path)?;
std::env::set_var(

View file

@ -21,8 +21,6 @@ forgejo = []
github = []
[dependencies]
native-tls = { workspace = true }
# logging
tracing = { workspace = true }
@ -59,9 +57,6 @@ mockall = { workspace = true }
#iters
take-until = { workspace = true }
[package.metadata.cargo-machete]
ignored = ["native-tls"]
[dev-dependencies]
# Testing
assert2 = { workspace = true }

View file

@ -84,7 +84,7 @@ impl super::OpenRepositoryLike for RealOpenRepository {
),
..Default::default()
})
.with_shell_allow_argument_splitting()
.with_shell_allow_manual_argument_splitting()
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?
@ -128,7 +128,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()?

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

@ -12,10 +12,21 @@ build:
# 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"