diff --git a/Cargo.toml b/Cargo.toml index 904d24e..350ae5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/git", "crates/forge", "crates/forge-forgejo", + "crates/forge-github", "crates/repo-actor", ] @@ -26,6 +27,7 @@ git-next-config = { path = "crates/config" } git-next-git = { path = "crates/git" } git-next-forge = { path = "crates/forge" } git-next-forge-forgejo = { path = "crates/forge-forgejo" } +git-next-forge-github = { path = "crates/forge-github" } git-next-repo-actor = { path = "crates/repo-actor" } # CLI parsing diff --git a/README.md b/README.md index 61d74e6..5bf6d94 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,46 @@ In the directory with your `git-next-server.toml` file, run the command: git next server start ``` +### Forges + +The following forges are supported: [ForgeJo](https://forgejo.org) and [GitHub](https://github.com/). + +#### ForgeJo + +Configure the forge in `git-next-server.toml` like: + +```toml +[forge.jo] +forge_type = "ForgeJo" +hostname = "git.myforgejo.com" +user = "bob" +token = "..." + +[forge.jo.repos] +hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://git.example.net/user/hello on the branch 'main' +world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch +``` + +The token is created `/user/settings/applications` and requires the `write:repository` permission. + +#### GitHub + +Configure the forge in `git-next-server.toml` like: + +```toml +[forge.gh] +forge_type = "GitHub" +hostname = "github.com" +user = "bob" +token = "..." + +[forge.gh.repos] +hello = { repo = "user/hello", branch = "main", gitdir = "/opt/git/projects/user/hello.git" } # maps to https://github.com/user/hello on the branch 'main' +world = { repo = "user/world", branch = "master", main = "master", next = "upcoming", "dev" = "develop" } # maps to the 'master' branch +``` + +The token is created [here](https://github.com/settings/tokens/new) and requires the `repo` and `admin:repo_hook` permissions. + ## Contributing Contributions to `git-next` are welcome! If you find a bug or have a feature @@ -201,15 +241,17 @@ stateDiagram-v2 forge --> config forge --> git forge --> forgejo + forge --> github forgejo --> config forgejo --> git + github --> config + github --> git + repo_actor --> config repo_actor --> git repo_actor --> forge - - ``` ## License diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 30d5810..49fe34d 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -4,7 +4,7 @@ version = { workspace = true } edition = { workspace = true } [features] -default = ["forgejo"] +default = ["forgejo", "github"] forgejo = [] github = [] diff --git a/crates/config/src/forge_type.rs b/crates/config/src/forge_type.rs index a92c477..fda6c1d 100644 --- a/crates/config/src/forge_type.rs +++ b/crates/config/src/forge_type.rs @@ -4,7 +4,8 @@ pub enum ForgeType { #[cfg(feature = "forgejo")] ForgeJo, // Gitea, - // GitHub, + #[cfg(feature = "github")] + GitHub, // GitLab, // BitBucket, #[default] diff --git a/crates/forge-forgejo/src/lib.rs b/crates/forge-forgejo/src/lib.rs index c80c752..fea992b 100644 --- a/crates/forge-forgejo/src/lib.rs +++ b/crates/forge-forgejo/src/lib.rs @@ -1,7 +1,8 @@ +use git::commit::Status; use git_next_git as git; use kxio::network::{self, Network}; -use tracing::{error, warn}; +use tracing::warn; #[derive(Clone, Debug)] pub struct ForgeJo { @@ -19,7 +20,7 @@ impl git::ForgeLike for ForgeJo { "forgejo".to_string() } - async fn commit_status(&self, commit: &git::Commit) -> git::commit::Status { + async fn commit_status(&self, commit: &git::Commit) -> Status { let repo_details = &self.repo_details; let hostname = &repo_details.forge.hostname(); let repo_path = &repo_details.repo_path; @@ -44,33 +45,33 @@ impl git::ForgeLike for ForgeJo { Ok(response) => { match response.response_body() { Some(status) => match status.state { - CommitStatusState::Success => git::commit::Status::Pass, - CommitStatusState::Pending => git::commit::Status::Pending, - CommitStatusState::Failure => git::commit::Status::Fail, - CommitStatusState::Error => git::commit::Status::Fail, - CommitStatusState::Blank => git::commit::Status::Pending, + ForgejoState::Success => Status::Pass, + ForgejoState::Pending => Status::Pending, + ForgejoState::Failure => Status::Fail, + ForgejoState::Error => Status::Fail, + ForgejoState::Blank => Status::Pending, }, None => { warn!("No status found for commit"); - git::commit::Status::Pending // assume issue is transient and allow retry + Status::Pending // assume issue is transient and allow retry } } } Err(e) => { - error!(?e, "Failed to get commit status"); - git::commit::Status::Pending // assume issue is transient and allow retry + warn!(?e, "Failed to get commit status"); + Status::Pending // assume issue is transient and allow retry } } } } #[derive(Debug, serde::Deserialize)] -pub struct CombinedStatus { - pub state: CommitStatusState, +struct CombinedStatus { + pub state: ForgejoState, } #[derive(Debug, serde::Deserialize)] -pub enum CommitStatusState { +enum ForgejoState { #[serde(rename = "success")] Success, #[serde(rename = "pending")] diff --git a/crates/forge-github/Cargo.toml b/crates/forge-github/Cargo.toml new file mode 100644 index 0000000..500233f --- /dev/null +++ b/crates/forge-github/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "git-next-forge-github" +version = { workspace = true } +edition = { workspace = true } + +[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" diff --git a/crates/forge-github/src/lib.rs b/crates/forge-github/src/lib.rs new file mode 100644 index 0000000..0ade38d --- /dev/null +++ b/crates/forge-github/src/lib.rs @@ -0,0 +1,100 @@ +// +use derive_more::Constructor; +use git::commit::Status; +use git_next_git as git; + +use kxio::network::{self, Network}; +use tracing::warn; + +#[derive(Clone, Debug, Constructor)] +pub struct Github { + repo_details: git::RepoDetails, + net: Network, +} +#[async_trait::async_trait] +impl git_next_git::ForgeLike for Github { + fn name(&self) -> String { + "github".to_string() + } + + /// Checks the results of any (e.g. CI) status checks for the commit. + /// + /// GitHub: https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#list-commit-statuses-for-a-reference + async fn commit_status(&self, commit: &git::Commit) -> Status { + let repo_details = &self.repo_details; + let repo_path = &repo_details.repo_path; + let api_token = &repo_details.forge.token(); + use secrecy::ExposeSecret; + let token = api_token.expose_secret(); + let url = network::NetUrl::new(format!( + "https://api.github.com/repos/${repo_path}/commits/{commit}/check-runs" + )); + + let headers = network::NetRequestHeaders::new() + .with("X-GitHub-Api-Version", "2022-11-28") + .with("Authorization", format!("Bearer: {token}").as_str()); + + let request = network::NetRequest::new( + network::RequestMethod::Get, + url, + headers, + network::RequestBody::None, + network::ResponseType::Json, + None, + network::NetRequestLogging::Both, // TODO: change this to None + ); + let result = self.net.get::>(request).await; + match result { + Ok(response) => response.response_body().map_or_else( + || { + warn!("No status found for commit"); + Status::Pending // assume issue is transient and allow retry + }, + |statuses| { + statuses + .into_iter() + .map(|status| match status.state { + GithubState::Success => Status::Pass, + GithubState::Pending => Status::Pending, + GithubState::Failure => Status::Fail, + GithubState::Error => Status::Fail, + GithubState::Blank => Status::Pending, + }) + .reduce(|l, r| match (l, r) { + (Status::Pass, Status::Pass) => Status::Pass, + (_, Status::Fail) => Status::Fail, + (_, Status::Pending) => Status::Pending, + (Status::Fail, _) => Status::Fail, + (Status::Pending, _) => Status::Pending, + }) + .unwrap_or_else(|| { + warn!("No status checks configured for 'next' branch",); + Status::Pass + }) + }, + ), + Err(e) => { + warn!(?e, "Failed to get commit status"); + Status::Pending // assume issue is transient and allow retry + } + } + } +} +#[derive(Debug, serde::Deserialize)] +struct GitHubStatus { + pub state: GithubState, + // other fields that we ignore +} +#[derive(Debug, serde::Deserialize)] +enum GithubState { + #[serde(rename = "success")] + Success, + #[serde(rename = "pending")] + Pending, + #[serde(rename = "failure")] + Failure, + #[serde(rename = "error")] + Error, + #[serde(rename = "")] + Blank, +} diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 407f512..551b277 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -4,14 +4,15 @@ version = { workspace = true } edition = { workspace = true } [features] -default = ["forgejo"] +default = ["forgejo", "github"] forgejo = ["git-next-forge-forgejo"] -github = [] +github = ["git-next-forge-github"] [dependencies] git-next-config = { workspace = true } git-next-git = { workspace = true } git-next-forge-forgejo = { workspace = true, optional = true } +git-next-forge-github = { workspace = true, optional = true } # logging console-subscriber = { workspace = true } diff --git a/crates/forge/src/github.rs b/crates/forge/src/github.rs deleted file mode 100644 index 7398d5a..0000000 --- a/crates/forge/src/github.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::network::Network; - -struct Github; -pub(super) struct GithubEnv { - net: Network, -} -impl GithubEnv { - pub(crate) const fn new(net: Network) -> GithubEnv { - Self { net } - } -} -#[async_trait::async_trait] -impl super::ForgeLike for GithubEnv { - fn name(&self) -> String { - "github".to_string() - } - - async fn branches_get_all(&self) -> Vec { - todo!() - } -} diff --git a/crates/forge/src/lib.rs b/crates/forge/src/lib.rs index 12c47ab..5aacc1f 100644 --- a/crates/forge/src/lib.rs +++ b/crates/forge/src/lib.rs @@ -1,34 +1,34 @@ -#![allow(dead_code)] - +// use git_next_forge_forgejo as forgejo; +use git_next_forge_github as github; use git_next_git as git; use kxio::network::Network; -#[cfg(feature = "github")] -mod github; - mod mock_forge; #[derive(Clone, Debug)] pub enum Forge { - Mock(mock_forge::MockForgeEnv), - #[allow(clippy::enum_variant_names)] + Mock(mock_forge::MockForge), + #[cfg(feature = "forgejo")] - ForgeJo(forgejo::ForgeJo), + ForgeJo(git_next_forge_forgejo::ForgeJo), + #[cfg(feature = "github")] - Github(github::GithubEnv), + Github(git_next_forge_github::Github), } impl Forge { pub const fn new_mock() -> Self { - Self::Mock(mock_forge::MockForgeEnv::new()) + Self::Mock(mock_forge::MockForge::new()) } + #[cfg(feature = "forgejo")] pub const fn new_forgejo(repo_details: git::RepoDetails, net: Network) -> Self { Self::ForgeJo(forgejo::ForgeJo::new(repo_details, net)) } + #[cfg(feature = "github")] - pub const fn new_github(net: Network) -> Self { - Self::Github(github::GithubEnv::new(net)) + pub const fn new_github(repo_details: git::RepoDetails, net: Network) -> Self { + Self::Github(github::Github::new(repo_details, net)) } } impl std::ops::Deref for Forge { @@ -39,7 +39,7 @@ impl std::ops::Deref for Forge { #[cfg(feature = "forgejo")] Self::ForgeJo(env) => env, #[cfg(feature = "github")] - Forge::Github(env) => env, + Self::Github(env) => env, } } } diff --git a/crates/forge/src/mock_forge.rs b/crates/forge/src/mock_forge.rs index a5f211a..ac8821b 100644 --- a/crates/forge/src/mock_forge.rs +++ b/crates/forge/src/mock_forge.rs @@ -1,18 +1,14 @@ // #![cfg(not(tarpaulin_include))] +use derive_more::Constructor; use git_next_git as git; -struct MockForge; -#[derive(Clone, Debug)] -pub struct MockForgeEnv; -impl MockForgeEnv { - pub(crate) const fn new() -> Self { - Self - } -} +#[derive(Clone, Debug, Constructor)] +pub struct MockForge; + #[async_trait::async_trait] -impl git::ForgeLike for MockForgeEnv { +impl git::ForgeLike for MockForge { fn name(&self) -> String { "mock".to_string() } diff --git a/crates/forge/src/tests/github.rs b/crates/forge/src/tests/github.rs deleted file mode 100644 index 6d355c9..0000000 --- a/crates/forge/src/tests/github.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::*; - -#[test] -fn test_name() { - let net = Network::new_mock(); - let forge = Forge::new_github(net); - assert_eq!(forge.name(), "github"); -} diff --git a/crates/forge/src/tests/mod.rs b/crates/forge/src/tests/mod.rs index 636f4fb..6436d9d 100644 --- a/crates/forge/src/tests/mod.rs +++ b/crates/forge/src/tests/mod.rs @@ -4,9 +4,6 @@ use super::*; use git_next_config as config; use git_next_git as git; -#[cfg(feature = "github")] -mod github; - #[test] fn test_mock_name() { let forge = Forge::new_mock(); diff --git a/crates/git/src/repository/mod.rs b/crates/git/src/repository/mod.rs index 1a14a21..653ffee 100644 --- a/crates/git/src/repository/mod.rs +++ b/crates/git/src/repository/mod.rs @@ -10,6 +10,7 @@ use git_next_config as config; use git_next_config::GitDir; pub use open::OpenRepository; +use tracing::info; use crate::{repository::mock::MockRepository, validation::repo::validate_repo}; @@ -29,18 +30,20 @@ pub fn mock() -> (Repository, MockRepository) { } /// Opens a repository, cloning if necessary +#[tracing::instrument(skip_all)] pub fn open( repository: &Repository, repo_details: &RepoDetails, gitdir: config::GitDir, ) -> Result { let repository = if !gitdir.exists() { - // info!("Local copy not found - cloning..."); + info!("Local copy not found - cloning..."); repository.git_clone(repo_details)? } else { + info!("Local copy found - opening..."); repository.open(&gitdir)? }; - // info!("Validating..."); + info!("Validating..."); validate_repo(&repository, repo_details).map_err(|e| Error::Validation(e.to_string()))?; Ok(repository) } diff --git a/crates/git/src/repository/real.rs b/crates/git/src/repository/real.rs index b693c7a..3284c12 100644 --- a/crates/git/src/repository/real.rs +++ b/crates/git/src/repository/real.rs @@ -15,13 +15,16 @@ impl RepositoryLike for RealRepository { Ok(OpenRepository::real(gix_repo)) } + #[tracing::instrument(skip_all)] fn git_clone(&self, repo_details: &RepoDetails) -> Result { + tracing::info!("creating"); use secrecy::ExposeSecret; let (gix_repo, _outcome) = gix::prepare_clone_bare( repo_details.origin().expose_secret().as_str(), repo_details.gitdir.deref(), )? .fetch_only(gix::progress::Discard, &AtomicBool::new(false))?; + tracing::info!("created"); Ok(OpenRepository::real(gix_repo)) } diff --git a/crates/repo-actor/Cargo.toml b/crates/repo-actor/Cargo.toml index 1738481..3125c9e 100644 --- a/crates/repo-actor/Cargo.toml +++ b/crates/repo-actor/Cargo.toml @@ -4,7 +4,7 @@ version = { workspace = true } edition = { workspace = true } [features] -default = ["forgejo"] +default = ["forgejo", "github"] forgejo = [] github = [] diff --git a/crates/repo-actor/src/lib.rs b/crates/repo-actor/src/lib.rs index 8ae8a08..20ea5e4 100644 --- a/crates/repo-actor/src/lib.rs +++ b/crates/repo-actor/src/lib.rs @@ -47,6 +47,7 @@ impl RepoActor { let forge = match details.forge.forge_type() { #[cfg(feature = "forgejo")] config::ForgeType::ForgeJo => forge::Forge::new_forgejo(details.clone(), net.clone()), + config::ForgeType::GitHub => forge::Forge::new_github(details.clone(), net.clone()), config::ForgeType::MockForge => forge::Forge::new_mock(), }; debug!(?forge, "new"); @@ -107,7 +108,7 @@ impl Handler for RepoActor { }); } } - Err(err) => warn!("Could not Clone repo: {err}"), + Err(err) => warn!("Could not open repo: {err}"), } } } diff --git a/crates/repo-actor/src/webhook.rs b/crates/repo-actor/src/webhook.rs index 3af2592..6d34071 100644 --- a/crates/repo-actor/src/webhook.rs +++ b/crates/repo-actor/src/webhook.rs @@ -1,3 +1,5 @@ +// +// FIXME: This whole module is ForgeJo specific use actix::prelude::*; use git_next_config::{ server::{Webhook, WebhookUrl}, diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 4b6049f..0b57586 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -3,11 +3,6 @@ name = "git-next-server" version = { workspace = true } edition = { workspace = true } -[features] -default = ["forgejo"] -forgejo = [] -github = [] - [dependencies] git-next-config = { workspace = true } git-next-git = { workspace = true }