refactor: extract repo-actor and gitforge crates
All checks were successful
Rust / build (push) Successful in 2m1s
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful

This commit is contained in:
Paul Campbell 2024-05-22 08:41:30 +01:00
parent 4c2bc19139
commit db9b4220ee
42 changed files with 633 additions and 471 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
//
pub mod commit;
pub mod common;
pub mod fetch;
mod generation;
mod git_ref;

View 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"

View file

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

View file

@ -4,4 +4,3 @@ mod validate_positions;
pub use get_all::get_all;
pub use validate_positions::validate_positions;
pub use validate_positions::ValidatedPositions;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()),
);

View file

@ -1,7 +1,5 @@
use super::*;
pub mod common;
#[cfg(feature = "forgejo")]
mod forgejo;

View 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"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,3 @@
pub mod file_watcher;
pub mod repo;
pub mod server;
pub mod webhook;

View file

@ -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::{
file_watcher::FileUpdated,
repo::{CloneRepo, RepoActor},
webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter},
},
config::{ServerConfig, ServerStorage, Webhook},
use crate::actors::{
file_watcher::FileUpdated,
webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter},
};
#[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),
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
mod actors;
mod config;
pub mod gitforge;
//
use actix::prelude::*;
use git_next_git::Repository;