refactor(server,config,git): extract modules config and git from server
All checks were successful
ci/woodpecker/push/cron-docker-builder Pipeline was successful
ci/woodpecker/push/push-next Pipeline was successful
ci/woodpecker/push/tag-created Pipeline was successful

This commit is contained in:
Paul Campbell 2024-05-11 19:46:20 +01:00
parent 740419ffb8
commit 4d352f005d
54 changed files with 1085 additions and 918 deletions

View file

@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["crates/cli", "crates/server"] members = ["crates/cli", "crates/server", "crates/config", "crates/git"]
[workspace.package] [workspace.package]
version = "0.3.0" version = "0.3.0"
@ -14,6 +14,8 @@ expect_used = "warn"
[workspace.dependencies] [workspace.dependencies]
git-next-server = { path = "crates/server" } git-next-server = { path = "crates/server" }
git-next-config = { path = "crates/config" }
git-next-git = { path = "crates/git" }
# CLI parsing # CLI parsing
clap = { version = "4.5", features = ["cargo", "derive"] } clap = { version = "4.5", features = ["cargo", "derive"] }

70
crates/config/Cargo.toml Normal file
View file

@ -0,0 +1,70 @@
[package]
name = "git-next-config"
version = { workspace = true }
edition = { workspace = true }
[features]
default = ["forgejo"]
forgejo = []
github = []
[dependencies]
# # logging
# console-subscriber = { workspace = true }
# tracing = { workspace = true }
# tracing-subscriber = { workspace = true }
# # base64 decoding
# base64 = { workspace = true }
#
# # git
# # gix = { workspace = true }
# gix = { workspace = true }
# async-trait = { workspace = true }
#
# # fs/network
# kxio = { workspace = true }
#
# # fs
# tempfile = { 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 }
#
# # error handling
# derive_more = { workspace = true }
# terrors = { workspace = true }
#
# # file watcher
# inotify = { workspace = true }
#
# # Actors
# actix = { workspace = true }
# actix-rt = { workspace = true }
# tokio = { workspace = true }
#
# [dev-dependencies]
# # Testing
# assert2 = { workspace = true }
# pretty_assertions = { workspace = true }
# test-log = { workspace = true }
# anyhow = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
# pedantic = "warn"
unwrap_used = "warn"
expect_used = "warn"

View file

@ -0,0 +1,16 @@
/// The API Token for the [user]
/// ForgeJo: https://{hostname}/user/settings/applications
/// Github: https://github.com/settings/tokens
#[derive(Clone, Debug)]
pub struct ApiToken(pub secrecy::Secret<String>);
impl From<String> for ApiToken {
fn from(value: String) -> Self {
Self(value.into())
}
}
/// The API Token is in effect a password, so it must be explicitly exposed to access its value
impl secrecy::ExposeSecret<String> for ApiToken {
fn expose_secret(&self) -> &String {
self.0.expose_secret()
}
}

View file

@ -0,0 +1,17 @@
use std::fmt::Formatter;
/// The name of a Branch
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BranchName(pub String);
impl std::fmt::Display for BranchName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::ops::Deref for BranchName {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -0,0 +1,51 @@
use std::collections::HashMap;
use serde::Deserialize;
use crate::{ApiToken, ForgeType, Hostname, RepoAlias, ServerRepoConfig, User};
/// Defines a Forge to connect to
/// Maps from `git-next-server.toml` at `forge.{forge}`
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct ForgeConfig {
pub forge_type: ForgeType,
pub hostname: String,
pub user: String,
pub token: String,
pub repos: HashMap<String, ServerRepoConfig>,
}
impl ForgeConfig {
#[allow(dead_code)]
pub const fn forge_type(&self) -> &ForgeType {
&self.forge_type
}
pub fn hostname(&self) -> Hostname {
Hostname(self.hostname.clone())
}
pub fn user(&self) -> User {
User(self.user.clone())
}
pub fn token(&self) -> ApiToken {
ApiToken(self.token.clone().into())
}
pub fn repos(&self) -> impl Iterator<Item = (RepoAlias, &ServerRepoConfig)> {
self.repos
.iter()
.map(|(name, repo)| (RepoAlias(name.clone()), repo))
}
}
impl std::fmt::Display for ForgeConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}@{}",
self.forge_type.to_string().to_lowercase(),
self.user,
self.hostname
)
}
}

View file

@ -0,0 +1,24 @@
use crate::{ApiToken, ForgeConfig, ForgeName, ForgeType, Hostname, User};
/// The derived information about a Forge, used to create interactions with it
#[derive(Clone, Debug)]
pub struct ForgeDetails {
pub forge_name: ForgeName,
pub forge_type: ForgeType,
pub hostname: Hostname,
pub user: User,
pub token: ApiToken,
// API Token
// Private SSH Key Path
}
impl From<(&ForgeName, &ForgeConfig)> for ForgeDetails {
fn from(forge: (&ForgeName, &ForgeConfig)) -> Self {
Self {
forge_name: forge.0.clone(),
forge_type: forge.1.forge_type.clone(),
hostname: forge.1.hostname(),
user: forge.1.user(),
token: forge.1.token(),
}
}
}

View file

@ -0,0 +1,15 @@
use std::path::PathBuf;
/// The name of a Forge to connect to
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForgeName(pub String);
impl std::fmt::Display for ForgeName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&ForgeName> for PathBuf {
fn from(value: &ForgeName) -> Self {
Self::from(&value.0)
}
}

View file

@ -0,0 +1,16 @@
/// Identifier for the type of Forge
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub enum ForgeType {
#[cfg(feature = "forgejo")]
ForgeJo,
// Gitea,
// GitHub,
// GitLab,
// BitBucket,
MockForge,
}
impl std::fmt::Display for ForgeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format!("{:?}", self).to_lowercase())
}
}

View file

@ -0,0 +1,45 @@
use std::{ops::Deref, path::PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
pub struct GitDir(PathBuf);
impl GitDir {
pub fn new(pathbuf: &std::path::Path) -> Self {
Self(pathbuf.to_path_buf())
}
pub const fn pathbuf(&self) -> &PathBuf {
&self.0
}
}
impl Deref for GitDir {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for GitDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0.display())
}
}
impl From<&str> for GitDir {
fn from(value: &str) -> Self {
Self(value.into())
}
}
impl From<&GitDir> for PathBuf {
fn from(value: &GitDir) -> Self {
value.to_path_buf()
}
}
impl From<PathBuf> for GitDir {
fn from(value: PathBuf) -> Self {
Self(value)
}
}
impl From<GitDir> for PathBuf {
fn from(value: GitDir) -> Self {
value.0
}
}

View file

@ -0,0 +1,10 @@
use std::fmt::{Display, Formatter};
/// The hostname of a forge
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hostname(pub String);
impl Display for Hostname {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

32
crates/config/src/lib.rs Normal file
View file

@ -0,0 +1,32 @@
//
mod api_token;
mod branch_name;
mod forge_config;
mod forge_details;
mod forge_name;
mod forge_type;
pub mod git_dir;
mod host_name;
mod repo_alias;
mod repo_branches;
mod repo_config;
mod repo_config_source;
mod repo_path;
mod server_repo_config;
mod user;
pub use api_token::ApiToken;
pub use branch_name::BranchName;
pub use forge_config::ForgeConfig;
pub use forge_details::ForgeDetails;
pub use forge_name::ForgeName;
pub use forge_type::ForgeType;
pub use git_dir::GitDir;
pub use host_name::Hostname;
pub use repo_alias::RepoAlias;
pub use repo_branches::RepoBranches;
pub use repo_config::RepoConfig;
pub use repo_config_source::RepoConfigSource;
pub use repo_path::RepoPath;
pub use server_repo_config::ServerRepoConfig;
pub use user::User;

View file

@ -0,0 +1,9 @@
/// The alias of a repo
/// This is the alias for the repo within `git-next-server.toml`
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RepoAlias(pub String);
impl std::fmt::Display for RepoAlias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View file

@ -0,0 +1,34 @@
use crate::BranchName;
/// Mapped from `.git-next.toml` file at `branches`
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct RepoBranches {
pub main: String,
pub next: String,
pub dev: String,
}
impl RepoBranches {
pub fn new(main: impl Into<String>, next: impl Into<String>, dev: impl Into<String>) -> Self {
Self {
main: main.into(),
next: next.into(),
dev: dev.into(),
}
}
pub fn main(&self) -> BranchName {
BranchName(self.main.clone())
}
pub fn next(&self) -> BranchName {
BranchName(self.next.clone())
}
pub fn dev(&self) -> BranchName {
BranchName(self.dev.clone())
}
}
impl std::fmt::Display for RepoBranches {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{},{},{}", self.main, self.next, self.dev)
}
}

View file

@ -0,0 +1,33 @@
use crate::RepoBranches;
use crate::RepoConfigSource;
/// Mapped from `.git-next.toml` file in target repo
/// Is also derived from the optional parameters in `git-next-server.toml` at
/// `forge.{forge}.repos.{repo}.(main|next|dev)`
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct RepoConfig {
pub branches: RepoBranches,
pub source: RepoConfigSource,
}
impl RepoConfig {
pub const fn new(branches: RepoBranches, source: RepoConfigSource) -> Self {
Self { branches, source }
}
pub fn load(toml: &str) -> Result<Self, toml::de::Error> {
toml::from_str(format!("source = \"Repo\"\n{}", toml).as_str())
}
pub const fn branches(&self) -> &RepoBranches {
&self.branches
}
pub const fn source(&self) -> RepoConfigSource {
self.source
}
}
impl std::fmt::Display for RepoConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.branches)
}
}

View file

@ -0,0 +1,5 @@
#[derive(Copy, Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub enum RepoConfigSource {
Repo,
Server,
}

View file

@ -0,0 +1,12 @@
use std::fmt::{Display, Formatter};
/// The path for the repo within the forge.
/// Typically this is composed of the user or organisation and the name of the repo
/// e.g. `{user}/{repo}`
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoPath(pub String);
impl Display for RepoPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View file

@ -0,0 +1,55 @@
use std::path::PathBuf;
use crate::{BranchName, GitDir, RepoBranches, RepoConfig, RepoConfigSource, RepoPath};
/// Defines a Repo within a ForgeConfig to be monitored by the server
/// Maps from `git-next-server.toml` at `forge.{forge}.repos.{name}`
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct ServerRepoConfig {
pub repo: String,
pub branch: String,
pub gitdir: Option<PathBuf>,
pub main: Option<String>,
pub next: Option<String>,
pub dev: Option<String>,
}
impl ServerRepoConfig {
pub fn repo(&self) -> RepoPath {
RepoPath(self.repo.clone())
}
#[allow(dead_code)]
pub fn branch(&self) -> BranchName {
BranchName(self.branch.clone())
}
pub fn gitdir(&self) -> Option<GitDir> {
self.gitdir.clone().map(GitDir::from)
}
/// Returns a RepoConfig from the server configuration if ALL THREE branches were provided
pub fn repo_config(&self) -> Option<RepoConfig> {
match (&self.main, &self.next, &self.dev) {
(Some(main), Some(next), Some(dev)) => Some(RepoConfig {
branches: RepoBranches {
main: main.to_string(),
next: next.to_string(),
dev: dev.to_string(),
},
source: RepoConfigSource::Server,
}),
_ => None,
}
}
}
#[cfg(test)]
impl AsRef<Self> for ServerRepoConfig {
fn as_ref(&self) -> &Self {
self
}
}
impl std::fmt::Display for ServerRepoConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.repo, self.branch)
}
}

View file

@ -0,0 +1,8 @@
/// The user within the forge to connect as
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct User(pub String);
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

72
crates/git/Cargo.toml Normal file
View file

@ -0,0 +1,72 @@
[package]
name = "git-next-git"
version = { workspace = true }
edition = { workspace = true }
[features]
default = ["forgejo"]
forgejo = []
github = []
[dependencies]
git-next-config = { workspace = true }
# logging
# console-subscriber = { workspace = true }
tracing = { workspace = true }
# tracing-subscriber = { workspace = true }
# # base64 decoding
# base64 = { workspace = true }
#
# git
# # gix = { workspace = true }
gix = { workspace = true }
# async-trait = { workspace = true }
#
# # fs/network
# kxio = { workspace = true }
#
# # fs
# tempfile = { 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 }
# error handling
derive_more = { workspace = true }
# terrors = { workspace = true }
# # file watcher
# inotify = { workspace = true }
#
# # Actors
# actix = { workspace = true }
# actix-rt = { workspace = true }
# tokio = { workspace = true }
#
# [dev-dependencies]
# # Testing
# assert2 = { workspace = true }
# pretty_assertions = { workspace = true }
# test-log = { workspace = true }
# anyhow = { workspace = true }
[lints.clippy]
nursery = { level = "warn", priority = -1 }
# pedantic = "warn"
unwrap_used = "warn"
expect_used = "warn"

50
crates/git/src/commit.rs Normal file
View file

@ -0,0 +1,50 @@
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Commit {
sha: Sha,
message: Message,
}
impl Commit {
pub fn new(sha: &str, message: &str) -> Self {
Self {
sha: Sha::new(sha.to_string()),
message: Message::new(message.to_string()),
}
}
pub const fn sha(&self) -> &Sha {
&self.sha
}
pub const fn message(&self) -> &Message {
&self.message
}
}
impl std::fmt::Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.sha)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sha(String);
impl Sha {
pub const fn new(value: String) -> Self {
Self(value)
}
}
impl std::fmt::Display for Sha {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Message(String);
impl Message {
pub const fn new(value: String) -> Self {
Self(value)
}
}
impl std::fmt::Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View file

@ -2,7 +2,7 @@ use std::ops::Deref;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::{config::RepoDetails, gitforge::Repository}; use super::{RepoDetails, Repository};
#[derive(Debug, derive_more::From, derive_more::Display)] #[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error { pub enum Error {

View file

@ -0,0 +1,15 @@
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Generation(u32);
impl Generation {
pub fn new() -> Self {
Self::default()
}
pub fn inc(&mut self) {
self.0 += 1
}
}
impl std::fmt::Display for Generation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

21
crates/git/src/git_ref.rs Normal file
View file

@ -0,0 +1,21 @@
use git_next_config::BranchName;
use crate::Commit;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct GitRef(pub String);
impl From<Commit> for GitRef {
fn from(value: Commit) -> Self {
Self(value.sha().to_string())
}
}
impl From<BranchName> for GitRef {
fn from(value: BranchName) -> Self {
Self(value.0)
}
}
impl std::fmt::Display for GitRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

View file

@ -0,0 +1,23 @@
use git_next_config::{Hostname, RepoPath};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GitRemote {
host: Hostname,
repo_path: RepoPath,
}
impl GitRemote {
pub const fn new(host: Hostname, repo_path: RepoPath) -> Self {
Self { host, repo_path }
}
pub const fn host(&self) -> &Hostname {
&self.host
}
pub const fn repo_path(&self) -> &RepoPath {
&self.repo_path
}
}
impl std::fmt::Display for GitRemote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.host, self.repo_path)
}
}

20
crates/git/src/lib.rs Normal file
View file

@ -0,0 +1,20 @@
//
pub mod commit;
pub mod fetch;
mod generation;
mod git_ref;
mod git_remote;
mod repo_details;
pub mod repository;
pub mod reset;
pub mod validate;
pub use commit::Commit;
pub use fetch::fetch;
pub use generation::Generation;
pub use git_ref::GitRef;
pub use git_remote::GitRemote;
pub use repo_details::RepoDetails;
pub use repository::Repository;
pub use reset::reset;
pub use validate::validate;

View file

@ -0,0 +1,75 @@
use git_next_config::{
BranchName, ForgeConfig, ForgeDetails, ForgeName, GitDir, RepoAlias, RepoConfig, RepoPath,
ServerRepoConfig,
};
use super::{Generation, GitRemote};
/// The derived information about a repo, used to interact with it
#[derive(Clone, Debug)]
pub struct RepoDetails {
pub generation: Generation,
pub repo_alias: RepoAlias,
pub repo_path: RepoPath,
pub branch: BranchName,
pub forge: ForgeDetails,
pub repo_config: Option<RepoConfig>,
pub gitdir: GitDir,
}
impl RepoDetails {
pub fn new(
generation: Generation,
repo_alias: &RepoAlias,
server_repo_config: &ServerRepoConfig,
forge_name: &ForgeName,
forge_config: &ForgeConfig,
gitdir: GitDir,
) -> Self {
Self {
generation,
repo_alias: repo_alias.clone(),
repo_path: RepoPath(server_repo_config.repo.clone()),
repo_config: server_repo_config.repo_config(),
branch: BranchName(server_repo_config.branch.clone()),
gitdir,
forge: ForgeDetails {
forge_name: forge_name.clone(),
forge_type: forge_config.forge_type.clone(),
hostname: forge_config.hostname(),
user: forge_config.user(),
token: forge_config.token(),
},
}
}
pub fn origin(&self) -> secrecy::Secret<String> {
let repo_details = self;
let user = &repo_details.forge.user;
let hostname = &repo_details.forge.hostname;
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let expose_secret = &repo_details.forge.token;
let token = expose_secret.expose_secret();
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
origin.into()
}
pub fn git_remote(&self) -> GitRemote {
GitRemote::new(self.forge.hostname.clone(), self.repo_path.clone())
}
}
impl std::fmt::Display for RepoDetails {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"gen-{}:{}:{}/{}:{}@{}/{}@{}",
self.generation,
self.forge.forge_type,
self.forge.forge_name,
self.repo_alias,
self.forge.user,
self.forge.hostname,
self.repo_path,
self.branch,
)
}
}

View file

@ -0,0 +1,70 @@
//
use std::{ops::Deref as _, path::PathBuf, sync::atomic::AtomicBool};
use super::RepoDetails;
#[derive(Debug, Clone, derive_more::From)]
pub struct Repository(gix::ThreadSafeRepository);
impl Repository {
pub fn open(gitdir: impl Into<PathBuf>) -> Result<Self, Error> {
Ok(Self(gix::ThreadSafeRepository::open(gitdir.into())?))
}
pub fn clone(repo_details: &RepoDetails) -> Result<Self, Error> {
use secrecy::ExposeSecret;
let origin = repo_details.origin();
let (repository, _outcome) =
gix::prepare_clone_bare(origin.expose_secret().as_str(), repo_details.gitdir.deref())?
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
Ok(repository.into_sync().into())
}
}
impl std::ops::Deref for Repository {
type Target = gix::ThreadSafeRepository;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug)]
pub enum Error {
InvalidGitDir(git_next_config::GitDir),
Io(std::io::Error),
Wait(std::io::Error),
Spawn(std::io::Error),
Validation(String),
GixClone(Box<gix::clone::Error>),
GixOpen(Box<gix::open::Error>),
GixFetch(Box<gix::clone::fetch::Error>),
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidGitDir(gitdir) => write!(f, "Invalid Git dir: {:?}", gitdir),
Self::Io(err) => write!(f, "IO error: {:?}", err),
Self::Wait(err) => write!(f, "Waiting for command: {:?}", err),
Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err),
Self::Validation(err) => write!(f, "Validation: {}", err),
Self::GixClone(err) => write!(f, "Clone: {:?}", err),
Self::GixOpen(err) => write!(f, "Open: {:?}", err),
Self::GixFetch(err) => write!(f, "Fetch: {:?}", err),
}
}
}
impl From<gix::clone::Error> for Error {
fn from(value: gix::clone::Error) -> Self {
Self::GixClone(Box::new(value))
}
}
impl From<gix::open::Error> for Error {
fn from(value: gix::open::Error) -> Self {
Self::GixOpen(Box::new(value))
}
}
impl From<gix::clone::fetch::Error> for Error {
fn from(value: gix::clone::fetch::Error) -> Self {
Self::GixFetch(Box::new(value))
}
}

View file

@ -1,13 +1,24 @@
use std::ops::Deref; use std::ops::Deref;
use git_next_config::BranchName;
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::{ use super::{GitRef, RepoDetails, Repository};
config::{BranchName, RepoDetails},
gitforge::{BranchResetError, BranchResetResult, Force, Repository}, #[derive(Debug)]
types::GitRef, pub enum Force {
}; No,
From(GitRef),
}
impl std::fmt::Display for Force {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::No => write!(f, "fast-foward"),
Self::From(from) => write!(f, "force-if-from:{}", from),
}
}
}
// TODO: (#72) reimplement using `gix` // TODO: (#72) reimplement using `gix`
#[tracing::instrument(skip_all, fields(branch = %branch_name, to = %to_commit, force = %force))] #[tracing::instrument(skip_all, fields(branch = %branch_name, to = %to_commit, force = %force))]
@ -17,7 +28,7 @@ pub fn reset(
branch_name: BranchName, branch_name: BranchName,
to_commit: GitRef, to_commit: GitRef,
force: Force, force: Force,
) -> BranchResetResult { ) -> Result {
let origin = repo_details.origin(); let origin = repo_details.origin();
let force = match force { let force = match force {
Force::No => "".to_string(), Force::No => "".to_string(),
@ -56,3 +67,13 @@ pub fn reset(
} }
} }
} }
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum BranchResetError {
Open(Box<gix::open::Error>),
Fetch(super::fetch::Error),
Push,
}
impl std::error::Error for BranchResetError {}
pub type Result = core::result::Result<(), BranchResetError>;

View file

@ -0,0 +1,93 @@
use std::ops::Deref as _;
use git_next_config::{Hostname, RepoPath};
use gix::remote::Direction;
use tracing::info;
use super::{GitRemote, RepoDetails, Repository};
#[tracing::instrument(skip_all)]
pub fn validate(repository: &Repository, repo_details: &RepoDetails) -> Result<()> {
let git_remote = repo_details.git_remote();
let push_remote = find_default_remote(repository, Direction::Push)?;
let fetch_remote = find_default_remote(repository, Direction::Fetch)?;
info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
if git_remote != push_remote {
return Err(Error::MismatchDefaultPushRemote {
found: push_remote,
expected: git_remote,
});
}
if git_remote != fetch_remote {
return Err(Error::MismatchDefaultFetchRemote {
found: fetch_remote,
expected: git_remote,
});
}
Ok(())
}
#[tracing::instrument(skip_all, fields(?direction))]
pub fn find_default_remote(
repository: &Repository,
direction: gix::remote::Direction,
) -> Result<GitRemote> {
let repository = repository.deref().to_thread_local();
let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else {
return Err(Error::NoDefaultPushRemote);
};
let Some(url) = remote.url(direction) else {
return Err(Error::NoUrlForDefaultPushRemote);
};
let Some(host) = url.host() else {
return Err(Error::NoHostnameForDefaultPushRemote);
};
let path = url.path.to_string();
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
let path = path.strip_suffix(".git").map_or(path, |path| path);
info!(%host, %path, "found");
Ok(GitRemote::new(
Hostname(host.to_string()),
RepoPath(path.to_string()),
))
}
type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
NoDefaultPushRemote,
NoUrlForDefaultPushRemote,
NoHostnameForDefaultPushRemote,
UnableToOpenRepo(String),
Io(std::io::Error),
MismatchDefaultPushRemote {
found: GitRemote,
expected: GitRemote,
},
MismatchDefaultFetchRemote {
found: GitRemote,
expected: GitRemote,
},
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoDefaultPushRemote => write!(f, "There is no default push remote"),
Self::NoUrlForDefaultPushRemote => write!(f, "The default push remote has no url"),
Self::NoHostnameForDefaultPushRemote => {
write!(f, "The default push remote has no hostname")
}
Self::UnableToOpenRepo(err) => write!(f, "Unable to open the git dir: {err}"),
Self::Io(err) => write!(f, "IO Error: {err:?}"),
Self::MismatchDefaultPushRemote { found, expected } => write!(
f,
"The default push remote doesn't match: {found}, expected: {expected}"
),
Self::MismatchDefaultFetchRemote { found, expected } => write!(
f,
"The default fetch remote doesn't match: {found}, expected: {expected}"
),
}
}
}

View file

@ -9,6 +9,9 @@ forgejo = []
github = [] github = []
[dependencies] [dependencies]
git-next-config = { workspace = true }
git-next-git = { workspace = true }
# logging # logging
console-subscriber = { workspace = true } console-subscriber = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View file

@ -2,22 +2,23 @@ use std::time::Duration;
use actix::prelude::*; use actix::prelude::*;
use git_next_config::{RepoConfig, RepoConfigSource};
use git_next_git as git;
use tracing::{info, warn}; use tracing::{info, warn};
use crate::{ use crate::{
actors::repo::{LoadConfigFromRepo, RepoActor, ValidateRepo}, actors::repo::{LoadConfigFromRepo, RepoActor, ValidateRepo},
config::{self, RepoConfigSource},
gitforge, gitforge,
}; };
// advance next to the next commit towards the head of the dev branch // advance next to the next commit towards the head of the dev branch
#[tracing::instrument(fields(next), skip_all)] #[tracing::instrument(fields(next), skip_all)]
pub async fn advance_next( pub async fn advance_next(
next: gitforge::Commit, next: git::Commit,
dev_commit_history: Vec<gitforge::Commit>, dev_commit_history: Vec<git::Commit>,
repo_config: config::RepoConfig, repo_config: RepoConfig,
forge: gitforge::Forge, forge: gitforge::Forge,
repository: gitforge::Repository, repository: git::Repository,
addr: Addr<super::RepoActor>, addr: Addr<super::RepoActor>,
message_token: super::MessageToken, message_token: super::MessageToken,
) { ) {
@ -35,7 +36,7 @@ pub async fn advance_next(
&repository, &repository,
repo_config.branches().next(), repo_config.branches().next(),
commit.into(), commit.into(),
gitforge::Force::No, git::reset::Force::No,
) { ) {
warn!(?err, "Failed") warn!(?err, "Failed")
} }
@ -44,7 +45,7 @@ pub async fn advance_next(
} }
#[tracing::instrument] #[tracing::instrument]
fn validate_commit_message(message: &gitforge::Message) -> Option<String> { fn validate_commit_message(message: &git::commit::Message) -> Option<String> {
let message = &message.to_string(); let message = &message.to_string();
if message.to_ascii_lowercase().starts_with("wip") { if message.to_ascii_lowercase().starts_with("wip") {
return Some("Is Work-In-Progress".to_string()); return Some("Is Work-In-Progress".to_string());
@ -62,10 +63,10 @@ fn validate_commit_message(message: &gitforge::Message) -> Option<String> {
} }
fn find_next_commit_on_dev( fn find_next_commit_on_dev(
next: gitforge::Commit, next: git::Commit,
dev_commit_history: Vec<gitforge::Commit>, dev_commit_history: Vec<git::Commit>,
) -> Option<gitforge::Commit> { ) -> Option<git::Commit> {
let mut next_commit: Option<gitforge::Commit> = None; let mut next_commit: Option<git::Commit> = None;
for commit in dev_commit_history.into_iter() { for commit in dev_commit_history.into_iter() {
if commit == next { if commit == next {
break; break;
@ -78,10 +79,10 @@ fn find_next_commit_on_dev(
// advance main branch to the commit 'next' // advance main branch to the commit 'next'
#[tracing::instrument(fields(next), skip_all)] #[tracing::instrument(fields(next), skip_all)]
pub async fn advance_main( pub async fn advance_main(
next: gitforge::Commit, next: git::Commit,
repo_config: config::RepoConfig, repo_config: RepoConfig,
forge: gitforge::Forge, forge: gitforge::Forge,
repository: gitforge::Repository, repository: git::Repository,
addr: Addr<RepoActor>, addr: Addr<RepoActor>,
message_token: super::MessageToken, message_token: super::MessageToken,
) { ) {
@ -90,7 +91,7 @@ pub async fn advance_main(
&repository, &repository,
repo_config.branches().main(), repo_config.branches().main(),
next.into(), next.into(),
gitforge::Force::No, git::reset::Force::No,
) { ) {
warn!(?err, "Failed") warn!(?err, "Failed")
}; };
@ -106,13 +107,13 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_find_next_commit_on_dev() { async fn test_find_next_commit_on_dev() {
let next = gitforge::Commit::new("current-next", "foo"); let next = git::Commit::new("current-next", "foo");
let expected = gitforge::Commit::new("dev-next", "next-should-go-here"); let expected = git::Commit::new("dev-next", "next-should-go-here");
let dev_commit_history = vec![ let dev_commit_history = vec![
gitforge::Commit::new("dev", "future"), git::Commit::new("dev", "future"),
expected.clone(), expected.clone(),
next.clone(), next.clone(),
gitforge::Commit::new("current-main", "history"), git::Commit::new("current-main", "history"),
]; ];
let next_commit = find_next_commit_on_dev(next, dev_commit_history); let next_commit = find_next_commit_on_dev(next, dev_commit_history);
assert_eq!(next_commit, Some(expected)); assert_eq!(next_commit, Some(expected));

View file

@ -1,4 +1,5 @@
use actix::prelude::*; use actix::prelude::*;
use git_next_git::RepoDetails;
use tracing::{error, info}; use tracing::{error, info};
use crate::{config, gitforge}; use crate::{config, gitforge};
@ -7,11 +8,7 @@ use super::{LoadedConfig, RepoActor};
/// Loads the [RepoConfig] from the `.git-next.toml` file in the repository /// Loads the [RepoConfig] from the `.git-next.toml` file in the repository
#[tracing::instrument(skip_all, fields(branch = %repo_details.branch))] #[tracing::instrument(skip_all, fields(branch = %repo_details.branch))]
pub async fn load( pub async fn load(repo_details: RepoDetails, addr: Addr<RepoActor>, forge: gitforge::Forge) {
repo_details: config::RepoDetails,
addr: Addr<RepoActor>,
forge: gitforge::Forge,
) {
info!("Loading .git-next.toml from repo"); info!("Loading .git-next.toml from repo");
let repo_config = match config::load::load(&repo_details, &forge).await { let repo_config = match config::load::load(&repo_details, &forge).await {
Ok(repo_config) => repo_config, Ok(repo_config) => repo_config,

View file

@ -4,46 +4,40 @@ pub mod status;
pub mod webhook; pub mod webhook;
use actix::prelude::*; use actix::prelude::*;
use git_next_config::{ForgeType, RepoConfig};
use git_next_git::{self as git, Generation, RepoDetails};
use kxio::network::Network; use kxio::network::Network;
use tracing::{debug, info, warn, Instrument}; use tracing::{debug, info, warn, Instrument};
use crate::{ use crate::{actors::repo::webhook::WebhookAuth, config::Webhook, gitforge, types::MessageToken};
actors::repo::webhook::WebhookAuth,
config::{RepoConfig, RepoDetails, Webhook},
gitforge::{self, Repository},
types::{MessageToken, ServerGeneration},
};
use self::webhook::WebhookId; use self::webhook::WebhookId;
pub struct RepoActor { pub struct RepoActor {
generation: ServerGeneration, generation: Generation,
message_token: MessageToken, message_token: MessageToken,
details: RepoDetails, details: RepoDetails,
webhook: Webhook, webhook: Webhook,
webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured webhook_id: Option<WebhookId>, // INFO: if [None] then no webhook is configured
webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured webhook_auth: Option<WebhookAuth>, // INFO: if [None] then no webhook is configured
last_main_commit: Option<gitforge::Commit>, last_main_commit: Option<git::Commit>,
last_next_commit: Option<gitforge::Commit>, last_next_commit: Option<git::Commit>,
last_dev_commit: Option<gitforge::Commit>, last_dev_commit: Option<git::Commit>,
repository: Option<Repository>, repository: Option<git::Repository>,
net: Network, net: Network,
forge: gitforge::Forge, forge: gitforge::Forge,
} }
impl RepoActor { impl RepoActor {
pub(crate) fn new( pub fn new(
details: RepoDetails, details: RepoDetails,
webhook: Webhook, webhook: Webhook,
generation: ServerGeneration, generation: Generation,
net: Network, net: Network,
) -> Self { ) -> Self {
let forge = match details.forge.forge_type { let forge = match details.forge.forge_type {
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
crate::config::ForgeType::ForgeJo => { ForgeType::ForgeJo => gitforge::Forge::new_forgejo(details.clone(), net.clone()),
gitforge::Forge::new_forgejo(details.clone(), net.clone()) ForgeType::MockForge => gitforge::Forge::new_mock(),
}
#[cfg(test)]
crate::config::ForgeType::MockForge => gitforge::Forge::new_mock(),
}; };
debug!(?forge, "new"); debug!(?forge, "new");
Self { Self {
@ -212,10 +206,10 @@ impl Handler<ValidateRepo> for RepoActor {
#[derive(Debug, Message)] #[derive(Debug, Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct StartMonitoring { pub struct StartMonitoring {
pub main: gitforge::Commit, pub main: git::Commit,
pub next: gitforge::Commit, pub next: git::Commit,
pub dev: gitforge::Commit, pub dev: git::Commit,
pub dev_commit_history: Vec<gitforge::Commit>, pub dev_commit_history: Vec<git::Commit>,
} }
impl Handler<StartMonitoring> for RepoActor { impl Handler<StartMonitoring> for RepoActor {
type Result = (); type Result = ();
@ -273,7 +267,7 @@ impl Handler<WebhookRegistered> for RepoActor {
#[derive(Message)] #[derive(Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct AdvanceMainTo(pub gitforge::Commit); pub struct AdvanceMainTo(pub git::Commit);
impl Handler<AdvanceMainTo> for RepoActor { impl Handler<AdvanceMainTo> for RepoActor {
type Result = (); type Result = ();
#[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.details, commit = %msg.0))] #[tracing::instrument(name = "RepoActor::AdvanceMainTo", skip_all, fields(repo = %self.details, commit = %msg.0))]

View file

@ -1,4 +1,5 @@
use actix::prelude::*; use actix::prelude::*;
use git_next_git as git;
use gix::trace::warn; use gix::trace::warn;
use tracing::info; use tracing::info;
@ -7,7 +8,7 @@ use crate::{actors::repo::ValidateRepo, gitforge, types::MessageToken};
use super::AdvanceMainTo; use super::AdvanceMainTo;
pub async fn check_next( pub async fn check_next(
next: gitforge::Commit, next: git::Commit,
addr: Addr<super::RepoActor>, addr: Addr<super::RepoActor>,
forge: gitforge::Forge, forge: gitforge::Forge,
message_token: MessageToken, message_token: MessageToken,

View file

@ -1,4 +1,6 @@
use actix::prelude::*; use actix::prelude::*;
use git_next_config::RepoBranches;
use git_next_git::{self as git, RepoDetails};
use kxio::network::{self, json}; use kxio::network::{self, json};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use ulid::DecodeError; use ulid::DecodeError;
@ -10,8 +12,7 @@ use crate::{
repo::{RepoActor, ValidateRepo, WebhookRegistered}, repo::{RepoActor, ValidateRepo, WebhookRegistered},
webhook::WebhookMessage, webhook::WebhookMessage,
}, },
config::{RepoBranches, Webhook, WebhookUrl}, config::{Webhook, WebhookUrl},
gitforge,
}; };
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -58,11 +59,7 @@ impl Deref for WebhookAuth {
} }
#[tracing::instrument(skip_all, fields(%webhook_id))] #[tracing::instrument(skip_all, fields(%webhook_id))]
pub async fn unregister( pub async fn unregister(webhook_id: WebhookId, repo_details: RepoDetails, net: network::Network) {
webhook_id: WebhookId,
repo_details: crate::config::RepoDetails,
net: network::Network,
) {
let hostname = &repo_details.forge.hostname; let hostname = &repo_details.forge.hostname;
let repo_path = repo_details.repo_path; let repo_path = repo_details.repo_path;
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
@ -88,7 +85,7 @@ pub async fn unregister(
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn register( pub async fn register(
repo_details: crate::config::RepoDetails, repo_details: RepoDetails,
webhook: Webhook, webhook: Webhook,
addr: actix::prelude::Addr<super::RepoActor>, addr: actix::prelude::Addr<super::RepoActor>,
net: network::Network, net: network::Network,
@ -147,7 +144,7 @@ pub async fn register(
} }
async fn find_existing_webhooks( async fn find_existing_webhooks(
repo_details: &crate::config::RepoDetails, repo_details: &RepoDetails,
webhook_url: &WebhookUrl, webhook_url: &WebhookUrl,
net: &network::Network, net: &network::Network,
) -> Vec<WebhookId> { ) -> Vec<WebhookId> {
@ -301,8 +298,8 @@ impl Push {
warn!(branch, "Unexpected branch"); warn!(branch, "Unexpected branch");
None None
} }
pub fn commit(&self) -> gitforge::Commit { pub fn commit(&self) -> git::Commit {
gitforge::Commit::new(&self.after, &self.head_commit.message) git::Commit::new(&self.after, &self.head_commit.message)
} }
} }

View file

@ -2,6 +2,8 @@ use std::path::PathBuf;
use actix::prelude::*; use actix::prelude::*;
use git_next_config::{ForgeConfig, ForgeName, GitDir, RepoAlias, ServerRepoConfig};
use git_next_git::{Generation, RepoDetails};
use kxio::{fs::FileSystem, network::Network}; use kxio::{fs::FileSystem, network::Network};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@ -11,11 +13,7 @@ use crate::{
repo::{CloneRepo, RepoActor}, repo::{CloneRepo, RepoActor},
webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter}, webhook::{AddWebhookRecipient, ShutdownWebhook, WebhookActor, WebhookRouter},
}, },
config::{ config::{ServerConfig, ServerStorage, Webhook},
ForgeConfig, ForgeName, GitDir, RepoAlias, RepoDetails, ServerConfig, ServerRepoConfig,
ServerStorage, Webhook,
},
types::ServerGeneration,
}; };
#[derive(Debug, derive_more::Display, derive_more::From)] #[derive(Debug, derive_more::Display, derive_more::From)]
@ -35,7 +33,7 @@ pub enum Error {
type Result<T> = core::result::Result<T, Error>; type Result<T> = core::result::Result<T, Error>;
pub struct Server { pub struct Server {
generation: ServerGeneration, generation: Generation,
webhook: Option<Addr<WebhookActor>>, webhook: Option<Addr<WebhookActor>>,
fs: FileSystem, fs: FileSystem,
net: Network, net: Network,
@ -113,7 +111,7 @@ impl Handler<ServerConfig> for Server {
} }
impl Server { impl Server {
pub fn new(fs: FileSystem, net: Network) -> Self { pub fn new(fs: FileSystem, net: Network) -> Self {
let generation = ServerGeneration::new(); let generation = Generation::new();
Self { Self {
generation, generation,
webhook: None, webhook: None,

View file

@ -1,9 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use actix::prelude::*; use actix::prelude::*;
use git_next_config::RepoAlias;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::{actors::webhook::message::WebhookMessage, config::RepoAlias}; use crate::actors::webhook::message::WebhookMessage;
pub struct WebhookRouter { pub struct WebhookRouter {
span: tracing::Span, span: tracing::Span,

View file

@ -1,10 +1,9 @@
use git_next_config::{BranchName, RepoConfig};
use git_next_git::RepoDetails;
use terrors::OneOf; use terrors::OneOf;
use tracing::error; use tracing::error;
use crate::{ use crate::gitforge::{self, ForgeFileError};
config::{BranchName, RepoConfig, RepoDetails},
gitforge::{self, ForgeFileError},
};
pub async fn load( pub async fn load(
details: &RepoDetails, details: &RepoDetails,
@ -43,7 +42,7 @@ pub async fn validate(
})?; })?;
if !branches if !branches
.iter() .iter()
.any(|branch| branch.name() == &config.branches().main()) .any(|branch| branch == &config.branches().main())
{ {
return Err(RepoConfigValidationErrors::BranchNotFound( return Err(RepoConfigValidationErrors::BranchNotFound(
config.branches().main(), config.branches().main(),
@ -51,7 +50,7 @@ pub async fn validate(
} }
if !branches if !branches
.iter() .iter()
.any(|branch| branch.name() == &config.branches().next()) .any(|branch| branch == &config.branches().next())
{ {
return Err(RepoConfigValidationErrors::BranchNotFound( return Err(RepoConfigValidationErrors::BranchNotFound(
config.branches().next(), config.branches().next(),
@ -59,7 +58,7 @@ pub async fn validate(
} }
if !branches if !branches
.iter() .iter()
.any(|branch| branch.name() == &config.branches().dev()) .any(|branch| branch == &config.branches().dev())
{ {
return Err(RepoConfigValidationErrors::BranchNotFound( return Err(RepoConfigValidationErrors::BranchNotFound(
config.branches().dev(), config.branches().dev(),

View file

@ -4,20 +4,15 @@ pub mod load;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::{Display, Formatter},
net::SocketAddr, net::SocketAddr,
ops::Deref,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
}; };
use serde::Deserialize; use git_next_config::{ForgeConfig, ForgeName};
use kxio::fs::FileSystem; use kxio::fs::FileSystem;
use tracing::info; use tracing::info;
use crate::{gitforge::Repository, types::ServerGeneration};
#[derive(Debug, derive_more::From, derive_more::Display)] #[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error { pub enum Error {
Io(std::io::Error), Io(std::io::Error),
@ -30,7 +25,7 @@ impl std::error::Error for Error {}
type Result<T> = core::result::Result<T, Error>; type Result<T> = core::result::Result<T, Error>;
/// Mapped from the `git-next-server.toml` file /// Mapped from the `git-next-server.toml` file
#[derive(Debug, PartialEq, Eq, Deserialize, Message)] #[derive(Debug, PartialEq, Eq, serde::Deserialize, Message)]
#[rtype(result = "()")] #[rtype(result = "()")]
pub struct ServerConfig { pub struct ServerConfig {
http: Http, http: Http,
@ -67,7 +62,7 @@ impl ServerConfig {
} }
/// Defines the port the server will listen to for incoming webhooks messages /// Defines the port the server will listen to for incoming webhooks messages
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct Http { pub struct Http {
addr: String, addr: String,
port: u16, port: u16,
@ -83,7 +78,7 @@ impl Http {
/// Defines the Webhook Forges should send updates to /// Defines the Webhook Forges should send updates to
/// Must be an address that is accessible from the remote forge /// Must be an address that is accessible from the remote forge
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct Webhook { pub struct Webhook {
url: String, url: String,
} }
@ -94,7 +89,7 @@ impl Webhook {
} }
/// The URL for the webhook where forges should send their updates /// The URL for the webhook where forges should send their updates
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct WebhookUrl(String); pub struct WebhookUrl(String);
impl AsRef<str> for WebhookUrl { impl AsRef<str> for WebhookUrl {
fn as_ref(&self) -> &str { fn as_ref(&self) -> &str {
@ -103,7 +98,7 @@ impl AsRef<str> for WebhookUrl {
} }
/// The directory to store server data, such as cloned repos /// The directory to store server data, such as cloned repos
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
pub struct ServerStorage { pub struct ServerStorage {
path: PathBuf, path: PathBuf,
} }
@ -113,533 +108,5 @@ impl ServerStorage {
} }
} }
/// Mapped from `.git-next.toml` file in target repo
/// Is also derived from the optional parameters in `git-next-server.toml` at
/// `forge.{forge}.repos.{repo}.(main|next|dev)`
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct RepoConfig {
branches: RepoBranches,
source: RepoConfigSource,
}
impl RepoConfig {
#[cfg(test)]
pub const fn new(branches: RepoBranches, source: RepoConfigSource) -> Self {
Self { branches, source }
}
pub fn load(toml: &str) -> Result<Self> {
toml::from_str(format!("source = \"Repo\"\n{}", toml).as_str()).map_err(Into::into)
}
pub const fn branches(&self) -> &RepoBranches {
&self.branches
}
pub const fn source(&self) -> RepoConfigSource {
self.source
}
}
impl Display for RepoConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.branches)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum RepoConfigSource {
Repo,
Server,
}
/// Mapped from `.git-next.toml` file at `branches`
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct RepoBranches {
main: String,
next: String,
dev: String,
}
impl RepoBranches {
#[cfg(test)]
pub fn new(main: impl Into<String>, next: impl Into<String>, dev: impl Into<String>) -> Self {
Self {
main: main.into(),
next: next.into(),
dev: dev.into(),
}
}
pub fn main(&self) -> BranchName {
BranchName(self.main.clone())
}
pub fn next(&self) -> BranchName {
BranchName(self.next.clone())
}
pub fn dev(&self) -> BranchName {
BranchName(self.dev.clone())
}
}
impl Display for RepoBranches {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{},{},{}", self.main, self.next, self.dev)
}
}
/// Defines a Forge to connect to
/// Maps from `git-next-server.toml` at `forge.{forge}`
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct ForgeConfig {
forge_type: ForgeType,
hostname: String,
user: String,
token: String,
// API Token
// Private SSH Key Path
repos: HashMap<String, ServerRepoConfig>,
}
impl ForgeConfig {
#[allow(dead_code)]
pub const fn forge_type(&self) -> &ForgeType {
&self.forge_type
}
pub fn hostname(&self) -> Hostname {
Hostname(self.hostname.clone())
}
pub fn user(&self) -> User {
User(self.user.clone())
}
pub fn token(&self) -> ApiToken {
ApiToken(self.token.clone().into())
}
pub fn repos(&self) -> impl Iterator<Item = (RepoAlias, &ServerRepoConfig)> {
self.repos
.iter()
.map(|(name, repo)| (RepoAlias(name.clone()), repo))
}
}
impl Display for ForgeConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}@{}",
self.forge_type.to_string().to_lowercase(),
self.user,
self.hostname
)
}
}
/// Defines a Repo within a ForgeConfig to be monitored by the server
/// Maps from `git-next-server.toml` at `forge.{forge}.repos.{name}`
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct ServerRepoConfig {
repo: String,
branch: String,
gitdir: Option<PathBuf>,
main: Option<String>,
next: Option<String>,
dev: Option<String>,
}
impl ServerRepoConfig {
pub fn repo(&self) -> RepoPath {
RepoPath(self.repo.clone())
}
#[allow(dead_code)]
pub fn branch(&self) -> BranchName {
BranchName(self.branch.clone())
}
pub fn gitdir(&self) -> Option<GitDir> {
self.gitdir.clone().map(GitDir::from)
}
/// Returns a RepoConfig from the server configuration if ALL THREE branches were provided
pub fn repo_config(&self) -> Option<RepoConfig> {
match (&self.main, &self.next, &self.dev) {
(Some(main), Some(next), Some(dev)) => Some(RepoConfig {
branches: RepoBranches {
main: main.to_string(),
next: next.to_string(),
dev: dev.to_string(),
},
source: RepoConfigSource::Server,
}),
_ => None,
}
}
}
#[cfg(test)]
impl AsRef<Self> for ServerRepoConfig {
fn as_ref(&self) -> &Self {
self
}
}
impl Display for ServerRepoConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.repo, self.branch)
}
}
/// The name of a Forge to connect to
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ForgeName(pub String);
impl Display for ForgeName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<&ForgeName> for PathBuf {
fn from(value: &ForgeName) -> Self {
Self::from(&value.0)
}
}
/// The hostname of a forge
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hostname(pub String);
impl Display for Hostname {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// The user within the forge to connect as
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct User(pub String);
impl Display for User {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// The API Token for the [user]
/// ForgeJo: https://{hostname}/user/settings/applications
/// Github: https://github.com/settings/tokens
#[derive(Clone, Debug)]
pub struct ApiToken(pub secrecy::Secret<String>);
impl From<String> for ApiToken {
fn from(value: String) -> Self {
Self(value.into())
}
}
/// The API Token is in effect a password, so it must be explicitly exposed to access its value
impl secrecy::ExposeSecret<String> for ApiToken {
fn expose_secret(&self) -> &String {
self.0.expose_secret()
}
}
/// The derived information about a Forge, used to create interactions with it
#[derive(Clone, Debug)]
pub struct ForgeDetails {
pub forge_name: ForgeName,
pub forge_type: ForgeType,
pub hostname: Hostname,
pub user: User,
pub token: ApiToken,
// API Token
// Private SSH Key Path
}
impl From<(&ForgeName, &ForgeConfig)> for ForgeDetails {
fn from(forge: (&ForgeName, &ForgeConfig)) -> Self {
Self {
forge_name: forge.0.clone(),
forge_type: forge.1.forge_type.clone(),
hostname: forge.1.hostname(),
user: forge.1.user(),
token: forge.1.token(),
}
}
}
/// The alias of a repo
/// This is the alias for the repo within `git-next-server.toml`
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RepoAlias(pub String);
impl Display for RepoAlias {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// The path for the repo within the forge.
/// Typically this is composed of the user or organisation and the name of the repo
/// e.g. `{user}/{repo}`
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoPath(pub String);
impl Display for RepoPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// The name of a Branch
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BranchName(pub String);
impl Display for BranchName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for BranchName {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// The derived information about a repo, used to interact with it
#[derive(Clone, Debug)]
pub struct RepoDetails {
pub generation: ServerGeneration,
pub repo_alias: RepoAlias,
pub repo_path: RepoPath,
pub branch: BranchName,
pub forge: ForgeDetails,
pub repo_config: Option<RepoConfig>,
pub gitdir: GitDir,
}
impl RepoDetails {
pub fn new(
generation: ServerGeneration,
repo_alias: &RepoAlias,
server_repo_config: &ServerRepoConfig,
forge_name: &ForgeName,
forge_config: &ForgeConfig,
gitdir: GitDir,
) -> Self {
Self {
generation,
repo_alias: repo_alias.clone(),
repo_path: RepoPath(server_repo_config.repo.clone()),
repo_config: server_repo_config.repo_config(),
branch: BranchName(server_repo_config.branch.clone()),
gitdir,
forge: ForgeDetails {
forge_name: forge_name.clone(),
forge_type: forge_config.forge_type.clone(),
hostname: forge_config.hostname(),
user: forge_config.user(),
token: forge_config.token(),
},
}
}
pub fn origin(&self) -> secrecy::Secret<String> {
let repo_details = self;
let user = &repo_details.forge.user;
let hostname = &repo_details.forge.hostname;
let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret;
let expose_secret = &repo_details.forge.token;
let token = expose_secret.expose_secret();
let origin = format!("https://{user}:{token}@{hostname}/{repo_path}.git");
origin.into()
}
pub fn git_remote(&self) -> GitRemote {
GitRemote::new(self.forge.hostname.clone(), self.repo_path.clone())
}
}
impl Display for RepoDetails {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"gen-{}:{}:{}/{}:{}@{}/{}@{}",
self.generation,
self.forge.forge_type,
self.forge.forge_name,
self.repo_alias,
self.forge.user,
self.forge.hostname,
self.repo_path,
self.branch,
)
}
}
type ValidationResult<T> = core::result::Result<T, RepoValidationError>;
#[derive(Debug)]
pub enum RepoValidationError {
NoDefaultPushRemote,
NoUrlForDefaultPushRemote,
NoHostnameForDefaultPushRemote,
UnableToOpenRepo(String),
Io(std::io::Error),
MismatchDefaultPushRemote {
found: GitRemote,
expected: GitRemote,
},
MismatchDefaultFetchRemote {
found: GitRemote,
expected: GitRemote,
},
}
impl std::error::Error for RepoValidationError {}
impl Display for RepoValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoDefaultPushRemote => write!(f, "There is no default push remote"),
Self::NoUrlForDefaultPushRemote => write!(f, "The default push remote has no url"),
Self::NoHostnameForDefaultPushRemote => {
write!(f, "The default push remote has no hostname")
}
Self::UnableToOpenRepo(err) => write!(f, "Unable to open the git dir: {err}"),
Self::Io(err) => write!(f, "IO Error: {err:?}"),
Self::MismatchDefaultPushRemote { found, expected } => write!(
f,
"The default push remote doesn't match: {found}, expected: {expected}"
),
Self::MismatchDefaultFetchRemote { found, expected } => write!(
f,
"The default fetch remote doesn't match: {found}, expected: {expected}"
),
}
}
}
/// Identifier for the type of Forge
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum ForgeType {
#[cfg(feature = "forgejo")]
ForgeJo,
// Gitea,
// GitHub,
// GitLab,
// BitBucket,
#[cfg(test)]
MockForge,
}
impl Display for ForgeType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", format!("{:?}", self).to_lowercase())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct GitDir(PathBuf);
impl GitDir {
#[cfg(test)]
pub(crate) fn new(pathbuf: &std::path::Path) -> Self {
info!("GitDir::new({pathbuf:?})");
Self(pathbuf.to_path_buf())
}
pub const fn pathbuf(&self) -> &PathBuf {
&self.0
}
#[tracing::instrument(skip_all)]
pub fn validate(
&self,
repository: &Repository,
repo_details: &RepoDetails,
) -> ValidationResult<()> {
let git_remote = repo_details.git_remote();
use gix::remote::Direction;
let push_remote = self.find_default_remote(repository, Direction::Push)?;
let fetch_remote = self.find_default_remote(repository, Direction::Fetch)?;
info!(config = %git_remote, push = %push_remote, fetch = %fetch_remote, "Check remotes match");
if git_remote != push_remote {
return Err(RepoValidationError::MismatchDefaultPushRemote {
found: push_remote,
expected: git_remote,
});
}
if git_remote != fetch_remote {
return Err(RepoValidationError::MismatchDefaultFetchRemote {
found: fetch_remote,
expected: git_remote,
});
}
Ok(())
}
#[tracing::instrument(skip_all, fields(?direction))]
fn find_default_remote(
&self,
repository: &Repository,
direction: gix::remote::Direction,
) -> ValidationResult<GitRemote> {
let repository = repository.deref().to_thread_local();
let Some(Ok(remote)) = repository.find_default_remote(gix::remote::Direction::Push) else {
return Err(RepoValidationError::NoDefaultPushRemote);
};
let Some(url) = remote.url(direction) else {
return Err(RepoValidationError::NoUrlForDefaultPushRemote);
};
let Some(host) = url.host() else {
return Err(RepoValidationError::NoHostnameForDefaultPushRemote);
};
let path = url.path.to_string();
let path = path.strip_prefix('/').map_or(path.as_str(), |path| path);
let path = path.strip_suffix(".git").map_or(path, |path| path);
info!(%host, %path, "found");
Ok(GitRemote::new(
Hostname(host.to_string()),
RepoPath(path.to_string()),
))
}
}
impl Deref for GitDir {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::fmt::Display for GitDir {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.0.display())
}
}
impl From<&str> for GitDir {
fn from(value: &str) -> Self {
Self(value.into())
}
}
impl From<&GitDir> for PathBuf {
fn from(value: &GitDir) -> Self {
value.to_path_buf()
}
}
impl From<PathBuf> for GitDir {
fn from(value: PathBuf) -> Self {
Self(value)
}
}
impl From<GitDir> for PathBuf {
fn from(value: GitDir) -> Self {
value.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GitRemote {
host: Hostname,
repo_path: RepoPath,
}
impl GitRemote {
pub const fn new(host: Hostname, repo_path: RepoPath) -> Self {
Self { host, repo_path }
}
pub const fn host(&self) -> &Hostname {
&self.host
}
pub const fn repo_path(&self) -> &RepoPath {
&self.repo_path
}
}
impl Display for GitRemote {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.host, self.repo_path)
}
}
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View file

@ -1,10 +1,14 @@
use assert2::let_assert; use assert2::let_assert;
use git_next_config::{
ForgeType, GitDir, Hostname, RepoBranches, RepoConfig, RepoConfigSource, RepoPath,
ServerRepoConfig,
};
use git_next_git::{self as git, Generation, GitRemote, Repository};
use gix::remote::Direction; use gix::remote::Direction;
use kxio::fs;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use crate::{gitforge::tests::common /* server::gitforge::tests::common */}; use crate::gitforge::tests::common;
use kxio::fs;
use super::*; use super::*;
@ -163,11 +167,11 @@ fn gitdir_should_display_as_pathbuf() {
// git.kemitix.net:kemitix/git-next // git.kemitix.net:kemitix/git-next
// If the default push remote is something else, then this test will fail // If the default push remote is something else, then this test will fail
fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> { fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
let cli_crate_dir = std::env::current_dir().map_err(RepoValidationError::Io)?; 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_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
let mut repo_details = common::repo_details( let mut repo_details = common::repo_details(
1, 1,
ServerGeneration::new(), Generation::new(),
common::forge_details(1, ForgeType::MockForge), common::forge_details(1, ForgeType::MockForge),
None, None,
GitDir::new(root), // Server GitDir - should be ignored GitDir::new(root), // Server GitDir - should be ignored
@ -176,7 +180,7 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
repo_details.repo_path = RepoPath("kemitix/git-next".to_string()); repo_details.repo_path = RepoPath("kemitix/git-next".to_string());
let gitdir = &repo_details.gitdir; let gitdir = &repo_details.gitdir;
let repository = Repository::open(gitdir)?; let repository = Repository::open(gitdir)?;
let found_git_remote = gitdir.find_default_remote(&repository, Direction::Push)?; let found_git_remote = git::validate::find_default_remote(&repository, Direction::Push)?;
let config_git_remote = repo_details.git_remote(); let config_git_remote = repo_details.git_remote();
assert_eq!( assert_eq!(
@ -189,11 +193,11 @@ fn repo_details_find_default_push_remote_finds_correct_remote() -> Result<()> {
#[test] #[test]
fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> { fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
let cli_crate_dir = std::env::current_dir().map_err(RepoValidationError::Io)?; 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_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
let mut repo_details = common::repo_details( let mut repo_details = common::repo_details(
1, 1,
ServerGeneration::new(), Generation::new(),
common::forge_details(1, ForgeType::MockForge), common::forge_details(1, ForgeType::MockForge),
None, None,
GitDir::new(root), // Server GitDir - should be ignored GitDir::new(root), // Server GitDir - should be ignored
@ -202,18 +206,18 @@ fn gitdir_validate_should_pass_a_valid_git_repo() -> Result<()> {
repo_details.repo_path = RepoPath("kemitix/git-next".to_string()); repo_details.repo_path = RepoPath("kemitix/git-next".to_string());
let gitdir = &repo_details.gitdir; let gitdir = &repo_details.gitdir;
let repository = Repository::open(gitdir)?; let repository = Repository::open(gitdir)?;
gitdir.validate(&repository, &repo_details)?; git::validate(&repository, &repo_details)?;
Ok(()) Ok(())
} }
#[test] #[test]
fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> 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(RepoValidationError::Io)); 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_assert!(Some(Some(root)) = cli_crate_dir.parent().map(|p| p.parent()));
let mut repo_details = common::repo_details( let mut repo_details = common::repo_details(
1, 1,
ServerGeneration::new(), Generation::new(),
common::forge_details(1, ForgeType::MockForge), common::forge_details(1, ForgeType::MockForge),
None, None,
GitDir::new(root), // Server GitDir - should be ignored GitDir::new(root), // Server GitDir - should be ignored
@ -222,7 +226,7 @@ fn gitdir_validate_should_fail_a_git_repo_with_wrong_remote() -> Result<()> {
repo_details.repo_path = RepoPath("hello/world".to_string()); repo_details.repo_path = RepoPath("hello/world".to_string());
let gitdir = &repo_details.gitdir; let gitdir = &repo_details.gitdir;
let repository = Repository::open(gitdir)?; let repository = Repository::open(gitdir)?;
let_assert!(Err(_) = gitdir.validate(&repository, &repo_details)); let_assert!(Err(_) = git::validate(&repository, &repo_details));
Ok(()) Ok(())
} }

View file

@ -1,3 +0,0 @@
pub mod reset;
pub use reset::reset;

View file

@ -1,4 +1,4 @@
use crate::config::{BranchName, GitDir}; use git_next_config::BranchName;
#[derive(Debug)] #[derive(Debug)]
pub enum ForgeFileError { pub enum ForgeFileError {
@ -39,45 +39,3 @@ impl std::fmt::Display for ForgeBranchError {
} }
} }
} }
#[derive(Debug)]
pub enum RepoCloneError {
InvalidGitDir(GitDir),
Io(std::io::Error),
Wait(std::io::Error),
Spawn(std::io::Error),
Validation(String),
GixClone(Box<gix::clone::Error>),
GixOpen(Box<gix::open::Error>),
GixFetch(Box<gix::clone::fetch::Error>),
}
impl std::error::Error for RepoCloneError {}
impl std::fmt::Display for RepoCloneError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidGitDir(gitdir) => write!(f, "Invalid Git dir: {:?}", gitdir),
Self::Io(err) => write!(f, "IO error: {:?}", err),
Self::Wait(err) => write!(f, "Waiting for command: {:?}", err),
Self::Spawn(err) => write!(f, "Spawning comming: {:?}", err),
Self::Validation(err) => write!(f, "Validation: {}", err),
Self::GixClone(err) => write!(f, "Clone: {:?}", err),
Self::GixOpen(err) => write!(f, "Open: {:?}", err),
Self::GixFetch(err) => write!(f, "Fetch: {:?}", err),
}
}
}
impl From<gix::clone::Error> for RepoCloneError {
fn from(value: gix::clone::Error) -> Self {
Self::GixClone(Box::new(value))
}
}
impl From<gix::open::Error> for RepoCloneError {
fn from(value: gix::open::Error) -> Self {
Self::GixOpen(Box::new(value))
}
}
impl From<gix::clone::fetch::Error> for RepoCloneError {
fn from(value: gix::clone::fetch::Error) -> Self {
Self::GixFetch(Box::new(value))
}
}

View file

@ -1,15 +1,14 @@
use git_next_config::BranchName;
use git_next_git::RepoDetails;
use kxio::network::{self, Network}; use kxio::network::{self, Network};
use tracing::error; use tracing::error;
use crate::{ use crate::gitforge::ForgeBranchError;
config::{BranchName, RepoDetails},
gitforge::{self, ForgeBranchError},
};
pub async fn get_all( pub async fn get_all(
repo_details: &RepoDetails, repo_details: &RepoDetails,
net: &Network, net: &Network,
) -> Result<Vec<gitforge::Branch>, ForgeBranchError> { ) -> Result<Vec<BranchName>, ForgeBranchError> {
let hostname = &repo_details.forge.hostname; let hostname = &repo_details.forge.hostname;
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
use secrecy::ExposeSecret; use secrecy::ExposeSecret;
@ -37,8 +36,7 @@ pub async fn get_all(
.response_body() .response_body()
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.map(|b| b.name()) .map(BranchName::from)
.map(gitforge::Branch)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(branches) Ok(branches)
} }
@ -47,8 +45,8 @@ pub async fn get_all(
struct Branch { struct Branch {
name: String, name: String,
} }
impl Branch { impl From<Branch> for BranchName {
fn name(&self) -> BranchName { fn from(value: Branch) -> Self {
BranchName(self.name.clone()) Self(value.name)
} }
} }

View file

@ -1,8 +1,6 @@
pub mod fetch;
mod get_all; mod get_all;
mod validate_positions; mod validate_positions;
pub use fetch::fetch;
pub use get_all::get_all; pub use get_all::get_all;
pub use validate_positions::validate_positions; pub use validate_positions::validate_positions;

View file

@ -1,10 +1,9 @@
use git_next_config::{BranchName, RepoConfig};
use git_next_git::{self as git, RepoDetails};
use kxio::network; use kxio::network;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::{ use crate::gitforge::{self, ForgeLike};
config::{BranchName, RepoConfig, RepoDetails},
gitforge::{self, ForgeLike},
};
#[derive(Debug, derive_more::Display)] #[derive(Debug, derive_more::Display)]
pub enum Error { pub enum Error {
@ -12,7 +11,7 @@ pub enum Error {
#[display("Failed to Reset Branch {branch} to {commit}")] #[display("Failed to Reset Branch {branch} to {commit}")]
FailedToResetBranch { FailedToResetBranch {
branch: BranchName, branch: BranchName,
commit: gitforge::Commit, commit: git::Commit,
}, },
BranchReset(BranchName), BranchReset(BranchName),
BranchHasNoCommits(BranchName), BranchHasNoCommits(BranchName),
@ -21,15 +20,15 @@ pub enum Error {
impl std::error::Error for Error {} impl std::error::Error for Error {}
pub struct ValidatedPositions { pub struct ValidatedPositions {
pub main: gitforge::Commit, pub main: git::Commit,
pub next: gitforge::Commit, pub next: git::Commit,
pub dev: gitforge::Commit, pub dev: git::Commit,
pub dev_commit_history: Vec<gitforge::Commit>, pub dev_commit_history: Vec<git::Commit>,
} }
pub async fn validate_positions( pub async fn validate_positions(
forge: &gitforge::forgejo::ForgeJoEnv, forge: &gitforge::forgejo::ForgeJoEnv,
repository: &gitforge::Repository, repository: &git::Repository,
repo_config: RepoConfig, repo_config: RepoConfig,
) -> Result<ValidatedPositions, Error> { ) -> Result<ValidatedPositions, Error> {
let repo_details = &forge.repo_details; let repo_details = &forge.repo_details;
@ -78,7 +77,7 @@ pub async fn validate_positions(
repository, repository,
repo_config.branches().next(), repo_config.branches().next(),
main.into(), main.into(),
gitforge::Force::From(next.clone().into()), git::reset::Force::From(next.clone().into()),
) { ) {
warn!(?err, "Failed to reset next to main"); warn!(?err, "Failed to reset next to main");
return Err(Error::FailedToResetBranch { return Err(Error::FailedToResetBranch {
@ -104,7 +103,7 @@ pub async fn validate_positions(
repository, repository,
repo_config.branches().next(), repo_config.branches().next(),
main.into(), main.into(),
gitforge::Force::From(next.clone().into()), git::reset::Force::From(next.clone().into()),
) { ) {
warn!(?err, "Failed to reset next to main"); warn!(?err, "Failed to reset next to main");
return Err(Error::FailedToResetBranch { return Err(Error::FailedToResetBranch {
@ -186,11 +185,11 @@ async fn get_commit_histories(
#[tracing::instrument(fields(%branch_name),skip_all)] #[tracing::instrument(fields(%branch_name),skip_all)]
async fn get_commit_history( async fn get_commit_history(
repo_details: &crate::config::RepoDetails, repo_details: &RepoDetails,
branch_name: &BranchName, branch_name: &BranchName,
find_commits: Vec<gitforge::Commit>, find_commits: Vec<git::Commit>,
net: &kxio::network::Network, net: &kxio::network::Network,
) -> Result<Vec<gitforge::Commit>, network::NetworkError> { ) -> Result<Vec<git::Commit>, network::NetworkError> {
let hostname = &repo_details.forge.hostname; let hostname = &repo_details.forge.hostname;
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
@ -223,7 +222,7 @@ async fn get_commit_history(
.response_body() .response_body()
.unwrap_or_default() .unwrap_or_default()
.into_iter() .into_iter()
.map(gitforge::Commit::from) .map(git::Commit::from)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let found = find_commits.is_empty() let found = find_commits.is_empty()
@ -253,7 +252,7 @@ struct Commit {
struct RepoCommit { struct RepoCommit {
message: String, message: String,
} }
impl From<Commit> for gitforge::Commit { impl From<Commit> for git::Commit {
fn from(value: Commit) -> Self { fn from(value: Commit) -> Self {
Self::new(&value.sha, &value.commit.message) Self::new(&value.sha, &value.commit.message)
} }

View file

@ -1,10 +1,9 @@
use git_next_config::BranchName;
use git_next_git::RepoDetails;
use kxio::network::{self, Network}; use kxio::network::{self, Network};
use tracing::{error, warn}; use tracing::{error, warn};
use crate::{ use crate::gitforge::ForgeFileError;
config::{BranchName, RepoDetails},
gitforge::ForgeFileError,
};
pub(super) async fn contents_get( pub(super) async fn contents_get(
repo_details: &RepoDetails, repo_details: &RepoDetails,

View file

@ -5,15 +5,15 @@ use std::time::Duration;
use actix::prelude::*; use actix::prelude::*;
use git_next_config::{BranchName, GitDir, RepoConfig};
use git_next_git::{self as git, GitRef, RepoDetails, Repository};
use kxio::network::{self, Network}; use kxio::network::{self, Network};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use crate::{ use crate::{
actors::repo::{RepoActor, StartMonitoring, ValidateRepo}, actors::repo::{RepoActor, StartMonitoring, ValidateRepo},
config::{BranchName, GitDir, RepoConfig, RepoDetails}, gitforge::{self, forgejo::branch::ValidatedPositions},
git, types::MessageToken,
gitforge::{self, forgejo::branch::ValidatedPositions, RepoCloneError, Repository},
types::{GitRef, MessageToken},
}; };
struct ForgeJo; struct ForgeJo;
@ -33,7 +33,7 @@ impl super::ForgeLike for ForgeJoEnv {
"forgejo".to_string() "forgejo".to_string()
} }
async fn branches_get_all(&self) -> Result<Vec<super::Branch>, gitforge::ForgeBranchError> { async fn branches_get_all(&self) -> Result<Vec<BranchName>, gitforge::ForgeBranchError> {
branch::get_all(&self.repo_details, &self.net).await branch::get_all(&self.repo_details, &self.net).await
} }
@ -47,7 +47,7 @@ impl super::ForgeLike for ForgeJoEnv {
async fn branches_validate_positions( async fn branches_validate_positions(
&self, &self,
repository: Repository, repository: git::Repository,
repo_config: RepoConfig, repo_config: RepoConfig,
addr: Addr<RepoActor>, addr: Addr<RepoActor>,
message_token: MessageToken, message_token: MessageToken,
@ -76,12 +76,12 @@ impl super::ForgeLike for ForgeJoEnv {
fn branch_reset( fn branch_reset(
&self, &self,
repository: &Repository, repository: &git::Repository,
branch_name: BranchName, branch_name: BranchName,
to_commit: GitRef, to_commit: GitRef,
force: gitforge::Force, force: git::reset::Force,
) -> gitforge::BranchResetResult { ) -> git::reset::Result {
branch::fetch(repository, &self.repo_details)?; git::fetch(repository, &self.repo_details)?;
git::reset( git::reset(
repository, repository,
&self.repo_details, &self.repo_details,
@ -91,7 +91,7 @@ impl super::ForgeLike for ForgeJoEnv {
) )
} }
async fn commit_status(&self, commit: &gitforge::Commit) -> gitforge::CommitStatus { async fn commit_status(&self, commit: &git::Commit) -> gitforge::CommitStatus {
let repo_details = &self.repo_details; let repo_details = &self.repo_details;
let hostname = &repo_details.forge.hostname; let hostname = &repo_details.forge.hostname;
let repo_path = &repo_details.repo_path; let repo_path = &repo_details.repo_path;
@ -135,17 +135,16 @@ impl super::ForgeLike for ForgeJoEnv {
} }
} }
fn repo_clone(&self, gitdir: GitDir) -> Result<Repository, RepoCloneError> { fn repo_clone(&self, gitdir: GitDir) -> Result<Repository, git::repository::Error> {
let repository = if !gitdir.exists() { let repository = if !gitdir.exists() {
info!("Local copy not found - cloning..."); info!("Local copy not found - cloning...");
Repository::clone(&self.repo_details)? Repository::clone(&self.repo_details)?
} else { } else {
Repository::open(gitdir.clone())? Repository::open(gitdir)?
}; };
info!("Validating..."); info!("Validating...");
gitdir git::validate(&repository, &self.repo_details)
.validate(&repository, &self.repo_details) .map_err(|e| git::repository::Error::Validation(e.to_string()))
.map_err(|e| RepoCloneError::Validation(e.to_string()))
.inspect(|_| info!("Validation - OK"))?; .inspect(|_| info!("Validation - OK"))?;
Ok(repository) Ok(repository)
} }

View file

@ -1,9 +1,7 @@
use crate::{ use git_next_config::{BranchName, GitDir, RepoConfig};
actors::repo::RepoActor, use git_next_git::{self as git, GitRef, Repository};
config::{BranchName, GitDir, RepoConfig},
gitforge::{self, RepoCloneError, Repository}, use crate::{actors::repo::RepoActor, gitforge, types::MessageToken};
types::{GitRef, MessageToken},
};
struct MockForge; struct MockForge;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -19,7 +17,7 @@ impl super::ForgeLike for MockForgeEnv {
"mock".to_string() "mock".to_string()
} }
async fn branches_get_all(&self) -> Result<Vec<super::Branch>, gitforge::ForgeBranchError> { async fn branches_get_all(&self) -> Result<Vec<BranchName>, gitforge::ForgeBranchError> {
todo!() todo!()
} }
@ -46,16 +44,16 @@ impl super::ForgeLike for MockForgeEnv {
_repository: &Repository, _repository: &Repository,
_branch_name: BranchName, _branch_name: BranchName,
_to_commit: GitRef, _to_commit: GitRef,
_force: gitforge::Force, _force: git::reset::Force,
) -> gitforge::BranchResetResult { ) -> git::reset::Result {
todo!() todo!()
} }
async fn commit_status(&self, _commit: &gitforge::Commit) -> gitforge::CommitStatus { async fn commit_status(&self, _commit: &git::Commit) -> gitforge::CommitStatus {
todo!() todo!()
} }
fn repo_clone(&self, _gitdir: GitDir) -> Result<Repository, RepoCloneError> { fn repo_clone(&self, _gitdir: GitDir) -> Result<Repository, git::repository::Error> {
todo!() todo!()
} }
} }

View file

@ -1,5 +1,7 @@
#![allow(dead_code)] #![allow(dead_code)]
use git_next_config::{BranchName, GitDir, RepoConfig};
use git_next_git::{self as git, GitRef, RepoDetails, Repository};
use kxio::network::Network; use kxio::network::Network;
#[cfg(feature = "forgejo")] #[cfg(feature = "forgejo")]
@ -16,22 +18,19 @@ pub use types::*;
mod errors; mod errors;
pub use errors::*; pub use errors::*;
use crate::{ use crate::types::MessageToken;
config::{BranchName, GitDir, RepoConfig, RepoDetails},
types::{GitRef, MessageToken},
};
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait ForgeLike { pub trait ForgeLike {
fn name(&self) -> String; fn name(&self) -> String;
/// Returns a list of all branches in the repo. /// Returns a list of all branches in the repo.
async fn branches_get_all(&self) -> Result<Vec<Branch>, ForgeBranchError>; async fn branches_get_all(&self) -> Result<Vec<BranchName>, ForgeBranchError>;
/// Returns the contents of the file. /// Returns the contents of the file.
async fn file_contents_get( async fn file_contents_get(
&self, &self,
branch: &super::config::BranchName, branch: &BranchName,
file_path: &str, file_path: &str,
) -> Result<String, ForgeFileError>; ) -> Result<String, ForgeFileError>;
@ -51,14 +50,14 @@ pub trait ForgeLike {
repository: &Repository, repository: &Repository,
branch_name: BranchName, branch_name: BranchName,
to_commit: GitRef, to_commit: GitRef,
force: Force, force: git::reset::Force,
) -> BranchResetResult; ) -> git::reset::Result;
/// Checks the results of any (e.g. CI) status checks for the commit. /// Checks the results of any (e.g. CI) status checks for the commit.
async fn commit_status(&self, commit: &Commit) -> CommitStatus; async fn commit_status(&self, commit: &git::Commit) -> CommitStatus;
/// Clones a repo to disk. /// Clones a repo to disk.
fn repo_clone(&self, gitdir: GitDir) -> Result<Repository, RepoCloneError>; fn repo_clone(&self, gitdir: GitDir) -> Result<Repository, git::repository::Error>;
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View file

@ -1,11 +1,10 @@
use crate::{ use git_next_config::{
config::{ ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, GitDir, Hostname, RepoAlias,
ApiToken, BranchName, ForgeDetails, ForgeName, ForgeType, GitDir, Hostname, RepoAlias, RepoBranches, RepoConfig, RepoConfigSource, RepoPath, User,
RepoBranches, RepoConfig, RepoConfigSource, RepoDetails, RepoPath, User,
},
types::ServerGeneration,
}; };
use git_next_git::{Generation, RepoDetails};
pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails { pub fn forge_details(n: u32, forge_type: ForgeType) -> ForgeDetails {
ForgeDetails { ForgeDetails {
forge_name: forge_name(n), forge_name: forge_name(n),
@ -33,7 +32,7 @@ pub fn forge_name(n: u32) -> ForgeName {
} }
pub fn repo_details( pub fn repo_details(
n: u32, n: u32,
generation: ServerGeneration, generation: Generation,
forge: ForgeDetails, forge: ForgeDetails,
repo_config: Option<RepoConfig>, repo_config: Option<RepoConfig>,
gitdir: GitDir, gitdir: GitDir,

View file

@ -1,11 +1,9 @@
use assert2::let_assert; use assert2::let_assert;
use git_next_config::{ForgeType, RepoConfigSource};
use kxio::network::{MockNetwork, StatusCode}; use kxio::network::{MockNetwork, StatusCode};
use crate::{ use git_next_git::Generation;
config::{BranchName, ForgeType, RepoConfigSource},
types::ServerGeneration,
};
use super::*; use super::*;
@ -17,7 +15,7 @@ fn test_name() {
let net = Network::new_mock(); let net = Network::new_mock();
let repo_details = common::repo_details( let repo_details = common::repo_details(
1, 1,
ServerGeneration::new(), Generation::new(),
common::forge_details(1, ForgeType::MockForge), common::forge_details(1, ForgeType::MockForge),
Some(common::repo_config(1, RepoConfigSource::Repo)), Some(common::repo_config(1, RepoConfigSource::Repo)),
GitDir::new(fs.base()), GitDir::new(fs.base()),
@ -44,7 +42,7 @@ async fn test_branches_get() {
let repo_details = common::repo_details( let repo_details = common::repo_details(
1, 1,
ServerGeneration::new(), Generation::new(),
common::forge_details(1, ForgeType::MockForge), common::forge_details(1, ForgeType::MockForge),
Some(common::repo_config(1, RepoConfigSource::Repo)), Some(common::repo_config(1, RepoConfigSource::Repo)),
GitDir::new(fs.base()), GitDir::new(fs.base()),
@ -57,5 +55,5 @@ async fn test_branches_get() {
let_assert!(Some(requests) = net.mocked_requests()); let_assert!(Some(requests) = net.mocked_requests());
assert_eq!(requests.len(), 1); assert_eq!(requests.len(), 1);
assert_eq!(branches, vec![Branch(BranchName("string".into()))]); assert_eq!(branches, vec![BranchName("string".into())]);
} }

View file

@ -1,41 +1,4 @@
use std::{ops::Deref as _, path::PathBuf, sync::atomic::AtomicBool}; use git_next_git as git;
use crate::{
config::{BranchName, RepoDetails},
gitforge::RepoCloneError,
};
#[derive(Debug, PartialEq, Eq)]
pub struct Branch(pub BranchName);
impl Branch {
pub const fn name(&self) -> &BranchName {
&self.0
}
}
#[derive(Debug)]
pub enum Force {
No,
From(crate::types::GitRef),
}
impl std::fmt::Display for Force {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::No => write!(f, "fast-foward"),
Self::From(from) => write!(f, "force-if-from:{}", from),
}
}
}
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum BranchResetError {
Open(Box<gix::open::Error>),
Fetch(crate::gitforge::forgejo::branch::fetch::Error),
Push,
}
impl std::error::Error for BranchResetError {}
pub type BranchResetResult = Result<(), BranchResetError>;
#[derive(Debug)] #[derive(Debug)]
pub enum CommitStatus { pub enum CommitStatus {
@ -46,82 +9,7 @@ pub enum CommitStatus {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CommitHistories { pub struct CommitHistories {
pub main: Vec<Commit>, pub main: Vec<git::Commit>,
pub next: Vec<Commit>, pub next: Vec<git::Commit>,
pub dev: Vec<Commit>, pub dev: Vec<git::Commit>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Commit {
sha: Sha,
message: Message,
}
impl Commit {
pub fn new(sha: &str, message: &str) -> Self {
Self {
sha: Sha::new(sha.to_string()),
message: Message::new(message.to_string()),
}
}
pub const fn sha(&self) -> &Sha {
&self.sha
}
pub const fn message(&self) -> &Message {
&self.message
}
}
impl std::fmt::Display for Commit {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.sha)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Sha(String);
impl Sha {
pub const fn new(value: String) -> Self {
Self(value)
}
}
impl std::fmt::Display for Sha {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Message(String);
impl Message {
pub const fn new(value: String) -> Self {
Self(value)
}
}
impl std::fmt::Display for Message {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, derive_more::From)]
pub struct Repository(gix::ThreadSafeRepository);
impl Repository {
pub fn open(gitdir: impl Into<PathBuf>) -> Result<Self, RepoCloneError> {
Ok(Self(gix::ThreadSafeRepository::open(gitdir.into())?))
}
pub fn clone(repo_details: &RepoDetails) -> Result<Self, RepoCloneError> {
use secrecy::ExposeSecret;
let origin = repo_details.origin();
let (repository, _outcome) =
gix::prepare_clone_bare(origin.expose_secret().as_str(), repo_details.gitdir.deref())?
.fetch_only(gix::progress::Discard, &AtomicBool::new(false))?;
Ok(repository.into_sync().into())
}
}
impl std::ops::Deref for Repository {
type Target = gix::ThreadSafeRepository;
fn deref(&self) -> &Self::Target {
&self.0
}
} }

View file

@ -1,6 +1,5 @@
mod actors; mod actors;
mod config; mod config;
pub mod git;
pub mod gitforge; pub mod gitforge;
pub mod types; pub mod types;

View file

@ -1,25 +1,3 @@
use std::fmt::Display;
use crate::{config::BranchName, gitforge};
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct GitRef(pub String);
impl From<gitforge::Commit> for GitRef {
fn from(value: gitforge::Commit) -> Self {
Self(value.sha().to_string())
}
}
impl From<BranchName> for GitRef {
fn from(value: BranchName) -> Self {
Self(value.0)
}
}
impl Display for GitRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct MessageToken(u32); pub struct MessageToken(u32);
impl MessageToken { impl MessageToken {
@ -35,19 +13,3 @@ impl std::fmt::Display for MessageToken {
write!(f, "{}", self.0) write!(f, "{}", self.0)
} }
} }
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ServerGeneration(u32);
impl ServerGeneration {
pub fn new() -> Self {
Self::default()
}
pub fn inc(&mut self) {
self.0 += 1
}
}
impl std::fmt::Display for ServerGeneration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}