forked from kemitix/git-next
refactor: extract repo-actor and gitforge crates
This commit is contained in:
parent
4c2bc19139
commit
db9b4220ee
42 changed files with 633 additions and 471 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -1,6 +1,13 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/cli", "crates/server", "crates/config", "crates/git"]
|
||||
members = [
|
||||
"crates/cli",
|
||||
"crates/server",
|
||||
"crates/config",
|
||||
"crates/git",
|
||||
"crates/gitforge",
|
||||
"crates/repo-actor",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.1"
|
||||
|
@ -16,6 +23,8 @@ expect_used = "warn"
|
|||
git-next-server = { path = "crates/server" }
|
||||
git-next-config = { path = "crates/config" }
|
||||
git-next-git = { path = "crates/git" }
|
||||
git-next-gitforge = { path = "crates/gitforge" }
|
||||
git-next-repo-actor = { path = "crates/repo-actor" }
|
||||
|
||||
# CLI parsing
|
||||
clap = { version = "4.5", features = ["cargo", "derive"] }
|
||||
|
|
13
README.md
13
README.md
|
@ -190,9 +190,22 @@ The following diagram shows the dependency between the crates that make up `git-
|
|||
stateDiagram-v2
|
||||
cli --> server
|
||||
cli --> git
|
||||
|
||||
server --> config
|
||||
server --> git
|
||||
server --> gitforge
|
||||
server --> repo_actor
|
||||
|
||||
git --> config
|
||||
|
||||
gitforge --> config
|
||||
gitforge --> git
|
||||
|
||||
repo_actor --> config
|
||||
repo_actor --> git
|
||||
repo_actor --> gitforge
|
||||
|
||||
|
||||
```
|
||||
|
||||
## License
|
||||
|
|
|
@ -9,9 +9,9 @@ forgejo = []
|
|||
github = []
|
||||
|
||||
[dependencies]
|
||||
# # logging
|
||||
# logging
|
||||
# console-subscriber = { workspace = true }
|
||||
# tracing = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
# tracing-subscriber = { workspace = true }
|
||||
|
||||
# # base64 decoding
|
||||
|
@ -21,9 +21,9 @@ github = []
|
|||
# # gix = { workspace = true }
|
||||
# gix = { workspace = true }
|
||||
# async-trait = { workspace = true }
|
||||
#
|
||||
# # fs/network
|
||||
# kxio = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
|
@ -47,12 +47,12 @@ derive-with = { workspace = true }
|
|||
#
|
||||
# # file watcher
|
||||
# inotify = { workspace = true }
|
||||
#
|
||||
# # Actors
|
||||
# actix = { workspace = true }
|
||||
|
||||
# Actors
|
||||
actix = { workspace = true }
|
||||
# actix-rt = { workspace = true }
|
||||
# tokio = { workspace = true }
|
||||
#
|
||||
|
||||
[dev-dependencies]
|
||||
# # Testing
|
||||
assert2 = { workspace = true }
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
use git_next_config::{
|
||||
ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, GitDir, Hostname, RepoAlias,
|
||||
RepoBranches, RepoConfig, RepoConfigSource, RepoPath, User,
|
||||
use crate::{
|
||||
ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, Hostname, RepoAlias, RepoBranches,
|
||||
RepoConfig, RepoConfigSource, RepoPath, User,
|
||||
};
|
||||
|
||||
use git_next_git::{Generation, RepoDetails};
|
||||
|
||||
pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
|
||||
ForgeDetails::new(
|
||||
forge_name(n),
|
||||
|
@ -30,23 +28,6 @@ pub fn hostname(n: u32) -> Hostname {
|
|||
pub fn forge_name(n: u32) -> ForgeName {
|
||||
ForgeName::new(format!("forge-name-{}", n))
|
||||
}
|
||||
pub fn repo_details(
|
||||
n: u32,
|
||||
generation: Generation,
|
||||
forge: ForgeDetails,
|
||||
repo_config: Option<RepoConfig>,
|
||||
gitdir: GitDir,
|
||||
) -> RepoDetails {
|
||||
RepoDetails {
|
||||
generation,
|
||||
repo_alias: repo_alias(n),
|
||||
repo_path: repo_path(n),
|
||||
gitdir,
|
||||
branch: branch_name(n),
|
||||
forge,
|
||||
repo_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn branch_name(n: u32) -> BranchName {
|
||||
BranchName::new(format!("branch-name-{}", n))
|
|
@ -1,6 +1,7 @@
|
|||
//
|
||||
mod api_token;
|
||||
mod branch_name;
|
||||
pub mod common;
|
||||
mod forge_config;
|
||||
mod forge_details;
|
||||
mod forge_name;
|
||||
|
@ -12,6 +13,7 @@ mod repo_branches;
|
|||
mod repo_config;
|
||||
mod repo_config_source;
|
||||
mod repo_path;
|
||||
pub mod server;
|
||||
mod server_repo_config;
|
||||
mod user;
|
||||
|
||||
|
|
105
crates/config/src/server.rs
Normal file
105
crates/config/src/server.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
//
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use kxio::fs::FileSystem;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{ForgeConfig, ForgeName};
|
||||
|
||||
#[derive(Debug, derive_more::From, derive_more::Display)]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
KxIoFs(kxio::fs::Error),
|
||||
TomlDe(toml::de::Error),
|
||||
AddressParse(std::net::AddrParseError),
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// Mapped from the `git-next-server.toml` file
|
||||
#[derive(Debug, PartialEq, Eq, serde::Deserialize, Message, derive_more::Constructor)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ServerConfig {
|
||||
http: Http,
|
||||
webhook: Webhook,
|
||||
storage: ServerStorage,
|
||||
pub forge: HashMap<String, ForgeConfig>,
|
||||
}
|
||||
impl ServerConfig {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub fn load(fs: &FileSystem) -> Result<Self> {
|
||||
let file = fs.base().join("git-next-server.toml");
|
||||
info!(?file, "");
|
||||
let str = fs.file_read_to_string(&file)?;
|
||||
toml::from_str(&str).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn forges(&self) -> impl Iterator<Item = (ForgeName, &ForgeConfig)> {
|
||||
self.forge
|
||||
.iter()
|
||||
.map(|(name, forge)| (ForgeName::new(name.clone()), forge))
|
||||
}
|
||||
|
||||
pub const fn storage(&self) -> &ServerStorage {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
pub const fn webhook(&self) -> &Webhook {
|
||||
&self.webhook
|
||||
}
|
||||
|
||||
pub fn http(&self) -> Result<SocketAddr> {
|
||||
self.http.socket_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the port the server will listen to for incoming webhooks messages
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::Constructor)]
|
||||
pub struct Http {
|
||||
addr: String,
|
||||
port: u16,
|
||||
}
|
||||
impl Http {
|
||||
fn socket_addr(&self) -> Result<SocketAddr> {
|
||||
Ok(SocketAddr::from_str(&format!(
|
||||
"{}:{}",
|
||||
self.addr, self.port
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the Webhook Forges should send updates to
|
||||
/// Must be an address that is accessible from the remote forge
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::Constructor)]
|
||||
pub struct Webhook {
|
||||
url: String,
|
||||
}
|
||||
impl Webhook {
|
||||
pub fn url(&self) -> WebhookUrl {
|
||||
WebhookUrl(self.url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// The URL for the webhook where forges should send their updates
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::AsRef)]
|
||||
pub struct WebhookUrl(String);
|
||||
|
||||
/// The directory to store server data, such as cloned repos
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::Constructor)]
|
||||
pub struct ServerStorage {
|
||||
path: PathBuf,
|
||||
}
|
||||
impl ServerStorage {
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
||||
//
|
||||
|
||||
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
|
||||
type TestResult = Result<()>;
|
||||
|
||||
mod server_repo_config {
|
||||
use std::path::PathBuf;
|
||||
|
@ -452,3 +455,123 @@ mod repo_branches {
|
|||
assert_eq!(repo_branches.dev(), BranchName::new("dev"));
|
||||
}
|
||||
}
|
||||
mod server {
|
||||
mod load {
|
||||
//
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use assert2::let_assert;
|
||||
|
||||
use crate::{
|
||||
server::{Http, ServerConfig, ServerStorage, Webhook},
|
||||
tests::TestResult,
|
||||
ForgeConfig, ForgeType, RepoBranches, RepoConfig, RepoConfigSource, ServerRepoConfig,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn load_should_parse_server_config() -> TestResult {
|
||||
let fs = kxio::fs::temp()?;
|
||||
fs.file_write(
|
||||
&fs.base().join("git-next-server.toml"),
|
||||
r#"
|
||||
[http]
|
||||
addr = "0.0.0.0"
|
||||
port = 8080
|
||||
|
||||
[webhook]
|
||||
url = "http://localhost:9909/webhook"
|
||||
|
||||
[storage]
|
||||
path = "/opt/git-next/data"
|
||||
|
||||
[forge.default]
|
||||
forge_type = "MockForge"
|
||||
hostname = "git.example.net"
|
||||
user = "Bob"
|
||||
token = "API-Token"
|
||||
|
||||
[forge.default.repos]
|
||||
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/user/hello.git" }
|
||||
world = { repo = "user/world", branch = "master", main = "main", next = "next", dev = "dev" }
|
||||
|
||||
[forge.default.repos.sam]
|
||||
repo = "user/sam"
|
||||
branch = "main"
|
||||
main = "master"
|
||||
next = "upcoming"
|
||||
dev = "sam-dev"
|
||||
"#,
|
||||
)
|
||||
?;
|
||||
let_assert!(Ok(config) = ServerConfig::load(&fs));
|
||||
let expected = ServerConfig::new(
|
||||
Http::new("0.0.0.0".to_string(), 8080),
|
||||
Webhook::new("http://localhost:9909/webhook".to_string()),
|
||||
ServerStorage::new("/opt/git-next/data".into()),
|
||||
HashMap::from([(
|
||||
"default".to_string(),
|
||||
ForgeConfig::new(
|
||||
ForgeType::MockForge,
|
||||
"git.example.net".to_string(),
|
||||
"Bob".to_string(),
|
||||
"API-Token".to_string(),
|
||||
BTreeMap::from([
|
||||
(
|
||||
"hello".to_string(),
|
||||
ServerRepoConfig::new(
|
||||
"user/hello".to_string(),
|
||||
"main".to_string(),
|
||||
Some("/opt/git/user/hello.git".into()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"world".to_string(),
|
||||
ServerRepoConfig::new(
|
||||
"user/world".to_string(),
|
||||
"master".to_string(),
|
||||
None,
|
||||
Some("main".to_string()),
|
||||
Some("next".to_string()),
|
||||
Some("dev".to_string()),
|
||||
),
|
||||
),
|
||||
(
|
||||
"sam".to_string(),
|
||||
ServerRepoConfig::new(
|
||||
"user/sam".to_string(),
|
||||
"main".to_string(),
|
||||
None,
|
||||
Some("master".to_string()),
|
||||
Some("upcoming".to_string()),
|
||||
Some("sam-dev".to_string()),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
)]),
|
||||
);
|
||||
assert_eq!(config, expected, "ServerConfig");
|
||||
|
||||
if let Some(forge) = config.forge.get("world") {
|
||||
if let Some(repo) = forge.get_repo("sam") {
|
||||
let repo_config = repo.repo_config();
|
||||
let expected = Some(RepoConfig::new(
|
||||
RepoBranches::new(
|
||||
"master".to_string(),
|
||||
"upcoming".to_string(),
|
||||
"sam-dev".to_string(),
|
||||
),
|
||||
RepoConfigSource::Server,
|
||||
));
|
||||
assert_eq!(repo_config, expected, "RepoConfig");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
26
crates/git/src/common.rs
Normal file
26
crates/git/src/common.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
|
||||
use git_next_config::{
|
||||
common::{branch_name, repo_alias, repo_path},
|
||||
ForgeDetails, GitDir, RepoConfig,
|
||||
};
|
||||
|
||||
use crate::{Generation, RepoDetails};
|
||||
|
||||
pub fn repo_details(
|
||||
n: u32,
|
||||
generation: Generation,
|
||||
forge: ForgeDetails,
|
||||
repo_config: Option<RepoConfig>,
|
||||
gitdir: GitDir,
|
||||
) -> RepoDetails {
|
||||
RepoDetails {
|
||||
generation,
|
||||
repo_alias: repo_alias(n),
|
||||
repo_path: repo_path(n),
|
||||
gitdir,
|
||||
branch: branch_name(n),
|
||||
forge,
|
||||
repo_config,
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//
|
||||
pub mod commit;
|
||||
pub mod common;
|
||||
pub mod fetch;
|
||||
mod generation;
|
||||
mod git_ref;
|
||||
|
|
64
crates/gitforge/Cargo.toml
Normal file
64
crates/gitforge/Cargo.toml
Normal file
|
@ -0,0 +1,64 @@
|
|||
[package]
|
||||
name = "git-next-gitforge"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["forgejo"]
|
||||
forgejo = []
|
||||
github = []
|
||||
|
||||
[dependencies]
|
||||
git-next-config = { workspace = true }
|
||||
git-next-git = { workspace = true }
|
||||
|
||||
# logging
|
||||
console-subscriber = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
# base64 decoding
|
||||
base64 = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = { workspace = true }
|
||||
|
||||
# Conventional Commit check
|
||||
git-conventional = { workspace = true }
|
||||
|
||||
# Webhooks
|
||||
bytes = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
warp = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
derive_more = { workspace = true }
|
||||
|
||||
# file watcher
|
||||
inotify = { workspace = true }
|
||||
|
||||
# # Actors
|
||||
# actix = { workspace = true }
|
||||
# actix-rt = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
assert2 = { workspace = true }
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
# pedantic = "warn"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
|
@ -3,7 +3,7 @@ use git_next_git::RepoDetails;
|
|||
use kxio::network::{self, Network};
|
||||
use tracing::error;
|
||||
|
||||
use crate::gitforge::ForgeBranchError;
|
||||
use crate::ForgeBranchError;
|
||||
|
||||
pub async fn get_all(
|
||||
repo_details: &RepoDetails,
|
|
@ -4,4 +4,3 @@ mod validate_positions;
|
|||
pub use get_all::get_all;
|
||||
|
||||
pub use validate_positions::validate_positions;
|
||||
pub use validate_positions::ValidatedPositions;
|
|
@ -3,34 +3,13 @@ use git_next_git::{self as git, RepoDetails};
|
|||
use kxio::network;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::gitforge::{self, ForgeLike};
|
||||
|
||||
#[derive(Debug, derive_more::Display)]
|
||||
pub enum Error {
|
||||
Network(Box<kxio::network::NetworkError>),
|
||||
#[display("Failed to Reset Branch {branch} to {commit}")]
|
||||
FailedToResetBranch {
|
||||
branch: BranchName,
|
||||
commit: git::Commit,
|
||||
},
|
||||
BranchReset(BranchName),
|
||||
BranchHasNoCommits(BranchName),
|
||||
DevBranchNotBasedOn(BranchName),
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub struct ValidatedPositions {
|
||||
pub main: git::Commit,
|
||||
pub next: git::Commit,
|
||||
pub dev: git::Commit,
|
||||
pub dev_commit_history: Vec<git::Commit>,
|
||||
}
|
||||
use crate::{forgejo::ForgeJoEnv, validation, CommitHistories, ForgeLike as _};
|
||||
|
||||
pub async fn validate_positions(
|
||||
forge: &gitforge::forgejo::ForgeJoEnv,
|
||||
forge: &ForgeJoEnv,
|
||||
repository: &git::OpenRepository,
|
||||
repo_config: RepoConfig,
|
||||
) -> Result<ValidatedPositions, Error> {
|
||||
) -> validation::Result {
|
||||
let repo_details = &forge.repo_details;
|
||||
// Collect Commit Histories for `main`, `next` and `dev` branches
|
||||
let commit_histories = get_commit_histories(repo_details, &repo_config, &forge.net).await;
|
||||
|
@ -38,7 +17,7 @@ pub async fn validate_positions(
|
|||
Ok(commit_histories) => commit_histories,
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to get commit histories");
|
||||
return Err(Error::Network(Box::new(err)));
|
||||
return Err(validation::Error::Network(Box::new(err)));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -48,7 +27,9 @@ pub async fn validate_positions(
|
|||
"No commits on main branch '{}'",
|
||||
repo_config.branches().main()
|
||||
);
|
||||
return Err(Error::BranchHasNoCommits(repo_config.branches().main()));
|
||||
return Err(validation::Error::BranchHasNoCommits(
|
||||
repo_config.branches().main(),
|
||||
));
|
||||
};
|
||||
// Dev must be on main branch, or user must rebase it
|
||||
let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main);
|
||||
|
@ -59,7 +40,9 @@ pub async fn validate_positions(
|
|||
repo_config.branches().main(),
|
||||
repo_config.branches().main(),
|
||||
);
|
||||
return Err(Error::DevBranchNotBasedOn(repo_config.branches().main()));
|
||||
return Err(validation::Error::DevBranchNotBasedOn(
|
||||
repo_config.branches().main(),
|
||||
));
|
||||
}
|
||||
// verify that next is an ancestor of dev, and force update to it main if it isn't
|
||||
let Some(next) = commit_histories.next.first().cloned() else {
|
||||
|
@ -67,7 +50,9 @@ pub async fn validate_positions(
|
|||
"No commits on next branch '{}",
|
||||
repo_config.branches().next()
|
||||
);
|
||||
return Err(Error::BranchHasNoCommits(repo_config.branches().next()));
|
||||
return Err(validation::Error::BranchHasNoCommits(
|
||||
repo_config.branches().next(),
|
||||
));
|
||||
};
|
||||
let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next);
|
||||
if !next_is_ancestor_of_dev {
|
||||
|
@ -80,12 +65,14 @@ pub async fn validate_positions(
|
|||
git::push::Force::From(next.clone().into()),
|
||||
) {
|
||||
warn!(?err, "Failed to reset next to main");
|
||||
return Err(Error::FailedToResetBranch {
|
||||
return Err(validation::Error::FailedToResetBranch {
|
||||
branch: repo_config.branches().next(),
|
||||
commit: next,
|
||||
});
|
||||
}
|
||||
return Err(Error::BranchReset(repo_config.branches().next()));
|
||||
return Err(validation::Error::BranchReset(
|
||||
repo_config.branches().next(),
|
||||
));
|
||||
}
|
||||
|
||||
let next_commits = commit_histories
|
||||
|
@ -106,19 +93,23 @@ pub async fn validate_positions(
|
|||
git::push::Force::From(next.clone().into()),
|
||||
) {
|
||||
warn!(?err, "Failed to reset next to main");
|
||||
return Err(Error::FailedToResetBranch {
|
||||
return Err(validation::Error::FailedToResetBranch {
|
||||
branch: repo_config.branches().next(),
|
||||
commit: next,
|
||||
});
|
||||
}
|
||||
return Err(Error::BranchReset(repo_config.branches().next()));
|
||||
return Err(validation::Error::BranchReset(
|
||||
repo_config.branches().next(),
|
||||
));
|
||||
}
|
||||
let Some(next) = next_commits.first().cloned() else {
|
||||
warn!(
|
||||
"No commits on next branch '{}'",
|
||||
repo_config.branches().next()
|
||||
);
|
||||
return Err(Error::BranchHasNoCommits(repo_config.branches().next()));
|
||||
return Err(validation::Error::BranchHasNoCommits(
|
||||
repo_config.branches().next(),
|
||||
));
|
||||
};
|
||||
let dev_has_next = commit_histories
|
||||
.dev
|
||||
|
@ -130,7 +121,9 @@ pub async fn validate_positions(
|
|||
repo_config.branches().dev(),
|
||||
repo_config.branches().next()
|
||||
);
|
||||
return Err(Error::DevBranchNotBasedOn(repo_config.branches().next())); // dev is not based on next
|
||||
return Err(validation::Error::DevBranchNotBasedOn(
|
||||
repo_config.branches().next(),
|
||||
)); // dev is not based on next
|
||||
}
|
||||
|
||||
let Some(dev) = commit_histories.dev.first().cloned() else {
|
||||
|
@ -138,10 +131,12 @@ pub async fn validate_positions(
|
|||
"No commits on dev branch '{}'",
|
||||
repo_config.branches().dev()
|
||||
);
|
||||
return Err(Error::BranchHasNoCommits(repo_config.branches().dev()));
|
||||
return Err(validation::Error::BranchHasNoCommits(
|
||||
repo_config.branches().dev(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(ValidatedPositions {
|
||||
Ok(validation::Positions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
|
@ -153,7 +148,7 @@ async fn get_commit_histories(
|
|||
repo_details: &RepoDetails,
|
||||
repo_config: &RepoConfig,
|
||||
net: &network::Network,
|
||||
) -> Result<gitforge::CommitHistories, network::NetworkError> {
|
||||
) -> Result<CommitHistories, network::NetworkError> {
|
||||
let main =
|
||||
(get_commit_history(repo_details, &repo_config.branches().main(), vec![], net).await)?;
|
||||
let main_head = main[0].clone();
|
||||
|
@ -178,7 +173,7 @@ async fn get_commit_histories(
|
|||
dev = dev.len(),
|
||||
"Commit histories"
|
||||
);
|
||||
let histories = gitforge::CommitHistories { main, next, dev };
|
||||
let histories = CommitHistories { main, next, dev };
|
||||
Ok(histories)
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ use git_next_git::RepoDetails;
|
|||
use kxio::network::{self, Network};
|
||||
use tracing::{error, warn};
|
||||
|
||||
use crate::gitforge::ForgeFileError;
|
||||
use crate::ForgeFileError;
|
||||
|
||||
pub(super) async fn contents_get(
|
||||
repo_details: &RepoDetails,
|
|
@ -1,20 +1,13 @@
|
|||
pub mod branch;
|
||||
mod file;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use git::OpenRepository;
|
||||
use git_next_config::{BranchName, GitDir, RepoConfig};
|
||||
use git_next_git::{self as git, GitRef, RepoDetails, Repository};
|
||||
use kxio::network::{self, Network};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
actors::repo::{RepoActor, StartMonitoring, ValidateRepo},
|
||||
gitforge,
|
||||
};
|
||||
use crate::{validation, CommitStatus, ForgeBranchError, ForgeFileError};
|
||||
|
||||
struct ForgeJo;
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -38,7 +31,7 @@ impl super::ForgeLike for ForgeJoEnv {
|
|||
"forgejo".to_string()
|
||||
}
|
||||
|
||||
async fn branches_get_all(&self) -> Result<Vec<BranchName>, gitforge::ForgeBranchError> {
|
||||
async fn branches_get_all(&self) -> Result<Vec<BranchName>, ForgeBranchError> {
|
||||
branch::get_all(&self.repo_details, &self.net).await
|
||||
}
|
||||
|
||||
|
@ -46,7 +39,7 @@ impl super::ForgeLike for ForgeJoEnv {
|
|||
&self,
|
||||
branch: &BranchName,
|
||||
file_path: &str,
|
||||
) -> Result<String, gitforge::ForgeFileError> {
|
||||
) -> Result<String, ForgeFileError> {
|
||||
file::contents_get(&self.repo_details, &self.net, branch, file_path).await
|
||||
}
|
||||
|
||||
|
@ -54,24 +47,8 @@ impl super::ForgeLike for ForgeJoEnv {
|
|||
&self,
|
||||
repository: git::OpenRepository,
|
||||
repo_config: RepoConfig,
|
||||
addr: Addr<RepoActor>,
|
||||
message_token: gitforge::MessageToken,
|
||||
) {
|
||||
match branch::validate_positions(self, &repository, repo_config).await {
|
||||
Ok(branch::ValidatedPositions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history,
|
||||
}) => {
|
||||
addr.do_send(StartMonitoring::new(main, next, dev, dev_commit_history));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("{:?}", err);
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
addr.do_send(ValidateRepo::new(message_token));
|
||||
}
|
||||
}
|
||||
) -> validation::Result {
|
||||
branch::validate_positions(self, &repository, repo_config).await
|
||||
}
|
||||
|
||||
fn branch_reset(
|
||||
|
@ -85,7 +62,7 @@ impl super::ForgeLike for ForgeJoEnv {
|
|||
repository.push(&self.repo_details, branch_name, to_commit, force)
|
||||
}
|
||||
|
||||
async fn commit_status(&self, commit: &git::Commit) -> gitforge::CommitStatus {
|
||||
async fn commit_status(&self, commit: &git::Commit) -> CommitStatus {
|
||||
let repo_details = &self.repo_details;
|
||||
let hostname = &repo_details.forge.hostname();
|
||||
let repo_path = &repo_details.repo_path;
|
||||
|
@ -110,21 +87,21 @@ impl super::ForgeLike for ForgeJoEnv {
|
|||
Ok(response) => {
|
||||
match response.response_body() {
|
||||
Some(status) => match status.state {
|
||||
CommitStatusState::Success => gitforge::CommitStatus::Pass,
|
||||
CommitStatusState::Pending => gitforge::CommitStatus::Pending,
|
||||
CommitStatusState::Failure => gitforge::CommitStatus::Fail,
|
||||
CommitStatusState::Error => gitforge::CommitStatus::Fail,
|
||||
CommitStatusState::Blank => gitforge::CommitStatus::Pending,
|
||||
CommitStatusState::Success => CommitStatus::Pass,
|
||||
CommitStatusState::Pending => CommitStatus::Pending,
|
||||
CommitStatusState::Failure => CommitStatus::Fail,
|
||||
CommitStatusState::Error => CommitStatus::Fail,
|
||||
CommitStatusState::Blank => CommitStatus::Pending,
|
||||
},
|
||||
None => {
|
||||
warn!("No status found for commit");
|
||||
gitforge::CommitStatus::Pending // assume issue is transient and allow retry
|
||||
CommitStatus::Pending // assume issue is transient and allow retry
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(?e, "Failed to get commit status");
|
||||
gitforge::CommitStatus::Pending // assume issue is transient and allow retry
|
||||
CommitStatus::Pending // assume issue is transient and allow retry
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,9 +39,7 @@ pub trait ForgeLike {
|
|||
&self,
|
||||
repository: OpenRepository,
|
||||
repo_config: RepoConfig,
|
||||
addr: actix::prelude::Addr<super::actors::repo::RepoActor>,
|
||||
message_token: MessageToken,
|
||||
);
|
||||
) -> validation::Result;
|
||||
|
||||
/// Moves a branch to a new commit.
|
||||
fn branch_reset(
|
||||
|
@ -98,5 +96,33 @@ impl std::ops::Deref for Forge {
|
|||
}
|
||||
}
|
||||
|
||||
pub mod validation {
|
||||
use git_next_config::BranchName;
|
||||
use git_next_git as git;
|
||||
|
||||
pub type Result = core::result::Result<Positions, Error>;
|
||||
|
||||
pub struct Positions {
|
||||
pub main: git::Commit,
|
||||
pub next: git::Commit,
|
||||
pub dev: git::Commit,
|
||||
pub dev_commit_history: Vec<git::Commit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, derive_more::Display)]
|
||||
pub enum Error {
|
||||
Network(Box<kxio::network::NetworkError>),
|
||||
#[display("Failed to Reset Branch {branch} to {commit}")]
|
||||
FailedToResetBranch {
|
||||
branch: BranchName,
|
||||
commit: git::Commit,
|
||||
},
|
||||
BranchReset(BranchName),
|
||||
BranchHasNoCommits(BranchName),
|
||||
DevBranchNotBasedOn(BranchName),
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
|
@ -2,7 +2,7 @@ use git::OpenRepository;
|
|||
use git_next_config::{BranchName, GitDir, RepoConfig};
|
||||
use git_next_git::{self as git, GitRef};
|
||||
|
||||
use crate::{actors::repo::RepoActor, gitforge};
|
||||
use crate::{validation, CommitStatus, ForgeBranchError, ForgeFileError};
|
||||
|
||||
struct MockForge;
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -18,7 +18,7 @@ impl super::ForgeLike for MockForgeEnv {
|
|||
"mock".to_string()
|
||||
}
|
||||
|
||||
async fn branches_get_all(&self) -> Result<Vec<BranchName>, gitforge::ForgeBranchError> {
|
||||
async fn branches_get_all(&self) -> Result<Vec<BranchName>, ForgeBranchError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ impl super::ForgeLike for MockForgeEnv {
|
|||
&self,
|
||||
_branch: &BranchName,
|
||||
_file_path: &str,
|
||||
) -> Result<String, gitforge::ForgeFileError> {
|
||||
) -> Result<String, ForgeFileError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
@ -34,9 +34,7 @@ impl super::ForgeLike for MockForgeEnv {
|
|||
&self,
|
||||
_repository: OpenRepository,
|
||||
_repo_config: RepoConfig,
|
||||
_addr: actix::prelude::Addr<RepoActor>,
|
||||
_message_token: gitforge::MessageToken,
|
||||
) {
|
||||
) -> validation::Result {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
@ -50,7 +48,7 @@ impl super::ForgeLike for MockForgeEnv {
|
|||
todo!()
|
||||
}
|
||||
|
||||
async fn commit_status(&self, _commit: &git::Commit) -> gitforge::CommitStatus {
|
||||
async fn commit_status(&self, _commit: &git::Commit) -> CommitStatus {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
//
|
||||
use assert2::let_assert;
|
||||
|
||||
use git_next_config::{ForgeType, RepoConfigSource};
|
||||
use git_next_config::{self as config, ForgeType, RepoConfigSource};
|
||||
use kxio::network::{MockNetwork, StatusCode};
|
||||
|
||||
use git_next_git::Generation;
|
||||
use git_next_git::{self as git, Generation};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -14,11 +15,11 @@ fn test_name() {
|
|||
};
|
||||
let net = Network::new_mock();
|
||||
let (repo, _reality) = git::repository::mock();
|
||||
let repo_details = common::repo_details(
|
||||
let repo_details = git::common::repo_details(
|
||||
1,
|
||||
Generation::new(),
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
Some(common::repo_config(1, RepoConfigSource::Repo)),
|
||||
config::common::forge_details(1, ForgeType::MockForge),
|
||||
Some(config::common::repo_config(1, RepoConfigSource::Repo)),
|
||||
GitDir::new(fs.base()),
|
||||
);
|
||||
let forge = Forge::new_forgejo(repo_details, net, repo);
|
||||
|
@ -31,9 +32,9 @@ async fn test_branches_get() {
|
|||
panic!("fs")
|
||||
};
|
||||
let mut net = MockNetwork::new();
|
||||
let hostname = common::hostname(1);
|
||||
let path = common::repo_path(1);
|
||||
let api_token = common::api_token(1);
|
||||
let hostname = config::common::hostname(1);
|
||||
let path = config::common::repo_path(1);
|
||||
let api_token = config::common::api_token(1);
|
||||
use secrecy::ExposeSecret;
|
||||
let token = api_token.expose_secret();
|
||||
let url = format!("https://{hostname}/api/v1/repos/{path}/branches?token={token}");
|
||||
|
@ -42,11 +43,11 @@ async fn test_branches_get() {
|
|||
let net = Network::from(net);
|
||||
let (repo, _reality) = git::repository::mock();
|
||||
|
||||
let repo_details = common::repo_details(
|
||||
let repo_details = git::common::repo_details(
|
||||
1,
|
||||
Generation::new(),
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
Some(common::repo_config(1, RepoConfigSource::Repo)),
|
||||
config::common::forge_details(1, ForgeType::MockForge),
|
||||
Some(config::common::repo_config(1, RepoConfigSource::Repo)),
|
||||
GitDir::new(fs.base()),
|
||||
);
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
use super::*;
|
||||
|
||||
pub mod common;
|
||||
|
||||
#[cfg(feature = "forgejo")]
|
||||
mod forgejo;
|
||||
|
65
crates/repo-actor/Cargo.toml
Normal file
65
crates/repo-actor/Cargo.toml
Normal file
|
@ -0,0 +1,65 @@
|
|||
[package]
|
||||
name = "git-next-repo-actor"
|
||||
version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["forgejo"]
|
||||
forgejo = []
|
||||
github = []
|
||||
|
||||
[dependencies]
|
||||
git-next-config = { workspace = true }
|
||||
git-next-git = { workspace = true }
|
||||
git-next-gitforge = { workspace = true }
|
||||
|
||||
# logging
|
||||
console-subscriber = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
# base64 decoding
|
||||
base64 = { workspace = true }
|
||||
|
||||
# git
|
||||
async-trait = { workspace = true }
|
||||
|
||||
# fs/network
|
||||
kxio = { workspace = true }
|
||||
|
||||
# TOML parsing
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
|
||||
# Secrets and Password
|
||||
secrecy = { workspace = true }
|
||||
|
||||
# Conventional Commit check
|
||||
git-conventional = { workspace = true }
|
||||
|
||||
# Webhooks
|
||||
bytes = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
warp = { workspace = true }
|
||||
|
||||
# boilerplate
|
||||
derive_more = { workspace = true }
|
||||
|
||||
# file watcher
|
||||
inotify = { workspace = true }
|
||||
|
||||
# Actors
|
||||
actix = { workspace = true }
|
||||
actix-rt = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing
|
||||
assert2 = { workspace = true }
|
||||
|
||||
[lints.clippy]
|
||||
nursery = { level = "warn", priority = -1 }
|
||||
# pedantic = "warn"
|
||||
unwrap_used = "warn"
|
||||
expect_used = "warn"
|
|
@ -2,14 +2,11 @@ use std::time::Duration;
|
|||
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_config::{RepoConfig, RepoConfigSource};
|
||||
use git_next_config::RepoConfig;
|
||||
use git_next_git as git;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{
|
||||
actors::repo::{LoadConfigFromRepo, RepoActor, ValidateRepo},
|
||||
gitforge,
|
||||
};
|
||||
use crate::{gitforge, ValidateRepo};
|
||||
|
||||
// advance next to the next commit towards the head of the dev branch
|
||||
#[tracing::instrument(fields(next), skip_all)]
|
||||
|
@ -80,23 +77,17 @@ pub fn find_next_commit_on_dev(
|
|||
#[tracing::instrument(fields(next), skip_all)]
|
||||
pub async fn advance_main(
|
||||
next: git::Commit,
|
||||
repo_config: RepoConfig,
|
||||
forge: gitforge::Forge,
|
||||
repository: git::OpenRepository,
|
||||
addr: Addr<RepoActor>,
|
||||
message_token: gitforge::MessageToken,
|
||||
repo_config: &RepoConfig,
|
||||
forge: &gitforge::Forge,
|
||||
repository: &git::OpenRepository,
|
||||
) {
|
||||
info!("Advancing main to next");
|
||||
if let Err(err) = forge.branch_reset(
|
||||
&repository,
|
||||
repository,
|
||||
repo_config.branches().main(),
|
||||
next.into(),
|
||||
git::push::Force::No,
|
||||
) {
|
||||
warn!(?err, "Failed")
|
||||
};
|
||||
match repo_config.source() {
|
||||
RepoConfigSource::Repo => addr.do_send(LoadConfigFromRepo),
|
||||
RepoConfigSource::Server => addr.do_send(ValidateRepo { message_token }),
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ use actix::prelude::*;
|
|||
use git_next_git::RepoDetails;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::{config, gitforge};
|
||||
use crate::{gitforge, load};
|
||||
|
||||
use super::{LoadedConfig, RepoActor};
|
||||
|
||||
|
@ -10,7 +10,7 @@ use super::{LoadedConfig, RepoActor};
|
|||
#[tracing::instrument(skip_all, fields(branch = %repo_details.branch))]
|
||||
pub async fn load(repo_details: RepoDetails, addr: Addr<RepoActor>, forge: gitforge::Forge) {
|
||||
info!("Loading .git-next.toml from repo");
|
||||
let repo_config = match config::load::load(&repo_details, &forge).await {
|
||||
let repo_config = match load::load(&repo_details, &forge).await {
|
||||
Ok(repo_config) => repo_config,
|
||||
Err(err) => {
|
||||
error!(?err, "Failed to load config");
|
|
@ -1,19 +1,25 @@
|
|||
mod branch;
|
||||
mod config;
|
||||
pub mod config;
|
||||
mod load;
|
||||
pub mod status;
|
||||
pub mod webhook;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::prelude::*;
|
||||
use git::OpenRepository;
|
||||
use git_next_config::{ForgeType, RepoConfig};
|
||||
use git_next_config::{server::Webhook, ForgeType, RepoConfig, RepoConfigSource};
|
||||
use git_next_git::{self as git, Generation, RepoDetails};
|
||||
use git_next_gitforge::{self as gitforge, validation};
|
||||
use kxio::network::Network;
|
||||
use tracing::{debug, info, warn, Instrument};
|
||||
|
||||
use crate::{actors::repo::webhook::WebhookAuth, config::Webhook, gitforge};
|
||||
// use crate::{actors::repo::webhook::WebhookAuth, config::Webhook, gitforge};
|
||||
|
||||
use crate::webhook::WebhookAuth;
|
||||
|
||||
use self::webhook::WebhookId;
|
||||
|
||||
|
@ -181,9 +187,24 @@ impl Handler<ValidateRepo> for RepoActor {
|
|||
let addr = ctx.address();
|
||||
let message_token = self.message_token;
|
||||
async move {
|
||||
forge
|
||||
.branches_validate_positions(repository, repo_config, addr, message_token)
|
||||
match forge
|
||||
.branches_validate_positions(repository, repo_config)
|
||||
.await
|
||||
{
|
||||
Ok(validation::Positions {
|
||||
main,
|
||||
next,
|
||||
dev,
|
||||
dev_commit_history,
|
||||
}) => {
|
||||
addr.do_send(StartMonitoring::new(main, next, dev, dev_commit_history));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("{:?}", err);
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
addr.do_send(ValidateRepo::new(message_token));
|
||||
}
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
|
@ -271,14 +292,14 @@ impl Handler<AdvanceMainTo> for RepoActor {
|
|||
};
|
||||
let forge = self.forge.clone();
|
||||
let addr = ctx.address();
|
||||
branch::advance_main(
|
||||
msg.0,
|
||||
repo_config,
|
||||
forge,
|
||||
repository,
|
||||
addr,
|
||||
self.message_token,
|
||||
)
|
||||
let message_token = self.message_token;
|
||||
async move {
|
||||
branch::advance_main(msg.0, &repo_config, &forge, &repository).await;
|
||||
match repo_config.source() {
|
||||
RepoConfigSource::Repo => addr.do_send(LoadConfigFromRepo),
|
||||
RepoConfigSource::Server => addr.do_send(ValidateRepo { message_token }),
|
||||
}
|
||||
}
|
||||
.in_current_span()
|
||||
.into_actor(self)
|
||||
.wait(ctx);
|
|
@ -1,4 +1,4 @@
|
|||
use git_next_config::{BranchName, RepoConfig};
|
||||
use git_next_config::{self as config, BranchName, RepoConfig};
|
||||
use git_next_git::RepoDetails;
|
||||
use tracing::error;
|
||||
|
||||
|
@ -16,7 +16,7 @@ pub async fn load(details: &RepoDetails, forge: &gitforge::Forge) -> Result<Repo
|
|||
#[derive(Debug, derive_more::From, derive_more::Display)]
|
||||
pub enum Error {
|
||||
File(ForgeFileError),
|
||||
Config(crate::config::Error),
|
||||
Config(config::server::Error),
|
||||
Toml(toml::de::Error),
|
||||
Forge(gitforge::ForgeBranchError),
|
||||
BranchNotFound(BranchName),
|
|
@ -1,29 +1,32 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_git as git;
|
||||
use git_next_gitforge::{CommitStatus, Forge, MessageToken};
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{actors::repo::ValidateRepo, gitforge};
|
||||
use crate::ValidateRepo;
|
||||
|
||||
use super::AdvanceMainTo;
|
||||
|
||||
pub async fn check_next(
|
||||
next: git::Commit,
|
||||
addr: Addr<super::RepoActor>,
|
||||
forge: gitforge::Forge,
|
||||
message_token: gitforge::MessageToken,
|
||||
forge: Forge,
|
||||
message_token: MessageToken,
|
||||
) {
|
||||
// get the status - pass, fail, pending (all others map to fail, e.g. error)
|
||||
let status = forge.commit_status(&next).await;
|
||||
info!(?status, "Checking next branch");
|
||||
match status {
|
||||
gitforge::CommitStatus::Pass => {
|
||||
CommitStatus::Pass => {
|
||||
addr.do_send(AdvanceMainTo(next));
|
||||
}
|
||||
gitforge::CommitStatus::Pending => {
|
||||
CommitStatus::Pending => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
|
||||
addr.do_send(ValidateRepo { message_token });
|
||||
}
|
||||
gitforge::CommitStatus::Fail => {
|
||||
CommitStatus::Fail => {
|
||||
warn!("Checks have failed");
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
use actix::prelude::*;
|
||||
use git_next_config::{BranchName, RepoBranches};
|
||||
use git_next_config::{
|
||||
server::{Webhook, WebhookUrl},
|
||||
BranchName, RepoAlias, RepoBranches,
|
||||
};
|
||||
use git_next_git::{self as git, RepoDetails};
|
||||
use kxio::network::{self, json};
|
||||
use tracing::{info, warn};
|
||||
|
@ -7,13 +10,7 @@ use ulid::DecodeError;
|
|||
|
||||
use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
repo::{RepoActor, ValidateRepo, WebhookRegistered},
|
||||
webhook::WebhookMessage,
|
||||
},
|
||||
config::{Webhook, WebhookUrl},
|
||||
};
|
||||
use crate::{RepoActor, ValidateRepo, WebhookRegistered};
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, derive_more::Constructor, derive_more::Deref, derive_more::Display,
|
||||
|
@ -23,7 +20,7 @@ pub struct WebhookId(String);
|
|||
#[derive(Clone, Debug, PartialEq, Eq, derive_more::Deref, derive_more::Display)]
|
||||
pub struct WebhookAuth(ulid::Ulid);
|
||||
impl WebhookAuth {
|
||||
pub fn from_str(authorisation: &str) -> Result<Self, DecodeError> {
|
||||
pub fn new(authorisation: &str) -> Result<Self, DecodeError> {
|
||||
let id = ulid::Ulid::from_str(authorisation)?;
|
||||
info!("Parse auth token: {}", id);
|
||||
Ok(Self(id))
|
||||
|
@ -307,3 +304,31 @@ pub enum Branch {
|
|||
struct HeadCommit {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Message, Debug, Clone, derive_more::Constructor)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct WebhookMessage {
|
||||
// forge // TODO: (#58) differentiate between multiple forges
|
||||
repo_alias: RepoAlias,
|
||||
authorisation: WebhookAuth,
|
||||
body: Body,
|
||||
}
|
||||
impl WebhookMessage {
|
||||
pub const fn repo_alias(&self) -> &RepoAlias {
|
||||
&self.repo_alias
|
||||
}
|
||||
pub const fn body(&self) -> &Body {
|
||||
&self.body
|
||||
}
|
||||
pub const fn authorisation(&self) -> &WebhookAuth {
|
||||
&self.authorisation
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, derive_more::Constructor)]
|
||||
pub struct Body(String);
|
||||
impl Body {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ github = []
|
|||
[dependencies]
|
||||
git-next-config = { workspace = true }
|
||||
git-next-git = { workspace = true }
|
||||
git-next-gitforge = { workspace = true }
|
||||
git-next-repo-actor = { workspace = true }
|
||||
|
||||
# logging
|
||||
console-subscriber = { workspace = true }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
pub mod file_watcher;
|
||||
pub mod repo;
|
||||
pub mod server;
|
||||
pub mod webhook;
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
//
|
||||
use std::path::PathBuf;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_config::{ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig};
|
||||
use config::server::{ServerConfig, ServerStorage, Webhook};
|
||||
use git_next_config::{
|
||||
self as config, ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig,
|
||||
};
|
||||
use git_next_git::{Generation, RepoDetails, Repository};
|
||||
use git_next_repo_actor::{CloneRepo, RepoActor};
|
||||
use kxio::{fs::FileSystem, network::Network};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
actors::{
|
||||
use crate::actors::{
|
||||
file_watcher::FileUpdated,
|
||||
repo::{CloneRepo, RepoActor},
|
||||
webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter},
|
||||
},
|
||||
config::{ServerConfig, ServerStorage, Webhook},
|
||||
};
|
||||
|
||||
#[derive(Debug, derive_more::Display, derive_more::From)]
|
||||
|
@ -26,7 +27,7 @@ pub enum Error {
|
|||
path: PathBuf,
|
||||
},
|
||||
|
||||
Config(crate::config::Error),
|
||||
Config(config::server::Error),
|
||||
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
//
|
||||
use actix::prelude::*;
|
||||
use git_next_config::RepoAlias;
|
||||
|
||||
use crate::actors::repo::webhook::WebhookAuth;
|
||||
|
||||
#[derive(Message, Debug, Clone, derive_more::Constructor)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct WebhookMessage {
|
||||
// forge // TODO: (#58) differentiate between multiple forges
|
||||
repo_alias: RepoAlias,
|
||||
authorisation: WebhookAuth,
|
||||
body: Body,
|
||||
}
|
||||
impl WebhookMessage {
|
||||
pub const fn repo_alias(&self) -> &RepoAlias {
|
||||
&self.repo_alias
|
||||
}
|
||||
pub const fn body(&self) -> &Body {
|
||||
&self.body
|
||||
}
|
||||
pub const fn authorisation(&self) -> &WebhookAuth {
|
||||
&self.authorisation
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, derive_more::Constructor)]
|
||||
pub struct Body(String);
|
||||
impl Body {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
// crate::server::actors::webhook
|
||||
|
||||
mod message;
|
||||
mod router;
|
||||
mod server;
|
||||
|
||||
|
@ -8,7 +7,7 @@ use std::net::SocketAddr;
|
|||
|
||||
use actix::prelude::*;
|
||||
|
||||
pub use message::WebhookMessage;
|
||||
use git_next_repo_actor::webhook::WebhookMessage;
|
||||
pub use router::AddWebhookRecipient;
|
||||
pub use router::WebhookRouter;
|
||||
use tracing::Instrument;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
//
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix::prelude::*;
|
||||
use git_next_config::RepoAlias;
|
||||
use git_next_repo_actor::webhook::WebhookMessage;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::actors::webhook::message::WebhookMessage;
|
||||
|
||||
pub struct WebhookRouter {
|
||||
span: tracing::Span,
|
||||
repos: HashMap<RepoAlias, Recipient<WebhookMessage>>,
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
//
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_config::RepoAlias;
|
||||
use git_next_repo_actor::webhook::{self, WebhookAuth, WebhookMessage};
|
||||
use tracing::{info, warn};
|
||||
use warp::reject::Rejection;
|
||||
|
||||
use crate::actors::{repo::webhook::WebhookAuth, webhook::message::WebhookMessage};
|
||||
|
||||
use super::message;
|
||||
|
||||
pub async fn start(
|
||||
socket_addr: SocketAddr,
|
||||
address: actix::prelude::Recipient<message::WebhookMessage>,
|
||||
) {
|
||||
pub async fn start(socket_addr: SocketAddr, address: actix::prelude::Recipient<WebhookMessage>) {
|
||||
// start webhook server
|
||||
use warp::Filter;
|
||||
// Define the Warp route to handle incoming HTTP requests
|
||||
|
@ -32,7 +27,7 @@ pub async fn start(
|
|||
info!("POST received");
|
||||
let repo_alias = RepoAlias::new(path);
|
||||
let bytes = body.to_vec();
|
||||
let body = message::Body::new(String::from_utf8_lossy(&bytes).to_string());
|
||||
let body = webhook::Body::new(String::from_utf8_lossy(&bytes).to_string());
|
||||
headers.get("Authorization").map_or_else(
|
||||
|| {
|
||||
warn!("No Authorization header");
|
||||
|
@ -70,7 +65,7 @@ pub async fn start(
|
|||
}
|
||||
|
||||
fn parse_auth(authorization_header: &warp::http::HeaderValue) -> Result<WebhookAuth, Rejection> {
|
||||
WebhookAuth::from_str(
|
||||
WebhookAuth::new(
|
||||
authorization_header
|
||||
.to_str()
|
||||
.map_err(|e| {
|
||||
|
|
|
@ -1,107 +1,2 @@
|
|||
use actix::prelude::*;
|
||||
|
||||
pub mod load;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use git_next_config::{ForgeConfig, ForgeName};
|
||||
use kxio::fs::FileSystem;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Debug, derive_more::From, derive_more::Display)]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
KxIoFs(kxio::fs::Error),
|
||||
TomlDe(toml::de::Error),
|
||||
AddressParse(std::net::AddrParseError),
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// Mapped from the `git-next-server.toml` file
|
||||
#[derive(Debug, PartialEq, Eq, serde::Deserialize, Message)]
|
||||
#[rtype(result = "()")]
|
||||
pub struct ServerConfig {
|
||||
http: Http,
|
||||
webhook: Webhook,
|
||||
storage: ServerStorage,
|
||||
forge: HashMap<String, ForgeConfig>,
|
||||
}
|
||||
impl ServerConfig {
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub(crate) fn load(fs: &FileSystem) -> Result<Self> {
|
||||
let file = fs.base().join("git-next-server.toml");
|
||||
info!(?file, "");
|
||||
let str = fs.file_read_to_string(&file)?;
|
||||
toml::from_str(&str).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) fn forges(&self) -> impl Iterator<Item = (ForgeName, &ForgeConfig)> {
|
||||
self.forge
|
||||
.iter()
|
||||
.map(|(name, forge)| (ForgeName::new(name.clone()), forge))
|
||||
}
|
||||
|
||||
pub const fn storage(&self) -> &ServerStorage {
|
||||
&self.storage
|
||||
}
|
||||
|
||||
pub const fn webhook(&self) -> &Webhook {
|
||||
&self.webhook
|
||||
}
|
||||
|
||||
pub fn http(&self) -> Result<SocketAddr> {
|
||||
self.http.socket_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the port the server will listen to for incoming webhooks messages
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||
pub struct Http {
|
||||
addr: String,
|
||||
port: u16,
|
||||
}
|
||||
impl Http {
|
||||
fn socket_addr(&self) -> Result<SocketAddr> {
|
||||
Ok(SocketAddr::from_str(&format!(
|
||||
"{}:{}",
|
||||
self.addr, self.port
|
||||
))?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the Webhook Forges should send updates to
|
||||
/// Must be an address that is accessible from the remote forge
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||
pub struct Webhook {
|
||||
url: String,
|
||||
}
|
||||
impl Webhook {
|
||||
pub fn url(&self) -> WebhookUrl {
|
||||
WebhookUrl(self.url.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// The URL for the webhook where forges should send their updates
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, derive_more::AsRef)]
|
||||
pub struct WebhookUrl(String);
|
||||
|
||||
/// The directory to store server data, such as cloned repos
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
|
||||
pub struct ServerStorage {
|
||||
path: PathBuf,
|
||||
}
|
||||
impl ServerStorage {
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
@ -1,132 +1,14 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
//
|
||||
use assert2::let_assert;
|
||||
use git::repository::Direction;
|
||||
use git_next_config::{
|
||||
ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
|
||||
ServerRepoConfig,
|
||||
self as config, ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource,
|
||||
RepoPath,
|
||||
};
|
||||
use git_next_git::{self as git, Generation, GitRemote};
|
||||
use kxio::fs;
|
||||
|
||||
use crate::gitforge::tests::common;
|
||||
|
||||
use super::*;
|
||||
|
||||
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[test]
|
||||
fn load_should_parse_server_config() -> Result<()> {
|
||||
let fs = fs::temp()?;
|
||||
fs.file_write(
|
||||
&fs.base().join("git-next-server.toml"),
|
||||
r#"
|
||||
[http]
|
||||
addr = "0.0.0.0"
|
||||
port = 8080
|
||||
|
||||
[webhook]
|
||||
url = "http://localhost:9909/webhook"
|
||||
|
||||
[storage]
|
||||
path = "/opt/git-next/data"
|
||||
|
||||
[forge.default]
|
||||
forge_type = "MockForge"
|
||||
hostname = "git.example.net"
|
||||
user = "Bob"
|
||||
token = "API-Token"
|
||||
|
||||
[forge.default.repos]
|
||||
hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/user/hello.git" }
|
||||
world = { repo = "user/world", branch = "master", main = "main", next = "next", dev = "dev" }
|
||||
|
||||
[forge.default.repos.sam]
|
||||
repo = "user/sam"
|
||||
branch = "main"
|
||||
main = "master"
|
||||
next = "upcoming"
|
||||
dev = "sam-dev"
|
||||
"#,
|
||||
)
|
||||
?;
|
||||
let_assert!(Ok(config) = ServerConfig::load(&fs));
|
||||
let expected = ServerConfig {
|
||||
http: Http {
|
||||
addr: "0.0.0.0".to_string(),
|
||||
port: 8080,
|
||||
},
|
||||
webhook: Webhook {
|
||||
url: "http://localhost:9909/webhook".to_string(),
|
||||
},
|
||||
storage: ServerStorage {
|
||||
path: "/opt/git-next/data".into(),
|
||||
},
|
||||
forge: HashMap::from([(
|
||||
"default".to_string(),
|
||||
ForgeConfig::new(
|
||||
ForgeType::MockForge,
|
||||
"git.example.net".to_string(),
|
||||
"Bob".to_string(),
|
||||
"API-Token".to_string(),
|
||||
BTreeMap::from([
|
||||
(
|
||||
"hello".to_string(),
|
||||
ServerRepoConfig::new(
|
||||
"user/hello".to_string(),
|
||||
"main".to_string(),
|
||||
Some("/opt/git/user/hello.git".into()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
),
|
||||
(
|
||||
"world".to_string(),
|
||||
ServerRepoConfig::new(
|
||||
"user/world".to_string(),
|
||||
"master".to_string(),
|
||||
None,
|
||||
Some("main".to_string()),
|
||||
Some("next".to_string()),
|
||||
Some("dev".to_string()),
|
||||
),
|
||||
),
|
||||
(
|
||||
"sam".to_string(),
|
||||
ServerRepoConfig::new(
|
||||
"user/sam".to_string(),
|
||||
"main".to_string(),
|
||||
None,
|
||||
Some("master".to_string()),
|
||||
Some("upcoming".to_string()),
|
||||
Some("sam-dev".to_string()),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
)]),
|
||||
};
|
||||
assert_eq!(config, expected, "ServerConfig");
|
||||
|
||||
if let Some(forge) = config.forge.get("world") {
|
||||
if let Some(repo) = forge.get_repo("sam") {
|
||||
let repo_config = repo.repo_config();
|
||||
let expected = Some(RepoConfig::new(
|
||||
RepoBranches::new(
|
||||
"master".to_string(),
|
||||
"upcoming".to_string(),
|
||||
"sam-dev".to_string(),
|
||||
),
|
||||
RepoConfigSource::Server,
|
||||
));
|
||||
assert_eq!(repo_config, expected, "RepoConfig");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repo_config_load() -> Result<()> {
|
||||
let toml = r#"[branches]
|
||||
|
@ -166,10 +48,10 @@ fn gitdir_should_display_as_pathbuf() {
|
|||
fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
|
||||
let cli_crate_dir = std::env::current_dir().map_err(git::validate::Error::Io)?;
|
||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||
let mut repo_details = common::repo_details(
|
||||
let mut repo_details = git::common::repo_details(
|
||||
1,
|
||||
Generation::new(),
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
config::common::forge_details(1, ForgeType::MockForge),
|
||||
None,
|
||||
GitDir::new(root), // Server GitDir - should be ignored
|
||||
);
|
||||
|
@ -197,10 +79,10 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
|
|||
fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
||||
let cli_crate_dir = std::env::current_dir().map_err(git::validate::Error::Io)?;
|
||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||
let mut repo_details = common::repo_details(
|
||||
let mut repo_details = git::common::repo_details(
|
||||
1,
|
||||
Generation::new(),
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
config::common::forge_details(1, ForgeType::MockForge),
|
||||
None,
|
||||
GitDir::new(root), // Server GitDir - should be ignored
|
||||
);
|
||||
|
@ -219,10 +101,10 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
|
|||
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
|
||||
let_assert!(Ok(cli_crate_dir) = std::env::current_dir().map_err(git::validate::Error::Io));
|
||||
let_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
|
||||
let mut repo_details = common::repo_details(
|
||||
let mut repo_details = git::common::repo_details(
|
||||
1,
|
||||
Generation::new(),
|
||||
common::forge_details(1, ForgeType::MockForge),
|
||||
config::common::forge_details(1, ForgeType::MockForge),
|
||||
None,
|
||||
GitDir::new(root), // Server GitDir - should be ignored
|
||||
);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
mod actors;
|
||||
mod config;
|
||||
pub mod gitforge;
|
||||
|
||||
//
|
||||
use actix::prelude::*;
|
||||
|
||||
use git_next_git::Repository;
|
||||
|
|
Loading…
Reference in a new issue