Compare commits
16 commits
Author | SHA1 | Date | |
---|---|---|---|
c8cc45ca7f | |||
52de3ef86e | |||
f71e28512d | |||
a605c3499a | |||
030129a746 | |||
92c24eafd1 | |||
1694347f00 | |||
|
5ea4fb8d6f | ||
|
cb76f2c7bd | ||
3833ba86db | |||
90779527cf | |||
|
7b4e679c65 | ||
|
a8298d943a | ||
|
f8625a59f8 | ||
|
6d66ccf391 | ||
|
de6875347d |
44 changed files with 1000 additions and 1013 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -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
1229
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
21
Cargo.toml
21
Cargo.toml
|
@ -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"] }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
|
@ -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,
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate as forgejo;
|
||||
use crate::forges::forgejo;
|
||||
|
||||
use git_next_core::{git, webhook};
|
||||
|
|
@ -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(
|
|
@ -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;
|
||||
|
|
@ -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")]
|
|
@ -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}"));
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
use crate as github;
|
||||
use crate::forges::github;
|
||||
|
||||
use git_next_core::{git, webhook};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
6
crates/cli/src/forges/mod.rs
Normal file
6
crates/cli/src/forges/mod.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
//
|
||||
#[cfg(feature = "forgejo")]
|
||||
pub mod forgejo;
|
||||
|
||||
#[cfg(feature = "github")]
|
||||
pub mod github;
|
|
@ -11,6 +11,8 @@ mod repo;
|
|||
mod root;
|
||||
mod server;
|
||||
|
||||
mod forges;
|
||||
|
||||
#[cfg(feature = "tui")]
|
||||
mod tui;
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<_>>()
|
||||
}
|
||||
|
|
|
@ -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<_>>()
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
|
8
crates/cli/src/tui/components/key_focus.rs
Normal file
8
crates/cli/src/tui/components/key_focus.rs
Normal 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,
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
mod configured_app;
|
||||
mod forge;
|
||||
mod history;
|
||||
pub mod key_focus;
|
||||
mod repo;
|
||||
|
||||
pub use configured_app::ConfiguredAppWidget;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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()?
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
[package]
|
||||
name = "git-next-forge-forgejo"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
description = "Forgejo support for git-next, the trunk-based development manager"
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
|
||||
[dependencies]
|
||||
git-next-core = { workspace = true }
|
||||
|
||||
# logging
|
||||
tracing = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = { workspace = true }
|
||||
|
||||
# # Actors
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
assert2 = { workspace = true }
|
||||
rand = { workspace = true }
|
|
@ -1,9 +0,0 @@
|
|||
# git-next
|
||||
|
||||
## Trunk-based developement manager.
|
||||
|
||||
`git-next` is a combined server and command-line tool that enables trunk-based
|
||||
development workflows where each commit must pass CI before being included in
|
||||
the main branch.
|
||||
|
||||
See [git-next](https://crates.io/crates/git-next) for more information.
|
|
@ -1,54 +0,0 @@
|
|||
[package]
|
||||
name = "git-next-forge-github"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
description = "GitHub support for git-next, the trunk-based development manager"
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] }
|
||||
|
||||
[dependencies]
|
||||
git-next-core = { workspace = true }
|
||||
|
||||
# own version for UserAgent requests to github.com
|
||||
clap = { workspace = true }
|
||||
|
||||
# logging
|
||||
tracing = { workspace = true }
|
||||
|
||||
# sha256 encoding (e.g. verify github webhooks)
|
||||
hmac = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
derive_more = { workspace = true }
|
||||
|
||||
# # Actors
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
assert2 = { workspace = true }
|
||||
rand = { workspace = true }
|
|
@ -1,9 +0,0 @@
|
|||
# git-next
|
||||
|
||||
## Trunk-based developement manager.
|
||||
|
||||
`git-next` is a combined server and command-line tool that enables trunk-based
|
||||
development workflows where each commit must pass CI before being included in
|
||||
the main branch.
|
||||
|
||||
See [git-next](https://crates.io/crates/git-next) for more information.
|
15
justfile
15
justfile
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue